UNPKG

5.64 kBPlain TextView Raw
1// @ts-ignore
2import builtins from 'builtin-modules'
3// @ts-ignore
4import detective from 'detective'
5import path from 'path'
6import semver from 'semver'
7
8import Parser from './Parser'
9import { Person, SoftwarePackage } from '@stencila/schema'
10
11/**
12 * Dockter `Parser` class for Node.js.
13 */
14export default class JavascriptParser extends Parser {
15
16 /**
17 * Parse a folder to detect any `package.json` or `*.js` source code files
18 * and return a `SoftwarePackage` instance
19 */
20 async parse (): Promise<SoftwarePackage | null> {
21 if (this.exists('package.json')) {
22 let data = JSON.parse(this.read('package.json'))
23 return this.createPackage(data)
24 } else {
25 const files = this.glob(['**/*.js'])
26 if (files.length) {
27 const data = {
28 name: path.basename(this.folder),
29 dependencies: {}
30 }
31 for (let file of files) {
32 const code = this.read(file)
33 const requires = detective(code)
34 for (let require of requires) {
35 if (!builtins.includes(require)) {
36 // @ts-ignore
37 data.dependencies[require] = 'latest'
38 }
39 }
40 }
41 return this.createPackage(data)
42 } else {
43 return null
44 }
45 }
46 }
47
48 /**
49 * Create a `SoftwarePackage` instance from a Node.js package meta-data object
50 *
51 * Meta-data for a packages dependencies is obtained from https://registry.npmjs.org/ using the
52 * JSON API documented at https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
53 * and https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md.
54 * Currently we fetch the abbreviated metadata because the full meta data can be very large.
55 *
56 * The column "NodeJS" in https://github.com/codemeta/codemeta/blob/master/crosswalk.csv
57 * is used to translate package meta-data into a `SoftwarePackage` instance.
58 *
59 * @param data Package object
60 */
61 private async createPackage (data: any): Promise<SoftwarePackage> {
62 // Create new package instance and populate it's
63 // properties in order of type hierarchy: Thing > CreativeWork > SoftwareSourceCode > SoftwarePackage
64 const pkg = new SoftwarePackage()
65
66 // schema:Thing
67 pkg.name = data.name
68
69 // schema:CreativeWork
70 pkg.version = data.version
71
72 // schema:SoftwareSourceCode
73 pkg.runtimePlatform = 'Node.js'
74
75 pkg.license = data.license
76 pkg.description = data.description
77
78 if (data.author) {
79 if (typeof data.author === 'string') {
80 pkg.authors = [Person.fromText(data.author)]
81 } else {
82 let authorStr = ''
83 if (data.author.name) authorStr = data.author.name
84 if (data.author.email) authorStr += ` <${data.author.email}>`
85 if (data.author.url) authorStr += ` (${data.author.url})`
86
87 pkg.authors = [Person.fromText(authorStr)]
88 }
89 }
90
91 if (data.repository) {
92 if (typeof data.repository === 'string') {
93 if (data.repository.match(/github:/)) {
94 pkg.codeRepository = data.repository.replace(/github:/, 'https://github.com') + '/'
95 } else if (data.repository.match(/gitlab:/)) {
96 pkg.codeRepository = data.repository.replace(/gitlab:/, 'https://gitlab.com') + '/'
97 } else if (data.repository.match(/bitbucket:/)) {
98 pkg.codeRepository = data.repository.replace(/bitbucket:/, 'https://bitbucket.com') + '/'
99 } else if (data.repository.match(/^[^\/]*\/[^\/]*$/)) {
100 pkg.codeRepository = data.repository.replace(/^([^\/]*)\/([^\/]*)$/, 'https://www.npmjs.com/package/$1/$2') + '/'
101 } else {
102 pkg.codeRepository = data.repository
103 }
104 } else {
105 pkg.codeRepository = data.repository.url
106 }
107 }
108
109 // stencila:SoftwarePackage
110 if (data.dependencies) {
111 pkg.softwareRequirements = await Promise.all(
112 Object.entries(data.dependencies).map(async ([name, versionRange]) => {
113 // Determine the minimum version that satisfies the range specified in the
114 // If we can't determine a minimum version from the versionRange
115 // (e.g. because it's a github url) then try to get latest
116 let version = 'latest'
117 if (versionRange !== 'latest' && versionRange !== '*') {
118 const range = semver.validRange(versionRange as string)
119 if (range) {
120 const match = range.match(/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)/)
121 if (match) version = match[0]
122 }
123 }
124
125 // For scoped packages (e.g. `@types/node`) replace any slashes in the package name
126 // and fetch the latest version (see https://github.com/stencila/dockter/issues/87).
127 if (name[0] === '@') {
128 name = name.replace('/', '%2f')
129 version = '*'
130 }
131
132 // Fetch meta-data from NPM
133 const data = await this.fetch(`https://registry.npmjs.org/${name}/${version}`, {
134 json: true,
135 headers: {
136 'Accept': 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'
137 }
138 })
139
140 if (data) {
141 return this.createPackage(data)
142 } else {
143 // All we know is name and version, so return that
144 const dependency = new SoftwarePackage()
145 dependency.name = name
146 dependency.version = versionRange as string
147 dependency.runtimePlatform = 'Node.js'
148 return dependency
149 }
150 })
151 )
152 }
153
154 return pkg
155 }
156}