@gjsify/cli 0.22.0 → 0.23.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/README.md +24 -1
- package/dist/cli.gjs.mjs +257 -184
- package/lib/bundler-pick.js +12 -4
- package/lib/commands/affected.js +22 -10
- package/lib/commands/check.js +38 -29
- package/lib/commands/flatpak/build.js +19 -18
- package/lib/commands/foreach.js +48 -41
- package/lib/commands/info.js +11 -5
- package/lib/commands/install.d.ts +16 -0
- package/lib/commands/install.js +103 -32
- package/lib/commands/onboard.js +6 -1
- package/lib/commands/pack.js +69 -4
- package/lib/commands/run.js +26 -14
- 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 +112 -6
- package/lib/utils/detect-native-packages.js +201 -29
- 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 +18 -55
- 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/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
package/lib/commands/onboard.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { spawn, spawnSync } from 'node:child_process';
|
|
28
28
|
import { DEFAULT_REGISTRY, whoami } from '@gjsify/npm-registry';
|
|
29
29
|
import { discoverWorkspaces, filterWorkspaces } from '@gjsify/workspace';
|
|
30
|
+
import { mergePublishables } from '../utils/publishable-packages.js';
|
|
30
31
|
import { hasAnyCredential, loadNpmrc } from '../utils/load-npmrc.js';
|
|
31
32
|
import { OtpProvider } from '../utils/npm-otp.js';
|
|
32
33
|
import { publishWorkspace } from './publish.js';
|
|
@@ -142,7 +143,11 @@ export const onboardCommand = {
|
|
|
142
143
|
const environment = args.environment;
|
|
143
144
|
// 2. Enumerate publishable workspaces (non-private, exclude @girs/*).
|
|
144
145
|
const root = findWorkspaceRoot(cwd) ?? cwd;
|
|
145
|
-
|
|
146
|
+
// Non-workspace publishables (packages/{node-gi,napi}/*) publish through
|
|
147
|
+
// release.yml but `discoverWorkspaces` cannot see them — without this
|
|
148
|
+
// merge the sweep reports "all done" while a package that was never
|
|
149
|
+
// published is simply absent from the list.
|
|
150
|
+
const all = mergePublishables(discoverWorkspaces(root, { includeRoot: true }), cwd);
|
|
146
151
|
const selected = filterWorkspaces(all, { noPrivate: true, exclude: ['@girs/*'] }).filter((ws) => ws.name);
|
|
147
152
|
if (selected.length === 0) {
|
|
148
153
|
const msg = 'gjsify onboard: no publishable workspace packages found.';
|
package/lib/commands/pack.js
CHANGED
|
@@ -29,6 +29,7 @@ import { join, resolve } from 'node:path';
|
|
|
29
29
|
import { createTarball, gzip } from '@gjsify/tar';
|
|
30
30
|
import { discoverWorkspaces } from '@gjsify/workspace';
|
|
31
31
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
32
|
+
import { readPackageJson } from '../utils/pkg-json.js';
|
|
32
33
|
import { runLifecycleScript } from '../utils/run-lifecycle-script.js';
|
|
33
34
|
export const packCommand = {
|
|
34
35
|
command: 'pack [path]',
|
|
@@ -488,23 +489,87 @@ function globToRegex(glob) {
|
|
|
488
489
|
pat = pat.replace(/\?/g, '[^/]');
|
|
489
490
|
return new RegExp(`^${pat}($|/)`);
|
|
490
491
|
}
|
|
492
|
+
const DEP_BLOCKS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
493
|
+
/** Every `<block>.<name>` in `pkg` whose range uses the `workspace:` protocol. */
|
|
494
|
+
function collectWorkspaceProtocolDeps(pkg) {
|
|
495
|
+
const found = [];
|
|
496
|
+
for (const block of DEP_BLOCKS) {
|
|
497
|
+
const deps = pkg[block];
|
|
498
|
+
if (!deps)
|
|
499
|
+
continue;
|
|
500
|
+
for (const [name, range] of Object.entries(deps)) {
|
|
501
|
+
if (typeof range === 'string' && range.startsWith('workspace:'))
|
|
502
|
+
found.push(`${block}.${name}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return found;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Monorepo root to resolve `workspace:` ranges against.
|
|
509
|
+
*
|
|
510
|
+
* `findWorkspaceRoot` is deliberately strict: it only accepts a candidate root
|
|
511
|
+
* whose OWN `workspaces` globs list `start` as a member (so an unrelated
|
|
512
|
+
* grand-parent monorepo can't be picked up). Packing needs a WIDER answer,
|
|
513
|
+
* because some packages live inside the monorepo on purpose WITHOUT being
|
|
514
|
+
* members — `packages/napi/**` and `packages/node-gi/**` are excluded from
|
|
515
|
+
* `package.json#workspaces` (own CI, own release cadence, see AGENTS.md). For
|
|
516
|
+
* those the strict lookup returns `null`, and the pre-fix code took that as
|
|
517
|
+
* "nothing to do" and shipped the literal `workspace:^` string.
|
|
518
|
+
*
|
|
519
|
+
* So: strict answer first, else the nearest ancestor that declares
|
|
520
|
+
* `workspaces` at all — the monorepo the package physically lives in, whose
|
|
521
|
+
* members are exactly the siblings a `workspace:` range can refer to.
|
|
522
|
+
*/
|
|
523
|
+
function findPackWorkspaceRoot(start) {
|
|
524
|
+
const strict = findWorkspaceRoot(start);
|
|
525
|
+
if (strict)
|
|
526
|
+
return strict;
|
|
527
|
+
let dir = start;
|
|
528
|
+
for (let i = 0; i < 12; i++) {
|
|
529
|
+
const pkgPath = join(dir, 'package.json');
|
|
530
|
+
if (existsSync(pkgPath) && readPackageJson(pkgPath)?.workspaces !== undefined)
|
|
531
|
+
return dir;
|
|
532
|
+
const parent = resolve(dir, '..');
|
|
533
|
+
if (parent === dir)
|
|
534
|
+
break;
|
|
535
|
+
dir = parent;
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
491
539
|
/**
|
|
492
540
|
* Walk pkg.dependencies / devDependencies / peerDependencies /
|
|
493
541
|
* optionalDependencies and replace `workspace:^` / `workspace:~` /
|
|
494
542
|
* `workspace:*` ranges with the resolved npm range based on each sibling
|
|
495
543
|
* workspace's version field.
|
|
544
|
+
*
|
|
545
|
+
* A `workspace:` range that survives into a published tarball is unresolvable
|
|
546
|
+
* by EVERY registry client (`npm install` fails with `Unsupported URL Type
|
|
547
|
+
* "workspace:"`), so there is no such thing as a harmless skip here: this
|
|
548
|
+
* either substitutes or throws. Same failure mode, same reasoning as
|
|
549
|
+
* `expandFilesPatterns` above — a packer that silently produces a broken
|
|
550
|
+
* tarball costs a whole release train (v0.4.20, v0.4.37).
|
|
496
551
|
*/
|
|
497
552
|
function rewriteWorkspaceDeps(pkg, wsDir) {
|
|
498
|
-
const root =
|
|
499
|
-
if (!root)
|
|
553
|
+
const root = findPackWorkspaceRoot(wsDir);
|
|
554
|
+
if (!root) {
|
|
555
|
+
// No enclosing monorepo. Fine for a standalone package — but then a
|
|
556
|
+
// `workspace:` range cannot possibly be resolved, so refuse rather
|
|
557
|
+
// than ship it.
|
|
558
|
+
const unresolvable = collectWorkspaceProtocolDeps(pkg);
|
|
559
|
+
if (unresolvable.length > 0) {
|
|
560
|
+
throw new Error(`gjsify pack: ${String(pkg.name)} declares workspace: ranges (${unresolvable.join(', ')}) ` +
|
|
561
|
+
`but ${wsDir} is not inside a monorepo (no ancestor package.json declares "workspaces"). ` +
|
|
562
|
+
`A workspace: range cannot be published — replace it with a real npm range, or pack from within the monorepo.`);
|
|
563
|
+
}
|
|
500
564
|
return pkg;
|
|
565
|
+
}
|
|
501
566
|
const siblings = new Map();
|
|
502
567
|
for (const ws of discoverWorkspaces(root)) {
|
|
503
568
|
if (ws.name && ws.version)
|
|
504
569
|
siblings.set(ws.name, ws.version);
|
|
505
570
|
}
|
|
506
571
|
const cloned = JSON.parse(JSON.stringify(pkg));
|
|
507
|
-
for (const block of
|
|
572
|
+
for (const block of DEP_BLOCKS) {
|
|
508
573
|
const deps = cloned[block];
|
|
509
574
|
if (!deps)
|
|
510
575
|
continue;
|
|
@@ -513,7 +578,7 @@ function rewriteWorkspaceDeps(pkg, wsDir) {
|
|
|
513
578
|
continue;
|
|
514
579
|
const wsVer = siblings.get(name);
|
|
515
580
|
if (!wsVer) {
|
|
516
|
-
throw new Error(`gjsify pack: ${cloned.name} declares
|
|
581
|
+
throw new Error(`gjsify pack: ${cloned.name} declares ${range} on ${name} (${block}) but no sibling workspace with that name exists in the monorepo at ${root}`);
|
|
517
582
|
}
|
|
518
583
|
const operator = range.slice('workspace:'.length);
|
|
519
584
|
if (operator === '*' || operator === '') {
|
package/lib/commands/run.js
CHANGED
|
@@ -342,6 +342,17 @@ async function runScript(script, extraArgs, cwd = process.cwd()) {
|
|
|
342
342
|
process.env.npm_package_version = env.npm_package_version;
|
|
343
343
|
if (env.FORCE_COLOR !== undefined)
|
|
344
344
|
process.env.FORCE_COLOR = env.FORCE_COLOR;
|
|
345
|
+
// Track the failure LOCALLY rather than through `process.exitCode`.
|
|
346
|
+
// Under GJS `process.exit()` is deferred (see the note below), so the
|
|
347
|
+
// `process.exit(1)` that used to live in the catch returned instead of
|
|
348
|
+
// terminating, execution fell through to the exit-code line below, and
|
|
349
|
+
// whatever that computed WON. The dispatched command's failure was then
|
|
350
|
+
// reported as success: `gjsify run build:gjsify` exited 0 while the
|
|
351
|
+
// `gjsify build` it dispatched exited 1, so `gjsify onboard` happily
|
|
352
|
+
// published a package whose build had just failed. Anything chaining on
|
|
353
|
+
// `gjsify run <script>` — every `a && b` build script, CI, onboard —
|
|
354
|
+
// got a false green from it.
|
|
355
|
+
let dispatchFailed = false;
|
|
345
356
|
try {
|
|
346
357
|
// Dynamic import to avoid a static cli-app ↔ run.ts import cycle;
|
|
347
358
|
// cli-app is already loaded (the CLI is running through it), so this
|
|
@@ -351,21 +362,22 @@ async function runScript(script, extraArgs, cwd = process.cwd()) {
|
|
|
351
362
|
}
|
|
352
363
|
catch (err) {
|
|
353
364
|
console.error(err.message);
|
|
354
|
-
|
|
365
|
+
dispatchFailed = true;
|
|
355
366
|
}
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
// a bare exit(0) here masked
|
|
359
|
-
process.
|
|
360
|
-
// `process.exit()
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
|
|
367
|
+
// A thrown command wins over `process.exitCode`; otherwise honour a
|
|
368
|
+
// non-zero code the handler set instead of throwing (e.g. a failing
|
|
369
|
+
// check) — a bare exit(0) here masked those as success too.
|
|
370
|
+
const dispatchCode = dispatchFailed ? 1 : process.exitCode != null ? Number(process.exitCode) : 0;
|
|
371
|
+
// `return process.exit(…)`, never a bare call. Under GJS `process.exit()`
|
|
372
|
+
// is NOT synchronous — with the GLib main loop armed (`ensureMainLoop`,
|
|
373
|
+
// e.g. by a bundle the in-process dispatch just ran) it defers the
|
|
374
|
+
// actual teardown and RETURNS, so execution continues past the call.
|
|
375
|
+
// Without the `return` the function falls through to the shell-spawn
|
|
376
|
+
// fallback below and runs the SAME command a SECOND time (the
|
|
377
|
+
// double-execution bug). Mirrors the file-mode path's `return` after
|
|
378
|
+
// `runGjsBundle`. The return is the real guard; the exit sets the status
|
|
379
|
+
// code for when the loop finally unwinds.
|
|
380
|
+
return process.exit(dispatchCode);
|
|
369
381
|
}
|
|
370
382
|
const fullCmd = extraArgs.length > 0 ? `${literal} ${extraArgs.map(shellEscape).join(' ')}` : literal;
|
|
371
383
|
// ensureMainLoop() (called inside spawn) keeps GJS alive after the
|
package/lib/commands/trust.js
CHANGED
|
@@ -16,70 +16,14 @@
|
|
|
16
16
|
//
|
|
17
17
|
// Reference: refs/npm-cli/lib/trust-cmd.js + lib/commands/trust/github.js.
|
|
18
18
|
import { spawnSync } from 'node:child_process';
|
|
19
|
-
import { existsSync
|
|
20
|
-
import {
|
|
19
|
+
import { existsSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
21
|
import { DEFAULT_REGISTRY, buildHeaders, registryFor, whoami } from '@gjsify/npm-registry';
|
|
22
22
|
import { discoverWorkspaces, filterWorkspaces } from '@gjsify/workspace';
|
|
23
|
+
import { mergePublishables } from '../utils/publishable-packages.js';
|
|
23
24
|
import { hasAnyCredential, loadNpmrc } from '../utils/load-npmrc.js';
|
|
24
25
|
import { OtpProvider, withOtpRetry } from '../utils/npm-otp.js';
|
|
25
26
|
import { classifyTrustList, githubTrustBody, normalizeWorkflowFile, parseRepoFromGitRemote, trustUrl, validateRepository, } from '../utils/trust-registry.js';
|
|
26
|
-
/**
|
|
27
|
-
* Publishable packages under `packages/node-gi/*` that are NOT root workspaces
|
|
28
|
-
* (the workspaces glob excludes `packages/node-gi/*`) yet publish via release.yml —
|
|
29
|
-
* `@gjsify/node-gi` + the `@gjsify/gtk-runtime-*` bundles. `discoverWorkspaces`
|
|
30
|
-
* misses them, so `gjsify trust` would never configure their Trusted Publisher.
|
|
31
|
-
* Enumerate them here as Workspace-shaped entries so the sweep covers them too.
|
|
32
|
-
* Best-effort: a missing dir / unreadable manifest is simply skipped.
|
|
33
|
-
*/
|
|
34
|
-
function discoverNodeGiPublishables(cwd) {
|
|
35
|
-
// Walk up from cwd to the repo root — the dir that holds `packages/node-gi` —
|
|
36
|
-
// so the command works from the root or any subdirectory.
|
|
37
|
-
let root = cwd;
|
|
38
|
-
for (let i = 0; i < 8; i++) {
|
|
39
|
-
if (existsSync(join(root, 'packages', 'node-gi')))
|
|
40
|
-
break;
|
|
41
|
-
const parent = dirname(root);
|
|
42
|
-
if (parent === root)
|
|
43
|
-
return [];
|
|
44
|
-
root = parent;
|
|
45
|
-
}
|
|
46
|
-
const base = join(root, 'packages', 'node-gi');
|
|
47
|
-
if (!existsSync(base))
|
|
48
|
-
return [];
|
|
49
|
-
const out = [];
|
|
50
|
-
let entries;
|
|
51
|
-
try {
|
|
52
|
-
entries = readdirSync(base, { withFileTypes: true });
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
for (const entry of entries) {
|
|
58
|
-
if (!entry.isDirectory())
|
|
59
|
-
continue;
|
|
60
|
-
const location = join(base, entry.name);
|
|
61
|
-
const pkgPath = join(location, 'package.json');
|
|
62
|
-
if (!existsSync(pkgPath))
|
|
63
|
-
continue;
|
|
64
|
-
try {
|
|
65
|
-
const manifest = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
66
|
-
if (!manifest || typeof manifest.name !== 'string' || manifest.name.length === 0)
|
|
67
|
-
continue;
|
|
68
|
-
out.push({
|
|
69
|
-
location,
|
|
70
|
-
relativeLocation: join('packages', 'node-gi', entry.name),
|
|
71
|
-
name: manifest.name,
|
|
72
|
-
version: typeof manifest.version === 'string' ? manifest.version : '0.0.0',
|
|
73
|
-
manifest,
|
|
74
|
-
private: manifest.private === true,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
/* unreadable / malformed — skip */
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return out;
|
|
82
|
-
}
|
|
83
27
|
export const trustCommand = {
|
|
84
28
|
command: 'trust [packages..]',
|
|
85
29
|
description: 'Configure npm Trusted Publishers (OIDC/GitHub Actions) for publishable workspace packages so release.yml can publish without a token. Native — no `npm` needed. Incremental: skips already-configured packages.',
|
|
@@ -197,9 +141,7 @@ export const trustCommand = {
|
|
|
197
141
|
// which publish via release.yml but are NOT root workspaces, so
|
|
198
142
|
// discoverWorkspaces misses them and their Trusted Publisher would never get
|
|
199
143
|
// configured. Dedup by name so a real workspace always wins.
|
|
200
|
-
const
|
|
201
|
-
const extra = discoverNodeGiPublishables(cwd).filter((ws) => !workspaceNames.has(ws.name));
|
|
202
|
-
const merged = [...all, ...extra];
|
|
144
|
+
const merged = mergePublishables(all, cwd);
|
|
203
145
|
const selected = filterWorkspaces(merged, {
|
|
204
146
|
noPrivate: args.private !== true,
|
|
205
147
|
include: args.packages && args.packages.length > 0 ? args.packages : undefined,
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
// `gjsify workspace <name> <script> [args..]` — yarn-workspace shortcut.
|
|
1
|
+
// `gjsify workspace <name> [run] <script> [args..]` — yarn-workspace shortcut.
|
|
2
2
|
//
|
|
3
3
|
// Equivalent to `yarn workspace <name> run <script>`: locates the named
|
|
4
4
|
// workspace in the current monorepo, then runs the script there. Used
|
|
5
5
|
// extensively in gjsify's own root `package.json` (17 call sites).
|
|
6
6
|
//
|
|
7
|
+
// BOTH spellings work — the yarn-style `run` prefix is accepted and optional.
|
|
8
|
+
// It is deliberately NOT the default and NOT a full CLI proxy: gjsify's command
|
|
9
|
+
// names collide with the most common script names, so `workspace <name> build`
|
|
10
|
+
// must keep meaning the SCRIPT. See the note at the normalization site below.
|
|
11
|
+
//
|
|
7
12
|
// With `--with-dependencies` / `-d` (synonym: `--topological` / `-t`) the
|
|
8
13
|
// command first runs the SAME script in every transitive workspace
|
|
9
14
|
// dependency, in topological build order, then in the target package.
|
|
@@ -14,7 +19,7 @@
|
|
|
14
19
|
// <B> build && …` chains in root `build:infra` scripts: cascade build
|
|
15
20
|
// failures on uninbuilt workspace deps are the single biggest local-dev
|
|
16
21
|
// pain point in this monorepo.
|
|
17
|
-
import {
|
|
22
|
+
import { spawnToCompletion } from '../utils/spawn.js';
|
|
18
23
|
import { buildDependencyGraph, discoverWorkspaces, topologicalSort } from '@gjsify/workspace';
|
|
19
24
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
20
25
|
import { BuildCacheRunner, buildCacheEnabledByEnv } from '../utils/build-cache.js';
|
|
@@ -22,7 +27,7 @@ import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
|
22
27
|
import { suggestClosest } from '../utils/suggest.js';
|
|
23
28
|
export const workspaceCommand = {
|
|
24
29
|
command: 'workspace <name> <script> [args..]',
|
|
25
|
-
description: 'Run a workspace script
|
|
30
|
+
description: 'Run a workspace script. Both `workspace <name> <script>` and the yarn spelling `workspace <name> run <script>` work.',
|
|
26
31
|
builder: (yargs) => yargs
|
|
27
32
|
.positional('name', {
|
|
28
33
|
description: 'Workspace name (matches package.json `name` field).',
|
|
@@ -97,10 +102,28 @@ export const workspaceCommand = {
|
|
|
97
102
|
const verbose = args.verbose === true;
|
|
98
103
|
const withDeps = args['with-dependencies'] === true;
|
|
99
104
|
const continueOnError = args['continue-on-error'] === true;
|
|
105
|
+
// yarn spells this `yarn workspace <name> run <script>`, and the header
|
|
106
|
+
// comment above has always advertised that form as the equivalent — so
|
|
107
|
+
// accept it. `run` is an OPTIONAL prefix, not the default: gjsify's own
|
|
108
|
+
// command surface collides head-on with the most common script names
|
|
109
|
+
// (`build`, `check`, `test`, `install`, `publish`, `pack`, `lint`,
|
|
110
|
+
// `format`), so proxying the whole CLI the way yarn does would make
|
|
111
|
+
// `gjsify workspace <name> build` ambiguous between the build COMMAND
|
|
112
|
+
// and the build SCRIPT — and the script reading is the one every script
|
|
113
|
+
// in this repo relies on.
|
|
114
|
+
//
|
|
115
|
+
// A package with a literal `run` script stays reachable: the prefix is
|
|
116
|
+
// only consumed when something follows it (`… run build`), so a bare
|
|
117
|
+
// `gjsify workspace <name> run` still runs that package's `run` script.
|
|
118
|
+
let script = args.script;
|
|
119
|
+
let extraArgs = args.args ?? [];
|
|
120
|
+
if (script === 'run' && extraArgs.length > 0) {
|
|
121
|
+
[script, ...extraArgs] = extraArgs;
|
|
122
|
+
}
|
|
100
123
|
const targetScripts = target.manifest.scripts ?? {};
|
|
101
|
-
if (typeof targetScripts[
|
|
102
|
-
const scriptHint = suggestClosest(
|
|
103
|
-
console.error(`gjsify workspace: workspace "${args.name}" has no script "${
|
|
124
|
+
if (typeof targetScripts[script] !== 'string') {
|
|
125
|
+
const scriptHint = suggestClosest(script, Object.keys(targetScripts));
|
|
126
|
+
console.error(`gjsify workspace: workspace "${args.name}" has no script "${script}"` +
|
|
104
127
|
(scriptHint ? ` — did you mean "${scriptHint}"?` : ''));
|
|
105
128
|
// `return process.exit` — see the not-found branch above for why a
|
|
106
129
|
// bare `process.exit` is unsafe under GJS.
|
|
@@ -129,26 +152,26 @@ export const workspaceCommand = {
|
|
|
129
152
|
? new BuildCacheRunner({
|
|
130
153
|
root,
|
|
131
154
|
workspaces: allWorkspaces,
|
|
132
|
-
script:
|
|
133
|
-
args:
|
|
155
|
+
script: script,
|
|
156
|
+
args: extraArgs,
|
|
134
157
|
targets: runList.map((w) => w.name),
|
|
135
158
|
})
|
|
136
159
|
: undefined;
|
|
137
160
|
const failures = [];
|
|
138
161
|
for (const ws of runList) {
|
|
139
162
|
const scripts = ws.manifest.scripts ?? {};
|
|
140
|
-
if (typeof scripts[
|
|
163
|
+
if (typeof scripts[script] !== 'string') {
|
|
141
164
|
// Skip deps that don't declare the script — mirrors
|
|
142
165
|
// `gjsify foreach`'s --if-present behaviour. The target
|
|
143
166
|
// itself was validated above; this branch only filters
|
|
144
167
|
// intermediate deps. A skipped package is also never touched
|
|
145
168
|
// by the build cache (the no-build-package safety rule).
|
|
146
169
|
if (verbose)
|
|
147
|
-
console.error(`[${ws.name}] (no "${
|
|
170
|
+
console.error(`[${ws.name}] (no "${script}" script — skipping)`);
|
|
148
171
|
continue;
|
|
149
172
|
}
|
|
150
173
|
try {
|
|
151
|
-
await runOne(ws,
|
|
174
|
+
await runOne(ws, script, extraArgs, runner, verbose, cache);
|
|
152
175
|
}
|
|
153
176
|
catch (err) {
|
|
154
177
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
@@ -230,26 +253,22 @@ async function runOne(ws, script, extraArgs, runner, verbose, cache) {
|
|
|
230
253
|
if (verbose) {
|
|
231
254
|
console.error(`[${ws.name}] $ ${runner} ${argv.join(' ')}`);
|
|
232
255
|
}
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
child.on('close', (code) => {
|
|
246
|
-
if (code === 0)
|
|
247
|
-
resolve();
|
|
248
|
-
else
|
|
249
|
-
reject(new Error(`${runner} ${argv.join(' ')} exited with code ${code}`));
|
|
250
|
-
});
|
|
251
|
-
child.on('error', reject);
|
|
256
|
+
// `completion: 'exit'` — every path out of this command's handler ends in
|
|
257
|
+
// `process.exit(…)`, which quits the GLib main loop `spawn()` arms under
|
|
258
|
+
// GJS, so the streaming async path stays safe here. See utils/spawn.ts.
|
|
259
|
+
//
|
|
260
|
+
// `color: true` defaults FORCE_COLOR=1 unless the user explicitly opted
|
|
261
|
+
// out (matches yarn / npm / gjsify run) — without it, tools that key on
|
|
262
|
+
// isTTY (chalk, picocolors, oxc) drop colors when stdout is a pipe,
|
|
263
|
+
// including GitHub Actions where the log viewer renders ANSI fine.
|
|
264
|
+
const { code } = await spawnToCompletion(runner, argv, {
|
|
265
|
+
completion: 'exit',
|
|
266
|
+
cwd: ws.location,
|
|
267
|
+
color: true,
|
|
252
268
|
});
|
|
269
|
+
if (code !== 0) {
|
|
270
|
+
throw new Error(`${runner} ${argv.join(' ')} exited with code ${code}`);
|
|
271
|
+
}
|
|
253
272
|
if (cache && before)
|
|
254
273
|
cache.storeAfterSuccess(ws, before);
|
|
255
274
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** The three sibling files a Windows bin entry consists of. */
|
|
2
|
+
export interface WindowsShimFiles {
|
|
3
|
+
/** POSIX `sh` script — the extension-less `<name>` file. */
|
|
4
|
+
sh: string;
|
|
5
|
+
/** Batch file — `<name>.cmd`. */
|
|
6
|
+
cmd: string;
|
|
7
|
+
/** PowerShell script — `<name>.ps1`. */
|
|
8
|
+
ps1: string;
|
|
9
|
+
}
|
|
10
|
+
/** A parsed `#!` line. */
|
|
11
|
+
export interface Shebang {
|
|
12
|
+
/** Leading `KEY=VALUE ` assignments from `env -S KEY=VALUE prog` (may be ''). */
|
|
13
|
+
variables: string;
|
|
14
|
+
/** The interpreter, e.g. `node` or `gjs`. */
|
|
15
|
+
prog: string;
|
|
16
|
+
/** Everything after the interpreter, e.g. ` -m` (may be ''). */
|
|
17
|
+
args: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parse the first line of a bin file. Returns `null` when it is not a shebang
|
|
21
|
+
* — cmd-shim then treats the target as directly executable.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseShebang(firstLine: string): Shebang | null;
|
|
24
|
+
/**
|
|
25
|
+
* Build the three Windows shim files for a package bin.
|
|
26
|
+
*
|
|
27
|
+
* @param relTargetPosix POSIX-style path from the `.bin` directory to the real
|
|
28
|
+
* bin file (i.e. `path.relative(binDir, target)` with `\` normalised to `/`).
|
|
29
|
+
* @param shebang the target's parsed `#!` line, or `null` when it has none —
|
|
30
|
+
* in which case the shims invoke the target directly, matching cmd-shim.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildCmdShim(relTargetPosix: string, shebang: Shebang | null): WindowsShimFiles;
|
|
33
|
+
/**
|
|
34
|
+
* `.cmd` / `.ps1` companions for a launcher gjsify *generates* itself
|
|
35
|
+
* (`linkGlobalBins`, `writeWorkspaceBinShims`) rather than links.
|
|
36
|
+
*
|
|
37
|
+
* Those launchers are not a symlink to a bin with a shebang — they are a
|
|
38
|
+
* hand-written `sh` script that picks an interpreter and passes an ABSOLUTE
|
|
39
|
+
* script path. `buildCmdShim` cannot express them (it derives everything from
|
|
40
|
+
* a relative target plus a shebang), so this builds the equivalent batch /
|
|
41
|
+
* PowerShell form: optional `SET` / `$env:` assignments, then
|
|
42
|
+
* `<interpreter> [args] "<absolute target>" <argv>`.
|
|
43
|
+
*
|
|
44
|
+
* Note the interpreter is invoked by NAME (resolved through `PATH`), matching
|
|
45
|
+
* the `sh` launcher's `exec gjs`/`exec node`. A missing interpreter therefore
|
|
46
|
+
* fails the same way on every OS.
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildLauncherShims(opts: {
|
|
49
|
+
/** Interpreter binary name, e.g. `node` or `gjs`. */
|
|
50
|
+
interpreter: string;
|
|
51
|
+
/** Arguments before the script path, e.g. `['-m']` for `gjs -m`. */
|
|
52
|
+
interpreterArgs?: readonly string[];
|
|
53
|
+
/** Absolute path of the script to run. */
|
|
54
|
+
target: string;
|
|
55
|
+
/**
|
|
56
|
+
* Search-path variables to PREPEND to before the call, preserving whatever
|
|
57
|
+
* the caller's environment already had. Rendered with each shell's own
|
|
58
|
+
* expansion syntax (`%K%` in batch, `$env:K` in PowerShell) — a plain
|
|
59
|
+
* assignment would clobber `PATH`, which on Windows is also the DLL search
|
|
60
|
+
* path the prebuilds have to be reachable through.
|
|
61
|
+
*/
|
|
62
|
+
prependEnv?: Readonly<Record<string, string>>;
|
|
63
|
+
}): {
|
|
64
|
+
cmd: string;
|
|
65
|
+
ps1: string;
|
|
66
|
+
};
|