@gjsify/cli 0.51.1 → 0.53.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 +539 -493
- package/lib/actions/build.d.ts +22 -0
- package/lib/actions/build.js +37 -9
- package/lib/bundler-pick.d.ts +37 -1
- package/lib/bundler-pick.js +108 -17
- package/lib/cli-app.js +7 -2
- package/lib/commands/affected-classify.d.ts +7 -1
- package/lib/commands/affected-classify.js +66 -9
- package/lib/commands/affected-script-refs.d.ts +20 -0
- package/lib/commands/affected-script-refs.js +218 -0
- package/lib/commands/affected.js +24 -2
- package/lib/commands/env.d.ts +31 -0
- package/lib/commands/env.js +114 -0
- package/lib/commands/exec.d.ts +10 -0
- package/lib/commands/exec.js +208 -0
- package/lib/commands/flatpak/diff.js +8 -2
- package/lib/commands/flatpak/init.js +4 -1
- package/lib/commands/flatpak/sources.js +4 -3
- package/lib/commands/flatpak/sync-flathub.d.ts +2 -0
- package/lib/commands/flatpak/sync-flathub.js +149 -7
- package/lib/commands/flatpak/utils.d.ts +10 -0
- package/lib/commands/flatpak/utils.js +10 -0
- package/lib/commands/foreach.js +5 -97
- package/lib/commands/format.js +26 -6
- package/lib/commands/index.d.ts +4 -0
- package/lib/commands/index.js +4 -0
- package/lib/commands/install.d.ts +2 -2
- package/lib/commands/install.js +81 -5
- package/lib/commands/link.d.ts +13 -0
- package/lib/commands/link.js +243 -0
- package/lib/commands/onboard.js +18 -2
- package/lib/commands/publish.d.ts +34 -0
- package/lib/commands/publish.js +153 -8
- package/lib/commands/self-update.js +2 -1
- package/lib/commands/ship.js +19 -1
- package/lib/commands/test.d.ts +18 -9
- package/lib/commands/test.js +56 -11
- package/lib/commands/tsc.js +4 -4
- package/lib/commands/upgrade.d.ts +15 -0
- package/lib/commands/upgrade.js +76 -2
- package/lib/commands/webext/index.d.ts +2 -0
- package/lib/commands/webext/index.js +15 -0
- package/lib/commands/webext/subcommands.d.ts +16 -0
- package/lib/commands/webext/subcommands.js +204 -0
- package/lib/types/config-data.d.ts +2 -2
- package/lib/utils/bin-shim.d.ts +8 -2
- package/lib/utils/bin-shim.js +6 -5
- package/lib/utils/bundle-inputs.d.ts +84 -0
- package/lib/utils/bundle-inputs.js +268 -0
- package/lib/utils/check-system-deps.d.ts +5 -19
- package/lib/utils/check-system-deps.js +31 -94
- package/lib/utils/dev-link.d.ts +225 -0
- package/lib/utils/dev-link.js +684 -0
- package/lib/utils/exec-bin.d.ts +137 -0
- package/lib/utils/exec-bin.js +321 -0
- package/lib/utils/gi-runtime-paths.js +3 -2
- package/lib/utils/gitignore.d.ts +15 -0
- package/lib/utils/gitignore.js +178 -0
- package/lib/utils/gjsify-shim.d.ts +47 -0
- package/lib/utils/gjsify-shim.js +97 -8
- package/lib/utils/hang-watchdog.d.ts +95 -0
- package/lib/utils/hang-watchdog.js +194 -0
- package/lib/utils/install-backend-native.d.ts +33 -1
- package/lib/utils/install-backend-native.js +184 -20
- package/lib/utils/install-extraneous.d.ts +90 -0
- package/lib/utils/install-extraneous.js +120 -0
- package/lib/utils/install-lock.js +4 -25
- package/lib/utils/install-tarball-cache.d.ts +37 -9
- package/lib/utils/install-tarball-cache.js +59 -14
- package/lib/utils/native-skip-scan.d.ts +20 -0
- package/lib/utils/native-skip-scan.js +343 -0
- package/lib/utils/node-script.js +1 -1
- package/lib/utils/normalize-bundler-options.js +11 -0
- package/lib/utils/oxc-resolve.js +42 -3
- package/lib/utils/process-table.d.ts +22 -0
- package/lib/utils/process-table.js +188 -0
- package/lib/utils/prune-prefix.js +1 -1
- package/lib/utils/publish-pins.d.ts +38 -0
- package/lib/utils/publish-pins.js +47 -0
- package/lib/utils/publish-readback.d.ts +103 -17
- package/lib/utils/publish-readback.js +326 -47
- package/lib/utils/run-gjs.d.ts +15 -1
- package/lib/utils/run-gjs.js +98 -13
- package/lib/utils/run-node.d.ts +2 -0
- package/lib/utils/run-node.js +2 -1
- package/lib/utils/runtimes.js +14 -13
- package/lib/utils/ship/gzip.js +1 -1
- package/lib/utils/ship/icns.d.ts +14 -0
- package/lib/utils/ship/icns.js +83 -0
- package/lib/utils/ship/ico.d.ts +25 -0
- package/lib/utils/ship/ico.js +120 -0
- package/lib/utils/ship/icons.d.ts +197 -0
- package/lib/utils/ship/icons.js +399 -0
- package/lib/utils/ship/launcher.js +16 -1
- package/lib/utils/ship/layout.d.ts +32 -0
- package/lib/utils/ship/layout.js +112 -38
- package/lib/utils/ship/msi.d.ts +1 -1
- package/lib/utils/ship/msi.js +30 -5
- package/lib/utils/ship/pe-launcher.d.ts +18 -0
- package/lib/utils/ship/pe-launcher.js +125 -4
- package/lib/utils/ship/plan.d.ts +1 -34
- package/lib/utils/ship/plan.js +7 -56
- package/lib/utils/ship/plist.d.ts +20 -10
- package/lib/utils/ship/plist.js +29 -12
- package/lib/utils/ship/settings.js +3 -2
- package/lib/utils/spawn.d.ts +13 -0
- package/lib/utils/spawn.js +10 -5
- package/lib/utils/watch-loop.d.ts +15 -1
- package/lib/utils/watch-loop.js +9 -5
- package/lib/utils/webext/build.d.ts +42 -0
- package/lib/utils/webext/build.js +120 -0
- package/lib/utils/webext/config.d.ts +72 -0
- package/lib/utils/webext/config.js +234 -0
- package/lib/utils/webext/icons.d.ts +24 -0
- package/lib/utils/webext/icons.js +83 -0
- package/lib/utils/webext/launch.d.ts +32 -0
- package/lib/utils/webext/launch.js +77 -0
- package/lib/utils/webext/manifest.d.ts +53 -0
- package/lib/utils/webext/manifest.js +219 -0
- package/lib/utils/webext/pages.d.ts +26 -0
- package/lib/utils/webext/pages.js +61 -0
- package/lib/utils/webext/targets.d.ts +13 -0
- package/lib/utils/webext/targets.js +36 -0
- package/lib/utils/win32-command.d.ts +17 -0
- package/lib/utils/win32-command.js +21 -6
- package/package.json +20 -20
package/lib/commands/install.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// `node_modules/` and symlinks `workspace:` refs to their source. Members with
|
|
11
11
|
// conflicting ranges of the same external still share ONE hoisted copy; the
|
|
12
12
|
// resolver warns naming both ranges, both requesters and the winner (per-member
|
|
13
|
-
// dedup is the open Phase D.8 item in status/open-todos.md).
|
|
13
|
+
// dedup is the open Phase D.8 item in status/open-todos/README.md).
|
|
14
14
|
//
|
|
15
15
|
// Concurrency (ADR 0001): every prefix mutation runs under a per-prefix
|
|
16
16
|
// cross-process lock (utils/install-lock.ts), so installs into different
|
|
@@ -35,6 +35,7 @@ import { binDirOnPath, BUNDLER_ENGINE_PACKAGE, defaultGlobalLayout, hasBundlerEn
|
|
|
35
35
|
import { resolveHostPlatform } from '../utils/platform-check.js';
|
|
36
36
|
import { pruneAfterInstall } from '../utils/prune-prefix.js';
|
|
37
37
|
import { addDependencyEntry, defaultRangeFromVersion, parseSpec, projectSpecsFromPackageJson, readPackageJson, writePackageJson, } from '../utils/pkg-json-edit.js';
|
|
38
|
+
import { applyDevLinks, assertNoDevLinkUnderImmutable, devLinkRoot, prepareDevLinks, } from '../utils/dev-link.js';
|
|
38
39
|
/**
|
|
39
40
|
* Link type for the workspace↔workspace directory links.
|
|
40
41
|
*
|
|
@@ -157,6 +158,12 @@ export const installCommand = {
|
|
|
157
158
|
'(--immutable forbids rewriting the lockfile). Drop one.');
|
|
158
159
|
return process.exit(1);
|
|
159
160
|
}
|
|
161
|
+
// FAIL CLOSED before anything is resolved or written: an active `gjsify
|
|
162
|
+
// link` override would make a local development tree an input to the one
|
|
163
|
+
// install shape whose whole promise is "exactly what is committed" — CI,
|
|
164
|
+
// the Flatpak build, a release. Throws rather than exits so `index.ts`
|
|
165
|
+
// prints the message that names the override file (utils/dev-link.ts).
|
|
166
|
+
assertNoDevLinkUnderImmutable(process.cwd());
|
|
160
167
|
}
|
|
161
168
|
if (args.global) {
|
|
162
169
|
if (!args.packages || args.packages.length === 0) {
|
|
@@ -426,6 +433,61 @@ function depKindFromArgs(args) {
|
|
|
426
433
|
return 'optionalDependencies';
|
|
427
434
|
return 'dependencies';
|
|
428
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* Wire the `gjsify link` development override for this prefix, and SAY SO.
|
|
438
|
+
*
|
|
439
|
+
* Called before the fetch phase for the same reason `wireWorkspaceSymlinks` is:
|
|
440
|
+
* these links point at local source and depend on no download, so writing them
|
|
441
|
+
* first means an install that dies mid-way still leaves a resolvable tree. Called
|
|
442
|
+
* again afterwards as a near no-op safety net.
|
|
443
|
+
*
|
|
444
|
+
* The announcement is not decoration. A linked tree changes what a build compiles
|
|
445
|
+
* without changing any file the consumer commits, so the one place it can be seen
|
|
446
|
+
* is the output of the command that re-applies it — this is exactly what a bare
|
|
447
|
+
* `npm link` does not do, and why the next `npm install` silently undoes it.
|
|
448
|
+
*
|
|
449
|
+
* Returns `null` when no override is present; throws when one names a checkout
|
|
450
|
+
* that is gone, is not a gjsify workspace, or is not BUILT (utils/dev-link.ts).
|
|
451
|
+
*
|
|
452
|
+
* `devLinkRoot` and not `prefix` verbatim: `assertNoDevLinkUnderImmutable` already
|
|
453
|
+
* walks up to the workspace root to decide whether an override is in play, and two
|
|
454
|
+
* answers to "which override governs this install" is one answer too many. Nothing
|
|
455
|
+
* rescued the difference either — the bare `install` path happens to delegate to
|
|
456
|
+
* the workspace root before reaching here, `install <pkg>` does not.
|
|
457
|
+
*/
|
|
458
|
+
function activateDevLinks(prefix, announce) {
|
|
459
|
+
const active = prepareDevLinks(devLinkRoot(prefix));
|
|
460
|
+
if (!active)
|
|
461
|
+
return null;
|
|
462
|
+
const written = applyDevLinks(active.links);
|
|
463
|
+
if (!announce)
|
|
464
|
+
return active;
|
|
465
|
+
console.log(`gjsify install: development link ACTIVE (${active.overridePath}) — ${active.links.length} ` +
|
|
466
|
+
`package(s) come from ${active.checkout}, linked rather than fetched ` +
|
|
467
|
+
`(${written.length} (re)written, ${active.links.length - written.length} already current):`);
|
|
468
|
+
const names = active.links.map((l) => l.name);
|
|
469
|
+
const shown = names.slice(0, 12);
|
|
470
|
+
console.log(` ${shown.join(', ')}${names.length > shown.length ? `, +${names.length - shown.length} more` : ''}`);
|
|
471
|
+
console.log(' `gjsify unlink` restores the registry copies; `gjsify install --immutable` refuses while it exists.');
|
|
472
|
+
return active;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Re-apply the dev links after the install, and SAY SO when it changed anything.
|
|
476
|
+
*
|
|
477
|
+
* `applyDevLinks` returns what it had to (re)write, and both post-install call
|
|
478
|
+
* sites used to throw that away — so an installer that clobbered a link and got it
|
|
479
|
+
* put back looked exactly like one that never touched it. The safety net is
|
|
480
|
+
* supposed to be a no-op; the run where it is not is the one worth a line.
|
|
481
|
+
*/
|
|
482
|
+
function reportRewiredDevLinks(active) {
|
|
483
|
+
if (!active)
|
|
484
|
+
return;
|
|
485
|
+
const rewired = applyDevLinks(active.links);
|
|
486
|
+
if (rewired.length === 0)
|
|
487
|
+
return;
|
|
488
|
+
console.log(`gjsify install: re-applied ${rewired.length} development link(s) the install had replaced: ` +
|
|
489
|
+
`${rewired.map((l) => l.name).join(', ')}`);
|
|
490
|
+
}
|
|
429
491
|
async function projectInstallNative(args, signal) {
|
|
430
492
|
const cwd = process.cwd();
|
|
431
493
|
const pkgPath = join(cwd, 'package.json');
|
|
@@ -497,6 +559,7 @@ async function projectInstallNative(args, signal) {
|
|
|
497
559
|
// re-acquires the same lock re-entrantly.
|
|
498
560
|
const lock = await acquireInstallLock(cwd, { signal });
|
|
499
561
|
try {
|
|
562
|
+
const devLinks = activateDevLinks(cwd, true);
|
|
500
563
|
// Typed as NativeInstallOptions so the native-backend-only `optionalSpecs`
|
|
501
564
|
// is CHECKED here instead of smuggled past an excess-property check;
|
|
502
565
|
// `installPackages` forwards the object to that backend unchanged.
|
|
@@ -518,8 +581,14 @@ async function projectInstallNative(args, signal) {
|
|
|
518
581
|
...optionalDependencyNames(pkg ? [pkg] : []),
|
|
519
582
|
...(args['save-optional'] ? (args.packages ?? []).map((s) => parseSpec(s).name) : []),
|
|
520
583
|
]),
|
|
584
|
+
linkedNames: devLinks?.names,
|
|
521
585
|
};
|
|
522
586
|
const result = await installPackages(nativeOpts);
|
|
587
|
+
// The link survives the install — the point of reading the override at all.
|
|
588
|
+
// The return value is REPORTED, not dropped: a non-empty one means the
|
|
589
|
+
// install disturbed a link that the pre-fetch pass had already wired, which
|
|
590
|
+
// is a fact about the installer, not a routine no-op.
|
|
591
|
+
reportRewiredDevLinks(devLinks);
|
|
523
592
|
// Only the `gjsify install <pkg>...` add-a-dep flow mutates the manifest;
|
|
524
593
|
// the no-args refresh must not.
|
|
525
594
|
if (args.packages && args.packages.length > 0 && pkg) {
|
|
@@ -572,7 +641,7 @@ function syncLockfileRequested(cwd, specs) {
|
|
|
572
641
|
*
|
|
573
642
|
* Hoisting is deliberately minimal — members with conflicting ranges of the same
|
|
574
643
|
* dep share one hoisted copy and the resolver warns. Per-member dedup and nested
|
|
575
|
-
* `node_modules/` are the open Phase D.8 item in status/open-todos.md.
|
|
644
|
+
* `node_modules/` are the open Phase D.8 item in status/open-todos/README.md.
|
|
576
645
|
*/
|
|
577
646
|
async function workspaceInstall(cwd, args, signal) {
|
|
578
647
|
// Held for the WHOLE flow: bin shims, workspace symlinks and the external
|
|
@@ -650,6 +719,10 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
650
719
|
}
|
|
651
720
|
}
|
|
652
721
|
console.log(`gjsify install: ${workspaces.length} workspace(s), ${externalSpecs.size} external dep spec(s), ${symlinks.length} workspace symlink(s)`);
|
|
722
|
+
// A `gjsify link` override at the workspace ROOT governs the whole install, and
|
|
723
|
+
// is wired here for the same reason the shims and workspace symlinks are: before
|
|
724
|
+
// anything can fail on the network.
|
|
725
|
+
const devLinks = activateDevLinks(cwd, true);
|
|
653
726
|
// EARLY, before the download/extract phase: the shims derive from workspace
|
|
654
727
|
// manifests alone, while the heavy phase that follows can die mid-way (network,
|
|
655
728
|
// timeout, crash). Writing them up front means a failed install can never leave
|
|
@@ -836,6 +909,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
836
909
|
// `types-dev/<lib>` workspace that also exists on npm and is pinned
|
|
837
910
|
// transitively in the lockfile).
|
|
838
911
|
workspaceNames: new Set(byName.keys()),
|
|
912
|
+
linkedNames: devLinks?.names,
|
|
839
913
|
specOrigins: new Map([...specOrigins].map(([k, v]) => [k, [...v]])),
|
|
840
914
|
// Aggregated over EVERY member, because the specs are: a name any member
|
|
841
915
|
// declares as a plain dependency stays required.
|
|
@@ -865,6 +939,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
865
939
|
frozen: args.immutable,
|
|
866
940
|
signal,
|
|
867
941
|
workspaceNames: new Set(byName.keys()),
|
|
942
|
+
linkedNames: devLinks?.names,
|
|
868
943
|
// Same aggregated answer as the root install: these specs are a SUBSET of
|
|
869
944
|
// the root's, so classifying them differently would make one dep optional
|
|
870
945
|
// at the root and required in a member.
|
|
@@ -874,6 +949,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
874
949
|
}
|
|
875
950
|
// A near no-op safety net in case a later step disturbed a link.
|
|
876
951
|
await wireWorkspaceSymlinks();
|
|
952
|
+
reportRewiredDevLinks(devLinks);
|
|
877
953
|
// Now that the tree is materialised, refresh the shims' GJS preamble with the
|
|
878
954
|
// native prebuild dirs that only became discoverable after the install (on a
|
|
879
955
|
// fresh checkout `detectNativePackages` finds nothing before extraction).
|
|
@@ -1196,7 +1272,7 @@ async function installGlobalAndLink(specs, opts) {
|
|
|
1196
1272
|
// A global install of `@gjsify/cli` must also lay down the GJS bundler engine
|
|
1197
1273
|
// + the sibling format/CSS bridges. They are OPTIONAL PEERS of the CLI (so a
|
|
1198
1274
|
// plain `npm install @gjsify/cli` on Node does not force a prebuild) and the
|
|
1199
|
-
// native backend
|
|
1275
|
+
// native backend, like npm ≥ 7, installs required peers only, so without this
|
|
1200
1276
|
// they never arrive and `gjsify build` hard-fails under GJS with "no usable
|
|
1201
1277
|
// bundler engine". Pinned to the CLI's resolved version so they move in
|
|
1202
1278
|
// lockstep with the bundle; a platform with no published prebuild warns.
|
|
@@ -1257,8 +1333,8 @@ function reportLinkedBins(created, binDir) {
|
|
|
1257
1333
|
*
|
|
1258
1334
|
* The engine is an OPTIONAL PEER of `@gjsify/cli` on purpose — a plain
|
|
1259
1335
|
* `npm install @gjsify/cli` on Node must not fetch prebuilds — but nobody resolves
|
|
1260
|
-
* it for a project: npm 7+ skips optional peers and the native backend
|
|
1261
|
-
*
|
|
1336
|
+
* it for a project: npm 7+ skips optional peers and so does the native backend
|
|
1337
|
+
* (it installs REQUIRED peers only). Under GJS there is no npm `rolldown`
|
|
1262
1338
|
* fallback, so `gjsify build` had nothing to load.
|
|
1263
1339
|
*
|
|
1264
1340
|
* Four conditions, and (b) is the one worth reading:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LeafCommand } from '../types/index.js';
|
|
2
|
+
interface LinkOptions {
|
|
3
|
+
checkout: string;
|
|
4
|
+
packages?: string[];
|
|
5
|
+
'dry-run'?: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface UnlinkOptions {
|
|
8
|
+
'dry-run'?: boolean;
|
|
9
|
+
install?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare const linkCommand: LeafCommand<unknown, LinkOptions>;
|
|
12
|
+
export declare const unlinkCommand: LeafCommand<unknown, UnlinkOptions>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// `gjsify link <checkout>` / `gjsify unlink` — develop a consumer against a local
|
|
2
|
+
// gjsify checkout without waiting for an npm release, and without changing a byte
|
|
3
|
+
// of what the consumer commits.
|
|
4
|
+
//
|
|
5
|
+
// The model, the trap it closes and why `--immutable` refuses it are the header of
|
|
6
|
+
// `utils/dev-link.ts` and ADR 0065. This file is the surface: two commands, the
|
|
7
|
+
// output that makes an active link visible, and the undo.
|
|
8
|
+
//
|
|
9
|
+
// `unlink` REINSTALLS by default. Removing a link leaves a hole where the registry
|
|
10
|
+
// copy used to be, and a command that promises to undo itself must not hand back a
|
|
11
|
+
// tree that no longer resolves; `--no-install` is there for the case where the next
|
|
12
|
+
// step is an install anyway.
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { relative, resolve } from 'node:path';
|
|
15
|
+
import { applyDevLinks, consumerKnownNames, DEV_LINK_FILE, devLinkPath, ensureLocallyIgnored, formatUnbuiltDevLinks, planDevLinks, prepareDevLinks, readDevLinkOverride, removeDevLinkOverride, removeDevLinks, removeLocalIgnore, resolveCheckoutWorkspaces, retireDevLinks, scanDevLinks, unbuiltDevLinks, writeDevLinkOverride, } from '../utils/dev-link.js';
|
|
16
|
+
export const linkCommand = {
|
|
17
|
+
command: 'link <checkout>',
|
|
18
|
+
description: 'Link this project against a local gjsify checkout for development: its workspace packages replace the installed registry copies, recorded in a git-ignored .gjsify-link.json that every later `gjsify install` re-applies. Undo with `gjsify unlink`.',
|
|
19
|
+
builder: (yargs) => yargs
|
|
20
|
+
.positional('checkout', {
|
|
21
|
+
description: 'Path to the gjsify checkout to link from (a repo whose package.json declares "workspaces").',
|
|
22
|
+
type: 'string',
|
|
23
|
+
demandOption: true,
|
|
24
|
+
})
|
|
25
|
+
.option('packages', {
|
|
26
|
+
description: "Name glob selecting which of the checkout's workspace packages take part (repeatable; default: all of them). Matched against the package NAME with the same single-segment dialect as `gjsify foreach --include`, so a whole scope is `@gjsify/*` — a bare `*` matches no scoped name. Only packages this project actually depends on are ever linked.",
|
|
27
|
+
type: 'string',
|
|
28
|
+
array: true,
|
|
29
|
+
})
|
|
30
|
+
.option('dry-run', {
|
|
31
|
+
description: 'Print the links that would be made and write nothing.',
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
default: false,
|
|
34
|
+
}),
|
|
35
|
+
handler: (args) => {
|
|
36
|
+
const consumerRoot = process.cwd();
|
|
37
|
+
const dryRun = args['dry-run'] ?? false;
|
|
38
|
+
const checkout = resolve(consumerRoot, args.checkout);
|
|
39
|
+
const overridePath = devLinkPath(consumerRoot);
|
|
40
|
+
// Validated BEFORE anything is written: a `.gjsify-link.json` naming a
|
|
41
|
+
// directory that is not a gjsify checkout would fail on every later install
|
|
42
|
+
// instead of on the command that created it.
|
|
43
|
+
const workspaces = resolveCheckoutWorkspaces(checkout, overridePath);
|
|
44
|
+
// No `--packages` means every workspace package, spelled as the EMPTY list:
|
|
45
|
+
// `['*']` would select nothing, because the glob dialect's `*` does not
|
|
46
|
+
// cross a `/` and every name here is scoped (utils/dev-link.ts).
|
|
47
|
+
const patterns = args.packages ?? [];
|
|
48
|
+
const links = planDevLinks({
|
|
49
|
+
consumerRoot,
|
|
50
|
+
workspaces,
|
|
51
|
+
patterns,
|
|
52
|
+
knownNames: consumerKnownNames(consumerRoot),
|
|
53
|
+
});
|
|
54
|
+
if (links.length === 0) {
|
|
55
|
+
console.error(`gjsify link: nothing to link — ${checkout} has ${workspaces.length} workspace package(s), ` +
|
|
56
|
+
`none of which ${consumerRoot} depends on${patterns.length === 0 ? '' : ` and matches ${patterns.join(', ')}`}.\n` +
|
|
57
|
+
`A link that links nothing is a silent no-op, so this is an error. Check the --packages ` +
|
|
58
|
+
`pattern, or run \`gjsify install\` first so the dependency tree exists.`);
|
|
59
|
+
return process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const label = `gjsify link${dryRun ? ' (dry-run)' : ''}`;
|
|
62
|
+
console.log(`${label} → ${checkout}`);
|
|
63
|
+
console.log(`${' '.repeat(label.length)} in ${consumerRoot}`);
|
|
64
|
+
for (const link of links) {
|
|
65
|
+
console.log(` ${link.name} → ${relative(checkout, link.target) || '.'}`);
|
|
66
|
+
}
|
|
67
|
+
// REFUSES rather than warns, and it refuses before anything is written.
|
|
68
|
+
// A link to an unbuilt package is not a smaller link, it is a tree whose
|
|
69
|
+
// first import fails — and the earlier draft's warning was printed once,
|
|
70
|
+
// at link time, while `gjsify install` re-applied the same link on every
|
|
71
|
+
// later run in silence (`utils/dev-link.ts`, `assertDevLinksBuilt`).
|
|
72
|
+
const unbuilt = unbuiltDevLinks(links);
|
|
73
|
+
if (unbuilt.length > 0) {
|
|
74
|
+
console.error(`\n${formatUnbuiltDevLinks(unbuilt)}`);
|
|
75
|
+
return process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
// A dry run that hides the REMOVALS is the half-report this feature keeps
|
|
78
|
+
// being caught by: retiring a link is the destructive half of a re-link,
|
|
79
|
+
// and it is the half worth seeing before it happens.
|
|
80
|
+
const sweep = sweepCheckouts(consumerRoot, checkout);
|
|
81
|
+
const keep = new Set(links.map((l) => l.name));
|
|
82
|
+
const stale = scanDevLinks(consumerRoot, sweep).filter((link) => !keep.has(link.name));
|
|
83
|
+
for (const link of stale)
|
|
84
|
+
console.log(` retire ${link.name} (no longer selected)`);
|
|
85
|
+
if (dryRun) {
|
|
86
|
+
console.log(`\n${label}: nothing written. Drop --dry-run to apply.`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// RE-linking is a re-SELECTION, not an addition: whatever the previous
|
|
90
|
+
// override linked and this one does not is removed here. Without it a
|
|
91
|
+
// narrower `--packages` left an orphan symlink out of `node_modules` that
|
|
92
|
+
// no later command would ever name, and every `gjsify install` after it
|
|
93
|
+
// aborted (utils/dev-link.ts, `scanDevLinks`). Both checkouts are swept,
|
|
94
|
+
// because the new override may point somewhere else entirely.
|
|
95
|
+
const retired = retireDevLinks(consumerRoot, sweep, keep);
|
|
96
|
+
const written = applyDevLinks(links);
|
|
97
|
+
writeDevLinkOverride(consumerRoot, { version: 1, checkout, packages: patterns });
|
|
98
|
+
const ignored = ensureLocallyIgnored(consumerRoot);
|
|
99
|
+
console.log(`\n ${written.length} link(s) written, ${links.length - written.length} already current.`);
|
|
100
|
+
// Counted against what was PLANNED, not just reported: `removeDevLinks`
|
|
101
|
+
// refuses a real directory sitting where a link belongs, and a retire that
|
|
102
|
+
// silently did less than it announced is the same half-report again.
|
|
103
|
+
if (stale.length > 0) {
|
|
104
|
+
console.log(` ${retired.length} of ${stale.length} retired link(s) removed.`);
|
|
105
|
+
}
|
|
106
|
+
console.log(` override: ${overridePath}`);
|
|
107
|
+
if (ignored === 'added')
|
|
108
|
+
console.log(` git: ${DEV_LINK_FILE} added to .git/info/exclude (local, never committed)`);
|
|
109
|
+
if (ignored === 'no-git')
|
|
110
|
+
console.log(` git: ${consumerRoot} is not a git repository — nothing to ignore`);
|
|
111
|
+
if (ignored === 'unreadable-git') {
|
|
112
|
+
console.warn(` ! git: ${consumerRoot} has a .git POINTER that does not parse, so ${DEV_LINK_FILE} is NOT ignored. ` +
|
|
113
|
+
`Add it to .gitignore by hand, or the next \`git add -A\` commits your local override.`);
|
|
114
|
+
}
|
|
115
|
+
console.log(`\nEvery \`gjsify install\` here now re-applies these links and says so.\n` +
|
|
116
|
+
`\`gjsify install --immutable\` refuses while the override exists. Undo: \`gjsify unlink\`.`);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
export const unlinkCommand = {
|
|
120
|
+
command: 'unlink',
|
|
121
|
+
description: 'Undo `gjsify link`: remove the development links and the .gjsify-link.json override, then reinstall the registry copies.',
|
|
122
|
+
builder: (yargs) => yargs
|
|
123
|
+
.option('dry-run', {
|
|
124
|
+
description: 'Print what would be removed and change nothing.',
|
|
125
|
+
type: 'boolean',
|
|
126
|
+
default: false,
|
|
127
|
+
})
|
|
128
|
+
.option('install', {
|
|
129
|
+
description: 'Reinstall after unlinking, to put the registry copies back where the links were. On by default; --no-install leaves the holes for a later `gjsify install`.',
|
|
130
|
+
type: 'boolean',
|
|
131
|
+
default: true,
|
|
132
|
+
}),
|
|
133
|
+
handler: async (args) => {
|
|
134
|
+
const consumerRoot = process.cwd();
|
|
135
|
+
const dryRun = args['dry-run'] ?? false;
|
|
136
|
+
const overridePath = devLinkPath(consumerRoot);
|
|
137
|
+
if (!existsSync(overridePath)) {
|
|
138
|
+
console.log(`gjsify unlink: no development link in ${consumerRoot} (no ${DEV_LINK_FILE}).`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// TWO readers, because the plan alone is not a census of what is linked
|
|
142
|
+
// (utils/dev-link.ts, `scanDevLinks`). A checkout that has since been
|
|
143
|
+
// deleted must still be unlinkable, so the dead-target refusal is caught
|
|
144
|
+
// here rather than propagated.
|
|
145
|
+
//
|
|
146
|
+
// The checkout is read separately from the PLAN, because the plan is the
|
|
147
|
+
// part that can fail (a checkout that has since been deleted, an unbuilt
|
|
148
|
+
// one) and the checkout path is what the tree sweep below needs.
|
|
149
|
+
const checkouts = [];
|
|
150
|
+
try {
|
|
151
|
+
const checkout = readDevLinkOverride(consumerRoot)?.checkout;
|
|
152
|
+
if (checkout)
|
|
153
|
+
checkouts.push(checkout);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Unparseable override: the sweep finds nothing, the plan below says
|
|
157
|
+
// why, and the override is still removed.
|
|
158
|
+
}
|
|
159
|
+
let links = [];
|
|
160
|
+
try {
|
|
161
|
+
links = prepareDevLinks(consumerRoot)?.links ?? [];
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
console.warn(`gjsify unlink: ${err instanceof Error ? err.message : String(err)}\n` +
|
|
165
|
+
` Removing the override anyway; any dangling links are cleaned by the reinstall.`);
|
|
166
|
+
}
|
|
167
|
+
const label = `gjsify unlink${dryRun ? ' (dry-run)' : ''}`;
|
|
168
|
+
console.log(`${label} in ${consumerRoot}`);
|
|
169
|
+
if (dryRun) {
|
|
170
|
+
const names = new Set(links.map((l) => l.name));
|
|
171
|
+
for (const link of scanDevLinks(consumerRoot, checkouts))
|
|
172
|
+
names.add(link.name);
|
|
173
|
+
for (const name of [...names].sort())
|
|
174
|
+
console.log(` would remove link ${name}`);
|
|
175
|
+
console.log(` would remove ${overridePath}`);
|
|
176
|
+
console.log(` would remove ${DEV_LINK_FILE} entry from .git/info/exclude (if this module wrote it)`);
|
|
177
|
+
console.log(`\n${label}: nothing changed. Drop --dry-run to apply.`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// ORDER IS THE FIX. The links go first, the override LAST: the override is
|
|
181
|
+
// the only record of which checkout to sweep, so removing it while a link
|
|
182
|
+
// is still standing destroys the escape route — measured, that left
|
|
183
|
+
// `gjsify unlink` reporting success, the reinstall aborting on the orphan,
|
|
184
|
+
// and no command able to name it again.
|
|
185
|
+
//
|
|
186
|
+
// And the sweep is by TREE, not by plan: `removeDevLinks(links)` only ever
|
|
187
|
+
// visits what the CURRENT override selects, which is exactly how the orphan
|
|
188
|
+
// survived in the first place.
|
|
189
|
+
const removed = removeDevLinks(links);
|
|
190
|
+
const kept = new Set(removed.map((l) => l.name));
|
|
191
|
+
for (const link of retireDevLinks(consumerRoot, checkouts, new Set())) {
|
|
192
|
+
if (kept.has(link.name))
|
|
193
|
+
continue;
|
|
194
|
+
removed.push(link);
|
|
195
|
+
}
|
|
196
|
+
for (const link of removed)
|
|
197
|
+
console.log(` unlinked ${link.name}`);
|
|
198
|
+
// Nothing may point out of `node_modules` any more, or the reinstall below
|
|
199
|
+
// dies on it. If something does, the override STAYS: it is what a later
|
|
200
|
+
// `gjsify unlink` needs, and a half-undone tree with no record of the
|
|
201
|
+
// checkout is the wedge this whole block exists to prevent.
|
|
202
|
+
const stragglers = scanDevLinks(consumerRoot, checkouts);
|
|
203
|
+
if (stragglers.length > 0) {
|
|
204
|
+
console.error(`gjsify unlink: ${stragglers.length} link(s) still point into a checkout and could not be removed:\n` +
|
|
205
|
+
stragglers.map((l) => ` ${l.name} ${l.linkPath} → ${l.target}`).join('\n') +
|
|
206
|
+
`\n ${overridePath} is kept so this stays undoable. Remove the paths above and re-run.`);
|
|
207
|
+
return process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
removeDevLinkOverride(consumerRoot);
|
|
210
|
+
console.log(` removed ${overridePath}`);
|
|
211
|
+
// The `.git/info/exclude` block is part of what `link` wrote, so it is part
|
|
212
|
+
// of what `unlink` owes back. Leaving it behind made the undo incomplete in
|
|
213
|
+
// a way nobody notices until they read the file and wonder what wrote it.
|
|
214
|
+
if (removeLocalIgnore(consumerRoot) === 'removed') {
|
|
215
|
+
console.log(` removed ${DEV_LINK_FILE} entry from .git/info/exclude`);
|
|
216
|
+
}
|
|
217
|
+
if (args.install === false) {
|
|
218
|
+
console.log(`\n ${removed.length} link(s) removed. Run \`gjsify install\` to restore the registry copies.`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
console.log(`\n ${removed.length} link(s) removed — reinstalling to restore the registry copies.\n`);
|
|
222
|
+
const { runCli } = await import('../cli-app.js');
|
|
223
|
+
await runCli(['install']);
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Which checkouts a re-link has to sweep: the one it is about to record, plus the
|
|
228
|
+
* one the CURRENT override names when that is a different directory.
|
|
229
|
+
*
|
|
230
|
+
* Every failure reading the old override is swallowed. A malformed or dead one
|
|
231
|
+
* must not stop a fresh `gjsify link` from fixing the situation, which is usually
|
|
232
|
+
* exactly why it is being run.
|
|
233
|
+
*/
|
|
234
|
+
function sweepCheckouts(consumerRoot, checkout) {
|
|
235
|
+
let previous = null;
|
|
236
|
+
try {
|
|
237
|
+
previous = readDevLinkOverride(consumerRoot)?.checkout ?? null;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
previous = null;
|
|
241
|
+
}
|
|
242
|
+
return previous && previous !== checkout ? [previous, checkout] : [checkout];
|
|
243
|
+
}
|
package/lib/commands/onboard.js
CHANGED
|
@@ -464,6 +464,11 @@ export const onboardCommand = {
|
|
|
464
464
|
};
|
|
465
465
|
// 4a. Bookkeeping-only rows, and the serial publish path.
|
|
466
466
|
const trustOnly = [];
|
|
467
|
+
// What THIS sweep has already put on the registry. A name is not served
|
|
468
|
+
// the instant its PUT is accepted, so the pin guard in `publishWorkspace`
|
|
469
|
+
// must not ask the registry about a package this loop created moments
|
|
470
|
+
// ago — target before dependent is the order, and the order is kept here.
|
|
471
|
+
const publishedHere = new Set();
|
|
467
472
|
for (let i = 0; i < plans.length; i++) {
|
|
468
473
|
const p = plans[i];
|
|
469
474
|
if (p.action === 'skip') {
|
|
@@ -494,6 +499,7 @@ export const onboardCommand = {
|
|
|
494
499
|
continue;
|
|
495
500
|
}
|
|
496
501
|
const pub = await publishWorkspace({
|
|
502
|
+
assumePresent: [...publishedHere],
|
|
497
503
|
wsDir: p.ws.location,
|
|
498
504
|
tag: 'latest',
|
|
499
505
|
access: args.access,
|
|
@@ -514,7 +520,13 @@ export const onboardCommand = {
|
|
|
514
520
|
failed++;
|
|
515
521
|
continue;
|
|
516
522
|
}
|
|
517
|
-
log(` published ${pub.name}@${'version' in pub ? pub.version : ''}`
|
|
523
|
+
log(` published ${pub.name}@${'version' in pub ? pub.version : ''}` +
|
|
524
|
+
('readback' in pub && pub.readback?.confirmed
|
|
525
|
+
? ` (verified on ${pub.registry} — ${pub.readback.attempts} probe(s))`
|
|
526
|
+
: pub.action === 'skipped-untrusted-new'
|
|
527
|
+
? ''
|
|
528
|
+
: ' (UNVERIFIED)'));
|
|
529
|
+
publishedHere.add(pub.name);
|
|
518
530
|
await configureTrust(p, i, pub.action !== 'skipped-untrusted-new');
|
|
519
531
|
}
|
|
520
532
|
// 4b. The trust-only packages, concurrently.
|
|
@@ -670,7 +682,11 @@ function describePublishFailure(pub) {
|
|
|
670
682
|
return (`npm ${pub.claim === 'already-published' ? 'reported it already published' : 'accepted the upload'} ` +
|
|
671
683
|
`(HTTP ${pub.putStatus}) but ${pub.name}@${pub.version} did not ` +
|
|
672
684
|
`resolve on ${pub.registry} after ${pub.readback.attempts} probe(s) over ` +
|
|
673
|
-
`${(pub.readback.elapsedMs / 1000).toFixed(1)}s`);
|
|
685
|
+
`${(pub.readback.elapsedMs / 1000).toFixed(1)}s [${pub.readback.verdict}: ${pub.readback.verdictDetail}]`);
|
|
686
|
+
case 'blocked-missing-dependency':
|
|
687
|
+
return (`not published — ${pub.missing.length} required pin(s) name a package ${pub.registry} does not ` +
|
|
688
|
+
`have: ${pub.missing.map((m) => `${m.block}.${m.name}@${m.spec}`).join(', ')}. Bootstrap the ` +
|
|
689
|
+
`target BEFORE its dependent; that order is a correctness property`);
|
|
674
690
|
case 'error':
|
|
675
691
|
return `HTTP ${pub.status} ${pub.statusText}`;
|
|
676
692
|
default:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Command } from '../types/index.js';
|
|
2
2
|
import { type Diagnose404Result } from '../utils/publish-diagnose.js';
|
|
3
3
|
import { type ReadbackResult } from '../utils/publish-readback.js';
|
|
4
|
+
import { type WorkspacePin } from '../utils/publish-pins.js';
|
|
4
5
|
import { OtpProvider } from '../utils/npm-otp.js';
|
|
5
6
|
interface PublishOptions {
|
|
6
7
|
path?: string;
|
|
@@ -29,6 +30,18 @@ export interface PublishWorkspaceInput {
|
|
|
29
30
|
tolerate: boolean;
|
|
30
31
|
/** Skip an un-bootstrapped new package on the OIDC path. */
|
|
31
32
|
tolerateUntrustedNew: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Names this CALLER has published in this same process, which the pin guard
|
|
35
|
+
* therefore does not ask the registry about.
|
|
36
|
+
*
|
|
37
|
+
* A brand-new name is not served the instant its PUT is accepted — the same
|
|
38
|
+
* eventual consistency that makes a version gate unsafe — so a sweep that
|
|
39
|
+
* bootstraps a target and its dependent in one run would otherwise block the
|
|
40
|
+
* dependent on a name it had just created itself. `gjsify onboard` is that
|
|
41
|
+
* sweep. The release sweep is `foreach --exec`, one process per package, so
|
|
42
|
+
* its children pass nothing and every edge is a real registry question.
|
|
43
|
+
*/
|
|
44
|
+
assumePresent?: readonly string[];
|
|
32
45
|
/** `--trusted` (force OIDC) / `undefined` | `'auto'` (auto-detect). */
|
|
33
46
|
trustedFlag: boolean | 'auto' | undefined;
|
|
34
47
|
/** Registry override. Default: `$npm_config_registry` → scope-aware npmrc → npmjs. */
|
|
@@ -94,6 +107,7 @@ export type PublishOutcome = {
|
|
|
94
107
|
name: string;
|
|
95
108
|
version: string;
|
|
96
109
|
status: number;
|
|
110
|
+
registry: string;
|
|
97
111
|
/** The read-back that CONFIRMED the conflicting version. */
|
|
98
112
|
readback?: ReadbackResult;
|
|
99
113
|
} | {
|
|
@@ -101,6 +115,26 @@ export type PublishOutcome = {
|
|
|
101
115
|
action: 'skipped-untrusted-new';
|
|
102
116
|
name: string;
|
|
103
117
|
version: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The tarball was NOT sent, because a required intra-repo pin in it names a
|
|
121
|
+
* package the registry does not have (#1713).
|
|
122
|
+
*
|
|
123
|
+
* `ok` is the TOLERANCE, not the health: under `--tolerate-untrusted-new`
|
|
124
|
+
* this is a skip the sweep continues past, because a fatal child stops
|
|
125
|
+
* `foreach --exec` mid-roster and leaves a PARTIAL publish. Either way the
|
|
126
|
+
* broken tarball stays off the registry, which is the difference from the
|
|
127
|
+
* shape this outcome exists to end — where the dependency was skipped, exit
|
|
128
|
+
* 0, and its dependent shipped a pin npm could not resolve.
|
|
129
|
+
*/
|
|
130
|
+
| {
|
|
131
|
+
ok: boolean;
|
|
132
|
+
action: 'blocked-missing-dependency';
|
|
133
|
+
name: string;
|
|
134
|
+
version: string;
|
|
135
|
+
registry: string;
|
|
136
|
+
tolerated: boolean;
|
|
137
|
+
missing: WorkspacePin[];
|
|
104
138
|
} | {
|
|
105
139
|
ok: false;
|
|
106
140
|
action: 'otp-required';
|