@gjsify/cli 0.1.13 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/actions/build.js +2 -1
- package/lib/commands/build.js +20 -0
- package/lib/config.js +28 -1
- package/lib/types/cli-build-options.d.ts +29 -0
- package/lib/types/config-data.d.ts +5 -0
- package/lib/utils/detect-native-packages.d.ts +10 -4
- package/lib/utils/detect-native-packages.js +19 -7
- package/package.json +12 -13
- package/src/actions/build.ts +2 -1
- package/src/commands/build.ts +20 -0
- package/src/config.ts +30 -1
- package/src/types/cli-build-options.ts +29 -0
- package/src/types/config-data.ts +5 -0
- package/src/utils/detect-native-packages.ts +18 -7
package/lib/actions/build.js
CHANGED
|
@@ -131,7 +131,7 @@ export class BuildAction {
|
|
|
131
131
|
}
|
|
132
132
|
/** Application mode */
|
|
133
133
|
async buildApp(app = 'gjs') {
|
|
134
|
-
const { verbose, esbuild, typescript, exclude, library: pgk } = this.configData;
|
|
134
|
+
const { verbose, esbuild, typescript, exclude, library: pgk, aliases } = this.configData;
|
|
135
135
|
const format = esbuild?.format ?? (esbuild?.outfile?.endsWith('.cjs') ? 'cjs' : 'esm');
|
|
136
136
|
// Set default outfile if no outdir is set
|
|
137
137
|
if (esbuild && !esbuild?.outfile && !esbuild?.outdir && (pgk?.main || pgk?.module)) {
|
|
@@ -145,6 +145,7 @@ export class BuildAction {
|
|
|
145
145
|
exclude,
|
|
146
146
|
reflection: typescript?.reflection,
|
|
147
147
|
consoleShim,
|
|
148
|
+
...(aliases ? { aliases } : {}),
|
|
148
149
|
};
|
|
149
150
|
const { autoMode, extras } = this.parseGlobalsValue(globals);
|
|
150
151
|
// --- Auto mode (with optional extras): iterative multi-pass build ---
|
package/lib/commands/build.js
CHANGED
|
@@ -97,6 +97,26 @@ export const buildCommand = {
|
|
|
97
97
|
type: 'boolean',
|
|
98
98
|
normalize: true,
|
|
99
99
|
default: false
|
|
100
|
+
})
|
|
101
|
+
.option('external', {
|
|
102
|
+
description: "Module names that should NOT be bundled. Repeat the flag or pass a comma-separated list (e.g. --external typedoc,prettier). Globs are forwarded to esbuild as-is. See https://esbuild.github.io/api/#external",
|
|
103
|
+
array: true,
|
|
104
|
+
type: 'string',
|
|
105
|
+
default: [],
|
|
106
|
+
coerce: (arg) => arg.flatMap((v) => v.split(',').map((s) => s.trim()).filter(Boolean)),
|
|
107
|
+
})
|
|
108
|
+
.option('define', {
|
|
109
|
+
description: "Substitute compile-time constants. Each entry is KEY=VALUE where VALUE is a JS expression (string literals must be quoted: --define VERSION='\"1.2.3\"'). Repeat the flag or pass comma-separated. See https://esbuild.github.io/api/#define",
|
|
110
|
+
array: true,
|
|
111
|
+
type: 'string',
|
|
112
|
+
default: [],
|
|
113
|
+
})
|
|
114
|
+
.option('alias', {
|
|
115
|
+
description: "Map module specifiers at bundle time. Each entry is FROM=TO (e.g. --alias typedoc=@gjsify/empty). Layered on top of the built-in alias map. Useful for stubbing heavy deps the test scenario never executes.",
|
|
116
|
+
array: true,
|
|
117
|
+
type: 'string',
|
|
118
|
+
default: [],
|
|
119
|
+
coerce: (arg) => arg.flatMap((v) => v.split(',').map((s) => s.trim()).filter(Boolean)),
|
|
100
120
|
});
|
|
101
121
|
},
|
|
102
122
|
handler: async (args) => {
|
package/lib/config.js
CHANGED
|
@@ -75,13 +75,40 @@ export class Config {
|
|
|
75
75
|
configData.shebang = cliArgs.shebang;
|
|
76
76
|
merge(configData.library ??= {}, pkg, configData.library);
|
|
77
77
|
merge(configData.typescript ??= {}, tsConfig, configData.typescript);
|
|
78
|
+
// Parse `KEY=VALUE` style flags into Record<string, string>.
|
|
79
|
+
// - `--define`: VALUE is a JS expression (string literals must be
|
|
80
|
+
// pre-quoted by the caller, e.g. `'"1.2.3"'`).
|
|
81
|
+
// - `--alias`: VALUE is the substitute module specifier.
|
|
82
|
+
const parseKvPairs = (entries, flag) => {
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const idx = entry.indexOf('=');
|
|
86
|
+
if (idx === -1) {
|
|
87
|
+
throw new Error(`Invalid --${flag} value '${entry}'. Expected KEY=VALUE.`);
|
|
88
|
+
}
|
|
89
|
+
const key = entry.slice(0, idx).trim();
|
|
90
|
+
const value = entry.slice(idx + 1);
|
|
91
|
+
if (!key) {
|
|
92
|
+
throw new Error(`Invalid --${flag} value '${entry}'. Empty key.`);
|
|
93
|
+
}
|
|
94
|
+
out[key] = value;
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
};
|
|
98
|
+
const defineMap = parseKvPairs(cliArgs.define ?? [], 'define');
|
|
99
|
+
const aliasMap = parseKvPairs(cliArgs.alias ?? [], 'alias');
|
|
100
|
+
if (Object.keys(aliasMap).length) {
|
|
101
|
+
configData.aliases = { ...(configData.aliases ?? {}), ...aliasMap };
|
|
102
|
+
}
|
|
78
103
|
merge(configData.esbuild ??= {}, {
|
|
79
104
|
format: cliArgs.format,
|
|
80
105
|
minify: cliArgs.minify,
|
|
81
106
|
entryPoints: cliArgs.entryPoints,
|
|
82
107
|
outfile: cliArgs.outfile,
|
|
83
108
|
outdir: cliArgs.outdir,
|
|
84
|
-
logLevel: cliArgs.logLevel || 'warning'
|
|
109
|
+
logLevel: cliArgs.logLevel || 'warning',
|
|
110
|
+
...(cliArgs.external?.length ? { external: cliArgs.external } : {}),
|
|
111
|
+
...(Object.keys(defineMap).length ? { define: defineMap } : {}),
|
|
85
112
|
});
|
|
86
113
|
if (configData.verbose)
|
|
87
114
|
console.debug("configData", configData);
|
|
@@ -60,4 +60,33 @@ export interface CliBuildOptions {
|
|
|
60
60
|
* `--outfile`. Default: false.
|
|
61
61
|
*/
|
|
62
62
|
shebang?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Module names that should NOT be bundled. Each name remains as a literal
|
|
65
|
+
* `import`/`require` in the output and is resolved by the runtime against
|
|
66
|
+
* its own `node_modules` (or equivalent) at execution time.
|
|
67
|
+
*
|
|
68
|
+
* Repeat the flag or pass a comma-separated value:
|
|
69
|
+
* `--external typedoc,prettier --external typescript`. Glob-style wildcards
|
|
70
|
+
* (`@inquirer/*`, `lodash-*`) are forwarded as-is to esbuild.
|
|
71
|
+
*
|
|
72
|
+
* @see https://esbuild.github.io/api/#external
|
|
73
|
+
*/
|
|
74
|
+
external?: string[];
|
|
75
|
+
/**
|
|
76
|
+
* Substitute compile-time constants in the bundle. Each entry is a
|
|
77
|
+
* `KEY=VALUE` pair where `VALUE` is an arbitrary JS expression — string
|
|
78
|
+
* literals must be quoted (`--define VERSION='"1.2.3"'`). Useful for
|
|
79
|
+
* upstream packages that read a build-time constant via
|
|
80
|
+
* `typeof __FOO__ !== 'undefined'`.
|
|
81
|
+
*
|
|
82
|
+
* @see https://esbuild.github.io/api/#define
|
|
83
|
+
*/
|
|
84
|
+
define?: string[];
|
|
85
|
+
/**
|
|
86
|
+
* Map module specifiers to alternative targets at bundle time. Each entry
|
|
87
|
+
* is `FROM=TO` where `FROM` is the imported package name and `TO` is the
|
|
88
|
+
* substitute (typically `@gjsify/empty` to drop a heavy dep that the test
|
|
89
|
+
* scenario never executes). Layered on top of the built-in alias map.
|
|
90
|
+
*/
|
|
91
|
+
alias?: string[];
|
|
63
92
|
}
|
|
@@ -22,4 +22,9 @@ export interface ConfigData {
|
|
|
22
22
|
* Prepend GJS shebang to output and mark executable. See CliBuildOptions.
|
|
23
23
|
*/
|
|
24
24
|
shebang?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Extra module aliases layered on top of the built-in alias map.
|
|
27
|
+
* Comes from `gjsify build --alias FROM=TO`.
|
|
28
|
+
*/
|
|
29
|
+
aliases?: Record<string, string>;
|
|
25
30
|
}
|
|
@@ -5,11 +5,17 @@ export interface NativePackage {
|
|
|
5
5
|
prebuildsDir: string;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* Walk up the directory tree from `startDir`
|
|
9
|
-
*
|
|
8
|
+
* Walk up the directory tree from `startDir` and merge native packages found
|
|
9
|
+
* in every `node_modules` encountered.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* We keep walking past the first node_modules because yarn v4 / pnpm hoisting
|
|
12
|
+
* puts a project's direct deps in a local node_modules (often just `.cache/`
|
|
13
|
+
* or a subset) while hoisted transitive deps live in a root `node_modules`
|
|
14
|
+
* higher up. Node's own resolver also walks the chain — returning only the
|
|
15
|
+
* first hit would miss root-hoisted native packages.
|
|
16
|
+
*
|
|
17
|
+
* Deduplication: the first match for a given package name wins (closer
|
|
18
|
+
* node_modules shadows outer ones), matching Node.js resolution semantics.
|
|
13
19
|
*/
|
|
14
20
|
export declare function detectNativePackages(startDir: string): NativePackage[];
|
|
15
21
|
/**
|
|
@@ -85,27 +85,39 @@ function checkPackage(pkgDir, name, arch) {
|
|
|
85
85
|
return { name, prebuildsDir };
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
|
-
* Walk up the directory tree from `startDir`
|
|
89
|
-
*
|
|
88
|
+
* Walk up the directory tree from `startDir` and merge native packages found
|
|
89
|
+
* in every `node_modules` encountered.
|
|
90
90
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
91
|
+
* We keep walking past the first node_modules because yarn v4 / pnpm hoisting
|
|
92
|
+
* puts a project's direct deps in a local node_modules (often just `.cache/`
|
|
93
|
+
* or a subset) while hoisted transitive deps live in a root `node_modules`
|
|
94
|
+
* higher up. Node's own resolver also walks the chain — returning only the
|
|
95
|
+
* first hit would miss root-hoisted native packages.
|
|
96
|
+
*
|
|
97
|
+
* Deduplication: the first match for a given package name wins (closer
|
|
98
|
+
* node_modules shadows outer ones), matching Node.js resolution semantics.
|
|
93
99
|
*/
|
|
94
100
|
export function detectNativePackages(startDir) {
|
|
95
101
|
const arch = nodeArchToLinuxArch(process.arch);
|
|
102
|
+
const merged = [];
|
|
103
|
+
const seen = new Set();
|
|
96
104
|
let dir = resolve(startDir);
|
|
97
|
-
// Walk up to filesystem root
|
|
98
105
|
while (true) {
|
|
99
106
|
const nodeModulesDir = join(dir, 'node_modules');
|
|
100
107
|
if (existsSync(nodeModulesDir)) {
|
|
101
|
-
|
|
108
|
+
for (const pkg of scanNodeModules(nodeModulesDir, arch)) {
|
|
109
|
+
if (seen.has(pkg.name))
|
|
110
|
+
continue;
|
|
111
|
+
seen.add(pkg.name);
|
|
112
|
+
merged.push(pkg);
|
|
113
|
+
}
|
|
102
114
|
}
|
|
103
115
|
const parent = resolve(dir, '..');
|
|
104
116
|
if (parent === dir)
|
|
105
117
|
break; // reached filesystem root
|
|
106
118
|
dir = parent;
|
|
107
119
|
}
|
|
108
|
-
return
|
|
120
|
+
return merged;
|
|
109
121
|
}
|
|
110
122
|
/** Walk up from dir to find the nearest package.json. */
|
|
111
123
|
function findNearestPackageJson(startDir) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -23,24 +23,23 @@
|
|
|
23
23
|
"cli"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@gjsify/create-app": "^0.
|
|
27
|
-
"@gjsify/esbuild-plugin-gjsify": "^0.
|
|
28
|
-
"@gjsify/example-dom-
|
|
29
|
-
"@gjsify/example-dom-
|
|
30
|
-
"@gjsify/example-dom-
|
|
31
|
-
"@gjsify/example-dom-three-
|
|
32
|
-
"@gjsify/example-
|
|
33
|
-
"@gjsify/
|
|
34
|
-
"@gjsify/
|
|
35
|
-
"@gjsify/web-polyfills": "^0.1.13",
|
|
26
|
+
"@gjsify/create-app": "^0.2.0",
|
|
27
|
+
"@gjsify/esbuild-plugin-gjsify": "^0.2.0",
|
|
28
|
+
"@gjsify/example-dom-canvas2d-fireworks": "^0.2.0",
|
|
29
|
+
"@gjsify/example-dom-excalibur-jelly-jumper": "^0.2.0",
|
|
30
|
+
"@gjsify/example-dom-three-geometry-teapot": "^0.2.0",
|
|
31
|
+
"@gjsify/example-dom-three-postprocessing-pixel": "^0.2.0",
|
|
32
|
+
"@gjsify/example-node-express-webserver": "^0.2.0",
|
|
33
|
+
"@gjsify/node-polyfills": "^0.2.0",
|
|
34
|
+
"@gjsify/web-polyfills": "^0.2.0",
|
|
36
35
|
"cosmiconfig": "^9.0.1",
|
|
37
36
|
"esbuild": "^0.28.0",
|
|
38
37
|
"get-tsconfig": "^4.14.0",
|
|
39
|
-
"pkg-types": "^2.3.
|
|
38
|
+
"pkg-types": "^2.3.1",
|
|
40
39
|
"yargs": "^18.0.0"
|
|
41
40
|
},
|
|
42
41
|
"devDependencies": {
|
|
43
42
|
"@types/yargs": "^17.0.35",
|
|
44
|
-
"typescript": "^6.0.
|
|
43
|
+
"typescript": "^6.0.3"
|
|
45
44
|
}
|
|
46
45
|
}
|
package/src/actions/build.ts
CHANGED
|
@@ -149,7 +149,7 @@ export class BuildAction {
|
|
|
149
149
|
/** Application mode */
|
|
150
150
|
async buildApp(app: App = 'gjs') {
|
|
151
151
|
|
|
152
|
-
const { verbose, esbuild, typescript, exclude, library: pgk } = this.configData;
|
|
152
|
+
const { verbose, esbuild, typescript, exclude, library: pgk, aliases } = this.configData;
|
|
153
153
|
|
|
154
154
|
const format: 'esm' | 'cjs' = (esbuild?.format as 'esm' | 'cjs') ?? (esbuild?.outfile?.endsWith('.cjs') ? 'cjs' : 'esm');
|
|
155
155
|
|
|
@@ -167,6 +167,7 @@ export class BuildAction {
|
|
|
167
167
|
exclude,
|
|
168
168
|
reflection: typescript?.reflection,
|
|
169
169
|
consoleShim,
|
|
170
|
+
...(aliases ? { aliases } : {}),
|
|
170
171
|
};
|
|
171
172
|
|
|
172
173
|
const { autoMode, extras } = this.parseGlobalsValue(globals);
|
package/src/commands/build.ts
CHANGED
|
@@ -100,6 +100,26 @@ export const buildCommand: Command<any, CliBuildOptions> = {
|
|
|
100
100
|
normalize: true,
|
|
101
101
|
default: false
|
|
102
102
|
})
|
|
103
|
+
.option('external', {
|
|
104
|
+
description: "Module names that should NOT be bundled. Repeat the flag or pass a comma-separated list (e.g. --external typedoc,prettier). Globs are forwarded to esbuild as-is. See https://esbuild.github.io/api/#external",
|
|
105
|
+
array: true,
|
|
106
|
+
type: 'string',
|
|
107
|
+
default: [] as string[],
|
|
108
|
+
coerce: (arg: string[]) => arg.flatMap((v) => v.split(',').map((s) => s.trim()).filter(Boolean)),
|
|
109
|
+
})
|
|
110
|
+
.option('define', {
|
|
111
|
+
description: "Substitute compile-time constants. Each entry is KEY=VALUE where VALUE is a JS expression (string literals must be quoted: --define VERSION='\"1.2.3\"'). Repeat the flag or pass comma-separated. See https://esbuild.github.io/api/#define",
|
|
112
|
+
array: true,
|
|
113
|
+
type: 'string',
|
|
114
|
+
default: [] as string[],
|
|
115
|
+
})
|
|
116
|
+
.option('alias', {
|
|
117
|
+
description: "Map module specifiers at bundle time. Each entry is FROM=TO (e.g. --alias typedoc=@gjsify/empty). Layered on top of the built-in alias map. Useful for stubbing heavy deps the test scenario never executes.",
|
|
118
|
+
array: true,
|
|
119
|
+
type: 'string',
|
|
120
|
+
default: [] as string[],
|
|
121
|
+
coerce: (arg: string[]) => arg.flatMap((v) => v.split(',').map((s) => s.trim()).filter(Boolean)),
|
|
122
|
+
})
|
|
103
123
|
},
|
|
104
124
|
handler: async (args) => {
|
|
105
125
|
const config = new Config();
|
package/src/config.ts
CHANGED
|
@@ -87,13 +87,42 @@ export class Config {
|
|
|
87
87
|
|
|
88
88
|
merge(configData.library ??= {}, pkg, configData.library);
|
|
89
89
|
merge(configData.typescript ??= {}, tsConfig, configData.typescript);
|
|
90
|
+
|
|
91
|
+
// Parse `KEY=VALUE` style flags into Record<string, string>.
|
|
92
|
+
// - `--define`: VALUE is a JS expression (string literals must be
|
|
93
|
+
// pre-quoted by the caller, e.g. `'"1.2.3"'`).
|
|
94
|
+
// - `--alias`: VALUE is the substitute module specifier.
|
|
95
|
+
const parseKvPairs = (entries: readonly string[], flag: string): Record<string, string> => {
|
|
96
|
+
const out: Record<string, string> = {};
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const idx = entry.indexOf('=');
|
|
99
|
+
if (idx === -1) {
|
|
100
|
+
throw new Error(`Invalid --${flag} value '${entry}'. Expected KEY=VALUE.`);
|
|
101
|
+
}
|
|
102
|
+
const key = entry.slice(0, idx).trim();
|
|
103
|
+
const value = entry.slice(idx + 1);
|
|
104
|
+
if (!key) {
|
|
105
|
+
throw new Error(`Invalid --${flag} value '${entry}'. Empty key.`);
|
|
106
|
+
}
|
|
107
|
+
out[key] = value;
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
};
|
|
111
|
+
const defineMap = parseKvPairs(cliArgs.define ?? [], 'define');
|
|
112
|
+
const aliasMap = parseKvPairs(cliArgs.alias ?? [], 'alias');
|
|
113
|
+
if (Object.keys(aliasMap).length) {
|
|
114
|
+
configData.aliases = { ...(configData.aliases ?? {}), ...aliasMap };
|
|
115
|
+
}
|
|
116
|
+
|
|
90
117
|
merge(configData.esbuild ??= {}, {
|
|
91
118
|
format: cliArgs.format,
|
|
92
119
|
minify: cliArgs.minify,
|
|
93
120
|
entryPoints: cliArgs.entryPoints,
|
|
94
121
|
outfile: cliArgs.outfile,
|
|
95
122
|
outdir: cliArgs.outdir,
|
|
96
|
-
logLevel: cliArgs.logLevel || 'warning'
|
|
123
|
+
logLevel: cliArgs.logLevel || 'warning',
|
|
124
|
+
...(cliArgs.external?.length ? { external: cliArgs.external } : {}),
|
|
125
|
+
...(Object.keys(defineMap).length ? { define: defineMap } : {}),
|
|
97
126
|
});
|
|
98
127
|
|
|
99
128
|
if(configData.verbose) console.debug("configData", configData);
|
|
@@ -61,4 +61,33 @@ export interface CliBuildOptions {
|
|
|
61
61
|
* `--outfile`. Default: false.
|
|
62
62
|
*/
|
|
63
63
|
shebang?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Module names that should NOT be bundled. Each name remains as a literal
|
|
66
|
+
* `import`/`require` in the output and is resolved by the runtime against
|
|
67
|
+
* its own `node_modules` (or equivalent) at execution time.
|
|
68
|
+
*
|
|
69
|
+
* Repeat the flag or pass a comma-separated value:
|
|
70
|
+
* `--external typedoc,prettier --external typescript`. Glob-style wildcards
|
|
71
|
+
* (`@inquirer/*`, `lodash-*`) are forwarded as-is to esbuild.
|
|
72
|
+
*
|
|
73
|
+
* @see https://esbuild.github.io/api/#external
|
|
74
|
+
*/
|
|
75
|
+
external?: string[];
|
|
76
|
+
/**
|
|
77
|
+
* Substitute compile-time constants in the bundle. Each entry is a
|
|
78
|
+
* `KEY=VALUE` pair where `VALUE` is an arbitrary JS expression — string
|
|
79
|
+
* literals must be quoted (`--define VERSION='"1.2.3"'`). Useful for
|
|
80
|
+
* upstream packages that read a build-time constant via
|
|
81
|
+
* `typeof __FOO__ !== 'undefined'`.
|
|
82
|
+
*
|
|
83
|
+
* @see https://esbuild.github.io/api/#define
|
|
84
|
+
*/
|
|
85
|
+
define?: string[];
|
|
86
|
+
/**
|
|
87
|
+
* Map module specifiers to alternative targets at bundle time. Each entry
|
|
88
|
+
* is `FROM=TO` where `FROM` is the imported package name and `TO` is the
|
|
89
|
+
* substitute (typically `@gjsify/empty` to drop a heavy dep that the test
|
|
90
|
+
* scenario never executes). Layered on top of the built-in alias map.
|
|
91
|
+
*/
|
|
92
|
+
alias?: string[];
|
|
64
93
|
}
|
package/src/types/config-data.ts
CHANGED
|
@@ -23,4 +23,9 @@ export interface ConfigData {
|
|
|
23
23
|
* Prepend GJS shebang to output and mark executable. See CliBuildOptions.
|
|
24
24
|
*/
|
|
25
25
|
shebang?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Extra module aliases layered on top of the built-in alias map.
|
|
28
|
+
* Comes from `gjsify build --alias FROM=TO`.
|
|
29
|
+
*/
|
|
30
|
+
aliases?: Record<string, string>;
|
|
26
31
|
}
|
|
@@ -94,28 +94,39 @@ function checkPackage(pkgDir: string, name: string, arch: string): NativePackage
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
|
-
* Walk up the directory tree from `startDir`
|
|
98
|
-
*
|
|
97
|
+
* Walk up the directory tree from `startDir` and merge native packages found
|
|
98
|
+
* in every `node_modules` encountered.
|
|
99
99
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
100
|
+
* We keep walking past the first node_modules because yarn v4 / pnpm hoisting
|
|
101
|
+
* puts a project's direct deps in a local node_modules (often just `.cache/`
|
|
102
|
+
* or a subset) while hoisted transitive deps live in a root `node_modules`
|
|
103
|
+
* higher up. Node's own resolver also walks the chain — returning only the
|
|
104
|
+
* first hit would miss root-hoisted native packages.
|
|
105
|
+
*
|
|
106
|
+
* Deduplication: the first match for a given package name wins (closer
|
|
107
|
+
* node_modules shadows outer ones), matching Node.js resolution semantics.
|
|
102
108
|
*/
|
|
103
109
|
export function detectNativePackages(startDir: string): NativePackage[] {
|
|
104
110
|
const arch = nodeArchToLinuxArch(process.arch);
|
|
111
|
+
const merged: NativePackage[] = [];
|
|
112
|
+
const seen = new Set<string>();
|
|
105
113
|
let dir = resolve(startDir);
|
|
106
114
|
|
|
107
|
-
// Walk up to filesystem root
|
|
108
115
|
while (true) {
|
|
109
116
|
const nodeModulesDir = join(dir, 'node_modules');
|
|
110
117
|
if (existsSync(nodeModulesDir)) {
|
|
111
|
-
|
|
118
|
+
for (const pkg of scanNodeModules(nodeModulesDir, arch)) {
|
|
119
|
+
if (seen.has(pkg.name)) continue;
|
|
120
|
+
seen.add(pkg.name);
|
|
121
|
+
merged.push(pkg);
|
|
122
|
+
}
|
|
112
123
|
}
|
|
113
124
|
const parent = resolve(dir, '..');
|
|
114
125
|
if (parent === dir) break; // reached filesystem root
|
|
115
126
|
dir = parent;
|
|
116
127
|
}
|
|
117
128
|
|
|
118
|
-
return
|
|
129
|
+
return merged;
|
|
119
130
|
}
|
|
120
131
|
|
|
121
132
|
/** Walk up from dir to find the nearest package.json. */
|