|
1 | | -import getPackageInfo, { |
2 | | - GetPackageInfoError, |
3 | | - GetPackageInfoResult, |
4 | | - GetPackageInfoResultSourceItem, |
5 | | -} from 'get-package-info'; |
6 | 1 | import parseAuthor from 'parse-author'; |
7 | 2 | import path from 'node:path'; |
8 | | -import resolve, { AsyncOpts } from 'resolve'; |
| 3 | +import { createRequire } from 'node:module'; |
| 4 | +import { promisifiedGracefulFs } from './util.js'; |
9 | 5 | import { debug } from './common.js'; |
10 | 6 | import { OfficialPlatform, Options, ProcessedOptions } from './types.js'; |
11 | 7 |
|
12 | | -function isMissingRequiredProperty(props: string[]) { |
13 | | - return props.some((prop) => prop === 'productName' || prop === 'dependencies.electron'); |
14 | | -} |
15 | | - |
16 | | -function errorMessageForProperty(prop: string) { |
17 | | - let hash, propDescription; |
18 | | - switch (prop) { |
19 | | - case 'productName': |
20 | | - hash = 'name'; |
21 | | - propDescription = 'application name'; |
22 | | - break; |
23 | | - case 'dependencies.electron': |
24 | | - hash = 'electronversion'; |
25 | | - propDescription = 'Electron version'; |
26 | | - break; |
27 | | - case 'version': |
28 | | - hash = 'appversion'; |
29 | | - propDescription = 'application version'; |
30 | | - break; |
31 | | - /* istanbul ignore next */ |
32 | | - default: |
33 | | - hash = ''; |
34 | | - propDescription = `[Unknown Property (${prop})]`; |
| 8 | +type PackageJSON = { |
| 9 | + name?: string; |
| 10 | + productName?: string; |
| 11 | + version?: string; |
| 12 | + author?: string | { name?: string }; |
| 13 | + dependencies?: Record<string, string>; |
| 14 | + devDependencies?: Record<string, string>; |
| 15 | +}; |
| 16 | + |
| 17 | +const ELECTRON_PACKAGES = ['electron', 'electron-nightly'] as const; |
| 18 | + |
| 19 | +async function* walkPackageJSONs(dir: string): AsyncGenerator<{ src: string; pkg: PackageJSON }> { |
| 20 | + let prev: string | undefined; |
| 21 | + let cur = path.resolve(dir); |
| 22 | + while (cur !== prev) { |
| 23 | + const src = path.join(cur, 'package.json'); |
| 24 | + try { |
| 25 | + const contents = await promisifiedGracefulFs.readFile(src, 'utf8'); |
| 26 | + yield { src, pkg: JSON.parse(contents.replace(/^\uFEFF/, '')) }; |
| 27 | + } catch (err) { |
| 28 | + if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err; |
| 29 | + } |
| 30 | + prev = cur; |
| 31 | + cur = path.dirname(cur); |
35 | 32 | } |
| 33 | +} |
36 | 34 |
|
| 35 | +function errorMessageForProperty(propDescription: string, hash: string) { |
37 | 36 | return ( |
38 | 37 | `Unable to determine ${propDescription}. Please specify an ${propDescription}\n\n` + |
39 | 38 | 'For more information, please see\n' + |
40 | 39 | `https://electron.github.io/packager/main/interfaces/Options.html#${hash}\n` |
41 | 40 | ); |
42 | 41 | } |
43 | 42 |
|
44 | | -function resolvePromise(id: string, options: AsyncOpts) { |
45 | | - return new Promise<[string | undefined, { version: string }]>( |
46 | | - // eslint-disable-next-line promise/param-names |
47 | | - (accept, reject) => { |
48 | | - resolve(id, options, (err, mainPath, pkg) => { |
49 | | - if (err) { |
50 | | - /* istanbul ignore next */ |
51 | | - reject(err); |
52 | | - } else { |
53 | | - accept([mainPath as string | undefined, pkg as { version: string }]); |
54 | | - } |
55 | | - }); |
56 | | - }, |
57 | | - ); |
58 | | -} |
59 | | - |
60 | | -async function getVersion(electronProp: GetPackageInfoResultSourceItem) { |
61 | | - const [, packageName] = electronProp.prop.split('.'); |
62 | | - const src = electronProp.src; |
63 | | - |
64 | | - const pkg = (await resolvePromise(packageName, { basedir: path.dirname(src) }))[1]; |
65 | | - debug(`Inferring target Electron version from ${packageName} in ${src}`); |
| 43 | +async function resolveElectronVersion(packageName: string, fromSrc: string) { |
| 44 | + const pkgJsonPath = createRequire(fromSrc).resolve(`${packageName}/package.json`); |
| 45 | + const pkg = JSON.parse(await promisifiedGracefulFs.readFile(pkgJsonPath, 'utf8')); |
| 46 | + debug(`Inferring target Electron version from ${packageName} in ${fromSrc}`); |
66 | 47 | return pkg.version; |
67 | 48 | } |
68 | 49 |
|
69 | | -async function handleMetadata( |
70 | | - opts: Options, |
71 | | - result: GetPackageInfoResult, |
72 | | -): Promise<Partial<ProcessedOptions>> { |
73 | | - const processedValues: Partial<ProcessedOptions> = {}; |
74 | | - |
75 | | - if (typeof result.values.productName === 'string') { |
76 | | - debug( |
77 | | - `Inferring application name from ${result.source.productName.prop} in ${result.source.productName.src}`, |
78 | | - ); |
79 | | - processedValues.name = result.values.productName; |
80 | | - } |
81 | | - |
82 | | - if (typeof result.values.version === 'string') { |
83 | | - debug(`Inferring appVersion from version in ${result.source.version.src}`); |
84 | | - processedValues.appVersion = result.values.version; |
85 | | - } |
86 | | - |
87 | | - if (result.values.author) { |
88 | | - const author = result.values.author as string | { name: string }; |
89 | | - const win32metadata = opts.win32metadata || {}; |
90 | | - |
91 | | - debug(`Inferring win32metadata.CompanyName from author in ${result.source.author.src}`); |
92 | | - if (typeof author === 'string') { |
93 | | - win32metadata.CompanyName = parseAuthor(author).name; |
94 | | - } else if (author.name) { |
95 | | - win32metadata.CompanyName = author.name; |
96 | | - } else { |
97 | | - debug('Cannot infer win32metadata.CompanyName from author, no name found'); |
98 | | - } |
99 | | - processedValues.win32metadata = win32metadata; |
100 | | - } |
101 | | - |
102 | | - if (Object.prototype.hasOwnProperty.call(result.values, 'dependencies.electron')) { |
103 | | - processedValues.electronVersion = await getVersion(result.source['dependencies.electron']); |
104 | | - } |
105 | | - |
106 | | - return processedValues; |
107 | | -} |
108 | | - |
109 | | -function handleMissingProperties( |
110 | | - opts: Options, |
111 | | - err: GetPackageInfoError, |
112 | | -): Promise<Partial<ProcessedOptions>> { |
113 | | - const missingProps = err.missingProps.map((prop) => { |
114 | | - return Array.isArray(prop) ? prop[0] : prop; |
115 | | - }); |
116 | | - |
117 | | - if (isMissingRequiredProperty(missingProps)) { |
118 | | - const messages = missingProps.map(errorMessageForProperty); |
119 | | - |
120 | | - debug(err.message); |
121 | | - err.message = messages.join('\n') + '\n'; |
122 | | - throw err; |
123 | | - } else { |
124 | | - // Missing props not required, can continue w/ partial result |
125 | | - return handleMetadata(opts, err.result); |
126 | | - } |
127 | | -} |
128 | | - |
129 | 50 | export async function getMetadataFromPackageJSON( |
130 | 51 | platforms: OfficialPlatform[], |
131 | 52 | opts: Options, |
132 | 53 | dir: string, |
133 | 54 | ): Promise<Partial<ProcessedOptions>> { |
134 | | - const props: Array<string | string[]> = []; |
| 55 | + const result: Partial<ProcessedOptions> = {}; |
135 | 56 |
|
136 | | - if (!opts.name) { |
137 | | - props.push(['productName', 'name']); |
138 | | - } |
| 57 | + let needName = !opts.name; |
| 58 | + let needVersion = !opts.appVersion; |
| 59 | + let needElectron = !opts.electronVersion; |
| 60 | + let needAuthor = platforms.includes('win32') && !opts.win32metadata?.CompanyName; |
139 | 61 |
|
140 | | - if (!opts.appVersion) { |
141 | | - props.push('version'); |
| 62 | + if (needAuthor) { |
| 63 | + debug('Requiring author in package.json, as CompanyName was not specified for win32metadata'); |
142 | 64 | } |
143 | 65 |
|
144 | | - if (!opts.electronVersion) { |
145 | | - props.push([ |
146 | | - 'dependencies.electron', |
147 | | - 'devDependencies.electron', |
148 | | - 'dependencies.electron-nightly', |
149 | | - 'devDependencies.electron-nightly', |
150 | | - ]); |
151 | | - } |
| 66 | + const initiallyNeeded = [ |
| 67 | + needName && 'productName', |
| 68 | + needVersion && 'version', |
| 69 | + needElectron && 'dependencies.electron', |
| 70 | + needAuthor && 'author', |
| 71 | + ].filter(Boolean) as string[]; |
| 72 | + |
| 73 | + if (!initiallyNeeded.length) return result; |
| 74 | + |
| 75 | + for await (const { src, pkg } of walkPackageJSONs(dir)) { |
| 76 | + if (needName) { |
| 77 | + const name = pkg.productName ?? pkg.name; |
| 78 | + if (name !== undefined) { |
| 79 | + const field = pkg.productName !== undefined ? 'productName' : 'name'; |
| 80 | + debug(`Inferring application name from ${field} in ${src}`); |
| 81 | + result.name = name; |
| 82 | + needName = false; |
| 83 | + } |
| 84 | + } |
152 | 85 |
|
153 | | - if (platforms.includes('win32') && !(opts.win32metadata && opts.win32metadata.CompanyName)) { |
154 | | - debug('Requiring author in package.json, as CompanyName was not specified for win32metadata'); |
155 | | - props.push('author'); |
156 | | - } |
| 86 | + if (needVersion && pkg.version !== undefined) { |
| 87 | + debug(`Inferring appVersion from version in ${src}`); |
| 88 | + result.appVersion = pkg.version; |
| 89 | + needVersion = false; |
| 90 | + } |
157 | 91 |
|
158 | | - // Search package.json files to infer name and version from |
159 | | - try { |
160 | | - const packageInfo = await getPackageInfo(props, dir); |
161 | | - return handleMetadata(opts, packageInfo); |
162 | | - } catch (e) { |
163 | | - const err = e as GetPackageInfoError; |
164 | | - |
165 | | - if (err.missingProps) { |
166 | | - if (err.missingProps.length === props.length) { |
167 | | - debug(err.message); |
168 | | - err.message = `Could not locate a package.json file in "${path.resolve(opts.dir)}" or its parent directories for an Electron app with the following fields: ${err.missingProps.join(', ')}`; |
| 92 | + if (needAuthor && pkg.author !== undefined) { |
| 93 | + debug(`Inferring win32metadata.CompanyName from author in ${src}`); |
| 94 | + const win32metadata = opts.win32metadata || {}; |
| 95 | + if (typeof pkg.author === 'string') { |
| 96 | + win32metadata.CompanyName = parseAuthor(pkg.author).name; |
| 97 | + } else if (pkg.author.name) { |
| 98 | + win32metadata.CompanyName = pkg.author.name; |
169 | 99 | } else { |
170 | | - return handleMissingProperties(opts, err); |
| 100 | + debug('Cannot infer win32metadata.CompanyName from author, no name found'); |
| 101 | + } |
| 102 | + result.win32metadata = win32metadata; |
| 103 | + needAuthor = false; |
| 104 | + } |
| 105 | + |
| 106 | + if (needElectron) { |
| 107 | + for (const packageName of ELECTRON_PACKAGES) { |
| 108 | + if ( |
| 109 | + pkg.dependencies?.[packageName] !== undefined || |
| 110 | + pkg.devDependencies?.[packageName] !== undefined |
| 111 | + ) { |
| 112 | + result.electronVersion = await resolveElectronVersion(packageName, src); |
| 113 | + needElectron = false; |
| 114 | + break; |
| 115 | + } |
171 | 116 | } |
172 | 117 | } |
173 | 118 |
|
174 | | - throw err; |
| 119 | + if (!needName && !needVersion && !needElectron && !needAuthor) break; |
175 | 120 | } |
| 121 | + |
| 122 | + const stillMissing = [ |
| 123 | + needName && 'productName', |
| 124 | + needVersion && 'version', |
| 125 | + needElectron && 'dependencies.electron', |
| 126 | + needAuthor && 'author', |
| 127 | + ].filter(Boolean) as string[]; |
| 128 | + |
| 129 | + if (stillMissing.length) { |
| 130 | + if (stillMissing.length === initiallyNeeded.length) { |
| 131 | + throw new Error( |
| 132 | + `Could not locate a package.json file in "${path.resolve(opts.dir)}" or its parent directories for an Electron app with the following fields: ${initiallyNeeded.join(', ')}`, |
| 133 | + ); |
| 134 | + } |
| 135 | + if (needName || needElectron) { |
| 136 | + const messages: string[] = []; |
| 137 | + if (needName) messages.push(errorMessageForProperty('application name', 'name')); |
| 138 | + if (needVersion) messages.push(errorMessageForProperty('application version', 'appversion')); |
| 139 | + if (needElectron) |
| 140 | + messages.push(errorMessageForProperty('Electron version', 'electronversion')); |
| 141 | + throw new Error(messages.join('\n') + '\n'); |
| 142 | + } |
| 143 | + } |
| 144 | + |
| 145 | + return result; |
176 | 146 | } |
0 commit comments