@gjsify/cli 0.22.0 → 0.24.1
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/README.md +28 -1
- package/dist/cli.gjs.mjs +265 -184
- package/lib/actions/build.d.ts +12 -0
- package/lib/actions/build.js +26 -0
- package/lib/bundler-pick.js +56 -7
- package/lib/commands/affected.js +84 -14
- package/lib/commands/check.js +38 -29
- package/lib/commands/flatpak/build.js +19 -18
- package/lib/commands/foreach.js +119 -42
- package/lib/commands/info.js +11 -5
- package/lib/commands/install.d.ts +16 -0
- package/lib/commands/install.js +123 -36
- package/lib/commands/onboard.js +6 -1
- package/lib/commands/pack.js +69 -4
- package/lib/commands/run.js +26 -14
- package/lib/commands/showcase.js +17 -13
- package/lib/commands/storybook.js +5 -2
- package/lib/commands/trust.js +4 -62
- package/lib/commands/workspace.js +49 -30
- package/lib/utils/bin-shim.d.ts +66 -0
- package/lib/utils/bin-shim.js +246 -0
- package/lib/utils/build-cache.d.ts +42 -6
- package/lib/utils/build-cache.js +114 -42
- package/lib/utils/detect-native-packages.d.ts +118 -7
- package/lib/utils/detect-native-packages.js +213 -29
- package/lib/utils/gjs-bundle-guard.d.ts +32 -0
- package/lib/utils/gjs-bundle-guard.js +103 -0
- package/lib/utils/install-backend-native.d.ts +24 -0
- package/lib/utils/install-backend-native.js +89 -16
- package/lib/utils/install-global.d.ts +16 -0
- package/lib/utils/install-global.js +58 -8
- package/lib/utils/oxc-resolve.js +25 -57
- package/lib/utils/publish-headers.d.ts +2 -3
- package/lib/utils/publish-headers.js +64 -3
- package/lib/utils/publishable-packages.d.ts +22 -0
- package/lib/utils/publishable-packages.js +93 -0
- package/lib/utils/run-gjs.d.ts +6 -7
- package/lib/utils/run-gjs.js +4 -3
- package/lib/utils/run-node.d.ts +16 -0
- package/lib/utils/run-node.js +28 -7
- package/lib/utils/runtimes.d.ts +21 -0
- package/lib/utils/runtimes.js +23 -0
- package/lib/utils/spawn.d.ts +82 -0
- package/lib/utils/spawn.js +139 -0
- package/lib/utils/stdin.d.ts +25 -0
- package/lib/utils/stdin.js +132 -0
- package/package.json +23 -18
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// SPDX-License-Identifier: ISC
|
|
2
|
+
// Adapted from npm's `cmd-shim`
|
|
3
|
+
// (refs/npm-cli/node_modules/bin-links/node_modules/cmd-shim/lib/index.js and
|
|
4
|
+
// .../to-batch-syntax.js). Copyright (c) npm, Inc. and Contributors. ISC.
|
|
5
|
+
// Modifications: rewritten as pure TypeScript string builders (no fs, no
|
|
6
|
+
// `process.platform` read) so the Windows branch is unit-testable from a Linux
|
|
7
|
+
// host, and split into two entry points — one for linking a package bin that
|
|
8
|
+
// carries its own shebang (`buildCmdShim`, the faithful npm port) and one for
|
|
9
|
+
// the *generated* launchers gjsify writes itself (`buildLauncherShims`).
|
|
10
|
+
//
|
|
11
|
+
// WHY THIS EXISTS
|
|
12
|
+
//
|
|
13
|
+
// A bin is made runnable on POSIX with a symlink into `node_modules/.bin/`;
|
|
14
|
+
// the kernel reads the target's `#!` line and runs the interpreter. Windows has
|
|
15
|
+
// neither of those mechanisms: `.bin/<name>` with no extension is not on
|
|
16
|
+
// PATHEXT, and there is no shebang handling. npm's answer — the de-facto
|
|
17
|
+
// standard every Windows Node toolchain follows — is to write three sibling
|
|
18
|
+
// files:
|
|
19
|
+
//
|
|
20
|
+
// `<name>` a POSIX `sh` script (used by git-bash / MSYS / WSL / Cygwin)
|
|
21
|
+
// `<name>.cmd` a batch file (used by cmd.exe and by `npx`/`npm run`)
|
|
22
|
+
// `<name>.ps1` a PowerShell script (used by pwsh)
|
|
23
|
+
//
|
|
24
|
+
// All three re-derive the interpreter from the target's shebang, so a bin
|
|
25
|
+
// declaring `#!/usr/bin/env node` and one declaring `#!/usr/bin/env -S gjs -m`
|
|
26
|
+
// both work without the caller knowing which it is.
|
|
27
|
+
// Matches `#!<prog> <args>` and `#!/usr/bin/env [-S] [K=V ...] <prog> <args>`.
|
|
28
|
+
// Verbatim from cmd-shim's `shebangExpr`.
|
|
29
|
+
const SHEBANG_RE = /^#!\s*(?:\/usr\/bin\/env\s+(?:-S\s+)?((?:[^ \t=]+=[^ \t=]+\s+)*))?([^ \t]+)(.*)$/;
|
|
30
|
+
/**
|
|
31
|
+
* Parse the first line of a bin file. Returns `null` when it is not a shebang
|
|
32
|
+
* — cmd-shim then treats the target as directly executable.
|
|
33
|
+
*/
|
|
34
|
+
export function parseShebang(firstLine) {
|
|
35
|
+
const m = SHEBANG_RE.exec(firstLine.trim());
|
|
36
|
+
if (!m)
|
|
37
|
+
return null;
|
|
38
|
+
return { variables: m[1] ?? '', prog: m[2] ?? '', args: m[3] ?? '' };
|
|
39
|
+
}
|
|
40
|
+
/** `$VAR` / `${VAR}` → `%VAR%`. Port of cmd-shim's `replaceDollarWithPercentPair`. */
|
|
41
|
+
function replaceDollarWithPercentPair(value) {
|
|
42
|
+
return value.replace(/\$\{?([^$@#?\- \t{}:]+)\}?/g, (_all, name) => `%${name}%`);
|
|
43
|
+
}
|
|
44
|
+
/** `K=V K2=V2 ` → `@SET K=V\r\n@SET K2=V2\r\n`. Port of `convertToSetCommands`. */
|
|
45
|
+
function convertToSetCommands(variableString) {
|
|
46
|
+
let out = '';
|
|
47
|
+
for (const declaration of variableString.split(' ')) {
|
|
48
|
+
const eq = declaration.indexOf('=');
|
|
49
|
+
if (eq <= 0)
|
|
50
|
+
continue;
|
|
51
|
+
const key = declaration.slice(0, eq).trim();
|
|
52
|
+
const value = declaration.slice(eq + 1).trim();
|
|
53
|
+
if (key && value)
|
|
54
|
+
out += `@SET ${key}=${replaceDollarWithPercentPair(value)}\r\n`;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
// The `:find_dp0` subroutine trick — see npm/cmd-shim#10 and npm/cli#969.
|
|
59
|
+
const CMD_HEAD = '@ECHO off\r\n' +
|
|
60
|
+
'GOTO start\r\n' +
|
|
61
|
+
':find_dp0\r\n' +
|
|
62
|
+
'SET dp0=%~dp0\r\n' +
|
|
63
|
+
'EXIT /b\r\n' +
|
|
64
|
+
':start\r\n' +
|
|
65
|
+
'SETLOCAL\r\n' +
|
|
66
|
+
'CALL :find_dp0\r\n';
|
|
67
|
+
// Resolves `$basedir` and, under git-bash/MSYS/WSL, the Windows-shaped
|
|
68
|
+
// `$basedir_win` the interpreter needs for its script argument.
|
|
69
|
+
const SH_BASEDIR = `basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')")\n` +
|
|
70
|
+
'basedir_win="$basedir"\n' +
|
|
71
|
+
'\n' +
|
|
72
|
+
'case `uname -a` in\n' +
|
|
73
|
+
' *CYGWIN*|*MINGW*|*MSYS*)\n' +
|
|
74
|
+
' if command -v cygpath > /dev/null 2>&1; then\n' +
|
|
75
|
+
' basedir_win=`cygpath -w "$basedir"`\n' +
|
|
76
|
+
' fi\n' +
|
|
77
|
+
' ;;\n' +
|
|
78
|
+
' *WSL2*)\n' +
|
|
79
|
+
' if command -v wslpath > /dev/null 2>&1; then\n' +
|
|
80
|
+
' basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"\n' +
|
|
81
|
+
' if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then\n' +
|
|
82
|
+
' echo "Error: wslpath failed to convert path. WSL environment may be misconfigured." >&2\n' +
|
|
83
|
+
' exit 1\n' +
|
|
84
|
+
' fi\n' +
|
|
85
|
+
' fi\n' +
|
|
86
|
+
' ;;\n' +
|
|
87
|
+
'esac\n' +
|
|
88
|
+
'\n';
|
|
89
|
+
const PS1_HEAD = '#!/usr/bin/env pwsh\n' +
|
|
90
|
+
'$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent\n' +
|
|
91
|
+
'\n' +
|
|
92
|
+
'$exe=""\n' +
|
|
93
|
+
'if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {\n' +
|
|
94
|
+
' # Fix case when both the Windows and Linux builds of Node\n' +
|
|
95
|
+
' # are installed in the same directory\n' +
|
|
96
|
+
' $exe=".exe"\n' +
|
|
97
|
+
'}\n';
|
|
98
|
+
/**
|
|
99
|
+
* Build the three Windows shim files for a package bin.
|
|
100
|
+
*
|
|
101
|
+
* @param relTargetPosix POSIX-style path from the `.bin` directory to the real
|
|
102
|
+
* bin file (i.e. `path.relative(binDir, target)` with `\` normalised to `/`).
|
|
103
|
+
* @param shebang the target's parsed `#!` line, or `null` when it has none —
|
|
104
|
+
* in which case the shims invoke the target directly, matching cmd-shim.
|
|
105
|
+
*/
|
|
106
|
+
export function buildCmdShim(relTargetPosix, shebang) {
|
|
107
|
+
let shTarget = relTargetPosix.split('\\').join('/');
|
|
108
|
+
let target = shTarget.split('/').join('\\');
|
|
109
|
+
let pwshTarget = shTarget;
|
|
110
|
+
let prog = shebang?.prog ?? '';
|
|
111
|
+
let args = shebang?.args ?? '';
|
|
112
|
+
const variables = shebang?.variables ?? '';
|
|
113
|
+
let shProg = prog ? prog.split('\\').join('/') : '';
|
|
114
|
+
let pwshProg = shProg ? `"${shProg}$exe"` : '';
|
|
115
|
+
let longProg = '';
|
|
116
|
+
let shLongProg = '';
|
|
117
|
+
let pwshLongProg = '';
|
|
118
|
+
if (!prog) {
|
|
119
|
+
prog = `"%dp0%\\${target}"`;
|
|
120
|
+
shProg = `"$basedir/${shTarget}"`;
|
|
121
|
+
pwshProg = shProg;
|
|
122
|
+
args = '';
|
|
123
|
+
target = '';
|
|
124
|
+
shTarget = '';
|
|
125
|
+
pwshTarget = '';
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
longProg = `"%dp0%\\${prog}.exe"`;
|
|
129
|
+
shLongProg = `"$basedir/${prog}"`;
|
|
130
|
+
pwshLongProg = `"$basedir/${prog}$exe"`;
|
|
131
|
+
target = `"%dp0%\\${target}"`;
|
|
132
|
+
shTarget = `"$basedir_win/${shTarget}"`;
|
|
133
|
+
pwshTarget = `"$basedir/${pwshTarget}"`;
|
|
134
|
+
}
|
|
135
|
+
let cmd;
|
|
136
|
+
if (longProg) {
|
|
137
|
+
const trimmedArgs = args.trim();
|
|
138
|
+
cmd =
|
|
139
|
+
CMD_HEAD +
|
|
140
|
+
convertToSetCommands(variables) +
|
|
141
|
+
'\r\n' +
|
|
142
|
+
`IF EXIST ${longProg} (\r\n` +
|
|
143
|
+
` SET "_prog=${longProg.replace(/(^")|("$)/g, '')}"\r\n` +
|
|
144
|
+
') ELSE (\r\n' +
|
|
145
|
+
` SET "_prog=${prog.replace(/(^")|("$)/g, '')}"\r\n` +
|
|
146
|
+
')\r\n' +
|
|
147
|
+
'\r\n' +
|
|
148
|
+
// Suppresses the "Terminate Batch Job? (Y/n)" prompt — npm/cli#969.
|
|
149
|
+
'endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & ' +
|
|
150
|
+
`set PATHEXT=%PATHEXT:;.JS;=;% & "%_prog%" ${trimmedArgs} ${target} %*\r\n`;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
cmd = `${CMD_HEAD}${prog} ${args} ${target} %*\r\n`;
|
|
154
|
+
}
|
|
155
|
+
let sh = `#!/bin/sh\n${SH_BASEDIR}`;
|
|
156
|
+
if (shLongProg) {
|
|
157
|
+
sh +=
|
|
158
|
+
`PROG_EXE=${shLongProg.replace(/"$/, '.exe"')}\n` +
|
|
159
|
+
'if ! [ -x "$PROG_EXE" ]; then\n' +
|
|
160
|
+
` PROG_EXE=${shLongProg}\n` +
|
|
161
|
+
' if ! [ -x "$PROG_EXE" ]; then\n' +
|
|
162
|
+
` PROG_EXE=${shProg}\n` +
|
|
163
|
+
' if ! [ -x "$PROG_EXE" ]; then\n' +
|
|
164
|
+
` PROG_EXE=${shProg}.exe\n` +
|
|
165
|
+
' fi\n' +
|
|
166
|
+
' fi\n' +
|
|
167
|
+
'fi\n' +
|
|
168
|
+
'\n' +
|
|
169
|
+
`exec ${variables}"$PROG_EXE" ${args} ${shTarget} "$@"\n`;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
sh += `exec ${shProg} ${args} ${shTarget} "$@"\n`;
|
|
173
|
+
}
|
|
174
|
+
let ps1 = PS1_HEAD;
|
|
175
|
+
if (pwshLongProg) {
|
|
176
|
+
ps1 +=
|
|
177
|
+
'$ret=0\n' +
|
|
178
|
+
`if (Test-Path ${pwshLongProg}) {\n` +
|
|
179
|
+
' # Support pipeline input\n' +
|
|
180
|
+
' if ($MyInvocation.ExpectingInput) {\n' +
|
|
181
|
+
` $input | & ${pwshLongProg} ${args} ${pwshTarget} $args\n` +
|
|
182
|
+
' } else {\n' +
|
|
183
|
+
` & ${pwshLongProg} ${args} ${pwshTarget} $args\n` +
|
|
184
|
+
' }\n' +
|
|
185
|
+
' $ret=$LASTEXITCODE\n' +
|
|
186
|
+
'} else {\n' +
|
|
187
|
+
' # Support pipeline input\n' +
|
|
188
|
+
' if ($MyInvocation.ExpectingInput) {\n' +
|
|
189
|
+
` $input | & ${pwshProg} ${args} ${pwshTarget} $args\n` +
|
|
190
|
+
' } else {\n' +
|
|
191
|
+
` & ${pwshProg} ${args} ${pwshTarget} $args\n` +
|
|
192
|
+
' }\n' +
|
|
193
|
+
' $ret=$LASTEXITCODE\n' +
|
|
194
|
+
'}\n' +
|
|
195
|
+
'exit $ret\n';
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
ps1 +=
|
|
199
|
+
'# Support pipeline input\n' +
|
|
200
|
+
'if ($MyInvocation.ExpectingInput) {\n' +
|
|
201
|
+
` $input | & ${pwshProg} ${pwshTarget} $args\n` +
|
|
202
|
+
'} else {\n' +
|
|
203
|
+
` & ${pwshProg} ${pwshTarget} $args\n` +
|
|
204
|
+
'}\n' +
|
|
205
|
+
'exit $LASTEXITCODE\n';
|
|
206
|
+
}
|
|
207
|
+
return { sh, cmd, ps1 };
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* `.cmd` / `.ps1` companions for a launcher gjsify *generates* itself
|
|
211
|
+
* (`linkGlobalBins`, `writeWorkspaceBinShims`) rather than links.
|
|
212
|
+
*
|
|
213
|
+
* Those launchers are not a symlink to a bin with a shebang — they are a
|
|
214
|
+
* hand-written `sh` script that picks an interpreter and passes an ABSOLUTE
|
|
215
|
+
* script path. `buildCmdShim` cannot express them (it derives everything from
|
|
216
|
+
* a relative target plus a shebang), so this builds the equivalent batch /
|
|
217
|
+
* PowerShell form: optional `SET` / `$env:` assignments, then
|
|
218
|
+
* `<interpreter> [args] "<absolute target>" <argv>`.
|
|
219
|
+
*
|
|
220
|
+
* Note the interpreter is invoked by NAME (resolved through `PATH`), matching
|
|
221
|
+
* the `sh` launcher's `exec gjs`/`exec node`. A missing interpreter therefore
|
|
222
|
+
* fails the same way on every OS.
|
|
223
|
+
*/
|
|
224
|
+
export function buildLauncherShims(opts) {
|
|
225
|
+
const { interpreter, interpreterArgs = [], target, prependEnv = {} } = opts;
|
|
226
|
+
const winTarget = target.split('/').join('\\');
|
|
227
|
+
const argv = interpreterArgs.length > 0 ? `${interpreterArgs.join(' ')} ` : '';
|
|
228
|
+
const entries = Object.entries(prependEnv);
|
|
229
|
+
let cmd = CMD_HEAD;
|
|
230
|
+
for (const [key, value] of entries)
|
|
231
|
+
cmd += `@SET "${key}=${value};%${key}%"\r\n`;
|
|
232
|
+
cmd += `${interpreter} ${argv}"${winTarget}" %*\r\n`;
|
|
233
|
+
let ps1 = '#!/usr/bin/env pwsh\n';
|
|
234
|
+
for (const [key, value] of entries)
|
|
235
|
+
ps1 += `$env:${key} = "${value};" + $env:${key}\n`;
|
|
236
|
+
const ps1Args = interpreterArgs.map((a) => `"${a}" `).join('');
|
|
237
|
+
ps1 +=
|
|
238
|
+
'# Support pipeline input\n' +
|
|
239
|
+
'if ($MyInvocation.ExpectingInput) {\n' +
|
|
240
|
+
` $input | & "${interpreter}" ${ps1Args}"${winTarget}" $args\n` +
|
|
241
|
+
'} else {\n' +
|
|
242
|
+
` & "${interpreter}" ${ps1Args}"${winTarget}" $args\n` +
|
|
243
|
+
'}\n' +
|
|
244
|
+
'exit $LASTEXITCODE\n';
|
|
245
|
+
return { cmd, ps1 };
|
|
246
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { type Workspace } from '@gjsify/workspace';
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Bump to invalidate every existing cache entry (schema/semantic changes).
|
|
4
|
+
*
|
|
5
|
+
* v2: output ownership moved from the whole candidate dir to per-child
|
|
6
|
+
* "output units" (`lib/esm` vs `lib/types`) — a v1 manifest recording `lib`
|
|
7
|
+
* would restore over the sibling subtree a different script owns.
|
|
8
|
+
*/
|
|
9
|
+
export declare const BUILD_CACHE_LAYOUT_VERSION = "v2";
|
|
4
10
|
/** Conventional per-package build output directories, checked in this order. */
|
|
5
11
|
export declare const OUTPUT_DIR_CANDIDATES: readonly ["lib", "dist", "dist-templates"];
|
|
6
12
|
/** Keep at most this many keys per package; evict the least recently stored/used. */
|
|
@@ -46,14 +52,44 @@ export declare function hashPackageInputs(location: string): string;
|
|
|
46
52
|
*/
|
|
47
53
|
export declare function hashOutputDirs(location: string, dirs: readonly string[]): string;
|
|
48
54
|
/**
|
|
49
|
-
*
|
|
55
|
+
* The cacheable output units of a package — the granularity at which a cache
|
|
56
|
+
* entry claims ownership.
|
|
57
|
+
*
|
|
58
|
+
* A candidate dir (`lib/`) is NOT automatically one unit, because the package
|
|
59
|
+
* convention splits it between two DIFFERENT scripts: `build:gjsify` writes
|
|
60
|
+
* `lib/esm/**` (bundler output) and `build:types` writes `lib/types/**` (the
|
|
61
|
+
* `gjsify tsc` declaration emit). At whole-`lib/` granularity both scripts
|
|
62
|
+
* claim the same unit, so a cache HIT on either one does its delete-then-copy
|
|
63
|
+
* over the WHOLE tree and silently destroys the other's output — measured:
|
|
64
|
+
* `build:gjsify` hitting the cache after `build:types` ran left the package
|
|
65
|
+
* with no `lib/types/` at all, and since the script never ran, nothing
|
|
66
|
+
* regenerated them. That ships a tarball whose `types` entry point does not
|
|
67
|
+
* exist (the failure `assertTypeDeclarationsShipped` exists to catch).
|
|
68
|
+
*
|
|
69
|
+
* So: when a candidate dir contains ONLY sub-directories, each child is its
|
|
70
|
+
* own unit (`lib/esm`, `lib/types`) and the two scripts stop overlapping. A
|
|
71
|
+
* candidate holding loose files degrades to the whole dir — a build writing
|
|
72
|
+
* directly into `lib/` genuinely owns it, and the coarse unit is still
|
|
73
|
+
* correct, just less precise.
|
|
74
|
+
*
|
|
75
|
+
* The safety rule is unchanged and in fact tightened: a unit the build never
|
|
76
|
+
* modifies is never recorded, so it can never be restored over (`@gjsify/tsc`'s
|
|
77
|
+
* committed, no-build `lib/lib*.d.ts` are loose files → one `lib` unit → never
|
|
78
|
+
* touched by any build → never captured).
|
|
79
|
+
*/
|
|
80
|
+
export declare function outputUnits(location: string): string[];
|
|
81
|
+
/**
|
|
82
|
+
* Cheap per-unit change signature (existence + file list + sizes + mtimes).
|
|
50
83
|
* Taken BEFORE running the script and compared AFTER, to detect which output
|
|
51
|
-
*
|
|
84
|
+
* units the build actually modified. Content hashing is deliberately avoided
|
|
52
85
|
* here — a build that rewrites identical bytes still bumps mtimes, and that
|
|
53
|
-
* is exactly the "this
|
|
86
|
+
* is exactly the "this unit belongs to the build" signal we want.
|
|
87
|
+
*
|
|
88
|
+
* Units absent before the run are recorded as `null` so a freshly created
|
|
89
|
+
* `lib/esm` still reads as "modified".
|
|
54
90
|
*/
|
|
55
91
|
export declare function snapshotOutputDirs(location: string): Record<string, string | null>;
|
|
56
|
-
/** Output
|
|
92
|
+
/** Output units whose signature changed (or that newly appeared) since `before`. */
|
|
57
93
|
export declare function modifiedOutputDirs(location: string, before: Record<string, string | null>): string[];
|
|
58
94
|
export interface ComposeCacheKeyInput {
|
|
59
95
|
script: string;
|
package/lib/utils/build-cache.js
CHANGED
|
@@ -27,14 +27,20 @@
|
|
|
27
27
|
// outputs/<dir>/… — copies of the output dirs the build MODIFIED
|
|
28
28
|
//
|
|
29
29
|
// Output contract: after a successful (miss) run, the candidate output dirs
|
|
30
|
-
// (`lib/`, `dist/`, `dist-templates/`) are
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
30
|
+
// (`lib/`, `dist/`, `dist-templates/`) are expanded into OUTPUT UNITS — the
|
|
31
|
+
// candidate's immediate sub-directories when it holds only directories
|
|
32
|
+
// (`lib/esm`, `lib/types`), else the candidate itself — compared against a
|
|
33
|
+
// pre-run snapshot, and ONLY the units the script actually created/modified
|
|
34
|
+
// are stored. On a later hit, EXACTLY those units are replaced
|
|
35
|
+
// (delete-then-copy within the package). This is the safety rule from the
|
|
36
|
+
// v0.x CI incident where a raw YAML cache restored over a package whose
|
|
37
|
+
// committed `lib/` is tracked source: a unit the build never touches is never
|
|
38
|
+
// recorded, so it can never be restored over. The sub-directory granularity
|
|
39
|
+
// additionally keeps the two halves of the dual emit apart — `build:gjsify`
|
|
40
|
+
// (→ `lib/esm`) and `build:types` (→ `lib/types`) both write under `lib/`, and
|
|
41
|
+
// at whole-dir granularity a cache hit on one WIPED the other's output. See
|
|
42
|
+
// `outputUnits`. Packages that don't define the script at all are filtered out
|
|
43
|
+
// by the calling command and never reach the cache.
|
|
38
44
|
//
|
|
39
45
|
// Growth is bounded: at most MAX_KEYS_PER_PACKAGE entries per package,
|
|
40
46
|
// oldest (by last store/use) evicted.
|
|
@@ -47,8 +53,14 @@ import { chmodSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readd
|
|
|
47
53
|
import { dirname, join } from 'node:path';
|
|
48
54
|
import { fileURLToPath } from 'node:url';
|
|
49
55
|
import { buildDependencyGraph } from '@gjsify/workspace';
|
|
50
|
-
/**
|
|
51
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Bump to invalidate every existing cache entry (schema/semantic changes).
|
|
58
|
+
*
|
|
59
|
+
* v2: output ownership moved from the whole candidate dir to per-child
|
|
60
|
+
* "output units" (`lib/esm` vs `lib/types`) — a v1 manifest recording `lib`
|
|
61
|
+
* would restore over the sibling subtree a different script owns.
|
|
62
|
+
*/
|
|
63
|
+
export const BUILD_CACHE_LAYOUT_VERSION = 'v2';
|
|
52
64
|
/** Conventional per-package build output directories, checked in this order. */
|
|
53
65
|
export const OUTPUT_DIR_CANDIDATES = ['lib', 'dist', 'dist-templates'];
|
|
54
66
|
/** Keep at most this many keys per package; evict the least recently stored/used. */
|
|
@@ -223,51 +235,111 @@ export function hashOutputDirs(location, dirs) {
|
|
|
223
235
|
return hash.digest('hex');
|
|
224
236
|
}
|
|
225
237
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
238
|
+
* The cacheable output units of a package — the granularity at which a cache
|
|
239
|
+
* entry claims ownership.
|
|
240
|
+
*
|
|
241
|
+
* A candidate dir (`lib/`) is NOT automatically one unit, because the package
|
|
242
|
+
* convention splits it between two DIFFERENT scripts: `build:gjsify` writes
|
|
243
|
+
* `lib/esm/**` (bundler output) and `build:types` writes `lib/types/**` (the
|
|
244
|
+
* `gjsify tsc` declaration emit). At whole-`lib/` granularity both scripts
|
|
245
|
+
* claim the same unit, so a cache HIT on either one does its delete-then-copy
|
|
246
|
+
* over the WHOLE tree and silently destroys the other's output — measured:
|
|
247
|
+
* `build:gjsify` hitting the cache after `build:types` ran left the package
|
|
248
|
+
* with no `lib/types/` at all, and since the script never ran, nothing
|
|
249
|
+
* regenerated them. That ships a tarball whose `types` entry point does not
|
|
250
|
+
* exist (the failure `assertTypeDeclarationsShipped` exists to catch).
|
|
251
|
+
*
|
|
252
|
+
* So: when a candidate dir contains ONLY sub-directories, each child is its
|
|
253
|
+
* own unit (`lib/esm`, `lib/types`) and the two scripts stop overlapping. A
|
|
254
|
+
* candidate holding loose files degrades to the whole dir — a build writing
|
|
255
|
+
* directly into `lib/` genuinely owns it, and the coarse unit is still
|
|
256
|
+
* correct, just less precise.
|
|
257
|
+
*
|
|
258
|
+
* The safety rule is unchanged and in fact tightened: a unit the build never
|
|
259
|
+
* modifies is never recorded, so it can never be restored over (`@gjsify/tsc`'s
|
|
260
|
+
* committed, no-build `lib/lib*.d.ts` are loose files → one `lib` unit → never
|
|
261
|
+
* touched by any build → never captured).
|
|
231
262
|
*/
|
|
232
|
-
export function
|
|
233
|
-
const
|
|
263
|
+
export function outputUnits(location) {
|
|
264
|
+
const units = [];
|
|
234
265
|
for (const dir of OUTPUT_DIR_CANDIDATES) {
|
|
235
266
|
const abs = join(location, dir);
|
|
236
|
-
if (!existsSync(abs))
|
|
237
|
-
snapshot[dir] = null;
|
|
267
|
+
if (!existsSync(abs))
|
|
238
268
|
continue;
|
|
269
|
+
let entries;
|
|
270
|
+
try {
|
|
271
|
+
entries = readdirSync(abs).filter((name) => name !== 'node_modules');
|
|
239
272
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
273
|
+
catch {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
// `lstatSync` rather than `withFileTypes` — the CLI also runs from the
|
|
277
|
+
// GJS bundle, where `@gjsify/fs` is the readdir implementation.
|
|
278
|
+
const allDirs = entries.every((name) => {
|
|
279
|
+
try {
|
|
280
|
+
return lstatSync(join(abs, name)).isDirectory();
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
if (entries.length === 0 || !allDirs) {
|
|
287
|
+
units.push(dir);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
for (const child of [...entries].sort())
|
|
291
|
+
units.push(`${dir}/${child}`);
|
|
292
|
+
}
|
|
293
|
+
return units;
|
|
294
|
+
}
|
|
295
|
+
/** Cheap change signature for ONE unit (file list + sizes + mtimes). */
|
|
296
|
+
function signatureOf(location, unit) {
|
|
297
|
+
const files = [];
|
|
298
|
+
walkFiles(join(location, unit), unit, files);
|
|
299
|
+
const hash = createHash('sha256');
|
|
300
|
+
for (const f of files) {
|
|
301
|
+
let stamp = 'link';
|
|
302
|
+
if (f.link === undefined) {
|
|
303
|
+
try {
|
|
304
|
+
const st = lstatSync(f.abs);
|
|
305
|
+
stamp = `${st.size}:${st.mtimeMs}`;
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
stamp = 'gone';
|
|
253
309
|
}
|
|
254
|
-
hash.update(`${f.rel} ${stamp}\n`);
|
|
255
310
|
}
|
|
256
|
-
|
|
311
|
+
hash.update(`${f.rel} ${stamp}\n`);
|
|
257
312
|
}
|
|
313
|
+
return hash.digest('hex');
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Cheap per-unit change signature (existence + file list + sizes + mtimes).
|
|
317
|
+
* Taken BEFORE running the script and compared AFTER, to detect which output
|
|
318
|
+
* units the build actually modified. Content hashing is deliberately avoided
|
|
319
|
+
* here — a build that rewrites identical bytes still bumps mtimes, and that
|
|
320
|
+
* is exactly the "this unit belongs to the build" signal we want.
|
|
321
|
+
*
|
|
322
|
+
* Units absent before the run are recorded as `null` so a freshly created
|
|
323
|
+
* `lib/esm` still reads as "modified".
|
|
324
|
+
*/
|
|
325
|
+
export function snapshotOutputDirs(location) {
|
|
326
|
+
const snapshot = {};
|
|
327
|
+
for (const dir of OUTPUT_DIR_CANDIDATES)
|
|
328
|
+
snapshot[dir] = null;
|
|
329
|
+
for (const unit of outputUnits(location))
|
|
330
|
+
snapshot[unit] = signatureOf(location, unit);
|
|
258
331
|
return snapshot;
|
|
259
332
|
}
|
|
260
|
-
/** Output
|
|
333
|
+
/** Output units whose signature changed (or that newly appeared) since `before`. */
|
|
261
334
|
export function modifiedOutputDirs(location, before) {
|
|
262
|
-
const after = snapshotOutputDirs(location);
|
|
263
335
|
const modified = [];
|
|
264
|
-
for (const
|
|
265
|
-
// Newly created
|
|
266
|
-
//
|
|
267
|
-
if (
|
|
268
|
-
modified.push(
|
|
336
|
+
for (const unit of outputUnits(location)) {
|
|
337
|
+
// Newly created or changed — both mean "the build owns this unit".
|
|
338
|
+
// A unit that is now ABSENT cannot be stored and is simply not listed.
|
|
339
|
+
if (signatureOf(location, unit) !== (before[unit] ?? null))
|
|
340
|
+
modified.push(unit);
|
|
269
341
|
}
|
|
270
|
-
return modified;
|
|
342
|
+
return modified.sort();
|
|
271
343
|
}
|
|
272
344
|
/** Deterministic cache key (32 hex chars) from all invalidation ingredients. */
|
|
273
345
|
export function composeCacheKey(input) {
|
|
@@ -1,9 +1,91 @@
|
|
|
1
1
|
export interface NativePackage {
|
|
2
2
|
/** npm package name, e.g. "@gjsify/webgl" */
|
|
3
3
|
name: string;
|
|
4
|
-
/** Absolute path to the arch-specific prebuilds dir, e.g. "/…/@gjsify/webgl/prebuilds/linux-
|
|
4
|
+
/** Absolute path to the arch-specific prebuilds dir, e.g. "/…/@gjsify/webgl/prebuilds/linux-x64" */
|
|
5
5
|
prebuildsDir: string;
|
|
6
6
|
}
|
|
7
|
+
/** The host target a resolution is performed for. Defaults to the running process. */
|
|
8
|
+
export interface HostTarget {
|
|
9
|
+
/** `process.platform` value — `linux` | `darwin` | `win32` | … */
|
|
10
|
+
platform?: string;
|
|
11
|
+
/** `process.arch` value — `x64` | `arm64` | `ppc64` | `s390x` | `riscv64` | … */
|
|
12
|
+
arch?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Environment a GJS process needs so `gi://Gjsify…` imports resolve against
|
|
16
|
+
* the detected prebuilds. `GI_TYPELIB_PATH` is universal (girepository reads
|
|
17
|
+
* it on every OS); the *library* search path is whatever the host's dynamic
|
|
18
|
+
* loader actually consults, which is a different variable per OS — see
|
|
19
|
+
* {@link buildNativeEnv}.
|
|
20
|
+
*
|
|
21
|
+
* The index signature exists for the Windows case, where the key is the host's
|
|
22
|
+
* own `PATH` spelling (`Path` on a stock Windows env block) rather than a
|
|
23
|
+
* fixed literal.
|
|
24
|
+
*/
|
|
25
|
+
export interface NativeEnv {
|
|
26
|
+
GI_TYPELIB_PATH: string;
|
|
27
|
+
/** ELF loader (Linux and other ELF platforms). */
|
|
28
|
+
LD_LIBRARY_PATH?: string;
|
|
29
|
+
/** Mach-O loader (macOS). `dyld` ignores `LD_LIBRARY_PATH` entirely. */
|
|
30
|
+
DYLD_LIBRARY_PATH?: string;
|
|
31
|
+
[key: string]: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Canonical `<os>-<arch>` form — the node spelling — so `linux-x86_64` and
|
|
35
|
+
* `linux-x64` compare equal. Only the *arch* half is normalised; the OS half
|
|
36
|
+
* is always a `process.platform` token (`linux`/`darwin`/`win32`), which has
|
|
37
|
+
* no competing spelling.
|
|
38
|
+
*/
|
|
39
|
+
export declare function canonicalPlatformToken(token: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* The prebuild directory names to probe for a host, most-specific first.
|
|
42
|
+
*
|
|
43
|
+
* The canonical name is `${process.platform}-${process.arch}` — `linux-x64`,
|
|
44
|
+
* `linux-arm64`, `darwin-arm64`, `win32-x64`. Every package in the release
|
|
45
|
+
* train declares it (`gjsify.platforms`), `scripts/stage-prebuild.mjs` is the
|
|
46
|
+
* only thing that creates it, and `scripts/audit-runtimes.mjs --check` rejects
|
|
47
|
+
* any other spelling, so the three can no longer drift.
|
|
48
|
+
*
|
|
49
|
+
* Two probes remain BEHIND that canonical name, and both are backward
|
|
50
|
+
* compatibility with tarballs already on npm — not a second convention:
|
|
51
|
+
*
|
|
52
|
+
* 1. **The package's own declared spelling.** A tarball published before the
|
|
53
|
+
* rename declares (and ships) `linux-x86_64`; probing its declaration
|
|
54
|
+
* first loads it without the CLI having to guess. This is also what makes
|
|
55
|
+
* the rename work in the OTHER direction — an older CLI resolving a newer
|
|
56
|
+
* package — because `canonicalPlatformToken` folds both spellings onto
|
|
57
|
+
* one form at both ends.
|
|
58
|
+
* 2. **The legacy uname spelling.** Only reachable for a package that ships
|
|
59
|
+
* a prebuild dir and declares NO `gjsify.platforms` at all — i.e. a
|
|
60
|
+
* tarball predating the OS-axis audit, or a third-party package using
|
|
61
|
+
* `gjsify.prebuilds` without the declaration. Inside this repo the audit
|
|
62
|
+
* makes that state impossible.
|
|
63
|
+
*
|
|
64
|
+
* Both are pure array entries matched against an already-read directory
|
|
65
|
+
* listing — no extra I/O — so tolerance on the READ side costs nothing, while
|
|
66
|
+
* the single spelling is enforced everywhere a name is WRITTEN.
|
|
67
|
+
*
|
|
68
|
+
* @param platform `process.platform` value.
|
|
69
|
+
* @param arch `process.arch` value.
|
|
70
|
+
* @param declaredPlatforms `package.json#gjsify.platforms`, when present.
|
|
71
|
+
*/
|
|
72
|
+
export declare function prebuildDirCandidates(platform: string, arch: string, declaredPlatforms?: readonly string[] | undefined): string[];
|
|
73
|
+
/**
|
|
74
|
+
* Pick the prebuild directory a host should load, given the directories that
|
|
75
|
+
* actually exist under `<pkg>/<prebuilds>/`.
|
|
76
|
+
*
|
|
77
|
+
* Pure: no `process.*` reads, no filesystem access. This is the function the
|
|
78
|
+
* darwin and win32 branches are unit-tested through on a Linux host.
|
|
79
|
+
*
|
|
80
|
+
* @returns the chosen directory NAME (not a path), or `null` when the package
|
|
81
|
+
* ships nothing loadable on this host.
|
|
82
|
+
*/
|
|
83
|
+
export declare function resolvePrebuildDirName(input: {
|
|
84
|
+
platform: string;
|
|
85
|
+
arch: string;
|
|
86
|
+
declaredPlatforms?: readonly string[] | undefined;
|
|
87
|
+
existingDirs: readonly string[];
|
|
88
|
+
}): string | null;
|
|
7
89
|
/**
|
|
8
90
|
* Walk up the directory tree from `startDir` and merge native packages found
|
|
9
91
|
* in every `node_modules` encountered.
|
|
@@ -16,13 +98,42 @@ export interface NativePackage {
|
|
|
16
98
|
*
|
|
17
99
|
* Deduplication: the first match for a given package name wins (closer
|
|
18
100
|
* node_modules shadows outer ones), matching Node.js resolution semantics.
|
|
101
|
+
*
|
|
102
|
+
* @param target overrides the host platform/arch. Only tests pass this; the
|
|
103
|
+
* production call sites take the defaults.
|
|
19
104
|
*/
|
|
20
|
-
export declare function detectNativePackages(startDir: string): NativePackage[];
|
|
105
|
+
export declare function detectNativePackages(startDir: string, target?: HostTarget): NativePackage[];
|
|
21
106
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
107
|
+
* The environment-variable name the host's dynamic loader consults for an
|
|
108
|
+
* additional shared-library search path, plus the separator its search paths
|
|
109
|
+
* use.
|
|
110
|
+
*
|
|
111
|
+
* * **Linux / other ELF** — `LD_LIBRARY_PATH`, `:`-separated.
|
|
112
|
+
* * **macOS** — `DYLD_LIBRARY_PATH`. `dyld` does not read `LD_LIBRARY_PATH`
|
|
113
|
+
* at all, which is why `.github/workflows/napi.yml` sets `DYLD_LIBRARY_PATH`
|
|
114
|
+
* by hand on every macOS gate step.
|
|
115
|
+
* * **Windows** — there is no dedicated variable; `LoadLibrary` searches the
|
|
116
|
+
* process `PATH`, `;`-separated. This is the same mechanism
|
|
117
|
+
* `@gjsify/node-gi`'s `maybePrependGtkRuntimeDllPath()` uses to make its
|
|
118
|
+
* bundled GTK DLLs resolvable.
|
|
24
119
|
*/
|
|
25
|
-
export declare function
|
|
26
|
-
|
|
27
|
-
|
|
120
|
+
export declare function libraryPathVar(platform: string): {
|
|
121
|
+
name: string;
|
|
122
|
+
separator: string;
|
|
28
123
|
};
|
|
124
|
+
/**
|
|
125
|
+
* Build the typelib + shared-library search-path environment for the detected
|
|
126
|
+
* native packages. Prepends the new paths to any existing values.
|
|
127
|
+
*
|
|
128
|
+
* Pure w.r.t. the host: `platform` and `env` are parameters, so the darwin and
|
|
129
|
+
* win32 branches are exercised by unit tests running on Linux.
|
|
130
|
+
*
|
|
131
|
+
* On Windows the `PATH` key is written back under the *host's own* spelling
|
|
132
|
+
* (a stock Windows env block uses `Path`) — Windows env names are
|
|
133
|
+
* case-insensitive, but a plain JS object is not, and `{...process.env,
|
|
134
|
+
* PATH: …}` would hand a child process two competing entries.
|
|
135
|
+
*/
|
|
136
|
+
export declare function buildNativeEnv(packages: NativePackage[], opts?: {
|
|
137
|
+
platform?: string;
|
|
138
|
+
env?: Record<string, string | undefined>;
|
|
139
|
+
}): NativeEnv;
|