@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
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// `gjsify webext build|zip|dev` — browser extensions as a project shape
|
|
2
|
+
// (ADR 0077). The command owns the folder around the bundles: one per target,
|
|
3
|
+
// with a manifest composed for that target, pages, icons, public files and
|
|
4
|
+
// locales. The bundles themselves are ordinary `gjsify build --app browser`
|
|
5
|
+
// runs, re-entered in-process like `gjsify dev` does.
|
|
6
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
8
|
+
import { buildWebext } from '../../utils/webext/build.js';
|
|
9
|
+
import { resolveWebextConfig, selectTargets } from '../../utils/webext/config.js';
|
|
10
|
+
import { defaultDevProfile, resolveWebExt, WEB_EXT_MISSING, webExtRunArgs, } from '../../utils/webext/launch.js';
|
|
11
|
+
function loadConfig(cwd) {
|
|
12
|
+
const manifest = join(cwd, 'package.json');
|
|
13
|
+
if (!existsSync(manifest))
|
|
14
|
+
throw new Error(`gjsify webext: no package.json in ${cwd}.`);
|
|
15
|
+
return resolveWebextConfig(JSON.parse(readFileSync(manifest, 'utf8')), cwd);
|
|
16
|
+
}
|
|
17
|
+
/** `--define K=V` entries on top of the config's map, the way `gjsify build` spells them. */
|
|
18
|
+
function effectiveDefines(config, flags) {
|
|
19
|
+
const out = { ...config.define };
|
|
20
|
+
for (const flag of flags) {
|
|
21
|
+
const at = flag.indexOf('=');
|
|
22
|
+
if (at <= 0)
|
|
23
|
+
throw new Error(`gjsify webext: --define "${flag}" is not KEY=VALUE.`);
|
|
24
|
+
out[flag.slice(0, at)] = flag.slice(at + 1);
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
/** One bundle, through the ordinary build command in this process. */
|
|
29
|
+
async function bundleInProcess(request) {
|
|
30
|
+
const { runCli } = await import('../../cli-app.js');
|
|
31
|
+
const argv = ['build', request.entry, '--app', 'browser', '--format', request.format, '--outfile', request.outfile];
|
|
32
|
+
for (const [key, value] of Object.entries(request.define))
|
|
33
|
+
argv.push('--define', `${key}=${value}`);
|
|
34
|
+
if (!request.minify)
|
|
35
|
+
argv.push('--no-minify');
|
|
36
|
+
await runCli(argv);
|
|
37
|
+
}
|
|
38
|
+
const targetOption = {
|
|
39
|
+
description: 'Build only these targets (default: every target in gjsify.webext.targets).',
|
|
40
|
+
type: 'string',
|
|
41
|
+
array: true,
|
|
42
|
+
};
|
|
43
|
+
const outDirOption = {
|
|
44
|
+
description: 'Output directory (default: gjsify.webext.outDir, else .output).',
|
|
45
|
+
type: 'string',
|
|
46
|
+
};
|
|
47
|
+
const defineOption = {
|
|
48
|
+
description: 'Compile-time constant for every bundle, KEY=VALUE (repeatable); wins over gjsify.webext.define.',
|
|
49
|
+
type: 'string',
|
|
50
|
+
array: true,
|
|
51
|
+
default: [],
|
|
52
|
+
};
|
|
53
|
+
async function runBuild(args, zip) {
|
|
54
|
+
const cwd = process.cwd();
|
|
55
|
+
const config = loadConfig(cwd);
|
|
56
|
+
const mode = args.mode === 'development' ? 'development' : 'production';
|
|
57
|
+
await buildWebext({
|
|
58
|
+
config,
|
|
59
|
+
targets: selectTargets(config, args.target),
|
|
60
|
+
outDir: args.outDir ? resolve(cwd, args.outDir) : config.outDir,
|
|
61
|
+
mode,
|
|
62
|
+
define: effectiveDefines(config, args.define),
|
|
63
|
+
inPlace: false,
|
|
64
|
+
zip,
|
|
65
|
+
bundle: bundleInProcess,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export const webextBuildCommand = {
|
|
69
|
+
command: 'build',
|
|
70
|
+
description: 'Build one extension folder per target: bundles, pages, icons, locales and the manifest.',
|
|
71
|
+
builder: (yargs) => yargs
|
|
72
|
+
.option('target', targetOption)
|
|
73
|
+
.option('out-dir', outDirOption)
|
|
74
|
+
.option('define', defineOption)
|
|
75
|
+
.option('mode', {
|
|
76
|
+
description: 'Passed to the manifest as ctx.mode; development also skips minification.',
|
|
77
|
+
type: 'string',
|
|
78
|
+
choices: ['production', 'development'],
|
|
79
|
+
default: 'production',
|
|
80
|
+
}),
|
|
81
|
+
handler: (args) => runBuild(args, false),
|
|
82
|
+
};
|
|
83
|
+
export const webextZipCommand = {
|
|
84
|
+
command: 'zip',
|
|
85
|
+
description: 'Production build, then one store-ready <name>-<version>-<target>.zip per target.',
|
|
86
|
+
builder: (yargs) => yargs.option('target', targetOption).option('out-dir', outDirOption).option('define', defineOption),
|
|
87
|
+
handler: (args) => runBuild(args, true),
|
|
88
|
+
};
|
|
89
|
+
/** The dev target when none is asked for: the first Firefox target, since web-ext's Firefox driver needs no extra binary. */
|
|
90
|
+
function devTarget(config, requested) {
|
|
91
|
+
if (requested && requested.length > 1)
|
|
92
|
+
throw new Error('gjsify webext dev: pass one --target.');
|
|
93
|
+
const [chosen] = selectTargets(config, requested);
|
|
94
|
+
if (requested && requested.length === 1)
|
|
95
|
+
return chosen;
|
|
96
|
+
return config.targets.find((t) => t.browser === 'firefox') ?? chosen;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* What the loop watches: every top-level directory except `node_modules`,
|
|
100
|
+
* dot-directories and the output, recursively; the project root itself only
|
|
101
|
+
* for its own entries (`manifest.ts`, `package.json`); and `watch` extras.
|
|
102
|
+
*/
|
|
103
|
+
function devWatchTargets(config, outDir) {
|
|
104
|
+
const skip = (name) => name === 'node_modules' || name.startsWith('.');
|
|
105
|
+
const out = [{ path: config.root, recursive: false }];
|
|
106
|
+
for (const entry of readdirSync(config.root, { withFileTypes: true })) {
|
|
107
|
+
const full = join(config.root, entry.name);
|
|
108
|
+
if (!entry.isDirectory() || skip(entry.name) || full === outDir || outDir.startsWith(full + sep))
|
|
109
|
+
continue;
|
|
110
|
+
out.push({ path: full, recursive: true });
|
|
111
|
+
}
|
|
112
|
+
for (const extra of config.watch)
|
|
113
|
+
out.push({ path: extra, recursive: true });
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
export const webextDevCommand = {
|
|
117
|
+
command: 'dev',
|
|
118
|
+
description: 'Build one target in development mode, launch it in a browser via web-ext, and rebuild in place on change.',
|
|
119
|
+
builder: (yargs) => yargs
|
|
120
|
+
.option('target', {
|
|
121
|
+
...targetOption,
|
|
122
|
+
description: 'The target to run (default: the first Firefox target).',
|
|
123
|
+
})
|
|
124
|
+
.option('out-dir', { description: 'Output directory (default: <outDir>-dev).', type: 'string' })
|
|
125
|
+
.option('define', defineOption)
|
|
126
|
+
.option('launch', {
|
|
127
|
+
description: 'Start a browser with the extension (--no-launch only rebuilds).',
|
|
128
|
+
type: 'boolean',
|
|
129
|
+
default: true,
|
|
130
|
+
})
|
|
131
|
+
.option('browser-binary', { description: 'Browser executable web-ext should start.', type: 'string' })
|
|
132
|
+
.option('profile', {
|
|
133
|
+
description: 'Browser profile directory (default: $XDG_CACHE_HOME/gjsify/webext/<name>/<target>).',
|
|
134
|
+
type: 'string',
|
|
135
|
+
})
|
|
136
|
+
.option('headless', { description: 'Start the browser without a window.', type: 'boolean', default: false })
|
|
137
|
+
.option('debounce', { description: 'Quiet window in ms before a rebuild.', type: 'number', default: 200 }),
|
|
138
|
+
handler: async (args) => {
|
|
139
|
+
const cwd = process.cwd();
|
|
140
|
+
const config = loadConfig(cwd);
|
|
141
|
+
const target = devTarget(config, args.target);
|
|
142
|
+
const outDir = args.outDir ? resolve(cwd, args.outDir) : `${config.outDir}-dev`;
|
|
143
|
+
const define = effectiveDefines(config, args.define);
|
|
144
|
+
const sourceDir = join(outDir, target.id);
|
|
145
|
+
let webExt = null;
|
|
146
|
+
if (args.launch) {
|
|
147
|
+
webExt = resolveWebExt(cwd);
|
|
148
|
+
if (webExt === null)
|
|
149
|
+
throw new Error(WEB_EXT_MISSING);
|
|
150
|
+
}
|
|
151
|
+
let browser = null;
|
|
152
|
+
const stopBrowser = () => {
|
|
153
|
+
browser?.kill();
|
|
154
|
+
};
|
|
155
|
+
// Registered BEFORE the loop's own handlers, which end in process.exit.
|
|
156
|
+
process.on('SIGINT', stopBrowser);
|
|
157
|
+
process.on('SIGTERM', stopBrowser);
|
|
158
|
+
const launch = async () => {
|
|
159
|
+
if (webExt === null || browser !== null)
|
|
160
|
+
return;
|
|
161
|
+
const profile = args.profile ? resolve(cwd, args.profile) : defaultDevProfile(config.name, target);
|
|
162
|
+
// web-ext creates the profile directory but not its parent (ENOENT on a fresh machine).
|
|
163
|
+
mkdirSync(dirname(profile), { recursive: true });
|
|
164
|
+
const argv = webExtRunArgs({
|
|
165
|
+
sourceDir,
|
|
166
|
+
target,
|
|
167
|
+
profile,
|
|
168
|
+
browserBinary: args.browserBinary,
|
|
169
|
+
headless: args.headless,
|
|
170
|
+
});
|
|
171
|
+
console.log(`[webext] launching ${target.id} with profile ${profile}`);
|
|
172
|
+
const { spawnSupervised } = await import('../../utils/watch-loop.js');
|
|
173
|
+
browser = await spawnSupervised(webExt.cmd, argv, webExt.env);
|
|
174
|
+
browser.on('exit', (code) => {
|
|
175
|
+
console.log(`[webext] browser closed (${code ?? 0}) — stopping`);
|
|
176
|
+
process.exit(0);
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
const { runWatchLoop } = await import('../../utils/watch-loop.js');
|
|
180
|
+
await runWatchLoop({
|
|
181
|
+
dir: cwd,
|
|
182
|
+
dirLabel: '.',
|
|
183
|
+
label: 'webext',
|
|
184
|
+
watch: devWatchTargets(config, outDir),
|
|
185
|
+
ignore: (changed) => changed === outDir || changed.startsWith(outDir + sep),
|
|
186
|
+
build: async () => {
|
|
187
|
+
await buildWebext({
|
|
188
|
+
config,
|
|
189
|
+
targets: [target],
|
|
190
|
+
outDir,
|
|
191
|
+
mode: 'development',
|
|
192
|
+
define,
|
|
193
|
+
inPlace: true,
|
|
194
|
+
zip: false,
|
|
195
|
+
bundle: bundleInProcess,
|
|
196
|
+
});
|
|
197
|
+
// After the first SUCCESSFUL build: web-ext on a folder with no manifest.json exits at once.
|
|
198
|
+
await launch();
|
|
199
|
+
},
|
|
200
|
+
spawnChild: null,
|
|
201
|
+
debounceMs: args.debounce,
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
};
|
|
@@ -743,7 +743,7 @@ export interface ShipFlatpakOptions {
|
|
|
743
743
|
* here (ADR 0024 § 8; the window is `utils/ship/flatpak-config.ts`). They are
|
|
744
744
|
* NOT marked `@deprecated`: for the subcommands in this group they are still
|
|
745
745
|
* the only spelling, and those commands have not moved under `ship` yet
|
|
746
|
-
* (`status/open-todos.md`). Nothing else in this block is deprecated at all —
|
|
746
|
+
* (`status/open-todos/README.md`). Nothing else in this block is deprecated at all —
|
|
747
747
|
* the {@link AppMetadata} half is a designed alias, not a legacy one.
|
|
748
748
|
*/
|
|
749
749
|
export interface ConfigDataFlatpak extends AppMetadata {
|
|
@@ -771,7 +771,7 @@ export interface ConfigDataFlatpak extends AppMetadata {
|
|
|
771
771
|
command?: string;
|
|
772
772
|
/** Finish-args (capabilities). Default depends on `runtime` + `--cli-only`. */
|
|
773
773
|
finishArgs?: string[];
|
|
774
|
-
/** Extra Flatpak modules prepended before the app's own meson/simple module (e.g.
|
|
774
|
+
/** Extra Flatpak modules prepended before the app's own meson/simple module (e.g. a GI library the runtime lacks). */
|
|
775
775
|
extraModules?: unknown[];
|
|
776
776
|
/**
|
|
777
777
|
* Full replacement for the manifest's `modules` array: used verbatim, with
|
package/lib/utils/bin-shim.d.ts
CHANGED
|
@@ -138,7 +138,7 @@ export declare function buildLauncherShims(opts: {
|
|
|
138
138
|
* in shell would be a knowingly partial THIRD copy of a two-copy rule, in the
|
|
139
139
|
* one language nothing here type-checks. The REMAINING gap — a host GI stack
|
|
140
140
|
* under a prefix dyld never defaulted to, `/opt/homebrew/lib` on Apple silicon —
|
|
141
|
-
* stays in `status/open-todos.md`, to be closed by teaching such a launcher to
|
|
141
|
+
* stays in `status/open-todos/README.md`, to be closed by teaching such a launcher to
|
|
142
142
|
* defer to the CLI rather than to re-derive.
|
|
143
143
|
*
|
|
144
144
|
* @param scanRoot Directory whose `node_modules` is globbed at launch time.
|
|
@@ -178,7 +178,7 @@ export declare function buildNativeEnvPreamble(scanRoot: string, bakedDirs?: rea
|
|
|
178
178
|
*
|
|
179
179
|
* NOT the host's GI libdirs: `/opt/homebrew/lib` was never in dyld's default
|
|
180
180
|
* either, so an ARM Mac has that gap with or without gjsify —
|
|
181
|
-
* `status/open-todos.md`.
|
|
181
|
+
* `status/open-todos/README.md`.
|
|
182
182
|
*/
|
|
183
183
|
export declare function dyldFallbackPreamble(platform: string): string;
|
|
184
184
|
/** POSIX `sh` single-quoting for a launcher's target path. */
|
|
@@ -263,6 +263,12 @@ export declare function buildShLauncher(targetAbs: string, opts: {
|
|
|
263
263
|
envPreamble?: string;
|
|
264
264
|
isGjsBundle: boolean;
|
|
265
265
|
nodeFallbackAbs?: string;
|
|
266
|
+
/**
|
|
267
|
+
* The gjs to exec — `GJS_CONSOLE` for the CLI's self-shim. NAMED rather
|
|
268
|
+
* than pathed by default, so a missing interpreter fails the same way on
|
|
269
|
+
* every OS.
|
|
270
|
+
*/
|
|
271
|
+
gjs?: string;
|
|
266
272
|
}): string;
|
|
267
273
|
/** True when a bin target is a GJS-runnable bundle rather than a Node script. */
|
|
268
274
|
export declare function isGjsBundlePath(target: string): boolean;
|
package/lib/utils/bin-shim.js
CHANGED
|
@@ -349,7 +349,7 @@ function shQuote(s) {
|
|
|
349
349
|
* in shell would be a knowingly partial THIRD copy of a two-copy rule, in the
|
|
350
350
|
* one language nothing here type-checks. The REMAINING gap — a host GI stack
|
|
351
351
|
* under a prefix dyld never defaulted to, `/opt/homebrew/lib` on Apple silicon —
|
|
352
|
-
* stays in `status/open-todos.md`, to be closed by teaching such a launcher to
|
|
352
|
+
* stays in `status/open-todos/README.md`, to be closed by teaching such a launcher to
|
|
353
353
|
* defer to the CLI rather than to re-derive.
|
|
354
354
|
*
|
|
355
355
|
* @param scanRoot Directory whose `node_modules` is globbed at launch time.
|
|
@@ -449,7 +449,7 @@ export function buildNativeEnvPreamble(scanRoot, bakedDirs = [], target = {}) {
|
|
|
449
449
|
*
|
|
450
450
|
* NOT the host's GI libdirs: `/opt/homebrew/lib` was never in dyld's default
|
|
451
451
|
* either, so an ARM Mac has that gap with or without gjsify —
|
|
452
|
-
* `status/open-todos.md`.
|
|
452
|
+
* `status/open-todos/README.md`.
|
|
453
453
|
*/
|
|
454
454
|
export function dyldFallbackPreamble(platform) {
|
|
455
455
|
if (platform !== 'darwin')
|
|
@@ -556,15 +556,16 @@ export function pickBinMap(pkgName, pkgJson) {
|
|
|
556
556
|
*/
|
|
557
557
|
export function buildShLauncher(targetAbs, opts) {
|
|
558
558
|
const preamble = opts.isGjsBundle ? (opts.envPreamble ?? '') : '';
|
|
559
|
+
const gjs = opts.gjs === undefined || opts.gjs === 'gjs' ? 'gjs' : shQuote(opts.gjs);
|
|
559
560
|
if (opts.isGjsBundle && opts.nodeFallbackAbs !== undefined) {
|
|
560
561
|
// The preamble only ever configures the GI search path, so it stays
|
|
561
562
|
// inside the gjs branch — exporting it around a Node exec would be
|
|
562
563
|
// inert at best and misleading to anyone reading the shim.
|
|
563
|
-
return (`#!/bin/sh\nif command -v gjs >/dev/null 2>&1; then\n${preamble}` +
|
|
564
|
-
`exec gjs -m ${shQuote(targetAbs)} "$@"\nfi\n` +
|
|
564
|
+
return (`#!/bin/sh\nif command -v ${gjs} >/dev/null 2>&1; then\n${preamble}` +
|
|
565
|
+
`exec ${gjs} -m ${shQuote(targetAbs)} "$@"\nfi\n` +
|
|
565
566
|
`exec node ${shQuote(opts.nodeFallbackAbs)} "$@"\n`);
|
|
566
567
|
}
|
|
567
|
-
const exec = opts.isGjsBundle ? `exec gjs -m ${shQuote(targetAbs)} "$@"` : `exec ${shQuote(targetAbs)} "$@"`;
|
|
568
|
+
const exec = opts.isGjsBundle ? `exec ${gjs} -m ${shQuote(targetAbs)} "$@"` : `exec ${shQuote(targetAbs)} "$@"`;
|
|
568
569
|
return `#!/bin/sh\n${preamble}${exec}\n`;
|
|
569
570
|
}
|
|
570
571
|
/** True when a bin target is a GJS-runnable bundle rather than a Node script. */
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { RolldownPluginOption } from 'rolldown';
|
|
2
|
+
/**
|
|
3
|
+
* Bumped when the shape changes; a manifest of any other version is ignored.
|
|
4
|
+
*
|
|
5
|
+
* v2 adds `watchList`. A v1 manifest says nothing about whether the engine that
|
|
6
|
+
* wrote it could report a watch list at all, and the safe reading of "unknown"
|
|
7
|
+
* is "do not trust it": see {@link readBundleInputs}.
|
|
8
|
+
*/
|
|
9
|
+
export declare const BUNDLE_INPUTS_VERSION = 2;
|
|
10
|
+
/** What a manifest says, once parsed. */
|
|
11
|
+
export interface BundleInputs {
|
|
12
|
+
inputs: string[];
|
|
13
|
+
/** `false` = this bundle's build could not see plugin-read inputs at all. */
|
|
14
|
+
watchList: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** The manifest that belongs to `outfile`. */
|
|
17
|
+
export declare function bundleInputsPath(outfile: string): string;
|
|
18
|
+
export interface BundleInputsRecorder {
|
|
19
|
+
/** Pass to the bundler as a plugin; it observes, it never transforms. */
|
|
20
|
+
plugin: RolldownPluginOption;
|
|
21
|
+
/**
|
|
22
|
+
* The build's own watch list — what the engine reports, which is its module
|
|
23
|
+
* set PLUS everything a plugin declared with `this.addWatchFile`. That is
|
|
24
|
+
* the only account of a file the bundler never turned into a module: the
|
|
25
|
+
* CSS plugin follows a stylesheet's `@import` chain with its own `readFile`,
|
|
26
|
+
* and no graph mentions the imported file.
|
|
27
|
+
*/
|
|
28
|
+
addWatchFiles(watch: {
|
|
29
|
+
files: readonly string[];
|
|
30
|
+
reportedByEngine: boolean;
|
|
31
|
+
}): void;
|
|
32
|
+
/** The recorded inputs so far: sorted, deduplicated, existing files only. */
|
|
33
|
+
inputs(): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Persist the manifest beside `outfile`, so the NEXT freshness check can
|
|
36
|
+
* judge against the same set. Never throws: a manifest that cannot be
|
|
37
|
+
* written must not fail a test run — it degrades the check to the package
|
|
38
|
+
* walk, and says so.
|
|
39
|
+
*/
|
|
40
|
+
write(): void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A plugin that records every module the build transforms, plus the writer for
|
|
44
|
+
* the manifest.
|
|
45
|
+
*
|
|
46
|
+
* `transform` rather than a graph API (`getModuleIds`/`getModuleInfo`) because
|
|
47
|
+
* it is the one hook both engines run and the native bridge translates
|
|
48
|
+
* (`bundler-pick.ts` picks it verbatim). Every module that reaches the
|
|
49
|
+
* transform stage was LOADED from disk, so the observed set is what the build
|
|
50
|
+
* read — the modules it then tree-shook away are still inputs of the next
|
|
51
|
+
* build, which is the direction that errs safely.
|
|
52
|
+
*
|
|
53
|
+
* The observation is KEPT alongside {@link BundleInputsRecorder.addWatchFiles}
|
|
54
|
+
* rather than replaced by it, and neither engine is the reason: the native
|
|
55
|
+
* engine has no watch list to hand over (`BundleResult` carries warnings and
|
|
56
|
+
* output only, and its JS context had no `addWatchFile` at all until
|
|
57
|
+
* `rolldown-native` grew one), so on GJS the observed set is the only account
|
|
58
|
+
* of the modules; on npm the watch list is a superset of it. Union: the cost of
|
|
59
|
+
* a redundant path is a `stat`, the cost of a missing one is a stale green.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createBundleInputsRecorder(outfile: string): BundleInputsRecorder;
|
|
62
|
+
/**
|
|
63
|
+
* The inputs recorded for `outfile` by the build that wrote it, or `null` when
|
|
64
|
+
* there is none to trust — never written, unreadable, or written by another
|
|
65
|
+
* version of this shape. `null` is the fallback the caller must handle, not an
|
|
66
|
+
* error: a bundle built by an older CLI, or copied in from elsewhere, has no
|
|
67
|
+
* manifest and is judged by the package walk alone.
|
|
68
|
+
*/
|
|
69
|
+
export declare function readBundleInputs(outfile: string): BundleInputs | null;
|
|
70
|
+
/**
|
|
71
|
+
* Newest mtime (ms) among recorded inputs, or 0 for an empty set.
|
|
72
|
+
*
|
|
73
|
+
* A recorded input that has since VANISHED counts as CHANGED, exactly as the
|
|
74
|
+
* package walk counts one that vanished mid-walk: an input we cannot read is
|
|
75
|
+
* not evidence that the artifact is current. A workspace-package source deleted
|
|
76
|
+
* while the bundle still imports it is precisely that case, and skipping it
|
|
77
|
+
* would rerun a bundle built from a file that no longer exists.
|
|
78
|
+
*
|
|
79
|
+
* Safe only because the toolchain's own scratch is never RECORDED
|
|
80
|
+
* ({@link isToolchainScratch}): the globals-inject stub and the cached plugin
|
|
81
|
+
* bundles are written and deleted between builds, and treating their absence as
|
|
82
|
+
* a change would rebuild on every run, forever.
|
|
83
|
+
*/
|
|
84
|
+
export declare function newestBundleInputMtimeMs(inputs: readonly string[]): number;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// The SECOND half of "what are this bundle's build inputs" — what the BUNDLER
|
|
3
|
+
// read, recorded at build time beside the bundle it produced.
|
|
4
|
+
//
|
|
5
|
+
// `package-inputs.ts` answers the other half by walking the package, and that
|
|
6
|
+
// walk cannot see outside the package. A consumer's test bundle routinely reads
|
|
7
|
+
// outside it: a workspace package reached through `node_modules/<name>` — a
|
|
8
|
+
// SYMLINK into a sibling directory — is a file the build read and the walk
|
|
9
|
+
// never enumerates. An edit there left `gjsify test` judging a fresh bundle and
|
|
10
|
+
// rerunning it: a stale GREEN, the worst kind of wrong, and the only remedy a
|
|
11
|
+
// consumer had was `rm -rf dist`.
|
|
12
|
+
//
|
|
13
|
+
// The set is recorded from the bundler's own module graph rather than guessed
|
|
14
|
+
// from a directory list: a bare specifier's resolution is known to the bundler
|
|
15
|
+
// and to nobody else, and a list of "likely dirs" is wrong for every layout
|
|
16
|
+
// nobody anticipated — the same failure `package-inputs.ts` documents twice.
|
|
17
|
+
//
|
|
18
|
+
// TWO halves, because the bundler is not the only reader: a `transform`
|
|
19
|
+
// observer sees every MODULE, and the engine's watch list carries what a plugin
|
|
20
|
+
// read and declared itself (a stylesheet's `@import` chain is read by
|
|
21
|
+
// `gjsify-css-as-string`, not by the bundler, so no graph names it). Neither
|
|
22
|
+
// engine reports both, so the recorder takes the union. Deciding freshness
|
|
23
|
+
// stays cheap: the manifest is written once per build and read as `stat` calls.
|
|
24
|
+
//
|
|
25
|
+
// `package-inputs.ts` is not replaced by this. Its walk answers "what MAY this
|
|
26
|
+
// package's build read", which covers a file the current build does not import
|
|
27
|
+
// yet; this answers "what DID this build read", which reaches outside the
|
|
28
|
+
// package. The union is the honest input set, and both halves must exist.
|
|
29
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
30
|
+
import { dirname, join, sep } from 'node:path';
|
|
31
|
+
/**
|
|
32
|
+
* Bumped when the shape changes; a manifest of any other version is ignored.
|
|
33
|
+
*
|
|
34
|
+
* v2 adds `watchList`. A v1 manifest says nothing about whether the engine that
|
|
35
|
+
* wrote it could report a watch list at all, and the safe reading of "unknown"
|
|
36
|
+
* is "do not trust it": see {@link readBundleInputs}.
|
|
37
|
+
*/
|
|
38
|
+
export const BUNDLE_INPUTS_VERSION = 2;
|
|
39
|
+
/** The manifest that belongs to `outfile`. */
|
|
40
|
+
export function bundleInputsPath(outfile) {
|
|
41
|
+
return `${outfile}.inputs.json`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A module id as a path, or `null` when it names no file we can stat.
|
|
45
|
+
*
|
|
46
|
+
* A query or a hash is PART OF THE ID, not of the path: `@scope/pkg/x.css?raw`
|
|
47
|
+
* and `main-window.blp?shared-tree` (the blueprint plugin's shared-tree exit)
|
|
48
|
+
* both stat as ENOENT, and dropping the id silently would drop the file with
|
|
49
|
+
* it. `\0` is the bundler's virtual-module marker — no file, ever.
|
|
50
|
+
*/
|
|
51
|
+
function idToPath(id) {
|
|
52
|
+
if (id === '' || id.startsWith('\0'))
|
|
53
|
+
return null;
|
|
54
|
+
const cut = id.search(/[?#]/);
|
|
55
|
+
if (cut === -1)
|
|
56
|
+
return id;
|
|
57
|
+
const stripped = id.slice(0, cut);
|
|
58
|
+
// The fallback is not a nicety: `#` is LEGAL in a Linux filename, and
|
|
59
|
+
// `a#b.blp` is that file's whole name. Stripping turned it into `a`, which
|
|
60
|
+
// does not exist, and the input was dropped. So the unstripped id gets its
|
|
61
|
+
// turn — and when neither is a file, `inputs()` drops it as before.
|
|
62
|
+
return isFile(stripped) ? stripped : id;
|
|
63
|
+
}
|
|
64
|
+
function isFile(abs) {
|
|
65
|
+
try {
|
|
66
|
+
return statSync(abs).isFile();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Files the TOOLCHAIN writes and deletes between builds, which therefore must
|
|
74
|
+
* never enter the input set.
|
|
75
|
+
*
|
|
76
|
+
* Not a filter on staleness but on membership: a vanished recorded input forces
|
|
77
|
+
* a rebuild (see {@link newestBundleInputMtimeMs}), so recording the
|
|
78
|
+
* globals-inject stub — `node_modules/.cache/gjsify/auto-globals-<hash>.mjs`,
|
|
79
|
+
* rewritten per build and DELETED when its config changes — would rebuild every
|
|
80
|
+
* run, forever. The same directory holds the cached GJS plugin bundles, which
|
|
81
|
+
* carry their own freshness rule (`bundleFileForGjsCached`).
|
|
82
|
+
*
|
|
83
|
+
* **Not a `tmpdir()` test**, which is the obvious wrong answer and the one this
|
|
84
|
+
* started as: the stub is written into the project's own cache directory
|
|
85
|
+
* (`utils/scan-globals.ts`), never to a temp dir, while a PROJECT under
|
|
86
|
+
* `/tmp` is a project — a CI job, a container, this suite's own fixtures. A
|
|
87
|
+
* freshness check that goes blind in those trees is the defect being fixed,
|
|
88
|
+
* reached from the other side.
|
|
89
|
+
*/
|
|
90
|
+
function isToolchainScratch(abs) {
|
|
91
|
+
return abs.includes(`${sep}node_modules${sep}.cache${sep}`);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The resolution files of the package a recorded input belongs to: the nearest
|
|
95
|
+
* `package.json` above it, and the `tsconfig.json` beside it when there is one.
|
|
96
|
+
*
|
|
97
|
+
* Neither is ever a module. A workspace package reached through a symlink
|
|
98
|
+
* contributes its files, but a `package.json#exports` edit — which moves what
|
|
99
|
+
* every bare specifier in the app resolves to — or a `tsconfig.json` that turns
|
|
100
|
+
* on a `paths` mapping changes the output with no file of its own in the graph.
|
|
101
|
+
* Per package, deduplicated, so the walk is a handful of stats however many
|
|
102
|
+
* modules that package contributed.
|
|
103
|
+
*/
|
|
104
|
+
function resolutionFilesFor(abs) {
|
|
105
|
+
const found = [];
|
|
106
|
+
for (let dir = dirname(abs);; dir = dirname(dir)) {
|
|
107
|
+
const manifest = join(dir, 'package.json');
|
|
108
|
+
if (existsSync(manifest)) {
|
|
109
|
+
found.push(manifest);
|
|
110
|
+
const tsconfig = join(dir, 'tsconfig.json');
|
|
111
|
+
if (existsSync(tsconfig))
|
|
112
|
+
found.push(tsconfig);
|
|
113
|
+
return found;
|
|
114
|
+
}
|
|
115
|
+
const parent = dirname(dir);
|
|
116
|
+
if (parent === dir)
|
|
117
|
+
return found;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A plugin that records every module the build transforms, plus the writer for
|
|
122
|
+
* the manifest.
|
|
123
|
+
*
|
|
124
|
+
* `transform` rather than a graph API (`getModuleIds`/`getModuleInfo`) because
|
|
125
|
+
* it is the one hook both engines run and the native bridge translates
|
|
126
|
+
* (`bundler-pick.ts` picks it verbatim). Every module that reaches the
|
|
127
|
+
* transform stage was LOADED from disk, so the observed set is what the build
|
|
128
|
+
* read — the modules it then tree-shook away are still inputs of the next
|
|
129
|
+
* build, which is the direction that errs safely.
|
|
130
|
+
*
|
|
131
|
+
* The observation is KEPT alongside {@link BundleInputsRecorder.addWatchFiles}
|
|
132
|
+
* rather than replaced by it, and neither engine is the reason: the native
|
|
133
|
+
* engine has no watch list to hand over (`BundleResult` carries warnings and
|
|
134
|
+
* output only, and its JS context had no `addWatchFile` at all until
|
|
135
|
+
* `rolldown-native` grew one), so on GJS the observed set is the only account
|
|
136
|
+
* of the modules; on npm the watch list is a superset of it. Union: the cost of
|
|
137
|
+
* a redundant path is a `stat`, the cost of a missing one is a stale green.
|
|
138
|
+
*/
|
|
139
|
+
export function createBundleInputsRecorder(outfile) {
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
// Sticky, and pessimistic: ONE pass without a watch list is enough to know
|
|
142
|
+
// the build cannot see a plugin-read input, and the artifact must not be
|
|
143
|
+
// judged fresh on the strength of the passes that could.
|
|
144
|
+
let watchList = true;
|
|
145
|
+
const record = (raw) => {
|
|
146
|
+
const abs = idToPath(raw);
|
|
147
|
+
if (abs !== null)
|
|
148
|
+
seen.add(abs);
|
|
149
|
+
};
|
|
150
|
+
const plugin = {
|
|
151
|
+
name: 'gjsify:bundle-inputs',
|
|
152
|
+
transform(_code, id) {
|
|
153
|
+
record(id);
|
|
154
|
+
return undefined;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const inputs = () => {
|
|
158
|
+
const out = [];
|
|
159
|
+
const packages = new Set();
|
|
160
|
+
for (const abs of seen) {
|
|
161
|
+
let isFile;
|
|
162
|
+
try {
|
|
163
|
+
isFile = statSync(abs).isFile();
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!isFile || isToolchainScratch(abs))
|
|
169
|
+
continue;
|
|
170
|
+
out.push(abs);
|
|
171
|
+
// `resolutionFilesFor` walks to the package root; one call per
|
|
172
|
+
// DIRECTORY keeps a 1000-module bundle from walking 1000 times.
|
|
173
|
+
const dir = dirname(abs);
|
|
174
|
+
if (packages.has(dir))
|
|
175
|
+
continue;
|
|
176
|
+
packages.add(dir);
|
|
177
|
+
for (const meta of resolutionFilesFor(abs))
|
|
178
|
+
out.push(meta);
|
|
179
|
+
}
|
|
180
|
+
return [...new Set(out)].sort();
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
plugin,
|
|
184
|
+
addWatchFiles(watch) {
|
|
185
|
+
if (!watch.reportedByEngine)
|
|
186
|
+
watchList = false;
|
|
187
|
+
for (const id of watch.files)
|
|
188
|
+
record(id);
|
|
189
|
+
},
|
|
190
|
+
inputs,
|
|
191
|
+
write() {
|
|
192
|
+
const path = bundleInputsPath(outfile);
|
|
193
|
+
const manifest = { version: BUNDLE_INPUTS_VERSION, inputs: inputs(), watchList };
|
|
194
|
+
const tmp = `${path}.tmp`;
|
|
195
|
+
try {
|
|
196
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
197
|
+
// Rename, not write: a manifest is read by the NEXT process, and
|
|
198
|
+
// a half-written one is a parse error there — which reads as "no
|
|
199
|
+
// manifest" and silently degrades the check to the package walk.
|
|
200
|
+
writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
|
|
201
|
+
renameSync(tmp, path);
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
try {
|
|
205
|
+
rmSync(tmp, { force: true });
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Nothing left to clean up — the write never got that far.
|
|
209
|
+
}
|
|
210
|
+
console.warn(`[gjsify test] could not record the build inputs beside ${path} (${err.message}) — ` +
|
|
211
|
+
'the next run judges freshness by the package walk alone, which cannot see outside this package.');
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The inputs recorded for `outfile` by the build that wrote it, or `null` when
|
|
218
|
+
* there is none to trust — never written, unreadable, or written by another
|
|
219
|
+
* version of this shape. `null` is the fallback the caller must handle, not an
|
|
220
|
+
* error: a bundle built by an older CLI, or copied in from elsewhere, has no
|
|
221
|
+
* manifest and is judged by the package walk alone.
|
|
222
|
+
*/
|
|
223
|
+
export function readBundleInputs(outfile) {
|
|
224
|
+
let parsed;
|
|
225
|
+
try {
|
|
226
|
+
parsed = JSON.parse(readFileSync(bundleInputsPath(outfile), 'utf-8'));
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
if (parsed?.version !== BUNDLE_INPUTS_VERSION || !Array.isArray(parsed.inputs))
|
|
232
|
+
return null;
|
|
233
|
+
return {
|
|
234
|
+
inputs: parsed.inputs.filter((abs) => typeof abs === 'string'),
|
|
235
|
+
// v2 only, so a missing flag cannot be v2 — but the pessimistic reading
|
|
236
|
+
// is kept anyway: a manifest that does not vouch for its engine's watch
|
|
237
|
+
// list is not one to judge freshness on.
|
|
238
|
+
watchList: parsed.watchList === true,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Newest mtime (ms) among recorded inputs, or 0 for an empty set.
|
|
243
|
+
*
|
|
244
|
+
* A recorded input that has since VANISHED counts as CHANGED, exactly as the
|
|
245
|
+
* package walk counts one that vanished mid-walk: an input we cannot read is
|
|
246
|
+
* not evidence that the artifact is current. A workspace-package source deleted
|
|
247
|
+
* while the bundle still imports it is precisely that case, and skipping it
|
|
248
|
+
* would rerun a bundle built from a file that no longer exists.
|
|
249
|
+
*
|
|
250
|
+
* Safe only because the toolchain's own scratch is never RECORDED
|
|
251
|
+
* ({@link isToolchainScratch}): the globals-inject stub and the cached plugin
|
|
252
|
+
* bundles are written and deleted between builds, and treating their absence as
|
|
253
|
+
* a change would rebuild on every run, forever.
|
|
254
|
+
*/
|
|
255
|
+
export function newestBundleInputMtimeMs(inputs) {
|
|
256
|
+
let newest = 0;
|
|
257
|
+
for (const abs of inputs) {
|
|
258
|
+
try {
|
|
259
|
+
const { mtimeMs } = statSync(abs);
|
|
260
|
+
if (mtimeMs > newest)
|
|
261
|
+
newest = mtimeMs;
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return Number.POSITIVE_INFINITY;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return newest;
|
|
268
|
+
}
|