@gjsify/cli 0.3.21 → 0.4.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/dist/cli.gjs.mjs +798 -0
- package/lib/actions/build.js +4 -17
- package/lib/bundler-pick.d.ts +79 -0
- package/lib/bundler-pick.js +428 -0
- package/lib/commands/foreach.d.ts +16 -0
- package/lib/commands/foreach.js +268 -0
- package/lib/commands/index.d.ts +2 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/install.d.ts +1 -0
- package/lib/commands/install.js +222 -26
- package/lib/commands/run.d.ts +1 -1
- package/lib/commands/run.js +133 -20
- package/lib/commands/workspace.d.ts +8 -0
- package/lib/commands/workspace.js +69 -0
- package/lib/config.js +12 -1
- package/lib/index.js +11 -3
- package/lib/types/config-data.d.ts +10 -1
- package/lib/utils/install-backend-native.d.ts +5 -1
- package/lib/utils/install-backend-native.js +88 -11
- package/lib/utils/install-backend.d.ts +11 -1
- package/lib/utils/install-backend.js +4 -2
- package/lib/utils/pkg-json-edit.d.ts +47 -0
- package/lib/utils/pkg-json-edit.js +108 -0
- package/package.json +36 -12
- package/src/actions/build.ts +0 -431
- package/src/actions/index.ts +0 -1
- package/src/commands/build.ts +0 -146
- package/src/commands/check.ts +0 -87
- package/src/commands/create.ts +0 -63
- package/src/commands/dlx.ts +0 -195
- package/src/commands/flatpak/build.ts +0 -225
- package/src/commands/flatpak/ci.ts +0 -173
- package/src/commands/flatpak/deps.ts +0 -120
- package/src/commands/flatpak/index.ts +0 -53
- package/src/commands/flatpak/init.ts +0 -191
- package/src/commands/flatpak/utils.ts +0 -76
- package/src/commands/gettext.ts +0 -258
- package/src/commands/gresource.ts +0 -97
- package/src/commands/gsettings.ts +0 -87
- package/src/commands/index.ts +0 -12
- package/src/commands/info.ts +0 -70
- package/src/commands/install.ts +0 -195
- package/src/commands/run.ts +0 -33
- package/src/commands/showcase.ts +0 -149
- package/src/config.ts +0 -304
- package/src/constants.ts +0 -1
- package/src/index.ts +0 -37
- package/src/types/cli-build-options.ts +0 -100
- package/src/types/command.ts +0 -10
- package/src/types/config-data-library.ts +0 -5
- package/src/types/config-data-typescript.ts +0 -6
- package/src/types/config-data.ts +0 -225
- package/src/types/cosmiconfig-result.ts +0 -5
- package/src/types/index.ts +0 -6
- package/src/utils/check-system-deps.ts +0 -480
- package/src/utils/detect-native-packages.ts +0 -153
- package/src/utils/discover-showcases.ts +0 -75
- package/src/utils/dlx-cache.ts +0 -135
- package/src/utils/install-backend-native.ts +0 -363
- package/src/utils/install-backend.ts +0 -88
- package/src/utils/install-global.ts +0 -182
- package/src/utils/normalize-bundler-options.ts +0 -129
- package/src/utils/parse-spec.ts +0 -48
- package/src/utils/resolve-gjs-entry.ts +0 -96
- package/src/utils/resolve-plugin-by-name.ts +0 -106
- package/src/utils/run-gjs.ts +0 -90
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// `gjsify foreach [flags] <script>` — yarn-workspaces-foreach replacement.
|
|
2
|
+
//
|
|
3
|
+
// Replaces every `yarn workspaces foreach -A -p --no-private --exclude
|
|
4
|
+
// '@girs/*' --topological run build` style invocation in monorepo
|
|
5
|
+
// scripts. Flags mirror yarn 4's shape so root package.json scripts can
|
|
6
|
+
// move over with a 1:1 substitution.
|
|
7
|
+
//
|
|
8
|
+
// Output is line-prefixed `[<workspace-name>]` when --parallel is set,
|
|
9
|
+
// matching yarn's interactive flow. Exit code is non-zero if any child
|
|
10
|
+
// process failed; first failure's stderr is forwarded.
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { cpus } from 'node:os';
|
|
13
|
+
import { buildDependencyGraph, discoverWorkspaces, filterWorkspaces, topologicalSort, } from '@gjsify/workspace';
|
|
14
|
+
export const foreachCommand = {
|
|
15
|
+
command: 'foreach <script> [args..]',
|
|
16
|
+
description: 'Run a workspace script across all (or filtered) workspaces. Drop-in for `yarn workspaces foreach`: -A/--all, -p/--parallel, -t/--topological, --include, --exclude, --no-private.',
|
|
17
|
+
builder: (yargs) => yargs
|
|
18
|
+
.positional('script', {
|
|
19
|
+
description: 'Script name to run in each workspace (`run <name>`-equivalent).',
|
|
20
|
+
type: 'string',
|
|
21
|
+
demandOption: true,
|
|
22
|
+
})
|
|
23
|
+
.positional('args', {
|
|
24
|
+
description: 'Extra arguments forwarded to each child invocation.',
|
|
25
|
+
type: 'string',
|
|
26
|
+
array: true,
|
|
27
|
+
})
|
|
28
|
+
.option('all', {
|
|
29
|
+
description: 'Include workspaces declared as `private: true`.',
|
|
30
|
+
type: 'boolean',
|
|
31
|
+
alias: 'A',
|
|
32
|
+
default: false,
|
|
33
|
+
})
|
|
34
|
+
.option('parallel', {
|
|
35
|
+
description: 'Run workspaces in parallel (capped by --jobs).',
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
alias: 'p',
|
|
38
|
+
default: false,
|
|
39
|
+
})
|
|
40
|
+
.option('topological', {
|
|
41
|
+
description: 'Wait for each workspace\'s deps to finish before starting it (production deps only).',
|
|
42
|
+
type: 'boolean',
|
|
43
|
+
alias: 't',
|
|
44
|
+
default: false,
|
|
45
|
+
})
|
|
46
|
+
.option('topological-dev', {
|
|
47
|
+
description: 'Like --topological but also respects devDependencies (often cyclic — use sparingly).',
|
|
48
|
+
type: 'boolean',
|
|
49
|
+
default: false,
|
|
50
|
+
})
|
|
51
|
+
.option('include', {
|
|
52
|
+
description: 'Glob pattern to include workspaces by name (repeatable).',
|
|
53
|
+
type: 'string',
|
|
54
|
+
array: true,
|
|
55
|
+
})
|
|
56
|
+
.option('exclude', {
|
|
57
|
+
description: 'Glob pattern to exclude workspaces by name (repeatable).',
|
|
58
|
+
type: 'string',
|
|
59
|
+
array: true,
|
|
60
|
+
})
|
|
61
|
+
.option('private', {
|
|
62
|
+
// Yargs auto-negates `--no-private` to `private=false`, so the
|
|
63
|
+
// user-facing flag stays `--no-private` (yarn-compatible).
|
|
64
|
+
description: 'Include private workspaces (default true). Pass --no-private to skip them.',
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
default: true,
|
|
67
|
+
})
|
|
68
|
+
.option('verbose', {
|
|
69
|
+
description: 'Echo every spawned command before running it.',
|
|
70
|
+
type: 'boolean',
|
|
71
|
+
alias: 'v',
|
|
72
|
+
default: false,
|
|
73
|
+
})
|
|
74
|
+
.option('jobs', {
|
|
75
|
+
description: 'Maximum concurrent workspaces in --parallel mode (default: cpu count).',
|
|
76
|
+
type: 'number',
|
|
77
|
+
alias: 'j',
|
|
78
|
+
}),
|
|
79
|
+
handler: async (args) => {
|
|
80
|
+
const cwd = process.cwd();
|
|
81
|
+
const allWorkspaces = discoverWorkspaces(cwd);
|
|
82
|
+
let selected = filterWorkspaces(allWorkspaces, {
|
|
83
|
+
include: args.include,
|
|
84
|
+
exclude: args.exclude,
|
|
85
|
+
noPrivate: args.private === false,
|
|
86
|
+
});
|
|
87
|
+
// Only run on workspaces that actually have the requested script —
|
|
88
|
+
// yarn does this too, otherwise every project that doesn't declare
|
|
89
|
+
// `<script>` would fail and force the user to `--exclude` it.
|
|
90
|
+
selected = selected.filter((ws) => {
|
|
91
|
+
const scripts = ws.manifest.scripts ?? {};
|
|
92
|
+
return typeof scripts[args.script] === 'string';
|
|
93
|
+
});
|
|
94
|
+
if (selected.length === 0) {
|
|
95
|
+
console.log(`gjsify foreach: no workspaces match (script="${args.script}", include=${JSON.stringify(args.include ?? [])}, exclude=${JSON.stringify(args.exclude ?? [])})`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (args.topological || args['topological-dev']) {
|
|
99
|
+
const graph = buildDependencyGraph(selected, {
|
|
100
|
+
includeDev: args['topological-dev'] === true,
|
|
101
|
+
});
|
|
102
|
+
selected = topologicalSort(graph);
|
|
103
|
+
}
|
|
104
|
+
const cmd = args.script;
|
|
105
|
+
const cmdArgs = args.args ?? [];
|
|
106
|
+
const verbose = args.verbose === true;
|
|
107
|
+
if (args.parallel && !args.topological && !args['topological-dev']) {
|
|
108
|
+
const jobs = args.jobs && args.jobs > 0 ? args.jobs : cpus().length;
|
|
109
|
+
await runParallel(selected, cmd, cmdArgs, jobs, verbose);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (args.parallel) {
|
|
113
|
+
// Topological + parallel: each workspace starts as soon as its
|
|
114
|
+
// deps (in the selected set) have finished. Yarn calls this
|
|
115
|
+
// "topological order with concurrency"; we cap at --jobs.
|
|
116
|
+
const jobs = args.jobs && args.jobs > 0 ? args.jobs : cpus().length;
|
|
117
|
+
await runTopologicalParallel(selected, cmd, cmdArgs, jobs, verbose, args['topological-dev'] === true);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
await runSequential(selected, cmd, cmdArgs, verbose);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
async function runSequential(workspaces, script, args, verbose) {
|
|
124
|
+
for (const ws of workspaces) {
|
|
125
|
+
await runOne(ws, script, args, /* prefixOutput */ false, verbose);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function runParallel(workspaces, script, args, concurrency, verbose) {
|
|
129
|
+
let cursor = 0;
|
|
130
|
+
const workers = [];
|
|
131
|
+
for (let w = 0; w < concurrency; w++) {
|
|
132
|
+
workers.push((async () => {
|
|
133
|
+
while (cursor < workspaces.length) {
|
|
134
|
+
const i = cursor++;
|
|
135
|
+
await runOne(workspaces[i], script, args, /* prefixOutput */ true, verbose);
|
|
136
|
+
}
|
|
137
|
+
})());
|
|
138
|
+
}
|
|
139
|
+
await Promise.all(workers);
|
|
140
|
+
}
|
|
141
|
+
async function runTopologicalParallel(workspaces, script, args, concurrency, verbose, includeDev) {
|
|
142
|
+
const selectedNames = new Set(workspaces.map((w) => w.name));
|
|
143
|
+
const remaining = new Map();
|
|
144
|
+
for (const ws of workspaces) {
|
|
145
|
+
const wsDeps = new Set();
|
|
146
|
+
const m = ws.manifest;
|
|
147
|
+
for (const block of [
|
|
148
|
+
m.dependencies,
|
|
149
|
+
includeDev ? m.devDependencies : undefined,
|
|
150
|
+
m.optionalDependencies,
|
|
151
|
+
]) {
|
|
152
|
+
if (!block)
|
|
153
|
+
continue;
|
|
154
|
+
for (const [name, spec] of Object.entries(block)) {
|
|
155
|
+
if (typeof spec !== 'string')
|
|
156
|
+
continue;
|
|
157
|
+
if (!spec.startsWith('workspace:'))
|
|
158
|
+
continue;
|
|
159
|
+
if (selectedNames.has(name))
|
|
160
|
+
wsDeps.add(name);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
remaining.set(ws.name, wsDeps);
|
|
164
|
+
}
|
|
165
|
+
const byName = new Map(workspaces.map((w) => [w.name, w]));
|
|
166
|
+
const done = new Set();
|
|
167
|
+
let inflight = 0;
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
let error = null;
|
|
170
|
+
const pump = () => {
|
|
171
|
+
if (error)
|
|
172
|
+
return;
|
|
173
|
+
while (inflight < concurrency) {
|
|
174
|
+
const ready = [...remaining.entries()]
|
|
175
|
+
.filter(([, deps]) => [...deps].every((d) => done.has(d)))
|
|
176
|
+
.map(([n]) => n);
|
|
177
|
+
if (ready.length === 0)
|
|
178
|
+
break;
|
|
179
|
+
const next = ready.sort()[0];
|
|
180
|
+
remaining.delete(next);
|
|
181
|
+
inflight++;
|
|
182
|
+
runOne(byName.get(next), script, args, /* prefixOutput */ true, verbose)
|
|
183
|
+
.then(() => {
|
|
184
|
+
inflight--;
|
|
185
|
+
done.add(next);
|
|
186
|
+
if (remaining.size === 0 && inflight === 0) {
|
|
187
|
+
resolve();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
pump();
|
|
191
|
+
})
|
|
192
|
+
.catch((e) => {
|
|
193
|
+
error = e instanceof Error ? e : new Error(String(e));
|
|
194
|
+
// Wait for in-flight tasks to finish (yarn does the
|
|
195
|
+
// same — surfaces all errors instead of abruptly
|
|
196
|
+
// killing siblings).
|
|
197
|
+
if (inflight === 0)
|
|
198
|
+
reject(error);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (remaining.size > 0 && inflight === 0 && !error) {
|
|
202
|
+
reject(new Error(`gjsify foreach --topological: stuck — workspaces ${[...remaining.keys()].join(', ')} have unsatisfied deps in the selected set`));
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
pump();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async function runOne(ws, script, args, prefixOutput, verbose) {
|
|
209
|
+
// Use the same package manager that invoked us — yarn under yarn,
|
|
210
|
+
// npm under npm, gjsify under gjsify. Default to `npm` for portability
|
|
211
|
+
// when nothing is detectable; the script-runner (D.5) will replace
|
|
212
|
+
// this once `gjsify run` ships.
|
|
213
|
+
const runner = detectPackageManager();
|
|
214
|
+
const argv = runner === 'gjsify'
|
|
215
|
+
? ['run', script, ...args]
|
|
216
|
+
: ['run', script, ...(args.length > 0 ? ['--', ...args] : [])];
|
|
217
|
+
if (verbose) {
|
|
218
|
+
console.error(`[${ws.name}] $ ${runner} ${argv.join(' ')}`);
|
|
219
|
+
}
|
|
220
|
+
await spawnPrefixed(runner, argv, ws.location, prefixOutput ? `[${ws.name}] ` : null);
|
|
221
|
+
}
|
|
222
|
+
function detectPackageManager() {
|
|
223
|
+
// `npm_config_user_agent` is set by npm/yarn/pnpm — first token is
|
|
224
|
+
// `<name>/<version>`. Reuse it so `gjsify foreach build` invoked
|
|
225
|
+
// through `yarn run` keeps using yarn, etc.
|
|
226
|
+
const ua = process.env.npm_config_user_agent ?? '';
|
|
227
|
+
if (ua.startsWith('yarn/'))
|
|
228
|
+
return 'yarn';
|
|
229
|
+
if (ua.startsWith('gjsify/'))
|
|
230
|
+
return 'gjsify';
|
|
231
|
+
return 'npm';
|
|
232
|
+
}
|
|
233
|
+
function spawnPrefixed(cmd, args, cwd, prefix) {
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const child = spawn(cmd, args, {
|
|
236
|
+
cwd,
|
|
237
|
+
stdio: prefix ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
238
|
+
env: process.env,
|
|
239
|
+
});
|
|
240
|
+
if (prefix && child.stdout && child.stderr) {
|
|
241
|
+
prefixLines(child.stdout, process.stdout, prefix);
|
|
242
|
+
prefixLines(child.stderr, process.stderr, prefix);
|
|
243
|
+
}
|
|
244
|
+
child.on('close', (code) => {
|
|
245
|
+
if (code === 0)
|
|
246
|
+
resolve();
|
|
247
|
+
else
|
|
248
|
+
reject(new Error(`${cmd} ${args.join(' ')} exited with code ${code}`));
|
|
249
|
+
});
|
|
250
|
+
child.on('error', (err) => reject(err));
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function prefixLines(src, sink, prefix) {
|
|
254
|
+
let buf = '';
|
|
255
|
+
src.setEncoding('utf-8');
|
|
256
|
+
src.on('data', (chunk) => {
|
|
257
|
+
buf += chunk;
|
|
258
|
+
let idx;
|
|
259
|
+
while ((idx = buf.indexOf('\n')) !== -1) {
|
|
260
|
+
sink.write(prefix + buf.slice(0, idx + 1));
|
|
261
|
+
buf = buf.slice(idx + 1);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
src.on('end', () => {
|
|
265
|
+
if (buf.length > 0)
|
|
266
|
+
sink.write(prefix + buf + '\n');
|
|
267
|
+
});
|
|
268
|
+
}
|
package/lib/commands/index.d.ts
CHANGED
package/lib/commands/index.js
CHANGED
package/lib/commands/install.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
// `gjsify install [pkg...]` — install packages with gjsify-aware post-checks.
|
|
2
2
|
//
|
|
3
3
|
// Modes:
|
|
4
|
-
// gjsify install → project install (
|
|
5
|
-
// gjsify install <pkg> [<pkg>...] → add package(s) to project (
|
|
4
|
+
// gjsify install → project install (native, reads pkg.json)
|
|
5
|
+
// gjsify install <pkg> [<pkg>...] → add package(s) to project (native)
|
|
6
6
|
// gjsify install -g <pkg> [...] → user-global install (XDG, GJS-runnable bin)
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// All three modes route through `@gjsify/{semver,npm-registry,tar}` via
|
|
9
|
+
// `installPackagesNative` — no Node/npm required at runtime. Set
|
|
10
|
+
// `GJSIFY_INSTALL_BACKEND=npm` to opt back into the legacy `npm install`
|
|
11
|
+
// subprocess flow (useful as escape-hatch for projects that hit a
|
|
12
|
+
// missing native-backend feature).
|
|
11
13
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// extracted package, so package-relative assets like `@ts-for-gir/cli`'s
|
|
18
|
-
// `dist-templates/` are found by ordinary `__dirname/..` resolution — no
|
|
19
|
-
// embedded asset stores, no separate release tarballs.
|
|
14
|
+
// Workspace-aware install (`gjsify install` in a monorepo root with a
|
|
15
|
+
// `"workspaces"` field) is Phase D.3 — for now we detect and surface a
|
|
16
|
+
// clear error pointing at the in-progress work.
|
|
17
|
+
import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from 'node:fs';
|
|
18
|
+
import { dirname, join, relative } from 'node:path';
|
|
20
19
|
import { spawn } from 'node:child_process';
|
|
21
|
-
import {
|
|
20
|
+
import { discoverWorkspaces } from '@gjsify/workspace';
|
|
22
21
|
import { buildInstallCommand, detectPackageManager, runMinimalChecks, } from '../utils/check-system-deps.js';
|
|
23
22
|
import { detectNativePackages } from '../utils/detect-native-packages.js';
|
|
24
23
|
import { installPackages } from '../utils/install-backend.js';
|
|
25
24
|
import { binDirOnPath, defaultGlobalLayout, linkGlobalBins, specToPackageName, } from '../utils/install-global.js';
|
|
25
|
+
import { addDependencyEntry, defaultRangeFromVersion, parseSpec, projectSpecsFromPackageJson, readPackageJson, writePackageJson, } from '../utils/pkg-json-edit.js';
|
|
26
26
|
export const installCommand = {
|
|
27
27
|
command: 'install [packages..]',
|
|
28
28
|
description: 'Install npm dependencies in the current project (or globally with -g), then run gjsify-aware post-checks.',
|
|
@@ -41,12 +41,31 @@ export const installCommand = {
|
|
|
41
41
|
.option('save-dev', { type: 'boolean', alias: 'D' })
|
|
42
42
|
.option('save-peer', { type: 'boolean' })
|
|
43
43
|
.option('save-optional', { type: 'boolean', alias: 'O' })
|
|
44
|
+
.option('immutable', {
|
|
45
|
+
description: 'CI mode: install strictly from gjsify-lock.json, fail if the lockfile is missing or stale. Equivalent to yarn --immutable / npm ci --frozen-lockfile.',
|
|
46
|
+
type: 'boolean',
|
|
47
|
+
default: false,
|
|
48
|
+
})
|
|
44
49
|
.option('verbose', {
|
|
45
50
|
description: 'Verbose install logging.',
|
|
46
51
|
type: 'boolean',
|
|
47
52
|
default: false,
|
|
48
53
|
}),
|
|
49
54
|
handler: async (args) => {
|
|
55
|
+
// --immutable is incompatible with explicit `<pkg>` adds and with
|
|
56
|
+
// `--global` (which has no lockfile concept). Matches yarn's
|
|
57
|
+
// behavior: `yarn add --immutable` is a hard error.
|
|
58
|
+
if (args.immutable) {
|
|
59
|
+
if (args.packages && args.packages.length > 0) {
|
|
60
|
+
console.error('gjsify install --immutable does not accept package arguments. ' +
|
|
61
|
+
'Remove the package names or drop --immutable.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
if (args.global) {
|
|
65
|
+
console.error('gjsify install --immutable is incompatible with --global.');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
50
69
|
if (args.global) {
|
|
51
70
|
if (!args.packages || args.packages.length === 0) {
|
|
52
71
|
console.error('gjsify install --global requires at least one <pkg> argument.');
|
|
@@ -60,22 +79,199 @@ export const installCommand = {
|
|
|
60
79
|
await installGlobalAndLink(args.packages, { verbose: args.verbose });
|
|
61
80
|
return;
|
|
62
81
|
}
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (args['save-optional'])
|
|
69
|
-
npmArgs.push('--save-optional');
|
|
70
|
-
if (args.verbose)
|
|
71
|
-
npmArgs.push('--loglevel', 'verbose');
|
|
72
|
-
if (args.packages && args.packages.length > 0) {
|
|
73
|
-
npmArgs.push(...args.packages);
|
|
82
|
+
// Escape-hatch: legacy npm subprocess flow.
|
|
83
|
+
if (process.env.GJSIFY_INSTALL_BACKEND === 'npm') {
|
|
84
|
+
await projectInstallViaNpm(args);
|
|
85
|
+
await runPostInstallChecks();
|
|
86
|
+
return;
|
|
74
87
|
}
|
|
75
|
-
await
|
|
88
|
+
await projectInstallNative(args);
|
|
76
89
|
await runPostInstallChecks();
|
|
77
90
|
},
|
|
78
91
|
};
|
|
92
|
+
function isWorkspaceRoot(cwd) {
|
|
93
|
+
const pkgPath = join(cwd, 'package.json');
|
|
94
|
+
const pkg = readPackageJson(pkgPath);
|
|
95
|
+
if (!pkg)
|
|
96
|
+
return false;
|
|
97
|
+
return pkg.workspaces !== undefined;
|
|
98
|
+
}
|
|
99
|
+
function depKindFromArgs(args) {
|
|
100
|
+
if (args['save-dev'])
|
|
101
|
+
return 'devDependencies';
|
|
102
|
+
if (args['save-peer'])
|
|
103
|
+
return 'peerDependencies';
|
|
104
|
+
if (args['save-optional'])
|
|
105
|
+
return 'optionalDependencies';
|
|
106
|
+
return 'dependencies';
|
|
107
|
+
}
|
|
108
|
+
async function projectInstallNative(args) {
|
|
109
|
+
const cwd = process.cwd();
|
|
110
|
+
const pkgPath = join(cwd, 'package.json');
|
|
111
|
+
// Yarn-Berry / PnP detection: fall back to yarn with a clear warning
|
|
112
|
+
// rather than producing a half-working node_modules tree.
|
|
113
|
+
if (existsSync(join(cwd, '.pnp.cjs')) || existsSync(join(cwd, '.pnp.loader.mjs'))) {
|
|
114
|
+
throw new Error('gjsify install: detected Yarn PnP (.pnp.cjs) — native install is ' +
|
|
115
|
+
'not PnP-aware yet. Use `yarn install` or set ' +
|
|
116
|
+
'GJSIFY_INSTALL_BACKEND=npm.');
|
|
117
|
+
}
|
|
118
|
+
// Workspace install (no args, root pkg.json has `workspaces`) — Phase D.3.
|
|
119
|
+
// Project-local `gjsify install <pkg>` inside a workspace child still
|
|
120
|
+
// goes through the single-package code path below (this branch only
|
|
121
|
+
// fires for the root no-args case, which is the `yarn install`
|
|
122
|
+
// equivalent).
|
|
123
|
+
if ((!args.packages || args.packages.length === 0) && isWorkspaceRoot(cwd)) {
|
|
124
|
+
await workspaceInstall(cwd, args);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
let specs;
|
|
128
|
+
const pkg = readPackageJson(pkgPath);
|
|
129
|
+
const existingSpecs = pkg ? projectSpecsFromPackageJson(pkg) : [];
|
|
130
|
+
if (args.packages && args.packages.length > 0) {
|
|
131
|
+
// Combine new specs with existing manifest deps so a single
|
|
132
|
+
// `gjsify install <new>` doesn't churn the lockfile (would drop
|
|
133
|
+
// every previously-pinned entry otherwise). New specs with the
|
|
134
|
+
// same name as an existing dep override.
|
|
135
|
+
const newNames = new Set(args.packages.map((s) => parseSpec(s).name));
|
|
136
|
+
const carryover = existingSpecs.filter((s) => !newNames.has(parseSpec(s).name));
|
|
137
|
+
specs = [...carryover, ...args.packages];
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
if (!pkg) {
|
|
141
|
+
throw new Error(`gjsify install: no package.json in ${cwd}`);
|
|
142
|
+
}
|
|
143
|
+
specs = existingSpecs;
|
|
144
|
+
if (specs.length === 0) {
|
|
145
|
+
console.log('gjsify install: no dependencies declared in package.json — nothing to do.');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
mkdirSync(cwd, { recursive: true });
|
|
150
|
+
const result = await installPackages({
|
|
151
|
+
prefix: cwd,
|
|
152
|
+
specs,
|
|
153
|
+
verbose: args.verbose,
|
|
154
|
+
// --immutable consumes the lockfile verbatim and must NOT rewrite
|
|
155
|
+
// it (the whole point is byte-stability under CI).
|
|
156
|
+
lockfile: !args.immutable,
|
|
157
|
+
frozen: args.immutable,
|
|
158
|
+
});
|
|
159
|
+
// Update package.json only when the user passed explicit packages
|
|
160
|
+
// (the `gjsify install <pkg>...` add-a-dep flow). The no-args refresh
|
|
161
|
+
// flow doesn't mutate manifest entries.
|
|
162
|
+
if (args.packages && args.packages.length > 0 && pkg) {
|
|
163
|
+
const kind = depKindFromArgs(args);
|
|
164
|
+
for (const spec of args.packages) {
|
|
165
|
+
const { name, range } = parseSpec(spec);
|
|
166
|
+
const installed = result.installed.find((r) => r.name === name);
|
|
167
|
+
const finalRange = range ?? (installed ? defaultRangeFromVersion(installed.version) : 'latest');
|
|
168
|
+
addDependencyEntry(pkg, name, finalRange, kind);
|
|
169
|
+
}
|
|
170
|
+
writePackageJson(pkgPath, pkg);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Phase D.3 — workspace-aware install. Mirrors what `yarn install` does
|
|
175
|
+
* at a monorepo root:
|
|
176
|
+
* 1. Discover every workspace under the root.
|
|
177
|
+
* 2. Aggregate the union of their external (non-`workspace:`) deps.
|
|
178
|
+
* 3. Run the native install backend ONCE at the root prefix so all
|
|
179
|
+
* externals land in a single `node_modules/` (poor-man's hoisting —
|
|
180
|
+
* we don't deduplicate version-range conflicts yet, the BFS resolver
|
|
181
|
+
* picks first-match).
|
|
182
|
+
* 4. For every `workspace:` reference, symlink the target workspace's
|
|
183
|
+
* directory into the requesting workspace's `node_modules/<dep>`
|
|
184
|
+
* so `import '@gjsify/utils'` resolves to the local source.
|
|
185
|
+
*
|
|
186
|
+
* Hoisting strategy is intentionally minimal — D.3 ships the working
|
|
187
|
+
* baseline; per-workspace dedup + nested `node_modules/` for version
|
|
188
|
+
* conflicts are tracked as a follow-up in STATUS.md "Open TODOs".
|
|
189
|
+
*/
|
|
190
|
+
async function workspaceInstall(cwd, args) {
|
|
191
|
+
const workspaces = discoverWorkspaces(cwd, { includeRoot: true });
|
|
192
|
+
if (workspaces.length === 0) {
|
|
193
|
+
throw new Error(`gjsify install: ${cwd} has a "workspaces" field but no workspaces were discovered`);
|
|
194
|
+
}
|
|
195
|
+
const byName = new Map(workspaces.map((w) => [w.name, w]));
|
|
196
|
+
const externalSpecs = new Set();
|
|
197
|
+
const symlinks = [];
|
|
198
|
+
for (const ws of workspaces) {
|
|
199
|
+
const m = ws.manifest;
|
|
200
|
+
for (const kind of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
201
|
+
const block = m[kind];
|
|
202
|
+
if (!block)
|
|
203
|
+
continue;
|
|
204
|
+
for (const [depName, spec] of Object.entries(block)) {
|
|
205
|
+
if (typeof spec !== 'string')
|
|
206
|
+
continue;
|
|
207
|
+
if (spec.startsWith('workspace:')) {
|
|
208
|
+
const target = byName.get(depName);
|
|
209
|
+
if (!target) {
|
|
210
|
+
throw new Error(`gjsify install: ${ws.name} declares "${depName}: ${spec}" but ` +
|
|
211
|
+
`no workspace with that name exists`);
|
|
212
|
+
}
|
|
213
|
+
symlinks.push({ fromWorkspaceName: ws.name, depName, targetLocation: target.location });
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (/^(link|file|portal|git\+|https?):/.test(spec))
|
|
217
|
+
continue;
|
|
218
|
+
externalSpecs.add(`${depName}@${spec}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
console.log(`gjsify install: ${workspaces.length} workspace(s), ${externalSpecs.size} external dep spec(s), ${symlinks.length} workspace symlink(s)`);
|
|
223
|
+
if (externalSpecs.size > 0) {
|
|
224
|
+
await installPackages({
|
|
225
|
+
prefix: cwd,
|
|
226
|
+
specs: [...externalSpecs],
|
|
227
|
+
verbose: args.verbose,
|
|
228
|
+
lockfile: !args.immutable,
|
|
229
|
+
frozen: args.immutable,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
else if (args.verbose) {
|
|
233
|
+
console.log('gjsify install: no external deps to fetch');
|
|
234
|
+
}
|
|
235
|
+
for (const link of symlinks) {
|
|
236
|
+
const target = byName.get(link.fromWorkspaceName);
|
|
237
|
+
if (!target)
|
|
238
|
+
continue;
|
|
239
|
+
const linkPath = join(target.location, 'node_modules', link.depName);
|
|
240
|
+
mkdirSync(dirname(linkPath), { recursive: true });
|
|
241
|
+
// Remove any prior entry (regular dir, broken symlink, file).
|
|
242
|
+
try {
|
|
243
|
+
const stat = lstatSync(linkPath);
|
|
244
|
+
if (stat.isSymbolicLink() || stat.isFile()) {
|
|
245
|
+
rmSync(linkPath, { force: true });
|
|
246
|
+
}
|
|
247
|
+
else if (stat.isDirectory()) {
|
|
248
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch { /* ENOENT — fine, nothing to remove */ }
|
|
252
|
+
// Relative symlink so the repo is portable across checkout paths.
|
|
253
|
+
const relTarget = relative(dirname(linkPath), link.targetLocation);
|
|
254
|
+
symlinkSync(relTarget, linkPath);
|
|
255
|
+
}
|
|
256
|
+
if (symlinks.length > 0) {
|
|
257
|
+
console.log(`gjsify install: wired ${symlinks.length} workspace symlink(s)`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function projectInstallViaNpm(args) {
|
|
261
|
+
const npmArgs = ['install'];
|
|
262
|
+
if (args['save-dev'])
|
|
263
|
+
npmArgs.push('--save-dev');
|
|
264
|
+
if (args['save-peer'])
|
|
265
|
+
npmArgs.push('--save-peer');
|
|
266
|
+
if (args['save-optional'])
|
|
267
|
+
npmArgs.push('--save-optional');
|
|
268
|
+
if (args.verbose)
|
|
269
|
+
npmArgs.push('--loglevel', 'verbose');
|
|
270
|
+
if (args.packages && args.packages.length > 0) {
|
|
271
|
+
npmArgs.push(...args.packages);
|
|
272
|
+
}
|
|
273
|
+
await spawnNpm(npmArgs);
|
|
274
|
+
}
|
|
79
275
|
async function spawnNpm(npmArgs) {
|
|
80
276
|
return new Promise((resolve, reject) => {
|
|
81
277
|
const child = spawn('npm', npmArgs, { stdio: 'inherit' });
|