@gjsify/cli 0.64.12-next.281 → 0.64.13-next.283
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 +278 -255
- package/lib/actions/build.js +3 -4
- package/lib/commands/link.js +4 -3
- package/lib/commands/ship.js +36 -3
- package/lib/config.js +5 -11
- package/lib/types/config-data.d.ts +10 -0
- package/lib/utils/cli-runtime-closure.d.ts +0 -62
- package/lib/utils/cli-runtime-closure.js +1 -90
- package/lib/utils/dev-link.d.ts +21 -3
- package/lib/utils/dev-link.js +76 -39
- package/lib/utils/ship/dmg.d.ts +50 -2
- package/lib/utils/ship/dmg.js +72 -8
- package/lib/utils/ship/entry-interpreter.js +1 -1
- package/lib/utils/ship/release-record.d.ts +47 -0
- package/lib/utils/ship/release-record.js +55 -0
- package/lib/utils/ship/signing.d.ts +10 -9
- package/lib/utils/ship/signing.js +10 -9
- package/package.json +21 -21
- package/lib/utils/ship/gi-namespaces.d.ts +0 -23
- package/lib/utils/ship/gi-namespaces.js +0 -275
- package/lib/utils/ship/gi-selection.d.ts +0 -18
- package/lib/utils/ship/gi-selection.js +0 -73
package/lib/actions/build.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { runBundle, runWatch, bundleToChunks } from '../bundler-pick.js';
|
|
2
|
-
import { gjsifyPlugin, textLoaderPlugin, resolveShebangLine, NODE_SHEBANG, createGjsExternalsPredicate, globToEntryPoints, } from '@gjsify/rolldown-plugin-gjsify';
|
|
2
|
+
import { gjsifyPlugin, textLoaderPlugin, resolveShebangLine, NODE_SHEBANG, createGjsExternalsPredicate, globToEntryPoints, scanGiNamespaces, planGiSelection, } from '@gjsify/rolldown-plugin-gjsify';
|
|
3
3
|
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
4
4
|
import { resolveUserPlugins } from '../utils/resolve-plugin-by-name.js';
|
|
5
5
|
import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals, detectNodeGiGlobals, } from '@gjsify/rolldown-plugin-gjsify/globals';
|
|
@@ -15,8 +15,6 @@ import { collectEntryPaths, describeJsxConfig, findJsxEntryPoint, jsxConfigMissi
|
|
|
15
15
|
import { giSystemProbes } from '../utils/gi-runtime-paths.js';
|
|
16
16
|
import { escapeRawNulForGjs } from '../utils/gjs-source-escape.js';
|
|
17
17
|
import { assertNodeBundleGlobalsShimmed } from '../utils/node-bundle-guard.js';
|
|
18
|
-
import { scanGiNamespaces } from '../utils/ship/gi-namespaces.js';
|
|
19
|
-
import { planGiSelection } from '../utils/ship/gi-selection.js';
|
|
20
18
|
import { DEP_CHANGE_LOCKFILES } from '../utils/package-inputs.js';
|
|
21
19
|
const DEFAULT_GJS_SHEBANG = '#!/usr/bin/env -S gjs -m';
|
|
22
20
|
/**
|
|
@@ -700,7 +698,7 @@ export class BuildAction {
|
|
|
700
698
|
throw new Error('gjsify build: an .html entry needs an explicit --outfile ' +
|
|
701
699
|
'(e.g. --outfile dist-app/app.js); the emitted index.html is written beside it.');
|
|
702
700
|
}
|
|
703
|
-
const { consoleShim, globals, dialect, giRenderer } = this.configData;
|
|
701
|
+
const { consoleShim, globals, dialect, giRenderer, nativescriptLocale } = this.configData;
|
|
704
702
|
const userExternal = Array.isArray(userBundler.external) ? userBundler.external : undefined;
|
|
705
703
|
const userBanner = typeof userBundler.output?.banner === 'string' ? userBundler.output.banner : undefined;
|
|
706
704
|
const pluginOpts = {
|
|
@@ -712,6 +710,7 @@ export class BuildAction {
|
|
|
712
710
|
consoleShim,
|
|
713
711
|
...(dialect !== undefined ? { dialect } : {}),
|
|
714
712
|
...(giRenderer === true ? { giRenderer: true } : {}),
|
|
713
|
+
...(nativescriptLocale ? { nativescriptLocale } : {}),
|
|
715
714
|
...(aliases ? { aliases } : {}),
|
|
716
715
|
...(opts.preserveDefaultExport ? { preserveDefaultExport: true } : {}),
|
|
717
716
|
...(opts.toolchainAnchor !== undefined ? { toolchainAnchor: opts.toolchainAnchor } : {}),
|
package/lib/commands/link.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// step is an install anyway.
|
|
13
13
|
import { existsSync } from 'node:fs';
|
|
14
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';
|
|
15
|
+
import { applyDevLinks, consumerKnownNames, consumerNestedPlacements, DEV_LINK_FILE, devLinkPath, ensureLocallyIgnored, formatUnbuiltDevLinks, planDevLinks, prepareDevLinks, readDevLinkOverride, removeDevLinkOverride, removeDevLinks, removeLocalIgnore, resolveCheckoutWorkspaces, retireDevLinks, scanDevLinks, unbuiltDevLinks, writeDevLinkOverride, } from '../utils/dev-link.js';
|
|
16
16
|
export const linkCommand = {
|
|
17
17
|
command: 'link <checkout>',
|
|
18
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`.',
|
|
@@ -23,7 +23,7 @@ export const linkCommand = {
|
|
|
23
23
|
demandOption: true,
|
|
24
24
|
})
|
|
25
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
|
|
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 uses are linked: declared dependencies plus everything in its lockfile or installed node_modules, transitive and nested copies included.",
|
|
27
27
|
type: 'string',
|
|
28
28
|
array: true,
|
|
29
29
|
})
|
|
@@ -50,10 +50,11 @@ export const linkCommand = {
|
|
|
50
50
|
workspaces,
|
|
51
51
|
patterns,
|
|
52
52
|
knownNames: consumerKnownNames(consumerRoot),
|
|
53
|
+
nested: consumerNestedPlacements(consumerRoot),
|
|
53
54
|
});
|
|
54
55
|
if (links.length === 0) {
|
|
55
56
|
console.error(`gjsify link: nothing to link — ${checkout} has ${workspaces.length} workspace package(s), ` +
|
|
56
|
-
`none of which ${consumerRoot}
|
|
57
|
+
`none of which ${consumerRoot} uses (declared, locked or installed)${patterns.length === 0 ? '' : ` and matches ${patterns.join(', ')}`}.\n` +
|
|
57
58
|
`A link that links nothing is a silent no-op, so this is an error. Check the --packages ` +
|
|
58
59
|
`pattern, or run \`gjsify install\` first so the dependency tree exists.`);
|
|
59
60
|
return process.exit(1);
|
package/lib/commands/ship.js
CHANGED
|
@@ -39,7 +39,7 @@ import { resolveNodeModulesClosure, stageNodeModules } from '../utils/ship/node-
|
|
|
39
39
|
import { acquireGtkRuntime } from '../utils/ship/gtk-runtime-acquire.js';
|
|
40
40
|
import { discoverPayload } from '../utils/ship/discover.js';
|
|
41
41
|
import { appDirFor, appDirPayload, appImageHostRequirements, appImageRuntimeNotice, buildAppImage, findPinnedRuntime, stampAppDirTimes, } from '../utils/ship/appimage.js';
|
|
42
|
-
import { buildDmgImage, dmgVolumeDir, dmgVolumeName } from '../utils/ship/dmg.js';
|
|
42
|
+
import { addApplicationsLink, buildDmgImage, dmgVolumeDir, dmgVolumeName, signDmgImage } from '../utils/ship/dmg.js';
|
|
43
43
|
import { assertNoDroppedFlatpakModules } from '../utils/ship/flatpak-config.js';
|
|
44
44
|
import { buildFlatpakBundle } from '../utils/ship/flatpak.js';
|
|
45
45
|
import { iconSizes, resolveAppIcon } from '../utils/ship/icons.js';
|
|
@@ -52,10 +52,11 @@ import { compileSchemasForPayload, compileSchemasForStage } from '../utils/ship/
|
|
|
52
52
|
import { buildMsi, MSI_PAYLOAD_DIR } from '../utils/ship/msi.js';
|
|
53
53
|
import { buildZip, zipEntriesFromPayload } from '../utils/ship/zip.js';
|
|
54
54
|
import { assertEntryRunsUnder } from '../utils/ship/entry-interpreter.js';
|
|
55
|
-
import { scanGiRequirements } from '
|
|
55
|
+
import { scanGiRequirements } from '@gjsify/rolldown-plugin-gjsify';
|
|
56
56
|
import { assertLauncherMatchesInterpreter, assertPayloadMatchesArch, buildTimestamp, isArchIndependent, linuxInstallDependent, readPayloadFacts, } from '../utils/ship/payload.js';
|
|
57
57
|
import { assertOverlayIsLicensed, planOverlay, planStage } from '../utils/ship/plan.js';
|
|
58
|
-
import {
|
|
58
|
+
import { recordedSignature, writeReleaseRecord } from '../utils/ship/release-record.js';
|
|
59
|
+
import { ADHOC_IDENTITY, assertHostCanSign, notarizeArtifact, stapleArtifact, resolveNotaryPlan, resolveSignPlan, signPayload, } from '../utils/ship/signing.js';
|
|
59
60
|
import { renderLauncher } from '../utils/ship/launcher.js';
|
|
60
61
|
import { buildRpm } from '../utils/ship/rpm.js';
|
|
61
62
|
import { resolveShipApp, resolveShipBundle, resolveShipKind, resolveShipSettings, } from '../utils/ship/settings.js';
|
|
@@ -1261,6 +1262,7 @@ async function packOne(input) {
|
|
|
1261
1262
|
// mounts showing `Contents/` — a window with no application in it.
|
|
1262
1263
|
const volumeDir = dmgVolumeDir(outRoot);
|
|
1263
1264
|
writePayload(volumeDir, payload, '');
|
|
1265
|
+
addApplicationsLink(volumeDir);
|
|
1264
1266
|
await buildDmgImage({
|
|
1265
1267
|
volumeName: dmgVolumeName(settings),
|
|
1266
1268
|
sourceDir: volumeDir,
|
|
@@ -1268,6 +1270,17 @@ async function packOne(input) {
|
|
|
1268
1270
|
workDir: dirname(volumeDir),
|
|
1269
1271
|
verbose: input.verbose,
|
|
1270
1272
|
});
|
|
1273
|
+
// The IMAGE's own signature, before the notarisation below submits
|
|
1274
|
+
// it and the staple writes into it — see `utils/ship/dmg.ts`.
|
|
1275
|
+
if (input.sign.kind === 'sign') {
|
|
1276
|
+
await signDmgImage({
|
|
1277
|
+
identity: input.sign.identity,
|
|
1278
|
+
file: target,
|
|
1279
|
+
adhoc: input.sign.identity === ADHOC_IDENTITY,
|
|
1280
|
+
workDir: dirname(volumeDir),
|
|
1281
|
+
verbose: input.verbose,
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1271
1284
|
break;
|
|
1272
1285
|
}
|
|
1273
1286
|
case 'macos-app-zip':
|
|
@@ -1339,6 +1352,8 @@ async function packOne(input) {
|
|
|
1339
1352
|
// installer, so a `<App>.app` has no submittable form of its own; on darwin
|
|
1340
1353
|
// the zip beside it is the one that goes. Refusing here would make
|
|
1341
1354
|
// `--notarize` unusable with the default format set, which is both rows.
|
|
1355
|
+
let notarized = false;
|
|
1356
|
+
let stapled = false;
|
|
1342
1357
|
if (input.notarize.kind === 'notarize') {
|
|
1343
1358
|
if (format.artifactKind === 'directory') {
|
|
1344
1359
|
console.warn(`${LOG} ${format.id} is a directory, and notarytool submits an archive or an installer — ` +
|
|
@@ -1351,6 +1366,7 @@ async function packOne(input) {
|
|
|
1351
1366
|
verbose: input.verbose,
|
|
1352
1367
|
log: (line) => console.log(`${LOG} ${line}`),
|
|
1353
1368
|
});
|
|
1369
|
+
notarized = true;
|
|
1354
1370
|
// AND THEN THE TICKET, on the artifacts that can hold one. Submitting
|
|
1355
1371
|
// and stapling are two questions with two answers, and the `.zip` is
|
|
1356
1372
|
// where they come apart: the notary service accepts it and `stapler`
|
|
@@ -1365,6 +1381,7 @@ async function packOne(input) {
|
|
|
1365
1381
|
verbose: input.verbose,
|
|
1366
1382
|
log: (line) => console.log(`${LOG} ${line}`),
|
|
1367
1383
|
});
|
|
1384
|
+
stapled = true;
|
|
1368
1385
|
}
|
|
1369
1386
|
else {
|
|
1370
1387
|
console.warn(`${LOG} ${format.id} is notarised and NOT stapled: \`stapler\` takes a UDIF image, a ` +
|
|
@@ -1374,6 +1391,22 @@ async function packOne(input) {
|
|
|
1374
1391
|
}
|
|
1375
1392
|
}
|
|
1376
1393
|
}
|
|
1394
|
+
// THE RELEASE RECORD, after every step that writes into the file (ADR 0124).
|
|
1395
|
+
// A directory is never a release asset; the container beside it is.
|
|
1396
|
+
if (format.artifactKind === 'file') {
|
|
1397
|
+
const record = writeReleaseRecord({
|
|
1398
|
+
file: target,
|
|
1399
|
+
format: format.id,
|
|
1400
|
+
os: format.layoutOs,
|
|
1401
|
+
arch: isArchIndependent(payload) ? 'noarch' : input.arch,
|
|
1402
|
+
signature: recordedSignature(input.sign.kind === 'sign' ? input.sign.identity : undefined, ADHOC_IDENTITY),
|
|
1403
|
+
notarized,
|
|
1404
|
+
stapled,
|
|
1405
|
+
generator: `@gjsify/cli ${cliVersion()}`,
|
|
1406
|
+
});
|
|
1407
|
+
if (input.verbose)
|
|
1408
|
+
console.log(`${LOG} release record: ${record}`);
|
|
1409
|
+
}
|
|
1377
1410
|
return { format: format.id, path: target, size };
|
|
1378
1411
|
}
|
|
1379
1412
|
function printArtifacts(base, artifacts) {
|
package/lib/config.js
CHANGED
|
@@ -5,7 +5,7 @@ import { basename, dirname } from 'node:path';
|
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
import { isGjs, hostRuntime } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
7
7
|
import { buildAppForRuntime } from './utils/runtimes.js';
|
|
8
|
-
import { DIALECT_APPS, isSourceDialect, SOURCE_DIALECTS } from '@gjsify/rolldown-plugin-gjsify';
|
|
8
|
+
import { assertGiNamespaceEntries, DIALECT_APPS, isSourceDialect, SOURCE_DIALECTS, } from '@gjsify/rolldown-plugin-gjsify';
|
|
9
9
|
import { GI_RENDERER_APPS } from '@gjsify/resolve-npm';
|
|
10
10
|
/**
|
|
11
11
|
* Does the failed `import()` of a config look like a MODULE-RESOLUTION failure
|
|
@@ -326,16 +326,10 @@ export class Config {
|
|
|
326
326
|
'It selects what an Android APK carries (ADR 0117); every other target resolves ' +
|
|
327
327
|
'`gi://` against a GI stack the host already has.');
|
|
328
328
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
// typo rather than the shape.
|
|
334
|
-
if (!/^[A-Za-z][A-Za-z\d_]*(-\d+(\.\d+)*)?$/.test(entry)) {
|
|
335
|
-
throw new Error(`gjsify build: nativescript.giNamespaces entry "${entry}" is not a GI namespace. ` +
|
|
336
|
-
'Write it as `Gtk` or `Gtk-4.0`.');
|
|
337
|
-
}
|
|
338
|
-
}
|
|
329
|
+
// Shape-checked through the shared helper, because the Vite path accepts the
|
|
330
|
+
// same key and a check that holds on only one of them is a project that builds
|
|
331
|
+
// one way and fails the other for a typo.
|
|
332
|
+
assertGiNamespaceEntries(configData.nativescript?.giNamespaces ?? [], 'gjsify build: nativescript.giNamespaces');
|
|
339
333
|
if (cliArgs.globals !== undefined)
|
|
340
334
|
configData.globals = cliArgs.globals;
|
|
341
335
|
// Fallback applied post-merge (not as a yargs `default:`) so a
|
|
@@ -110,6 +110,16 @@ export interface ConfigData {
|
|
|
110
110
|
* into a hard build failure.
|
|
111
111
|
*/
|
|
112
112
|
giRenderer?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* `--app nativescript`: compile `poDirectory/<lang>.po` into
|
|
115
|
+
* `locale/<lang>/LC_MESSAGES/<domain>.mo` assets of the app, so the Android GLib gettext
|
|
116
|
+
* finds them under `<filesDir>/app/locale`. Config-file only (an object has no flag).
|
|
117
|
+
*/
|
|
118
|
+
nativescriptLocale?: {
|
|
119
|
+
poDirectory: string;
|
|
120
|
+
domain: string;
|
|
121
|
+
localeNames?: Record<string, string[]>;
|
|
122
|
+
};
|
|
113
123
|
/** Comma-separated global identifiers to register. Format: see CliBuildOptions. */
|
|
114
124
|
globals?: string;
|
|
115
125
|
/**
|
|
@@ -1,66 +1,4 @@
|
|
|
1
1
|
import type { Workspace } from '@gjsify/workspace';
|
|
2
|
-
/**
|
|
3
|
-
* Literal module specifiers of `code`: static `import`/`export … from`, plus
|
|
4
|
-
* `import('<literal>')`.
|
|
5
|
-
*
|
|
6
|
-
* acorn rather than a regex — see the header. `allowHashBang` because the CLI's
|
|
7
|
-
* own entry is a bin (`#!/usr/bin/env node`), and a parse failure yields NO
|
|
8
|
-
* specifiers, which under-selects rather than mis-selects.
|
|
9
|
-
*/
|
|
10
|
-
export interface SpecifierNode {
|
|
11
|
-
type?: string;
|
|
12
|
-
value?: unknown;
|
|
13
|
-
expressions?: unknown[];
|
|
14
|
-
quasis?: Array<{
|
|
15
|
-
value?: {
|
|
16
|
-
cooked?: unknown;
|
|
17
|
-
};
|
|
18
|
-
}>;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* The string a node names statically, or `null` when it is computed.
|
|
22
|
-
*
|
|
23
|
-
* A substitution-free TEMPLATE LITERAL is as static as a quoted string, and
|
|
24
|
-
* skipping it is not hypothetical: the minifier rewrites `await
|
|
25
|
-
* import('gi://GLib?version=2.0')` to a backtick form, so a Literal-only reader
|
|
26
|
-
* silently loses every dynamic import in a built bundle.
|
|
27
|
-
*
|
|
28
|
-
* Not named for specifiers, because it does not only read them: the same
|
|
29
|
-
* question is asked of a CALL ARGUMENT by `utils/ship/gi-namespaces.ts`, whose
|
|
30
|
-
* `requireGi("Gtk", "4.0")` carries a namespace and a version in exactly the two
|
|
31
|
-
* spellings above.
|
|
32
|
-
*/
|
|
33
|
-
export declare function staticStringValue(node: SpecifierNode | undefined): string | null;
|
|
34
|
-
/** One node of the walk: whatever acorn produced, seen as a bag of properties. */
|
|
35
|
-
export type AstNode = {
|
|
36
|
-
type?: string;
|
|
37
|
-
} & Record<string, unknown>;
|
|
38
|
-
/**
|
|
39
|
-
* Parse `code` as a module and call `visit` for every node in it.
|
|
40
|
-
*
|
|
41
|
-
* Exported because a SECOND parse is how the two readers of a built bundle
|
|
42
|
-
* would drift apart: {@link moduleSpecifiers} answers "what does this file
|
|
43
|
-
* import" and `utils/ship/gi-namespaces.ts` answers "which GI namespaces does
|
|
44
|
-
* it load", and the second question has an answer the first cannot see — a
|
|
45
|
-
* `--app node` bundle carries no `gi://` specifier at all, only the
|
|
46
|
-
* `requireGi()` calls the bundler rewrote them into. One walker, two readers.
|
|
47
|
-
*
|
|
48
|
-
* A parse failure yields NO nodes rather than throwing: every caller is
|
|
49
|
-
* summarising a file it did not write, and under-selecting is the failure mode
|
|
50
|
-
* that keeps a correct project buildable.
|
|
51
|
-
*/
|
|
52
|
-
export declare function walkModuleAst(code: string, visit: (node: AstNode) => void): void;
|
|
53
|
-
/**
|
|
54
|
-
* The module `node` imports from, or `null` when `node` is not an import at all.
|
|
55
|
-
*
|
|
56
|
-
* FOUR node types, in one place. Every reader of a built bundle in this tree asks
|
|
57
|
-
* the same question of the same four — a static `import`, both `export … from`
|
|
58
|
-
* forms and `import()` — and each copy of that list is a chance for one reader to
|
|
59
|
-
* miss a form the others catch. The bare side-effect `import "gi://Soup"` is what
|
|
60
|
-
* that costs when it happens (`utils/ship/gi-namespaces.ts`).
|
|
61
|
-
*/
|
|
62
|
-
export declare function importedSpecifier(node: AstNode): string | null;
|
|
63
|
-
export declare function moduleSpecifiers(code: string): string[];
|
|
64
2
|
export interface CliRuntimeClosureOptions {
|
|
65
3
|
/**
|
|
66
4
|
* File to start the walk from. Defaults to the running CLI's Node entry
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
// optional PEER dependencies. So the peers of every package in the graph are
|
|
52
52
|
// added: one declaration closes both blind spots, and it stays a declaration the
|
|
53
53
|
// package owns rather than a list this file keeps.
|
|
54
|
-
import * as acorn from 'acorn';
|
|
55
54
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
56
55
|
import { dirname, join, resolve as resolvePath } from 'node:path';
|
|
56
|
+
import { moduleSpecifiers } from '@gjsify/rolldown-plugin-gjsify';
|
|
57
57
|
import { cliPackageDir } from './publish-headers.js';
|
|
58
58
|
/**
|
|
59
59
|
* Split a bare specifier into a WORKSPACE package name + its `exports` key, or
|
|
@@ -78,95 +78,6 @@ function splitWorkspaceSpecifier(specifier, byName) {
|
|
|
78
78
|
}
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
|
-
/**
|
|
82
|
-
* The string a node names statically, or `null` when it is computed.
|
|
83
|
-
*
|
|
84
|
-
* A substitution-free TEMPLATE LITERAL is as static as a quoted string, and
|
|
85
|
-
* skipping it is not hypothetical: the minifier rewrites `await
|
|
86
|
-
* import('gi://GLib?version=2.0')` to a backtick form, so a Literal-only reader
|
|
87
|
-
* silently loses every dynamic import in a built bundle.
|
|
88
|
-
*
|
|
89
|
-
* Not named for specifiers, because it does not only read them: the same
|
|
90
|
-
* question is asked of a CALL ARGUMENT by `utils/ship/gi-namespaces.ts`, whose
|
|
91
|
-
* `requireGi("Gtk", "4.0")` carries a namespace and a version in exactly the two
|
|
92
|
-
* spellings above.
|
|
93
|
-
*/
|
|
94
|
-
export function staticStringValue(node) {
|
|
95
|
-
if (node?.type === 'Literal')
|
|
96
|
-
return typeof node.value === 'string' ? node.value : null;
|
|
97
|
-
if (node?.type === 'TemplateLiteral' && node.expressions?.length === 0) {
|
|
98
|
-
const cooked = node.quasis?.[0]?.value?.cooked;
|
|
99
|
-
return typeof cooked === 'string' ? cooked : null;
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Parse `code` as a module and call `visit` for every node in it.
|
|
105
|
-
*
|
|
106
|
-
* Exported because a SECOND parse is how the two readers of a built bundle
|
|
107
|
-
* would drift apart: {@link moduleSpecifiers} answers "what does this file
|
|
108
|
-
* import" and `utils/ship/gi-namespaces.ts` answers "which GI namespaces does
|
|
109
|
-
* it load", and the second question has an answer the first cannot see — a
|
|
110
|
-
* `--app node` bundle carries no `gi://` specifier at all, only the
|
|
111
|
-
* `requireGi()` calls the bundler rewrote them into. One walker, two readers.
|
|
112
|
-
*
|
|
113
|
-
* A parse failure yields NO nodes rather than throwing: every caller is
|
|
114
|
-
* summarising a file it did not write, and under-selecting is the failure mode
|
|
115
|
-
* that keeps a correct project buildable.
|
|
116
|
-
*/
|
|
117
|
-
export function walkModuleAst(code, visit) {
|
|
118
|
-
let ast;
|
|
119
|
-
try {
|
|
120
|
-
ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', allowHashBang: true });
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
const descend = (node) => {
|
|
126
|
-
if (!node || typeof node !== 'object')
|
|
127
|
-
return;
|
|
128
|
-
if (Array.isArray(node)) {
|
|
129
|
-
for (const child of node)
|
|
130
|
-
descend(child);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const n = node;
|
|
134
|
-
visit(n);
|
|
135
|
-
for (const [key, value] of Object.entries(n)) {
|
|
136
|
-
if (key === 'type' || key === 'start' || key === 'end' || key === 'loc')
|
|
137
|
-
continue;
|
|
138
|
-
descend(value);
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
descend(ast);
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* The module `node` imports from, or `null` when `node` is not an import at all.
|
|
145
|
-
*
|
|
146
|
-
* FOUR node types, in one place. Every reader of a built bundle in this tree asks
|
|
147
|
-
* the same question of the same four — a static `import`, both `export … from`
|
|
148
|
-
* forms and `import()` — and each copy of that list is a chance for one reader to
|
|
149
|
-
* miss a form the others catch. The bare side-effect `import "gi://Soup"` is what
|
|
150
|
-
* that costs when it happens (`utils/ship/gi-namespaces.ts`).
|
|
151
|
-
*/
|
|
152
|
-
export function importedSpecifier(node) {
|
|
153
|
-
if (node.type !== 'ImportDeclaration' &&
|
|
154
|
-
node.type !== 'ExportNamedDeclaration' &&
|
|
155
|
-
node.type !== 'ExportAllDeclaration' &&
|
|
156
|
-
node.type !== 'ImportExpression') {
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
return staticStringValue(node.source);
|
|
160
|
-
}
|
|
161
|
-
export function moduleSpecifiers(code) {
|
|
162
|
-
const out = [];
|
|
163
|
-
walkModuleAst(code, (node) => {
|
|
164
|
-
const specifier = importedSpecifier(node);
|
|
165
|
-
if (specifier !== null)
|
|
166
|
-
out.push(specifier);
|
|
167
|
-
});
|
|
168
|
-
return out;
|
|
169
|
-
}
|
|
170
81
|
/** The file a relative specifier names, probing the extensionless spellings Node accepts. */
|
|
171
82
|
function resolveRelative(fromFile, specifier) {
|
|
172
83
|
const base = resolvePath(dirname(fromFile), specifier);
|
package/lib/utils/dev-link.d.ts
CHANGED
|
@@ -83,6 +83,22 @@ export declare function resolveCheckoutWorkspaces(checkout: string, overridePath
|
|
|
83
83
|
* loads — are covered too).
|
|
84
84
|
*/
|
|
85
85
|
export declare function consumerKnownNames(consumerRoot: string): Set<string>;
|
|
86
|
+
/** A package copy that sits below another package's `node_modules`, not at the consumer's top level. */
|
|
87
|
+
export interface NestedPlacement {
|
|
88
|
+
name: string;
|
|
89
|
+
/** Absolute path of the copy. */
|
|
90
|
+
dir: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Real (non-symlink) copies installed below `<consumer>/node_modules/<pkg>/node_modules`.
|
|
94
|
+
*
|
|
95
|
+
* Node resolves the nearest copy, so a top-level link alone leaves a nested
|
|
96
|
+
* registry copy in charge for everything under its parent — the same silent
|
|
97
|
+
* "testing the published copy" this feature exists to prevent. Only what is on
|
|
98
|
+
* disk counts: the installer excludes linked names from its fetch set at every
|
|
99
|
+
* depth, so a nested copy that is not there yet will not appear.
|
|
100
|
+
*/
|
|
101
|
+
export declare function consumerNestedPlacements(consumerRoot: string): NestedPlacement[];
|
|
86
102
|
/**
|
|
87
103
|
* The links an override asks for: the checkout's workspace packages, narrowed by
|
|
88
104
|
* the name globs, INTERSECTED with what the consumer actually depends on.
|
|
@@ -98,6 +114,8 @@ export declare function planDevLinks(opts: {
|
|
|
98
114
|
workspaces: readonly Workspace[];
|
|
99
115
|
patterns: readonly string[];
|
|
100
116
|
knownNames: ReadonlySet<string>;
|
|
117
|
+
/** Installed copies below another package's `node_modules`; see {@link consumerNestedPlacements}. */
|
|
118
|
+
nested?: readonly NestedPlacement[];
|
|
101
119
|
}): DevLink[];
|
|
102
120
|
/**
|
|
103
121
|
* Read + validate the override at `consumerRoot` and plan its links, or `null`
|
|
@@ -136,9 +154,9 @@ export declare function removeDevLinks(links: readonly DevLink[]): DevLink[];
|
|
|
136
154
|
* it removed the override (the escape route) before hitting the same abort. Only a
|
|
137
155
|
* hand-written `rm` got the project back.
|
|
138
156
|
*
|
|
139
|
-
* So the ground truth for "what is linked" is the tree, not the file.
|
|
140
|
-
*
|
|
141
|
-
*
|
|
157
|
+
* So the ground truth for "what is linked" is the tree, not the file. Walks every
|
|
158
|
+
* `node_modules` level (top-level and nested copies) without following links;
|
|
159
|
+
* `.bin` and real directories are skipped by construction.
|
|
142
160
|
*/
|
|
143
161
|
export declare function scanDevLinks(consumerRoot: string, checkouts: readonly string[]): DevLink[];
|
|
144
162
|
/**
|
package/lib/utils/dev-link.js
CHANGED
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
// committed lockfile the moment a link is active, which is the one thing this
|
|
36
36
|
// feature promises not to do. `linkedNames` therefore applies after the lockfile
|
|
37
37
|
// is written and only to what gets fetched.
|
|
38
|
-
import { existsSync, lstatSync, mkdirSync,
|
|
39
|
-
import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
38
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
39
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
40
40
|
import { discoverWorkspaces, filterWorkspaces } from '@gjsify/workspace';
|
|
41
41
|
import { linkDirSync } from './dir-link.js';
|
|
42
42
|
import { readPackageJson } from './pkg-json.js';
|
|
@@ -179,8 +179,51 @@ export function consumerKnownNames(consumerRoot) {
|
|
|
179
179
|
}
|
|
180
180
|
for (const pkg of scanPrefix(consumerRoot))
|
|
181
181
|
names.add(pkg.name);
|
|
182
|
+
// The lockfile answers for a consumer that has not installed yet (or was
|
|
183
|
+
// cleaned): its keys are the whole resolved closure, transitive packages
|
|
184
|
+
// included, so a link made before the first install covers them too.
|
|
185
|
+
for (const name of lockfileNames(consumerRoot))
|
|
186
|
+
names.add(name);
|
|
182
187
|
return names;
|
|
183
188
|
}
|
|
189
|
+
/** Package names behind the `node_modules/…` keys of `gjsify-lock.json`; empty when unreadable. */
|
|
190
|
+
function lockfileNames(consumerRoot) {
|
|
191
|
+
try {
|
|
192
|
+
const lock = JSON.parse(readFileSync(join(consumerRoot, 'gjsify-lock.json'), 'utf-8'));
|
|
193
|
+
const names = [];
|
|
194
|
+
for (const key of Object.keys(lock.packages ?? {})) {
|
|
195
|
+
const at = key.lastIndexOf('node_modules/');
|
|
196
|
+
if (at !== -1)
|
|
197
|
+
names.push(key.slice(at + 'node_modules/'.length));
|
|
198
|
+
}
|
|
199
|
+
return names;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Real (non-symlink) copies installed below `<consumer>/node_modules/<pkg>/node_modules`.
|
|
207
|
+
*
|
|
208
|
+
* Node resolves the nearest copy, so a top-level link alone leaves a nested
|
|
209
|
+
* registry copy in charge for everything under its parent — the same silent
|
|
210
|
+
* "testing the published copy" this feature exists to prevent. Only what is on
|
|
211
|
+
* disk counts: the installer excludes linked names from its fetch set at every
|
|
212
|
+
* depth, so a nested copy that is not there yet will not appear.
|
|
213
|
+
*/
|
|
214
|
+
export function consumerNestedPlacements(consumerRoot) {
|
|
215
|
+
const top = join(consumerRoot, 'node_modules') + sep;
|
|
216
|
+
const nested = [];
|
|
217
|
+
for (const pkg of scanPrefix(consumerRoot)) {
|
|
218
|
+
if (pkg.linked)
|
|
219
|
+
continue;
|
|
220
|
+
const rel = pkg.dir.startsWith(top) ? pkg.dir.slice(top.length) : '';
|
|
221
|
+
if (!rel.includes(`${sep}node_modules${sep}`))
|
|
222
|
+
continue;
|
|
223
|
+
nested.push({ name: pkg.name, dir: pkg.dir });
|
|
224
|
+
}
|
|
225
|
+
return nested;
|
|
226
|
+
}
|
|
184
227
|
/**
|
|
185
228
|
* The links an override asks for: the checkout's workspace packages, narrowed by
|
|
186
229
|
* the name globs, INTERSECTED with what the consumer actually depends on.
|
|
@@ -209,7 +252,19 @@ export function planDevLinks(opts) {
|
|
|
209
252
|
linkPath: join(opts.consumerRoot, 'node_modules', ws.name),
|
|
210
253
|
});
|
|
211
254
|
}
|
|
212
|
-
|
|
255
|
+
// A nested copy under a package that is itself being linked disappears with
|
|
256
|
+
// that package's directory and resolves into the checkout's own tree instead.
|
|
257
|
+
const topLevel = links.map((l) => l.linkPath + sep);
|
|
258
|
+
const byName = new Map(links.map((l) => [l.name, l]));
|
|
259
|
+
for (const copy of opts.nested ?? []) {
|
|
260
|
+
const link = byName.get(copy.name);
|
|
261
|
+
if (!link)
|
|
262
|
+
continue;
|
|
263
|
+
if (topLevel.some((prefix) => copy.dir.startsWith(prefix)))
|
|
264
|
+
continue;
|
|
265
|
+
links.push({ name: copy.name, target: link.target, linkPath: copy.dir });
|
|
266
|
+
}
|
|
267
|
+
links.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : a.linkPath < b.linkPath ? -1 : a.linkPath > b.linkPath ? 1 : 0);
|
|
213
268
|
return links;
|
|
214
269
|
}
|
|
215
270
|
/**
|
|
@@ -228,6 +283,7 @@ export function prepareDevLinks(consumerRoot) {
|
|
|
228
283
|
workspaces,
|
|
229
284
|
patterns: override.packages,
|
|
230
285
|
knownNames: consumerKnownNames(consumerRoot),
|
|
286
|
+
nested: consumerNestedPlacements(consumerRoot),
|
|
231
287
|
});
|
|
232
288
|
return {
|
|
233
289
|
overridePath,
|
|
@@ -325,9 +381,9 @@ function linkAlreadyPointsAt(linkPath, target) {
|
|
|
325
381
|
* it removed the override (the escape route) before hitting the same abort. Only a
|
|
326
382
|
* hand-written `rm` got the project back.
|
|
327
383
|
*
|
|
328
|
-
* So the ground truth for "what is linked" is the tree, not the file.
|
|
329
|
-
*
|
|
330
|
-
*
|
|
384
|
+
* So the ground truth for "what is linked" is the tree, not the file. Walks every
|
|
385
|
+
* `node_modules` level (top-level and nested copies) without following links;
|
|
386
|
+
* `.bin` and real directories are skipped by construction.
|
|
331
387
|
*/
|
|
332
388
|
export function scanDevLinks(consumerRoot, checkouts) {
|
|
333
389
|
const roots = [];
|
|
@@ -343,50 +399,31 @@ export function scanDevLinks(consumerRoot, checkouts) {
|
|
|
343
399
|
}
|
|
344
400
|
if (roots.length === 0)
|
|
345
401
|
return [];
|
|
346
|
-
|
|
402
|
+
// Every depth: `assertNodeModulesDest` aborts on a link out of `node_modules`
|
|
403
|
+
// wherever it sits, and `scanPrefix` never descends into a link itself.
|
|
347
404
|
const found = [];
|
|
348
|
-
const
|
|
349
|
-
|
|
405
|
+
for (const pkg of scanPrefix(consumerRoot)) {
|
|
406
|
+
if (!pkg.linked)
|
|
407
|
+
continue;
|
|
350
408
|
let target;
|
|
351
409
|
try {
|
|
352
|
-
|
|
353
|
-
return;
|
|
354
|
-
target = realpathSync(linkPath);
|
|
410
|
+
target = realpathSync(pkg.dir);
|
|
355
411
|
}
|
|
356
412
|
catch {
|
|
357
|
-
|
|
413
|
+
continue;
|
|
358
414
|
}
|
|
359
415
|
if (!roots.some((root) => target === root || target.startsWith(root + sep)))
|
|
360
|
-
return;
|
|
361
|
-
found.push({ name, target, linkPath });
|
|
362
|
-
};
|
|
363
|
-
let entries;
|
|
364
|
-
try {
|
|
365
|
-
entries = readdirSync(nodeModules);
|
|
366
|
-
}
|
|
367
|
-
catch {
|
|
368
|
-
return [];
|
|
369
|
-
}
|
|
370
|
-
for (const entry of entries) {
|
|
371
|
-
if (entry.startsWith('.'))
|
|
372
416
|
continue;
|
|
373
|
-
|
|
374
|
-
let scoped;
|
|
375
|
-
try {
|
|
376
|
-
scoped = readdirSync(join(nodeModules, entry));
|
|
377
|
-
}
|
|
378
|
-
catch {
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
381
|
-
for (const inner of scoped)
|
|
382
|
-
consider(`${entry}/${inner}`);
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
consider(entry);
|
|
417
|
+
found.push({ name: packageNameAt(consumerRoot, pkg.dir), target, linkPath: pkg.dir });
|
|
386
418
|
}
|
|
387
|
-
found.sort((a, b) => (a.
|
|
419
|
+
found.sort((a, b) => (a.linkPath < b.linkPath ? -1 : a.linkPath > b.linkPath ? 1 : 0));
|
|
388
420
|
return found;
|
|
389
421
|
}
|
|
422
|
+
/** `@scope/name` of the last `node_modules/` segment in `dir`. */
|
|
423
|
+
function packageNameAt(consumerRoot, dir) {
|
|
424
|
+
const rel = relative(consumerRoot, dir).split(sep).join('/');
|
|
425
|
+
return rel.slice(rel.lastIndexOf('node_modules/') + 'node_modules/'.length);
|
|
426
|
+
}
|
|
390
427
|
/**
|
|
391
428
|
* Remove every dev link into `checkouts` that `keep` does not name.
|
|
392
429
|
*
|