@gjsify/cli 0.14.0 → 0.16.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 +45 -44
- package/lib/actions/build.js +24 -1
- package/lib/cli-app.js +2 -1
- package/lib/commands/foreach.d.ts +1 -0
- package/lib/commands/foreach.js +48 -13
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/install.js +132 -65
- package/lib/commands/login.d.ts +29 -0
- package/lib/commands/login.js +122 -96
- package/lib/commands/onboard.d.ts +14 -0
- package/lib/commands/onboard.js +453 -0
- package/lib/commands/publish.d.ts +92 -0
- package/lib/commands/publish.js +270 -295
- package/lib/commands/trust.d.ts +21 -0
- package/lib/commands/trust.js +40 -70
- package/lib/commands/workspace.d.ts +2 -0
- package/lib/commands/workspace.js +64 -14
- package/lib/utils/build-cache.d.ts +157 -0
- package/lib/utils/build-cache.js +670 -0
- package/lib/utils/install-backend-native.js +104 -9
- package/lib/utils/install-backend.d.ts +10 -0
- package/lib/utils/install-cache-fs.d.ts +9 -0
- package/lib/utils/install-cache-fs.js +20 -6
- package/lib/utils/install-lock.d.ts +15 -0
- package/lib/utils/install-lock.js +293 -0
- package/lib/utils/library-output.d.ts +30 -0
- package/lib/utils/library-output.js +92 -0
- package/lib/utils/npm-otp.d.ts +67 -0
- package/lib/utils/npm-otp.js +117 -0
- package/lib/utils/suggest.d.ts +13 -0
- package/lib/utils/suggest.js +51 -0
- package/package.json +19 -18
package/lib/actions/build.js
CHANGED
|
@@ -8,6 +8,7 @@ import { dirname, extname, join } from 'node:path';
|
|
|
8
8
|
import { pathToFileURL } from 'node:url';
|
|
9
9
|
import { chmod, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
10
10
|
import { normalizeBundlerOptions, mergeBundlerOptions } from '../utils/normalize-bundler-options.js';
|
|
11
|
+
import { inputSourceDirs, isOutdirInsideSource, libraryOutputLeakError } from '../utils/library-output.js';
|
|
11
12
|
const DEFAULT_GJS_SHEBANG = '#!/usr/bin/env -S gjs -m';
|
|
12
13
|
/**
|
|
13
14
|
* `true` when `path` points at a location that's unsafe to use as a build
|
|
@@ -83,6 +84,19 @@ export class BuildAction {
|
|
|
83
84
|
const { verbose, library, typescript, exclude, aliases } = this.configData;
|
|
84
85
|
const lib = library ?? {};
|
|
85
86
|
const userBundler = normalizeBundlerOptions(this.configData);
|
|
87
|
+
// Refuse to emit the compiled tree into the input source directory.
|
|
88
|
+
// The outdir is derived below from `dirname(module ?? main)`; when a
|
|
89
|
+
// package points `main`/`module` at a source entry (`src/index.ts`)
|
|
90
|
+
// that derivation is `src/`, so preserve-modules would write `.js`
|
|
91
|
+
// duplicates + a `_virtual/` dir + a nested `src/<pkg>/src` tree next
|
|
92
|
+
// to the sources. An explicit `--outdir` is the escape hatch and is
|
|
93
|
+
// trusted; only the DERIVED default is guarded.
|
|
94
|
+
const cwd = process.cwd();
|
|
95
|
+
const sourceDirs = inputSourceDirs(userBundler.input, cwd);
|
|
96
|
+
const assertDerivedOutdirSafe = (outdir) => {
|
|
97
|
+
if (outdir && isOutdirInsideSource(outdir, sourceDirs, cwd))
|
|
98
|
+
throw libraryOutputLeakError(outdir);
|
|
99
|
+
};
|
|
86
100
|
const moduleOutdir = lib.module ? dirname(lib.module) : undefined;
|
|
87
101
|
const mainOutdir = lib.main ? dirname(lib.main) : undefined;
|
|
88
102
|
const moduleOutExt = lib.module ? extname(lib.module) : '.js';
|
|
@@ -92,6 +106,10 @@ export class BuildAction {
|
|
|
92
106
|
const pnpPlugins = pnp ? [pnp] : [];
|
|
93
107
|
const results = [];
|
|
94
108
|
if (multipleBuilds) {
|
|
109
|
+
// Both outdirs are derived from module/main — always guard them
|
|
110
|
+
// (`--outdir` is not consulted on the multi-output path).
|
|
111
|
+
assertDerivedOutdirSafe(moduleOutdir);
|
|
112
|
+
assertDerivedOutdirSafe(mainOutdir);
|
|
95
113
|
const moduleFormat = moduleOutdir.includes('/cjs') || moduleOutExt === '.cjs' ? 'cjs' : 'esm';
|
|
96
114
|
results.push(await runOneLibraryBuild({
|
|
97
115
|
pluginOpts: {
|
|
@@ -122,9 +140,14 @@ export class BuildAction {
|
|
|
122
140
|
}));
|
|
123
141
|
}
|
|
124
142
|
else {
|
|
143
|
+
const explicitOutdir = userBundler.output?.dir;
|
|
125
144
|
const outfilePath = userBundler.output?.file ?? lib.module ?? lib.main;
|
|
126
145
|
const outExt = outfilePath ? extname(outfilePath) : '.js';
|
|
127
|
-
const outdir =
|
|
146
|
+
const outdir = explicitOutdir ?? (outfilePath ? dirname(outfilePath) : undefined);
|
|
147
|
+
// Only the DERIVED default is a footgun; a user-supplied --outdir
|
|
148
|
+
// is trusted (the documented escape hatch).
|
|
149
|
+
if (explicitOutdir === undefined)
|
|
150
|
+
assertDerivedOutdirSafe(outdir);
|
|
128
151
|
const format = userBundler.output?.format ??
|
|
129
152
|
(outdir?.includes('/cjs') || outExt === '.cjs' ? 'cjs' : 'esm');
|
|
130
153
|
results.push(await runOneLibraryBuild({
|
package/lib/cli-app.js
CHANGED
|
@@ -19,7 +19,7 @@ import { readFileSync } from 'node:fs';
|
|
|
19
19
|
import { dirname, join } from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import yargs from 'yargs';
|
|
22
|
-
import { buildCommand as build, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
|
|
22
|
+
import { buildCommand as build, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
|
|
23
23
|
import { APP_NAME } from './constants.js';
|
|
24
24
|
import { isNode } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
25
25
|
import { ensureGjsifyShimOnPath } from './utils/gjsify-shim.js';
|
|
@@ -103,6 +103,7 @@ export async function runCli(argv) {
|
|
|
103
103
|
.command(login.command, login.description, login.builder, login.handler)
|
|
104
104
|
.command(logout.command, logout.description, logout.builder, logout.handler)
|
|
105
105
|
.command(trust.command, trust.description, trust.builder, trust.handler)
|
|
106
|
+
.command(onboard.command, onboard.description, onboard.builder, onboard.handler)
|
|
106
107
|
.command(selfUpdate.command, selfUpdate.description, selfUpdate.builder, selfUpdate.handler)
|
|
107
108
|
.command(generateInstaller.command, generateInstaller.description, generateInstaller.builder, generateInstaller.handler)
|
|
108
109
|
.command(uninstall.command, uninstall.description, uninstall.builder, uninstall.handler)
|
package/lib/commands/foreach.js
CHANGED
|
@@ -14,6 +14,7 @@ import { cpus } from 'node:os';
|
|
|
14
14
|
import { buildDependencyGraph, discoverWorkspaces, filterWorkspaces, topologicalSort, } from '@gjsify/workspace';
|
|
15
15
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
16
16
|
import { prefixLines } from '../utils/prefixed-output.js';
|
|
17
|
+
import { BuildCacheRunner, buildCacheEnabledByEnv } from '../utils/build-cache.js';
|
|
17
18
|
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
18
19
|
// Every child spawned by spawnPrefixed registers here so fail-fast can
|
|
19
20
|
// terminate the whole in-flight set instead of waiting on it. On CI a
|
|
@@ -238,6 +239,10 @@ export const foreachCommand = {
|
|
|
238
239
|
description: 'Treat <script> [args..] as an arbitrary command (yarn `workspaces foreach exec`-equivalent) instead of a package.json script lookup. Workspace filtering by script presence is skipped. Use `-- <cmd> <args...>` to pass flags to the command without yargs intercepting them.',
|
|
239
240
|
type: 'boolean',
|
|
240
241
|
default: false,
|
|
242
|
+
})
|
|
243
|
+
.option('cached', {
|
|
244
|
+
description: 'Content-hash build cache (ADR 0006): skip a workspace whose inputs (src/**, package.json, tsconfig*.json, transitive workspace deps, toolchain versions) are unchanged and restore its stored outputs instead; on miss run the script and store the output dirs it modified. Script mode only. Default from GJSIFY_BUILD_CACHE=1.',
|
|
245
|
+
type: 'boolean',
|
|
241
246
|
})
|
|
242
247
|
.option('shard', {
|
|
243
248
|
description: 'Run only a deterministic slice "<index>/<total>" (1-based) of the matched workspaces, e.g. --shard 2/4. For fanning a long run across parallel CI jobs. Partitions by sorted name (round-robin) so the shards are disjoint and their union is the full set; order-independent, so safe for tests but NOT for ordered builds.',
|
|
@@ -352,20 +357,43 @@ export const foreachCommand = {
|
|
|
352
357
|
// `cmd` is guaranteed string at this point — both branches above
|
|
353
358
|
// exit on undefined, but TS doesn't narrow through them.
|
|
354
359
|
const finalCmd = cmd;
|
|
360
|
+
// Content-hash build cache (ADR 0006 phase 1). Explicit --cached /
|
|
361
|
+
// --no-cached wins; GJSIFY_BUILD_CACHE=1 is the env default so a
|
|
362
|
+
// whole root-script chain (`gjsify run build`) can opt in without
|
|
363
|
+
// editing every nested invocation. Keys are computed against ALL
|
|
364
|
+
// discovered workspaces (a dep outside the --include filter still
|
|
365
|
+
// participates in its dependents' keys).
|
|
366
|
+
const cached = args.cached ?? buildCacheEnabledByEnv();
|
|
367
|
+
if (args.cached === true && exec) {
|
|
368
|
+
// Only the EXPLICIT flag errors — the GJSIFY_BUILD_CACHE env
|
|
369
|
+
// default covers whole script chains (CI sets it once for the
|
|
370
|
+
// build job) where an --exec invocation is simply not cacheable.
|
|
371
|
+
console.error('gjsify foreach: --cached only applies to script mode, not --exec.');
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
const cache = cached && !exec
|
|
375
|
+
? new BuildCacheRunner({
|
|
376
|
+
root: cwd,
|
|
377
|
+
workspaces: allWorkspaces,
|
|
378
|
+
script: finalCmd,
|
|
379
|
+
args: cmdArgs,
|
|
380
|
+
targets: selected.map((w) => w.name),
|
|
381
|
+
})
|
|
382
|
+
: undefined;
|
|
355
383
|
try {
|
|
356
384
|
if (args.parallel && !args.topological && !args['topological-dev']) {
|
|
357
385
|
const jobs = args.jobs && args.jobs > 0 ? args.jobs : cpus().length;
|
|
358
|
-
await runParallel(selected, finalCmd, cmdArgs, jobs, verbose, exec);
|
|
386
|
+
await runParallel(selected, finalCmd, cmdArgs, jobs, verbose, exec, cache);
|
|
359
387
|
}
|
|
360
388
|
else if (args.parallel) {
|
|
361
389
|
// Topological + parallel: each workspace starts as soon as its
|
|
362
390
|
// deps (in the selected set) have finished. Yarn calls this
|
|
363
391
|
// "topological order with concurrency"; we cap at --jobs.
|
|
364
392
|
const jobs = args.jobs && args.jobs > 0 ? args.jobs : cpus().length;
|
|
365
|
-
await runTopologicalParallel(selected, finalCmd, cmdArgs, jobs, verbose, args['topological-dev'] === true, exec);
|
|
393
|
+
await runTopologicalParallel(selected, finalCmd, cmdArgs, jobs, verbose, args['topological-dev'] === true, exec, cache);
|
|
366
394
|
}
|
|
367
395
|
else {
|
|
368
|
-
await runSequential(selected, finalCmd, cmdArgs, verbose, exec);
|
|
396
|
+
await runSequential(selected, finalCmd, cmdArgs, verbose, exec, cache);
|
|
369
397
|
}
|
|
370
398
|
}
|
|
371
399
|
catch (err) {
|
|
@@ -382,25 +410,25 @@ export const foreachCommand = {
|
|
|
382
410
|
process.exit(0);
|
|
383
411
|
},
|
|
384
412
|
};
|
|
385
|
-
async function runSequential(workspaces, script, args, verbose, exec) {
|
|
413
|
+
async function runSequential(workspaces, script, args, verbose, exec, cache) {
|
|
386
414
|
for (const ws of workspaces) {
|
|
387
|
-
await runOne(ws, script, args, /* prefixOutput */ false, verbose, exec);
|
|
415
|
+
await runOne(ws, script, args, /* prefixOutput */ false, verbose, exec, cache);
|
|
388
416
|
}
|
|
389
417
|
}
|
|
390
|
-
async function runParallel(workspaces, script, args, concurrency, verbose, exec) {
|
|
418
|
+
async function runParallel(workspaces, script, args, concurrency, verbose, exec, cache) {
|
|
391
419
|
let cursor = 0;
|
|
392
420
|
const workers = [];
|
|
393
421
|
for (let w = 0; w < concurrency; w++) {
|
|
394
422
|
workers.push((async () => {
|
|
395
423
|
while (cursor < workspaces.length) {
|
|
396
424
|
const i = cursor++;
|
|
397
|
-
await runOne(workspaces[i], script, args, /* prefixOutput */ true, verbose, exec);
|
|
425
|
+
await runOne(workspaces[i], script, args, /* prefixOutput */ true, verbose, exec, cache);
|
|
398
426
|
}
|
|
399
427
|
})());
|
|
400
428
|
}
|
|
401
429
|
await Promise.all(workers);
|
|
402
430
|
}
|
|
403
|
-
async function runTopologicalParallel(workspaces, script, args, concurrency, verbose, includeDev, exec) {
|
|
431
|
+
async function runTopologicalParallel(workspaces, script, args, concurrency, verbose, includeDev, exec, cache) {
|
|
404
432
|
const selectedNames = new Set(workspaces.map((w) => w.name));
|
|
405
433
|
const remaining = new Map();
|
|
406
434
|
for (const ws of workspaces) {
|
|
@@ -439,9 +467,7 @@ async function runTopologicalParallel(workspaces, script, args, concurrency, ver
|
|
|
439
467
|
return new Promise((resolve, reject) => {
|
|
440
468
|
let error = null;
|
|
441
469
|
let lastActivity = Date.now();
|
|
442
|
-
const describeInflight = () => [...inflightStarts.entries()]
|
|
443
|
-
.map(([n, t]) => `${n} (${Math.round((Date.now() - t) / 1000)}s)`)
|
|
444
|
-
.join(', ');
|
|
470
|
+
const describeInflight = () => [...inflightStarts.entries()].map(([n, t]) => `${n} (${Math.round((Date.now() - t) / 1000)}s)`).join(', ');
|
|
445
471
|
const failFast = (e) => {
|
|
446
472
|
// First error wins. KILL the in-flight siblings instead of
|
|
447
473
|
// waiting for them (yarn waits — but a sibling's nested build
|
|
@@ -488,7 +514,7 @@ async function runTopologicalParallel(workspaces, script, args, concurrency, ver
|
|
|
488
514
|
if (liveProgress) {
|
|
489
515
|
console.error(`[gjsify foreach] start ${next} (${done.size}/${total} done, ${inflight} in flight)`);
|
|
490
516
|
}
|
|
491
|
-
runOne(byName.get(next), script, args, /* prefixOutput */ true, verbose, exec)
|
|
517
|
+
runOne(byName.get(next), script, args, /* prefixOutput */ true, verbose, exec, cache)
|
|
492
518
|
.then(() => {
|
|
493
519
|
done.add(next);
|
|
494
520
|
})
|
|
@@ -528,7 +554,7 @@ async function runTopologicalParallel(workspaces, script, args, concurrency, ver
|
|
|
528
554
|
pump();
|
|
529
555
|
});
|
|
530
556
|
}
|
|
531
|
-
async function runOne(ws, script, args, prefixOutput, verbose, exec) {
|
|
557
|
+
async function runOne(ws, script, args, prefixOutput, verbose, exec, cache) {
|
|
532
558
|
if (exec) {
|
|
533
559
|
// Arbitrary-command mode: spawn `<script> <args...>` directly
|
|
534
560
|
// (yarn `workspaces foreach exec`-equivalent). Used by callers
|
|
@@ -540,6 +566,13 @@ async function runOne(ws, script, args, prefixOutput, verbose, exec) {
|
|
|
540
566
|
await spawnPrefixed(script, args, ws.location, prefixOutput ? `[${ws.name}] ` : null);
|
|
541
567
|
return;
|
|
542
568
|
}
|
|
569
|
+
// Content-hash build cache: on a key hit the stored outputs are
|
|
570
|
+
// restored and the script is skipped entirely; on a miss the output
|
|
571
|
+
// dirs are snapshotted so only the dirs the script MODIFIED are stored
|
|
572
|
+
// after it succeeds (never a committed, no-build lib/).
|
|
573
|
+
if (cache && cache.tryRestore(ws))
|
|
574
|
+
return;
|
|
575
|
+
const before = cache?.snapshotOutputs(ws);
|
|
543
576
|
// Use the same package manager that invoked us — yarn under yarn,
|
|
544
577
|
// npm under npm, gjsify under gjsify. Default to `npm` for portability
|
|
545
578
|
// when nothing is detectable; the script-runner (D.5) will replace
|
|
@@ -550,6 +583,8 @@ async function runOne(ws, script, args, prefixOutput, verbose, exec) {
|
|
|
550
583
|
console.error(`[${ws.name}] $ ${runner} ${argv.join(' ')}`);
|
|
551
584
|
}
|
|
552
585
|
await spawnPrefixed(runner, argv, ws.location, prefixOutput ? `[${ws.name}] ` : null);
|
|
586
|
+
if (cache && before)
|
|
587
|
+
cache.storeAfterSuccess(ws, before);
|
|
553
588
|
}
|
|
554
589
|
function detectPackageManager() {
|
|
555
590
|
// Under GJS there is no Node — neither `npm` nor `yarn` can run scripts, so
|
package/lib/commands/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export * from './whoami.js';
|
|
|
20
20
|
export * from './login.js';
|
|
21
21
|
export * from './logout.js';
|
|
22
22
|
export * from './trust.js';
|
|
23
|
+
export * from './onboard.js';
|
|
23
24
|
export * from './self-update.js';
|
|
24
25
|
export * from './generate-installer.js';
|
|
25
26
|
export * from './uninstall.js';
|
package/lib/commands/index.js
CHANGED
|
@@ -20,6 +20,7 @@ export * from './whoami.js';
|
|
|
20
20
|
export * from './login.js';
|
|
21
21
|
export * from './logout.js';
|
|
22
22
|
export * from './trust.js';
|
|
23
|
+
export * from './onboard.js';
|
|
23
24
|
export * from './self-update.js';
|
|
24
25
|
export * from './generate-installer.js';
|
|
25
26
|
export * from './uninstall.js';
|
package/lib/commands/install.js
CHANGED
|
@@ -15,9 +15,18 @@
|
|
|
15
15
|
// Workspace install (`gjsify install` in a monorepo root with a
|
|
16
16
|
// `"workspaces"` field) hoists every workspace's externals into the root
|
|
17
17
|
// `node_modules/` and symlinks `workspace:*` / `workspace:^` / `workspace:~`
|
|
18
|
-
// refs to their target source.
|
|
19
|
-
//
|
|
20
|
-
//
|
|
18
|
+
// refs to their target source. Conflicting version ranges of the same
|
|
19
|
+
// external dep from different workspaces still share ONE hoisted root copy
|
|
20
|
+
// (a real per-workspace dedup pass is Phase D.8, see STATUS.md), but since
|
|
21
|
+
// ADR 0001 step 3 the resolver surfaces every such conflict loudly —
|
|
22
|
+
// `[gjsify] warning: version conflict for <pkg> …` names both ranges, the
|
|
23
|
+
// workspaces that requested them, and the version that actually won.
|
|
24
|
+
//
|
|
25
|
+
// Concurrency (ADR 0001 step 2): every mutation of an install prefix —
|
|
26
|
+
// project node_modules/, gjsify-lock.json, the user-global prefix — runs
|
|
27
|
+
// under a per-prefix cross-process lock (utils/install-lock.ts). Installs
|
|
28
|
+
// into different prefixes stay parallel; the shared XDG tarball/packument
|
|
29
|
+
// caches need no lock because their writes are atomic tmp+rename.
|
|
21
30
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
22
31
|
import { dirname, join, relative } from 'node:path';
|
|
23
32
|
import { spawn, spawnSync } from 'node:child_process';
|
|
@@ -25,6 +34,8 @@ import { discoverWorkspaces } from '@gjsify/workspace';
|
|
|
25
34
|
import { buildInstallCommand, detectPackageManager, runMinimalChecks } from '../utils/check-system-deps.js';
|
|
26
35
|
import { detectNativePackages } from '../utils/detect-native-packages.js';
|
|
27
36
|
import { installPackages, makeProgressReporter } from '../utils/install-backend.js';
|
|
37
|
+
import { atomicWriteStrict } from '../utils/install-cache-fs.js';
|
|
38
|
+
import { acquireInstallLock } from '../utils/install-lock.js';
|
|
28
39
|
import { binDirOnPath, defaultGlobalLayout, installGjsEnginePackages, linkGlobalBins, specToPackageName, } from '../utils/install-global.js';
|
|
29
40
|
import { addDependencyEntry, defaultRangeFromVersion, parseSpec, projectSpecsFromPackageJson, readPackageJson, writePackageJson, } from '../utils/pkg-json-edit.js';
|
|
30
41
|
// Default 30min wall-clock budget for the full install. Big workspaces
|
|
@@ -268,40 +279,50 @@ async function projectInstallNative(args, signal) {
|
|
|
268
279
|
const progress = makeProgressReporter({
|
|
269
280
|
enabled: !args.verbose && !args.quiet && args.progress !== false,
|
|
270
281
|
});
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
282
|
+
// Hold the per-prefix lock across the install AND the manifest/lockfile
|
|
283
|
+
// sync that follows, so a concurrent `gjsify install <pkg>` in the same
|
|
284
|
+
// project can't interleave its package.json / gjsify-lock.json writes
|
|
285
|
+
// with ours. `installPackages` re-acquires the same lock re-entrantly.
|
|
286
|
+
const lock = await acquireInstallLock(cwd, { signal });
|
|
287
|
+
try {
|
|
288
|
+
const result = await installPackages({
|
|
289
|
+
prefix: cwd,
|
|
290
|
+
specs,
|
|
291
|
+
verbose: args.verbose,
|
|
292
|
+
// --immutable consumes the lockfile verbatim and must NOT rewrite
|
|
293
|
+
// it (the whole point is byte-stability under CI).
|
|
294
|
+
lockfile: !args.immutable,
|
|
295
|
+
frozen: args.immutable,
|
|
296
|
+
refreshLockfile: args['refresh-lockfile'],
|
|
297
|
+
signal,
|
|
298
|
+
progress,
|
|
299
|
+
});
|
|
300
|
+
// Update package.json only when the user passed explicit packages
|
|
301
|
+
// (the `gjsify install <pkg>...` add-a-dep flow). The no-args refresh
|
|
302
|
+
// flow doesn't mutate manifest entries.
|
|
303
|
+
if (args.packages && args.packages.length > 0 && pkg) {
|
|
304
|
+
const kind = depKindFromArgs(args);
|
|
305
|
+
for (const spec of args.packages) {
|
|
306
|
+
const { name, range } = parseSpec(spec);
|
|
307
|
+
const installed = result.installed.find((r) => r.name === name);
|
|
308
|
+
const finalRange = range ?? (installed ? defaultRangeFromVersion(installed.version) : 'latest');
|
|
309
|
+
addDependencyEntry(pkg, name, finalRange, kind);
|
|
310
|
+
}
|
|
311
|
+
writePackageJson(pkgPath, pkg);
|
|
312
|
+
// Re-sync the lockfile's `requested` field with what
|
|
313
|
+
// `projectSpecsFromPackageJson()` will return on the next
|
|
314
|
+
// invocation. Without this, a `gjsify install foo` (bare name,
|
|
315
|
+
// lockfile records `"foo"`) followed by `gjsify install
|
|
316
|
+
// --immutable` (reads package.json → spec `"foo@^1.2.3"`) would
|
|
317
|
+
// surface a spurious drift error.
|
|
318
|
+
if (!args.immutable) {
|
|
319
|
+
syncLockfileRequested(cwd, projectSpecsFromPackageJson(pkg));
|
|
320
|
+
}
|
|
303
321
|
}
|
|
304
322
|
}
|
|
323
|
+
finally {
|
|
324
|
+
lock.release();
|
|
325
|
+
}
|
|
305
326
|
}
|
|
306
327
|
function syncLockfileRequested(cwd, specs) {
|
|
307
328
|
const lockPath = join(cwd, 'gjsify-lock.json');
|
|
@@ -315,7 +336,9 @@ function syncLockfileRequested(cwd, specs) {
|
|
|
315
336
|
return; // Already in sync; preserve byte-stability.
|
|
316
337
|
}
|
|
317
338
|
lock.requested = specs;
|
|
318
|
-
|
|
339
|
+
// Atomic tmp+rename — same rationale as the backend's lockfile
|
|
340
|
+
// writer: a torn gjsify-lock.json breaks the next `--immutable`.
|
|
341
|
+
atomicWriteStrict(lockPath, JSON.stringify(lock, null, 2) + '\n');
|
|
319
342
|
}
|
|
320
343
|
catch {
|
|
321
344
|
// Best-effort sync; if the lockfile is malformed, the next
|
|
@@ -328,23 +351,45 @@ function syncLockfileRequested(cwd, specs) {
|
|
|
328
351
|
* 2. Aggregate the union of their external (non-`workspace:`) deps.
|
|
329
352
|
* 3. Run the native install backend ONCE at the root prefix so all
|
|
330
353
|
* externals land in a single `node_modules/` (poor-man's hoisting —
|
|
331
|
-
*
|
|
332
|
-
*
|
|
354
|
+
* workspaces with conflicting ranges of the same dep share one hoisted
|
|
355
|
+
* copy; the resolver emits a loud `[gjsify] warning: version conflict`
|
|
356
|
+
* naming both sides and the version that won).
|
|
333
357
|
* 4. For every `workspace:` reference, symlink the target workspace's
|
|
334
358
|
* directory into the requesting workspace's `node_modules/<dep>`
|
|
335
359
|
* so `import '@gjsify/utils'` resolves to the local source.
|
|
336
360
|
*
|
|
337
361
|
* Hoisting strategy is intentionally minimal — per-workspace dedup +
|
|
338
|
-
* nested `node_modules/` for version conflicts are
|
|
339
|
-
* in STATUS.md
|
|
362
|
+
* nested `node_modules/` for cross-workspace version conflicts are the
|
|
363
|
+
* Phase D.8 follow-up tracked in STATUS.md.
|
|
340
364
|
*/
|
|
341
365
|
async function workspaceInstall(cwd, args, signal) {
|
|
366
|
+
// Hold the root-prefix lock for the WHOLE workspace flow: bin shims,
|
|
367
|
+
// workspace symlinks and the external install all mutate the root
|
|
368
|
+
// node_modules/, and a second concurrent workspace install interleaving
|
|
369
|
+
// its rm+symlink/extract steps with ours corrupts both. The inner
|
|
370
|
+
// `installPackages` calls re-acquire the same lock re-entrantly (root
|
|
371
|
+
// prefix) or take the child-workspace lock (scoped overrides) — always
|
|
372
|
+
// in root→child order, so there is no lock-ordering cycle.
|
|
373
|
+
const lock = await acquireInstallLock(cwd, { signal });
|
|
374
|
+
try {
|
|
375
|
+
await workspaceInstallLocked(cwd, args, signal);
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
lock.release();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/** Body of {@link workspaceInstall}, run while holding the root-prefix lock. */
|
|
382
|
+
async function workspaceInstallLocked(cwd, args, signal) {
|
|
342
383
|
const workspaces = discoverWorkspaces(cwd, { includeRoot: true });
|
|
343
384
|
if (workspaces.length === 0) {
|
|
344
385
|
throw new Error(`gjsify install: ${cwd} has a "workspaces" field but no workspaces were discovered`);
|
|
345
386
|
}
|
|
346
387
|
const byName = new Map(workspaces.map((w) => [w.name, w]));
|
|
347
388
|
const externalSpecs = new Set();
|
|
389
|
+
// Which workspace(s) declared each external spec — `"<name>@<range>"` →
|
|
390
|
+
// workspace names. Feeds the resolver's version-conflict warning so it
|
|
391
|
+
// can attribute both sides of a cross-workspace range conflict.
|
|
392
|
+
const specOrigins = new Map();
|
|
348
393
|
const symlinks = [];
|
|
349
394
|
for (const ws of workspaces) {
|
|
350
395
|
const m = ws.manifest;
|
|
@@ -391,7 +436,14 @@ async function workspaceInstall(cwd, args, signal) {
|
|
|
391
436
|
}
|
|
392
437
|
if (/^(link|file|portal|git\+|https?):/.test(spec))
|
|
393
438
|
continue;
|
|
394
|
-
|
|
439
|
+
const specKey = `${depName}@${spec}`;
|
|
440
|
+
externalSpecs.add(specKey);
|
|
441
|
+
let origins = specOrigins.get(specKey);
|
|
442
|
+
if (!origins) {
|
|
443
|
+
origins = new Set();
|
|
444
|
+
specOrigins.set(specKey, origins);
|
|
445
|
+
}
|
|
446
|
+
origins.add(ws.name);
|
|
395
447
|
}
|
|
396
448
|
}
|
|
397
449
|
}
|
|
@@ -624,6 +676,7 @@ async function workspaceInstall(cwd, args, signal) {
|
|
|
624
676
|
// symlinks (e.g. a `types-dev/<lib>` workspace that also exists on
|
|
625
677
|
// npm and is pinned transitively in the lockfile).
|
|
626
678
|
workspaceNames: new Set(byName.keys()),
|
|
679
|
+
specOrigins: new Map([...specOrigins].map(([k, v]) => [k, [...v]])),
|
|
627
680
|
});
|
|
628
681
|
}
|
|
629
682
|
else if (args.verbose) {
|
|
@@ -926,28 +979,42 @@ async function installGlobalAndLink(specs, opts) {
|
|
|
926
979
|
mkdirSync(layout.prefix, { recursive: true });
|
|
927
980
|
console.log(`gjsify install --global → ${layout.prefix}`);
|
|
928
981
|
console.log(` bins → ${layout.binDir}`);
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
982
|
+
// The global prefix is SHARED user-wide state — two concurrent
|
|
983
|
+
// `gjsify install -g` runs (e.g. parallel agent sessions bootstrapping
|
|
984
|
+
// tooling) used to interleave rm+extract on the same package dirs and
|
|
985
|
+
// race the launcher writes in ~/.local/bin. Hold the prefix lock across
|
|
986
|
+
// the install, the engine-package top-up AND the bin linking (ADR 0001).
|
|
987
|
+
const lock = await acquireInstallLock(layout.prefix, {});
|
|
988
|
+
try {
|
|
989
|
+
const result = await installPackages({
|
|
990
|
+
prefix: layout.prefix,
|
|
991
|
+
specs,
|
|
992
|
+
verbose: opts.verbose,
|
|
993
|
+
});
|
|
994
|
+
const packageNames = specs.map(specToPackageName);
|
|
995
|
+
// A global GJS install of `@gjsify/cli` must also lay down the GJS-native
|
|
996
|
+
// bundler engine (`@gjsify/rolldown-native`) + the sibling format/CSS
|
|
997
|
+
// bridges. They are declared OPTIONAL peers of `@gjsify/cli` (so plain
|
|
998
|
+
// `npm install @gjsify/cli` on Node doesn't force a linux prebuild), and
|
|
999
|
+
// the native backend doesn't resolve peerDependencies at all — so without
|
|
1000
|
+
// this they'd never be installed, and `gjsify build` would hard-fail under
|
|
1001
|
+
// GJS ("no usable bundler engine"). Installed best-effort at the cli's
|
|
1002
|
+
// resolved version so they move in lockstep with the bundle; a platform
|
|
1003
|
+
// with no published prebuild degrades to a warning. Must run BEFORE
|
|
1004
|
+
// linkGlobalBins so the launcher's detectNativePackages() bakes the
|
|
1005
|
+
// engine's prebuild dirs into the wrapper's GI_TYPELIB_PATH/LD_LIBRARY_PATH.
|
|
1006
|
+
if (packageNames.includes('@gjsify/cli')) {
|
|
1007
|
+
const cliVersion = result.installed.find((r) => r.name === '@gjsify/cli')?.version ?? 'latest';
|
|
1008
|
+
await installGjsEnginePackages(layout.prefix, cliVersion, { verbose: opts.verbose });
|
|
1009
|
+
}
|
|
1010
|
+
const created = linkGlobalBins(packageNames, layout);
|
|
1011
|
+
reportLinkedBins(created, layout.binDir);
|
|
1012
|
+
}
|
|
1013
|
+
finally {
|
|
1014
|
+
lock.release();
|
|
949
1015
|
}
|
|
950
|
-
|
|
1016
|
+
}
|
|
1017
|
+
function reportLinkedBins(created, binDir) {
|
|
951
1018
|
if (created.length === 0) {
|
|
952
1019
|
console.warn('\nNo bins declared (neither `gjsify.bin` nor `bin` in package.json) — nothing was symlinked.');
|
|
953
1020
|
}
|
|
@@ -957,9 +1024,9 @@ async function installGlobalAndLink(specs, opts) {
|
|
|
957
1024
|
console.log(` • ${e.link} → ${e.target}`);
|
|
958
1025
|
}
|
|
959
1026
|
}
|
|
960
|
-
if (created.length > 0 && !binDirOnPath(
|
|
961
|
-
console.warn(`\nNote: ${
|
|
962
|
-
`Add it to your shell rc file:\n export PATH="${
|
|
1027
|
+
if (created.length > 0 && !binDirOnPath(binDir)) {
|
|
1028
|
+
console.warn(`\nNote: ${binDir} is not on your PATH.\n` +
|
|
1029
|
+
`Add it to your shell rc file:\n export PATH="${binDir}:$PATH"`);
|
|
963
1030
|
}
|
|
964
1031
|
}
|
|
965
1032
|
async function runPostInstallChecks() {
|
package/lib/commands/login.d.ts
CHANGED
|
@@ -6,5 +6,34 @@ interface LoginOptions {
|
|
|
6
6
|
otp?: string;
|
|
7
7
|
json?: boolean;
|
|
8
8
|
}
|
|
9
|
+
/** Options for the reusable {@link runLogin} flow. */
|
|
10
|
+
export interface RunLoginOptions {
|
|
11
|
+
registry?: string;
|
|
12
|
+
scope?: string;
|
|
13
|
+
username?: string;
|
|
14
|
+
otp?: string;
|
|
15
|
+
/** Directory whose `.npmrc` seeds scope/registry resolution. Default: cwd. */
|
|
16
|
+
cwd?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Successful login result. */
|
|
19
|
+
export interface LoginResult {
|
|
20
|
+
username: string;
|
|
21
|
+
/** Registry URL (with trailing slash). */
|
|
22
|
+
registry: string;
|
|
23
|
+
/** Path of the `.npmrc` the token was written to. */
|
|
24
|
+
npmrcPath: string;
|
|
25
|
+
}
|
|
26
|
+
/** Thrown by {@link runLogin} on any recoverable login failure (bad creds, no token, …). */
|
|
27
|
+
export declare class LoginError extends Error {
|
|
28
|
+
constructor(message: string);
|
|
29
|
+
}
|
|
9
30
|
export declare const loginCommand: Command<unknown, LoginOptions>;
|
|
31
|
+
/**
|
|
32
|
+
* The `gjsify login` core, extracted so `gjsify onboard` can authenticate the
|
|
33
|
+
* user when the whoami gate fails. Prompts for username + password (unless
|
|
34
|
+
* provided), PUTs the CouchDB user doc, writes the token to the userconfig
|
|
35
|
+
* `.npmrc`, and verifies it with `whoami`. Throws {@link LoginError} on any
|
|
36
|
+
* recoverable failure (the command handler / onboard present it).
|
|
37
|
+
*/
|
|
38
|
+
export declare function runLogin(opts: RunLoginOptions): Promise<LoginResult>;
|
|
10
39
|
export {};
|