@gjsify/cli 0.26.0 → 0.27.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 +184 -182
- package/lib/cli-app.js +3 -1
- package/lib/commands/affected.js +146 -15
- package/lib/commands/build.js +21 -14
- package/lib/commands/check.js +40 -4
- package/lib/commands/clear.d.ts +8 -0
- package/lib/commands/clear.js +90 -0
- package/lib/commands/copy.d.ts +8 -0
- package/lib/commands/copy.js +87 -0
- package/lib/commands/dlx.js +8 -1
- package/lib/commands/index.d.ts +2 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/install.d.ts +4 -0
- package/lib/commands/install.js +164 -15
- package/lib/commands/onboard.js +17 -14
- package/lib/commands/publish.js +18 -4
- package/lib/commands/storybook.js +5 -1
- package/lib/commands/test.js +29 -2
- package/lib/commands/uninstall.js +44 -2
- package/lib/commands/upgrade.js +1 -4
- package/lib/templates/install.mjs.tmpl +110 -19
- package/lib/utils/bin-shim.d.ts +73 -0
- package/lib/utils/bin-shim.js +97 -2
- package/lib/utils/build-cache.d.ts +7 -27
- package/lib/utils/build-cache.js +34 -4
- package/lib/utils/check-system-deps.d.ts +15 -1
- package/lib/utils/check-system-deps.js +126 -2
- package/lib/utils/clear-targets.d.ts +30 -0
- package/lib/utils/clear-targets.js +114 -0
- package/lib/utils/copy-targets.d.ts +41 -0
- package/lib/utils/copy-targets.js +145 -0
- package/lib/utils/detect-native-packages.d.ts +160 -5
- package/lib/utils/detect-native-packages.js +326 -50
- package/lib/utils/dir-link.d.ts +86 -0
- package/lib/utils/dir-link.js +140 -0
- package/lib/utils/dlx-cache.d.ts +30 -3
- package/lib/utils/dlx-cache.js +61 -7
- package/lib/utils/install-backend-native.d.ts +109 -1
- package/lib/utils/install-backend-native.js +622 -59
- package/lib/utils/install-backend.js +17 -15
- package/lib/utils/install-global.js +6 -34
- package/lib/utils/install-lock.js +1 -1
- package/lib/utils/install-packument-cache.d.ts +51 -9
- package/lib/utils/install-packument-cache.js +88 -25
- package/lib/utils/oxc-resolve.d.ts +27 -1
- package/lib/utils/oxc-resolve.js +53 -26
- package/lib/utils/pkg-json.d.ts +24 -0
- package/lib/utils/pkg-json.js +27 -1
- package/lib/utils/platform-check.d.ts +215 -0
- package/lib/utils/platform-check.js +377 -0
- package/lib/utils/resolve-npm-package.js +9 -2
- package/lib/utils/run-gjs.d.ts +1 -1
- package/lib/utils/run-gjs.js +19 -3
- package/lib/utils/spawn.js +50 -2
- package/lib/utils/win32-command.d.ts +54 -0
- package/lib/utils/win32-command.js +140 -0
- package/package.json +19 -20
package/lib/cli-app.js
CHANGED
|
@@ -19,7 +19,7 @@ import { readFileSync } from 'node:fs';
|
|
|
19
19
|
import { dirname, join } from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import yargs from 'yargs';
|
|
22
|
-
import { buildCommand as build, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
|
|
22
|
+
import { buildCommand as build, clearCommand as clear, copyCommand as copy, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
|
|
23
23
|
import { APP_NAME } from './constants.js';
|
|
24
24
|
import { isBun, isDeno, isNode, gjsSystemVersion } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
25
25
|
import { ensureGjsifyShimOnPath } from './utils/gjsify-shim.js';
|
|
@@ -89,6 +89,8 @@ export async function runCli(argv) {
|
|
|
89
89
|
.command(create.command, create.description, create.builder, create.handler)
|
|
90
90
|
.command(install.command, install.description, install.builder, install.handler)
|
|
91
91
|
.command(build.command, build.description, build.builder, build.handler)
|
|
92
|
+
.command(clear.command, clear.description, clear.builder, clear.handler)
|
|
93
|
+
.command(copy.command, copy.description, copy.builder, copy.handler)
|
|
92
94
|
.command(test.command, test.description, test.builder, test.handler)
|
|
93
95
|
.command(run.command, run.description, run.builder, run.handler)
|
|
94
96
|
.command(dlx.command, dlx.description, dlx.builder, dlx.handler)
|
package/lib/commands/affected.js
CHANGED
|
@@ -7,15 +7,30 @@
|
|
|
7
7
|
// up running the touched workspace + its downstream consumers instead of
|
|
8
8
|
// the whole monorepo.
|
|
9
9
|
//
|
|
10
|
+
// EVERY VERDICT THIS FILE EMITS IS SCOPED TO ONE WORKFLOW: `main.yml`. That
|
|
11
|
+
// scoping is the single most misread thing here, so it is stated before the
|
|
12
|
+
// tables rather than after. The classifier is booted only by `main.yml`'s
|
|
13
|
+
// `changes` job, and `main.yml` is the only workflow whose jobs consume its
|
|
14
|
+
// outputs. So "ignored" NEVER means "irrelevant to the repository" — it means
|
|
15
|
+
// "cannot change what `main.yml` builds or tests". Sibling workflows
|
|
16
|
+
// (`prebuilds.yml`, `audit-runtimes.yml`, `node-gi.yml`, `napi.yml`,
|
|
17
|
+
// `deploy-docs.yml`, …) gate themselves with their OWN `paths:` filters and are
|
|
18
|
+
// unaffected by anything decided here. `IGNORE` has always worked this way —
|
|
19
|
+
// `packages/node-gi/**` and `packages/napi/**` are load-bearing build inputs to
|
|
20
|
+
// their own workflows and ignored here — but the table read as a global claim,
|
|
21
|
+
// which is how a directory that is genuinely irrelevant to `main.yml` still
|
|
22
|
+
// looks like something that must not be ignored.
|
|
23
|
+
//
|
|
10
24
|
// Classifier table (first-match wins) handles the cases that aren't a
|
|
11
25
|
// straightforward "file lives in workspace X":
|
|
12
26
|
//
|
|
13
|
-
// 1. IGNORE —
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
27
|
+
// 1. IGNORE — cannot change what `main.yml` builds or tests: pure-docs /
|
|
28
|
+
// *.md / website / refs/ submodule / other workflows' files and their
|
|
29
|
+
// inputs / LICENSE / .gitignore. Discarded FIRST, before every other
|
|
30
|
+
// rule: an ignored file is never build-relevant to this workflow, not
|
|
31
|
+
// even inside a GLOBAL_TRIGGERS directory (e.g.
|
|
32
|
+
// `packages/infra/cli/README.md` must not force a full run just because
|
|
33
|
+
// `packages/infra/cli/` is a global trigger). Ignore wins over global.
|
|
19
34
|
//
|
|
20
35
|
// 2. GLOBAL_TRIGGERS — a non-ignored change touching infra the classifier
|
|
21
36
|
// itself depends on (`@gjsify/workspace`, `@gjsify/cli`, the bundler
|
|
@@ -24,12 +39,17 @@
|
|
|
24
39
|
// can't trust the closure when the algorithm that computes it just
|
|
25
40
|
// changed.
|
|
26
41
|
//
|
|
27
|
-
// 3.
|
|
42
|
+
// 3. SCRIPT_COUPLINGS — a build input that reaches its consumer through a
|
|
43
|
+
// BUILD SCRIPT instead of a dependency, so the closure walk has no edge
|
|
44
|
+
// to follow. Contributes EXTRA seeds (and may force a test tier). See
|
|
45
|
+
// the table for why this cannot be inferred and must be declared.
|
|
46
|
+
//
|
|
47
|
+
// 4. TEST_ONLY — every changed file under ONE workspace is a spec
|
|
28
48
|
// file, e2e fixture, or integration test. Seed = that workspace
|
|
29
49
|
// but SKIP the closure expansion (downstream consumers don't care
|
|
30
50
|
// about test code changes).
|
|
31
51
|
//
|
|
32
|
-
//
|
|
52
|
+
// 5. CODE (default) — `workspacesForChangedFiles` maps file → ws,
|
|
33
53
|
// then `affectedClosure` walks reverse-dep edges.
|
|
34
54
|
//
|
|
35
55
|
// Integration tests are gated separately: they run when any workspace
|
|
@@ -162,13 +182,55 @@ const GLOBAL_TRIGGERS = [
|
|
|
162
182
|
/^\.github\/workflows\/main\.yml$/,
|
|
163
183
|
/^scripts\/audit-runtimes\.mjs$/,
|
|
164
184
|
];
|
|
185
|
+
/**
|
|
186
|
+
* Inputs OWNED BY ANOTHER WORKFLOW — build-relevant, just not to `main.yml`.
|
|
187
|
+
*
|
|
188
|
+
* This is the sub-table where the workflow scoping stated at the top of the
|
|
189
|
+
* file is load-bearing, so each entry NAMES the workflow that does gate it.
|
|
190
|
+
* The test for membership is not "is this file unimportant" (none of these
|
|
191
|
+
* are) but "can `main.yml` build or test differently because of it" — and for
|
|
192
|
+
* every entry the answer is no, while the owning workflow has the path in its
|
|
193
|
+
* own `paths:` filter and would go red on a break.
|
|
194
|
+
*
|
|
195
|
+
* Adding a regex here is therefore a claim with two halves, and BOTH must
|
|
196
|
+
* hold: `main.yml` must not read the path, and some other workflow must.
|
|
197
|
+
*/
|
|
198
|
+
const OTHER_WORKFLOW_INPUTS = [
|
|
199
|
+
// Each sibling workflow's own definition. `main.yml` is deliberately
|
|
200
|
+
// absent — it is a GLOBAL_TRIGGER, since a job-shape change there is
|
|
201
|
+
// invisible until the workflow re-runs.
|
|
202
|
+
/^\.github\/workflows\/(deploy-docs|commitlint|release|release-cut|audit-runtimes|prebuilds|node-gi|napi|cli-cross-platform|build-ci-image|cancel-pr-runs)\.yml$/,
|
|
203
|
+
// `prebuilds.yml`'s toolchain (#838): the QEMU-emulated build script and
|
|
204
|
+
// the `changed-packages` classifier that decides which native packages a
|
|
205
|
+
// prebuild run rebuilds. `prebuilds.yml` lists `.github/prebuild-toolchain/**`
|
|
206
|
+
// in BOTH its `push` and `pull_request` `paths:` filters, so a break here
|
|
207
|
+
// reds that workflow on the same PR. `main.yml` never executes any of it —
|
|
208
|
+
// it builds no prebuild and runs no emulated leg.
|
|
209
|
+
/^\.github\/prebuild-toolchain\//,
|
|
210
|
+
// The REPO-SCOPED manifest-conformance rules (#847) — `tier`, `refs-pin`,
|
|
211
|
+
// `platforms-ci` plus the `unchecked-fields` ledger. Their gate is
|
|
212
|
+
// `audit-runtimes.yml`, which runs `node scripts/audit-runtimes.mjs
|
|
213
|
+
// --check --strict` on EVERY pull_request with no `paths:` filter at all,
|
|
214
|
+
// so this content is verified on every PR regardless of what is ignored
|
|
215
|
+
// here; `prebuilds.yml` additionally declares it a shared input (#843) and
|
|
216
|
+
// rebuilds every native package when it changes.
|
|
217
|
+
//
|
|
218
|
+
// The distinction that makes this safe is a real one, not a technicality:
|
|
219
|
+
// `main.yml` DOES run manifest-conformance code, but it reaches it through
|
|
220
|
+
// `scripts/verify-package-outputs.mjs` importing
|
|
221
|
+
// `packages/infra/manifest-conformance/lib/**` — the PACKAGE, a normal
|
|
222
|
+
// workspace under the `packages/infra/*` glob that maps and seeds like any
|
|
223
|
+
// other. Only the `scripts/`-side repo-scoped half is ignored, and that
|
|
224
|
+
// half is precisely the half `main.yml` does not load.
|
|
225
|
+
/^scripts\/manifest-conformance\//,
|
|
226
|
+
];
|
|
165
227
|
/** Patterns that contribute no seed and don't force a full run. */
|
|
166
228
|
const IGNORE = [
|
|
167
229
|
/\.md$/i,
|
|
168
230
|
/^refs\//,
|
|
169
231
|
/^website\//,
|
|
170
232
|
/^docs\//,
|
|
171
|
-
|
|
233
|
+
...OTHER_WORKFLOW_INPUTS,
|
|
172
234
|
/^\.githooks\//,
|
|
173
235
|
// @gjsify/node-gi (the Node-native GObject-Introspection engine, Axis 5) is
|
|
174
236
|
// NOT a gjsify workspace member — the GJS-first install/foreach tooling can't
|
|
@@ -200,6 +262,31 @@ const IGNORE = [
|
|
|
200
262
|
];
|
|
201
263
|
/** Patterns that suggest a test-only change. */
|
|
202
264
|
const TEST_PATHS = [/\.spec\.[mc]?[tj]sx?$/, /^tests\/(e2e|integration)\//];
|
|
265
|
+
const SCRIPT_COUPLINGS = [
|
|
266
|
+
{
|
|
267
|
+
// `templates/*` ARE workspaces (root `package.json#workspaces` carries
|
|
268
|
+
// a `templates/*` glob), so a template change already seeds its own
|
|
269
|
+
// `@gjsify/template-<name>`. That is not the consumer that matters.
|
|
270
|
+
//
|
|
271
|
+
// The consumer is `@gjsify/create-app`, and it is unreachable twice
|
|
272
|
+
// over: it lives at `packages/infra/create-gjsify/`, so no
|
|
273
|
+
// directory-name search finds it, and it takes the templates in
|
|
274
|
+
// through `node scripts/process-template.mjs` in its `build` script
|
|
275
|
+
// (which resolves each template's `workspace:^` specifiers and copies
|
|
276
|
+
// the result into `dist-templates/`), so there is no dependency edge
|
|
277
|
+
// either. `dist-templates/` is a build-output cache candidate, so once
|
|
278
|
+
// it is stale it stays stale.
|
|
279
|
+
match: /^templates\//,
|
|
280
|
+
seeds: ['@gjsify/create-app'],
|
|
281
|
+
// …and `tests/e2e/create-app` is the ONLY thing that would notice: it
|
|
282
|
+
// scaffolds a project out of `dist-templates/` and builds it. Without
|
|
283
|
+
// this a templates-only PR got neither the regenerated templates nor
|
|
284
|
+
// the suite that exercises them. PR #853 passed only by accident of
|
|
285
|
+
// also touching root `package.json` and tripping a global trigger.
|
|
286
|
+
runE2E: true,
|
|
287
|
+
why: '@gjsify/create-app build → node scripts/process-template.mjs reads templates/',
|
|
288
|
+
},
|
|
289
|
+
];
|
|
203
290
|
function classifyAndExpand(workspaces, changedFiles) {
|
|
204
291
|
const files = changedFiles.map((f) => f.replace(/\\/g, '/')).filter((f) => f.length > 0);
|
|
205
292
|
if (files.length === 0) {
|
|
@@ -250,23 +337,64 @@ function classifyAndExpand(workspaces, changedFiles) {
|
|
|
250
337
|
}
|
|
251
338
|
// Map files → workspaces. Files outside any workspace stay in `unmatched`.
|
|
252
339
|
const { matched, unmatched } = workspacesForChangedFiles(workspaces, remaining);
|
|
340
|
+
// SCRIPT_COUPLINGS: add the seeds no graph edge can reach. Applied BEFORE
|
|
341
|
+
// the `unmatched` bail-out so the table works for a coupled directory that
|
|
342
|
+
// is NOT itself a workspace — such a file would otherwise force a full run
|
|
343
|
+
// and the declared seeds would never be consulted.
|
|
344
|
+
const knownNames = new Set(workspaces.map((w) => w.name));
|
|
345
|
+
const couplingSeeds = new Set();
|
|
346
|
+
const couplingAccounted = new Set();
|
|
347
|
+
let couplingRunE2E = false;
|
|
348
|
+
for (const c of SCRIPT_COUPLINGS) {
|
|
349
|
+
const hits = remaining.filter((f) => c.match.test(f));
|
|
350
|
+
if (hits.length === 0)
|
|
351
|
+
continue;
|
|
352
|
+
for (const h of hits)
|
|
353
|
+
couplingAccounted.add(h);
|
|
354
|
+
for (const s of c.seeds) {
|
|
355
|
+
// A declared seed that is not a workspace means the table has
|
|
356
|
+
// drifted from the tree (package renamed, moved, removed). Fail
|
|
357
|
+
// towards the full run and SAY SO — the whole point of this table
|
|
358
|
+
// is that a missing rebuild here is otherwise invisible, so it must
|
|
359
|
+
// never degrade quietly back into the bug it exists to prevent.
|
|
360
|
+
if (!knownNames.has(s)) {
|
|
361
|
+
return {
|
|
362
|
+
global: true,
|
|
363
|
+
reason: `script-coupling seed ${s} is not a workspace (${c.match.source} → ${c.why}); ` +
|
|
364
|
+
`SCRIPT_COUPLINGS in commands/affected.ts is stale`,
|
|
365
|
+
workspaces: workspaces.map((w) => w.name),
|
|
366
|
+
runE2E: true,
|
|
367
|
+
runIntegration: true,
|
|
368
|
+
skipAll: false,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
couplingSeeds.add(s);
|
|
372
|
+
}
|
|
373
|
+
if (c.runE2E)
|
|
374
|
+
couplingRunE2E = true;
|
|
375
|
+
}
|
|
253
376
|
// Unmatched-but-not-ignored files are suspicious enough to fall back to
|
|
254
377
|
// the conservative "full run" path. Examples: a new top-level dotfile,
|
|
255
378
|
// a script in `scripts/` we haven't carved out, a refs/-adjacent file.
|
|
256
|
-
|
|
379
|
+
// A file a coupling claimed is accounted for and does not count here.
|
|
380
|
+
const stillUnmatched = unmatched.filter((f) => !couplingAccounted.has(f));
|
|
381
|
+
if (stillUnmatched.length > 0) {
|
|
257
382
|
return {
|
|
258
383
|
global: true,
|
|
259
|
-
reason: `unmatched files (${
|
|
384
|
+
reason: `unmatched files (${stillUnmatched.length}): ${stillUnmatched.slice(0, 3).join(', ')}${stillUnmatched.length > 3 ? '…' : ''}`,
|
|
260
385
|
workspaces: workspaces.map((w) => w.name),
|
|
261
386
|
runE2E: true,
|
|
262
387
|
runIntegration: true,
|
|
263
388
|
skipAll: false,
|
|
264
389
|
};
|
|
265
390
|
}
|
|
391
|
+
for (const s of couplingSeeds)
|
|
392
|
+
matched.add(s);
|
|
266
393
|
// TEST_ONLY shortcut: every remaining file is a spec / e2e / integration
|
|
267
394
|
// path, all under ONE workspace. Skip the closure expansion — test code
|
|
268
|
-
// has no downstream consumers.
|
|
269
|
-
|
|
395
|
+
// has no downstream consumers. A coupling deliberately disables it: the
|
|
396
|
+
// extra seeds exist precisely because a downstream consumer DOES care.
|
|
397
|
+
const testOnly = couplingSeeds.size === 0 && remaining.every((f) => TEST_PATHS.some((re) => re.test(f)));
|
|
270
398
|
if (testOnly && matched.size === 1) {
|
|
271
399
|
const only = [...matched][0];
|
|
272
400
|
// E2E or integration test-only changes still need their own job.
|
|
@@ -317,10 +445,13 @@ function classifyAndExpand(workspaces, changedFiles) {
|
|
|
317
445
|
}
|
|
318
446
|
}
|
|
319
447
|
}
|
|
320
|
-
|
|
448
|
+
// A `tests/e2e/**` touch turns the tier on, and so does any coupling that
|
|
449
|
+
// declares e2e as its only real coverage (see SCRIPT_COUPLINGS).
|
|
450
|
+
const runE2E = remaining.some((f) => f.startsWith('tests/e2e/')) || couplingRunE2E;
|
|
321
451
|
return {
|
|
322
452
|
global: false,
|
|
323
|
-
reason: `closure (${closure.size} ws from ${matched.size} seed(s))
|
|
453
|
+
reason: `closure (${closure.size} ws from ${matched.size} seed(s))` +
|
|
454
|
+
(couplingSeeds.size > 0 ? `, ${couplingSeeds.size} via script-coupling` : ''),
|
|
324
455
|
workspaces: [...closure].sort(),
|
|
325
456
|
runE2E,
|
|
326
457
|
runIntegration,
|
package/lib/commands/build.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { Config } from '../config.js';
|
|
2
2
|
import { BuildAction } from '../actions/build.js';
|
|
3
|
+
// NO `normalize: true` ON THE GLOB-BEARING OPTIONS — `entry-points` and
|
|
4
|
+
// `exclude` are PATTERNS, not paths.
|
|
5
|
+
//
|
|
6
|
+
// yargs' `normalize` runs `path.normalize()` over every value, which on win32
|
|
7
|
+
// rewrites separators: `src/**/*.{ts,js}` becomes `src\**\*.{ts,js}`. Those
|
|
8
|
+
// patterns go to fast-glob, whose own source says why that must not happen
|
|
9
|
+
// ("we cannot use the standard `path.normalize` method, because on Windows
|
|
10
|
+
// platform it will use of backslashes") — and the failure is not a clean
|
|
11
|
+
// no-match. With no `/` left, fast-glob finds no static base, walks from `.`,
|
|
12
|
+
// follows the workspace's relative `node_modules/@gjsify/*` symlinks with
|
|
13
|
+
// unbounded depth and no cycle detection, and dies of heap exhaustion after
|
|
14
|
+
// tens of minutes without writing one output file. That is issue #914's
|
|
15
|
+
// "`--library` is pathologically slow" report, in full: every `build:gjsify`
|
|
16
|
+
// script in the tree passes its entry points as globs. See
|
|
17
|
+
// `rolldown-plugin-gjsify/src/utils/entry-points.ts` for the second layer.
|
|
18
|
+
//
|
|
19
|
+
// The flag had also been copied onto twelve options that are not paths at all
|
|
20
|
+
// (booleans, `choices` enums, comma-separated identifier lists). Normalizing
|
|
21
|
+
// those is meaningless, and it is what made the one place it is harmful look
|
|
22
|
+
// like house style. `outfile`/`outdir` keep it: they ARE single paths, and
|
|
23
|
+
// nothing globs them.
|
|
3
24
|
export const buildCommand = {
|
|
4
25
|
command: 'build [entryPoints..]',
|
|
5
26
|
description: 'Build and bundle your Gjs project',
|
|
@@ -9,7 +30,6 @@ export const buildCommand = {
|
|
|
9
30
|
description: 'The entry points you want to bundle. Defaults to bundler.input from package.json#gjsify or .gjsifyrc.js, falling back to src/index.ts when neither is set.',
|
|
10
31
|
array: true,
|
|
11
32
|
type: 'string',
|
|
12
|
-
normalize: true,
|
|
13
33
|
// No yargs `default` here on purpose. A yargs default value
|
|
14
34
|
// is indistinguishable from "user passed the flag" in the
|
|
15
35
|
// parsed args (cliArgs.entryPoints?.length is truthy either
|
|
@@ -29,20 +49,17 @@ export const buildCommand = {
|
|
|
29
49
|
description: 'An array of glob patterns to exclude entry-points and aliases',
|
|
30
50
|
array: true,
|
|
31
51
|
type: 'string',
|
|
32
|
-
normalize: true,
|
|
33
52
|
default: [],
|
|
34
53
|
})
|
|
35
54
|
.option('verbose', {
|
|
36
55
|
description: 'Switch on the verbose mode',
|
|
37
56
|
type: 'boolean',
|
|
38
|
-
normalize: true,
|
|
39
57
|
default: false,
|
|
40
58
|
})
|
|
41
59
|
.option('app', {
|
|
42
60
|
description: 'Build target for an application. Defaults to the target of the HOST runtime running the CLI: gjs when run under gjs, node when run under node/bun/deno (both consume the --app node bundle). Set `gjsify.app` in package.json#gjsify to override the host default.',
|
|
43
61
|
type: 'string',
|
|
44
62
|
choices: ['gjs', 'node', 'browser', 'nativescript'],
|
|
45
|
-
normalize: true,
|
|
46
63
|
// No yargs `default: 'gjs'` — a yargs default is
|
|
47
64
|
// indistinguishable from a user-set value and would clobber
|
|
48
65
|
// `package.json#gjsify.app` in config.ts (same footgun as
|
|
@@ -55,18 +72,15 @@ export const buildCommand = {
|
|
|
55
72
|
description: 'Override the default output format',
|
|
56
73
|
type: 'string',
|
|
57
74
|
choices: ['iife', 'esm', 'cjs'],
|
|
58
|
-
normalize: true,
|
|
59
75
|
})
|
|
60
76
|
.option('minify', {
|
|
61
77
|
description: 'Minify the bundled output. Defaults to true; use --no-minify to emit pretty-printed code (e.g. for debugging or readable bundle review).',
|
|
62
78
|
type: 'boolean',
|
|
63
|
-
normalize: true,
|
|
64
79
|
defaultDescription: 'true',
|
|
65
80
|
})
|
|
66
81
|
.option('library', {
|
|
67
82
|
description: 'Use this if you want to build a library for Gjsify',
|
|
68
83
|
type: 'boolean',
|
|
69
|
-
normalize: true,
|
|
70
84
|
default: false,
|
|
71
85
|
})
|
|
72
86
|
.option('outfile', {
|
|
@@ -85,26 +99,22 @@ export const buildCommand = {
|
|
|
85
99
|
alias: 'r',
|
|
86
100
|
description: "Enables TypeScript types on runtime using Deepkit's type compiler",
|
|
87
101
|
type: 'boolean',
|
|
88
|
-
normalize: true,
|
|
89
102
|
default: false,
|
|
90
103
|
})
|
|
91
104
|
.option('log-level', {
|
|
92
105
|
description: 'The log level can be changed to prevent esbuild from printing warning and/or error messages to the terminal',
|
|
93
106
|
type: 'string',
|
|
94
107
|
choices: ['silent', 'error', 'warning', 'info', 'debug', 'verbose'],
|
|
95
|
-
normalize: true,
|
|
96
108
|
default: 'warning',
|
|
97
109
|
})
|
|
98
110
|
.option('console-shim', {
|
|
99
111
|
description: 'Inject a console shim into GJS builds for clean output without the GLib prefix and with working ANSI colors. Use --no-console-shim to disable. Only applies to GJS app builds.',
|
|
100
112
|
type: 'boolean',
|
|
101
|
-
normalize: true,
|
|
102
113
|
default: true,
|
|
103
114
|
})
|
|
104
115
|
.option('globals', {
|
|
105
116
|
description: "Comma-separated list of global identifiers, 'auto' (default) to detect automatically from the bundled output, or 'none' to disable. The 'auto' token may be combined with explicit identifiers/groups (e.g. 'auto,dom') for cases where the detector cannot statically see a global because it's accessed via indirection. Each identifier is mapped to the corresponding `@gjsify/<pkg>/register` module and injected into the bundle. See the CLI Reference docs for the full list of known identifiers. Only applies to GJS app builds.",
|
|
106
117
|
type: 'string',
|
|
107
|
-
normalize: true,
|
|
108
118
|
// No yargs `default: 'auto'` — a yargs default is
|
|
109
119
|
// indistinguishable from a user-set value and would always
|
|
110
120
|
// clobber `package.json#gjsify.globals` / `.gjsifyrc.*` in
|
|
@@ -115,7 +125,6 @@ export const buildCommand = {
|
|
|
115
125
|
.option('shebang', {
|
|
116
126
|
description: 'Prepend a target-appropriate shebang to the output and mark it executable (chmod 755): `#!/usr/bin/env -S gjs -m` for --app gjs, `#!/usr/bin/env node` for --app node. Applies to GJS and Node app builds with a single --outfile. Default: false (use --shebang to enable, or set `shebang: true` in `.gjsifyrc.js`).',
|
|
117
127
|
type: 'boolean',
|
|
118
|
-
normalize: true,
|
|
119
128
|
})
|
|
120
129
|
.option('external', {
|
|
121
130
|
description: 'Module names that should NOT be bundled — EXACT package names only, matched literally on both bundler engines (no globs). Repeat the flag or pass a comma-separated list (e.g. --external typedoc,prettier). Appended to the per-app built-in externals; register subpaths (`<pkg>/register[/…]`) are always force-inlined for --app gjs regardless of this flag.',
|
|
@@ -146,13 +155,11 @@ export const buildCommand = {
|
|
|
146
155
|
.option('exclude-globals', {
|
|
147
156
|
description: 'Comma-separated global identifiers to remove from auto-detection results. Use for false positives from dead browser-compat code whose polyfills require unavailable native libraries (e.g. --exclude-globals fetch,XMLHttpRequest).',
|
|
148
157
|
type: 'string',
|
|
149
|
-
normalize: true,
|
|
150
158
|
})
|
|
151
159
|
.option('watch', {
|
|
152
160
|
alias: 'w',
|
|
153
161
|
description: 'Watch source files and rebuild on change. Logs each rebuild with duration; clean SIGINT shutdown. Only valid with --app gjs|node|browser (rejected with --library). Requires the npm `rolldown` engine — run under Node, not the GJS-bundled CLI.',
|
|
154
162
|
type: 'boolean',
|
|
155
|
-
normalize: true,
|
|
156
163
|
default: false,
|
|
157
164
|
});
|
|
158
165
|
},
|
package/lib/commands/check.js
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
// — if both `system-check`-style flags (`--json`) and `check`-style scripts
|
|
15
15
|
// are reachable here, the `--json` flag routes through this command first
|
|
16
16
|
// (since the typescript-check binding is positional `[paths..]`).
|
|
17
|
-
import { spawnSync } from 'node:child_process';
|
|
18
17
|
import { prefixLines } from '../utils/prefixed-output.js';
|
|
19
18
|
import { spawnToCompletion } from '../utils/spawn.js';
|
|
20
19
|
import { readPackageJson } from '../utils/pkg-json.js';
|
|
@@ -22,6 +21,7 @@ import { join } from 'node:path';
|
|
|
22
21
|
import { cpus } from 'node:os';
|
|
23
22
|
import { discoverWorkspaces, filterWorkspaces } from '@gjsify/workspace';
|
|
24
23
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
24
|
+
import { detectPackageManager } from './workspace.js';
|
|
25
25
|
/**
|
|
26
26
|
* Run a single workspace's `check` script. Returns the exit code; non-zero
|
|
27
27
|
* indicates failure. Output is forwarded to the parent's stdout/stderr,
|
|
@@ -33,8 +33,25 @@ async function runCheck(ws, prefix) {
|
|
|
33
33
|
// GJS, so the STREAMING async path is safe here (and required: the
|
|
34
34
|
// blocking one would serialize the parallel workers). See utils/spawn.ts.
|
|
35
35
|
const flushers = [];
|
|
36
|
+
// The RUNNER is detected, never hardcoded — `foreach` and `workspace` have
|
|
37
|
+
// always done this and this site did not, which made `gjsify check` the one
|
|
38
|
+
// orchestrator that still required Node. Under GJS `detectPackageManager()`
|
|
39
|
+
// returns `gjsify` and the script goes through the shim on PATH.
|
|
40
|
+
//
|
|
41
|
+
// It failed in the worst possible shape: `spawnToCompletion` maps the
|
|
42
|
+
// `spawn npm ENOENT` into the rejection the `catch` below turns into a bare
|
|
43
|
+
// `exit 1`, so a Node-less host reported `✗ <pkg> (exit 1)` for a check
|
|
44
|
+
// script that never ran — measured on postmarketOS/aarch64 against a
|
|
45
|
+
// package whose `check` was literally `echo`.
|
|
46
|
+
const runner = detectPackageManager();
|
|
47
|
+
// `--if-present` is an npm/yarn flag with no `gjsify run` equivalent, so for
|
|
48
|
+
// the GJS runner the absence test moves here rather than being dropped.
|
|
49
|
+
if (runner === 'gjsify' && readPackageJson(join(ws.location, 'package.json'))?.scripts?.check === undefined) {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
const argv = runner === 'gjsify' ? ['run', 'check'] : ['run', 'check', '--if-present'];
|
|
36
53
|
try {
|
|
37
|
-
const { code } = await spawnToCompletion(
|
|
54
|
+
const { code } = await spawnToCompletion(runner, argv, {
|
|
38
55
|
completion: 'exit',
|
|
39
56
|
cwd: ws.location,
|
|
40
57
|
stdio: prefix === null ? 'inherit' : 'pipe',
|
|
@@ -111,11 +128,30 @@ export const checkCommand = {
|
|
|
111
128
|
if (args.verbose) {
|
|
112
129
|
console.log(`[check] cwd=${cwd} → npm run check`);
|
|
113
130
|
}
|
|
114
|
-
|
|
131
|
+
// Through `spawnToCompletion`, like workspace mode above and
|
|
132
|
+
// like every other spawn in the CLI. This site kept a raw
|
|
133
|
+
// `spawnSync('npm', …)` when the six wrappers were consolidated
|
|
134
|
+
// (see utils/spawn.ts), and that made it the last place where
|
|
135
|
+
// `npm` was handed to `spawn` as a bare name — ENOENT on
|
|
136
|
+
// Windows, where npm is a `.cmd` shim `CreateProcess` never
|
|
137
|
+
// finds. Reading only `r.status` then turned that into
|
|
138
|
+
// `process.exit(1)` with NO diagnostic at all: `gjsify check`
|
|
139
|
+
// inside any package failed silently on win32. Measured there
|
|
140
|
+
// before the change; `--verbose` printed the command line and
|
|
141
|
+
// nothing else.
|
|
142
|
+
// Runner detected, not hardcoded — same reason as workspace mode
|
|
143
|
+
// above. This branch already established that the package HAS a
|
|
144
|
+
// check script, so there is no `--if-present` to translate.
|
|
145
|
+
const { code } = await spawnToCompletion(detectPackageManager(), ['run', 'check'], {
|
|
146
|
+
cwd,
|
|
147
|
+
// The handler exits the process on every path below, so the
|
|
148
|
+
// streaming path is safe under GJS too.
|
|
149
|
+
completion: 'exit',
|
|
150
|
+
});
|
|
115
151
|
// `return` — a bare `process.exit()` is deferred under GJS (no
|
|
116
152
|
// atexit), so execution fell through into workspace mode and
|
|
117
153
|
// re-checked the whole monorepo after the single-package run.
|
|
118
|
-
return process.exit(
|
|
154
|
+
return process.exit(code ?? 1);
|
|
119
155
|
}
|
|
120
156
|
// Fall through to workspace mode when the local package has no
|
|
121
157
|
// check script (cd'd into a non-package dir under the root).
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// `gjsify clear <paths…>` — portable recursive delete for package `clear` scripts.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS
|
|
4
|
+
//
|
|
5
|
+
// Every `clear` script in this workspace was `rm -rf <dirs…> || exit 0`. npm runs
|
|
6
|
+
// package scripts through `cmd.exe` on Windows, which has neither `rm` nor that
|
|
7
|
+
// syntax, so `gjsify foreach clear` — and with it the documented full gate
|
|
8
|
+
// (`gjsify install --immutable && gjsify run clear && gjsify run build …`) —
|
|
9
|
+
// could not run there at all. 224 package.json files carried that line, so the
|
|
10
|
+
// fix is one portable implementation, not 224 hand-written platform branches
|
|
11
|
+
// (issue #914, finding 3).
|
|
12
|
+
//
|
|
13
|
+
// `force: true` already ignores a missing path, which is all the `|| exit 0` tail
|
|
14
|
+
// ever did — so the rewritten scripts drop it and stop swallowing REAL failures.
|
|
15
|
+
// A permission error used to exit 0 and read as a successful clean.
|
|
16
|
+
//
|
|
17
|
+
// THE BOOTSTRAP QUESTION, ANSWERED DELIBERATELY
|
|
18
|
+
//
|
|
19
|
+
// A `clear` script that calls `gjsify` makes cleaning depend on a built CLI —
|
|
20
|
+
// the same circularity `gjsify run chmod` had in the CLI's own build (removed in
|
|
21
|
+
// ce47b6a). It is acceptable HERE, and for a stronger reason than "you cannot
|
|
22
|
+
// clear a tree you never built": for the single-command case there is no second
|
|
23
|
+
// process at all. `gjsify run clear` recognises a script that is one
|
|
24
|
+
// `gjsify <subcommand>` and dispatches it IN-PROCESS (see `cli-app.ts`), so the
|
|
25
|
+
// nested-gjs cost the chmod case paid does not arise. `gjsify foreach clear`
|
|
26
|
+
// still spawns one child per package, exactly as it did for `rm -rf`, and the
|
|
27
|
+
// root script already passes `-p`.
|
|
28
|
+
//
|
|
29
|
+
// What it does mean: `clear` is a NEW command, so it does not exist in the
|
|
30
|
+
// committed `dist/cli.gjs.mjs` until that bundle is rebuilt. On a checkout
|
|
31
|
+
// carrying a stale bundle, `gjsify run clear` fails with "Unknown command" rather
|
|
32
|
+
// than silently clearing nothing — which is the honest failure of the two.
|
|
33
|
+
import { rm, stat } from 'node:fs/promises';
|
|
34
|
+
import { readdirSync } from 'node:fs';
|
|
35
|
+
import { relative } from 'node:path';
|
|
36
|
+
import { resolveClearTargets } from '../utils/clear-targets.js';
|
|
37
|
+
export const clearCommand = {
|
|
38
|
+
command: 'clear [paths..]',
|
|
39
|
+
description: 'Delete build output recursively. Portable replacement for `rm -rf` in package `clear` scripts.',
|
|
40
|
+
// No `normalize: true` anywhere below — see the comment block in
|
|
41
|
+
// `commands/build.ts`. It is meaningless for a path `fs.rm` resolves itself,
|
|
42
|
+
// and on win32 it is what would backslash a wildcard segment.
|
|
43
|
+
builder: (yargs) => {
|
|
44
|
+
return yargs
|
|
45
|
+
.positional('paths', {
|
|
46
|
+
description: 'Files and directories to delete, relative to the current package. A missing path is not an error. `*` and `?` are honoured in the last segment.',
|
|
47
|
+
type: 'string',
|
|
48
|
+
array: true,
|
|
49
|
+
})
|
|
50
|
+
.option('dry-run', {
|
|
51
|
+
description: 'Print what would be deleted and exit without touching anything.',
|
|
52
|
+
type: 'boolean',
|
|
53
|
+
default: false,
|
|
54
|
+
})
|
|
55
|
+
.option('verbose', {
|
|
56
|
+
alias: 'v',
|
|
57
|
+
description: 'Print each path as it is deleted.',
|
|
58
|
+
type: 'boolean',
|
|
59
|
+
default: false,
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
handler: async (args) => {
|
|
63
|
+
const cwd = process.cwd();
|
|
64
|
+
const paths = args.paths ?? [];
|
|
65
|
+
if (paths.length === 0) {
|
|
66
|
+
throw new Error('gjsify clear: no paths given. Usage: gjsify clear <paths…>');
|
|
67
|
+
}
|
|
68
|
+
// Resolution validates every target and throws on anything outside the
|
|
69
|
+
// package — before a single delete runs.
|
|
70
|
+
const targets = resolveClearTargets(paths, { cwd, readdir: readdirSync });
|
|
71
|
+
for (const target of targets) {
|
|
72
|
+
const shown = relative(cwd, target);
|
|
73
|
+
if (args.dryRun) {
|
|
74
|
+
const present = await stat(target).then(() => true, () => false);
|
|
75
|
+
console.log(`gjsify clear: would remove ${shown}${present ? '' : ' (absent)'}`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// `maxRetries` is for Windows: a file another process still holds
|
|
79
|
+
// open fails there with EBUSY/EPERM instead of being unlinked while
|
|
80
|
+
// open as on POSIX — and clearing `@gjsify/cli` itself deletes the
|
|
81
|
+
// `lib/`+`dist/` the running process was loaded from. Both runtimes
|
|
82
|
+
// read a module fully and close it, so this covers a transient lock
|
|
83
|
+
// (a scanner, an editor), not a genuinely held handle; if it does
|
|
84
|
+
// fail, the error names the path instead of exiting 0.
|
|
85
|
+
await rm(target, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
86
|
+
if (args.verbose)
|
|
87
|
+
console.log(`gjsify clear: removed ${shown}`);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
};
|