@gjsify/cli 0.4.0 → 0.4.3
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 +166 -173
- package/lib/bundler-pick.js +10 -2
- package/lib/commands/foreach.d.ts +2 -1
- package/lib/commands/foreach.js +107 -34
- package/lib/commands/install.js +179 -1
- package/lib/commands/run.js +6 -26
- package/lib/commands/workspace.js +11 -1
- package/lib/utils/install-backend-native.js +242 -60
- package/lib/utils/workspace-root.d.ts +1 -0
- package/lib/utils/workspace-root.js +46 -0
- package/package.json +68 -66
package/lib/bundler-pick.js
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
// as build errors rather than silent wrong behavior.
|
|
25
25
|
import { promises as fs } from 'node:fs';
|
|
26
26
|
import * as path from 'node:path';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
import { pathToFileURL } from 'node:url';
|
|
27
29
|
// npm `rolldown` is a Rust crate with platform-specific prebuilds; loading
|
|
28
30
|
// it eagerly at module init pulls musl-detection code that does
|
|
29
31
|
// `require('node:fs')` synchronously — fine on Node, but fatal under GJS
|
|
@@ -140,13 +142,19 @@ async function tryLoadNative() {
|
|
|
140
142
|
// a real path, then dynamic-import the resulting file:// URL.
|
|
141
143
|
// Under Node a bare specifier import works directly, so we keep
|
|
142
144
|
// the simpler form there.
|
|
145
|
+
//
|
|
146
|
+
// `createRequire` + `pathToFileURL` are statically imported at
|
|
147
|
+
// the top of this file so the GJS bundle inlines them via
|
|
148
|
+
// `@gjsify/module` / `@gjsify/url`. A *dynamic* `import('node:…')`
|
|
149
|
+
// would instead hit the GJS native ESM loader which doesn't
|
|
150
|
+
// know the `node:` URI scheme and throws — silently swallowed
|
|
151
|
+
// by the surrounding catch, leaving the caller to fall back
|
|
152
|
+
// to npm rolldown (which then throws ImportError for `rolldown`).
|
|
143
153
|
const specifier = '@gjsify/rolldown-native';
|
|
144
154
|
let target = specifier;
|
|
145
155
|
if (isGjs) {
|
|
146
|
-
const { createRequire } = await import('node:module');
|
|
147
156
|
const require = createRequire(import.meta.url);
|
|
148
157
|
const resolved = require.resolve(specifier);
|
|
149
|
-
const { pathToFileURL } = await import('node:url');
|
|
150
158
|
target = pathToFileURL(resolved).href;
|
|
151
159
|
}
|
|
152
160
|
const mod = (await import(/* @vite-ignore */ target));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Command } from '../types/index.js';
|
|
2
2
|
interface ForeachOptions {
|
|
3
|
-
script
|
|
3
|
+
script?: string;
|
|
4
4
|
args?: string[];
|
|
5
5
|
all?: boolean;
|
|
6
6
|
parallel?: boolean;
|
|
@@ -11,6 +11,7 @@ interface ForeachOptions {
|
|
|
11
11
|
private?: boolean;
|
|
12
12
|
verbose?: boolean;
|
|
13
13
|
jobs?: number;
|
|
14
|
+
exec?: boolean;
|
|
14
15
|
}
|
|
15
16
|
export declare const foreachCommand: Command<any, ForeachOptions>;
|
|
16
17
|
export {};
|
package/lib/commands/foreach.js
CHANGED
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
import { spawn } from 'node:child_process';
|
|
12
12
|
import { cpus } from 'node:os';
|
|
13
13
|
import { buildDependencyGraph, discoverWorkspaces, filterWorkspaces, topologicalSort, } from '@gjsify/workspace';
|
|
14
|
+
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
14
15
|
export const foreachCommand = {
|
|
15
|
-
command: 'foreach
|
|
16
|
-
description: 'Run a workspace script across all (or filtered) workspaces. Drop-in for `yarn workspaces foreach`: -A/--all, -p/--parallel, -t/--topological, --include, --exclude, --no-private.',
|
|
16
|
+
command: 'foreach [script] [args..]',
|
|
17
|
+
description: 'Run a workspace script across all (or filtered) workspaces. Drop-in for `yarn workspaces foreach`: -A/--all, -p/--parallel, -t/--topological, --include, --exclude, --no-private. Pass --exec to run an arbitrary command instead of a script.',
|
|
17
18
|
builder: (yargs) => yargs
|
|
18
19
|
.positional('script', {
|
|
19
|
-
description: 'Script name to run in each workspace (`run <name>`-equivalent).',
|
|
20
|
+
description: 'Script name to run in each workspace (`run <name>`-equivalent). With --exec, the command to run instead.',
|
|
20
21
|
type: 'string',
|
|
21
|
-
demandOption: true,
|
|
22
22
|
})
|
|
23
23
|
.positional('args', {
|
|
24
24
|
description: 'Extra arguments forwarded to each child invocation.',
|
|
@@ -75,24 +75,75 @@ export const foreachCommand = {
|
|
|
75
75
|
description: 'Maximum concurrent workspaces in --parallel mode (default: cpu count).',
|
|
76
76
|
type: 'number',
|
|
77
77
|
alias: 'j',
|
|
78
|
+
})
|
|
79
|
+
.option('exec', {
|
|
80
|
+
description: 'Treat <script> [args..] as an arbitrary command (yarn `workspaces foreach exec`-equivalent) instead of a package.json script lookup. Workspace filtering by script presence is skipped. Use `-- <cmd> <args...>` to pass flags to the command without yargs intercepting them.',
|
|
81
|
+
type: 'boolean',
|
|
82
|
+
default: false,
|
|
83
|
+
})
|
|
84
|
+
.parserConfiguration({
|
|
85
|
+
// Preserve `--` as args._['--'] so callers can write
|
|
86
|
+
// gjsify foreach --exec -- npm publish --tag latest
|
|
87
|
+
// without yargs grabbing --tag/--access/etc.
|
|
88
|
+
'populate--': true,
|
|
78
89
|
}),
|
|
79
90
|
handler: async (args) => {
|
|
80
|
-
|
|
91
|
+
// Walk up to the monorepo root — foreach is sometimes invoked
|
|
92
|
+
// from inside a child workspace's script chain.
|
|
93
|
+
const cwd = findWorkspaceRoot(process.cwd()) ?? process.cwd();
|
|
81
94
|
const allWorkspaces = discoverWorkspaces(cwd);
|
|
95
|
+
const exec = args.exec === true;
|
|
96
|
+
// In --exec mode, support both
|
|
97
|
+
// gjsify foreach --exec npm something (no flags in command)
|
|
98
|
+
// gjsify foreach --exec -- npm publish --tag X (flags in command)
|
|
99
|
+
// The `--` form is the typical one: `populate--: true` puts the
|
|
100
|
+
// post-separator argv into args._['--'], where yargs cannot grab
|
|
101
|
+
// --tag/--access/etc. as its own options.
|
|
102
|
+
let cmd = args.script;
|
|
103
|
+
let cmdArgs = args.args ?? [];
|
|
104
|
+
if (exec) {
|
|
105
|
+
// With populate--:true, anything after the literal `--`
|
|
106
|
+
// separator lands in top-level args['--']. yargs DOES NOT
|
|
107
|
+
// attach it to args._ — it's a sibling array.
|
|
108
|
+
const fromDoubleDash = (args['--'] ?? [])
|
|
109
|
+
.filter((v) => typeof v === 'string');
|
|
110
|
+
if (fromDoubleDash.length > 0) {
|
|
111
|
+
if (!cmd) {
|
|
112
|
+
cmd = fromDoubleDash[0];
|
|
113
|
+
cmdArgs = [...cmdArgs, ...fromDoubleDash.slice(1)];
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
cmdArgs = [...cmdArgs, ...fromDoubleDash];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!cmd) {
|
|
120
|
+
console.error('gjsify foreach --exec: missing command. Pass it after `--`, e.g. `gjsify foreach --exec -- npm publish --tag latest`.');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
82
124
|
let selected = filterWorkspaces(allWorkspaces, {
|
|
83
125
|
include: args.include,
|
|
84
126
|
exclude: args.exclude,
|
|
85
127
|
noPrivate: args.private === false,
|
|
86
128
|
});
|
|
87
|
-
//
|
|
88
|
-
// yarn does this too, otherwise every project
|
|
89
|
-
// `<script>` would fail and force the user to
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
129
|
+
// In script mode, only run on workspaces that actually have the
|
|
130
|
+
// requested script — yarn does this too, otherwise every project
|
|
131
|
+
// that doesn't declare `<script>` would fail and force the user to
|
|
132
|
+
// `--exclude` it. In --exec mode the command runs unconditionally
|
|
133
|
+
// (yarn's `workspaces foreach exec` semantics).
|
|
134
|
+
if (!exec) {
|
|
135
|
+
if (!cmd) {
|
|
136
|
+
console.error('gjsify foreach: missing <script> positional. Pass --exec to run an arbitrary command instead.');
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
const scriptName = cmd;
|
|
140
|
+
selected = selected.filter((ws) => {
|
|
141
|
+
const scripts = ws.manifest.scripts ?? {};
|
|
142
|
+
return typeof scripts[scriptName] === 'string';
|
|
143
|
+
});
|
|
144
|
+
}
|
|
94
145
|
if (selected.length === 0) {
|
|
95
|
-
console.log(`gjsify foreach: no workspaces match (script="${
|
|
146
|
+
console.log(`gjsify foreach: no workspaces match (${exec ? 'exec' : 'script'}="${cmd}", include=${JSON.stringify(args.include ?? [])}, exclude=${JSON.stringify(args.exclude ?? [])})`);
|
|
96
147
|
return;
|
|
97
148
|
}
|
|
98
149
|
if (args.topological || args['topological-dev']) {
|
|
@@ -101,44 +152,55 @@ export const foreachCommand = {
|
|
|
101
152
|
});
|
|
102
153
|
selected = topologicalSort(graph);
|
|
103
154
|
}
|
|
104
|
-
const cmd = args.script;
|
|
105
|
-
const cmdArgs = args.args ?? [];
|
|
106
155
|
const verbose = args.verbose === true;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
156
|
+
// `cmd` is guaranteed string at this point — both branches above
|
|
157
|
+
// exit on undefined, but TS doesn't narrow through them.
|
|
158
|
+
const finalCmd = cmd;
|
|
159
|
+
try {
|
|
160
|
+
if (args.parallel && !args.topological && !args['topological-dev']) {
|
|
161
|
+
const jobs = args.jobs && args.jobs > 0 ? args.jobs : cpus().length;
|
|
162
|
+
await runParallel(selected, finalCmd, cmdArgs, jobs, verbose, exec);
|
|
163
|
+
}
|
|
164
|
+
else if (args.parallel) {
|
|
165
|
+
// Topological + parallel: each workspace starts as soon as its
|
|
166
|
+
// deps (in the selected set) have finished. Yarn calls this
|
|
167
|
+
// "topological order with concurrency"; we cap at --jobs.
|
|
168
|
+
const jobs = args.jobs && args.jobs > 0 ? args.jobs : cpus().length;
|
|
169
|
+
await runTopologicalParallel(selected, finalCmd, cmdArgs, jobs, verbose, args['topological-dev'] === true, exec);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
await runSequential(selected, finalCmd, cmdArgs, verbose, exec);
|
|
173
|
+
}
|
|
111
174
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// "topological order with concurrency"; we cap at --jobs.
|
|
116
|
-
const jobs = args.jobs && args.jobs > 0 ? args.jobs : cpus().length;
|
|
117
|
-
await runTopologicalParallel(selected, cmd, cmdArgs, jobs, verbose, args['topological-dev'] === true);
|
|
118
|
-
return;
|
|
175
|
+
catch (err) {
|
|
176
|
+
console.error(err.message);
|
|
177
|
+
process.exit(1);
|
|
119
178
|
}
|
|
120
|
-
|
|
179
|
+
// ensureMainLoop() (called inside spawn) keeps GJS alive after every
|
|
180
|
+
// child exits — without an explicit process.exit() the success path
|
|
181
|
+
// would park the loop forever.
|
|
182
|
+
process.exit(0);
|
|
121
183
|
},
|
|
122
184
|
};
|
|
123
|
-
async function runSequential(workspaces, script, args, verbose) {
|
|
185
|
+
async function runSequential(workspaces, script, args, verbose, exec) {
|
|
124
186
|
for (const ws of workspaces) {
|
|
125
|
-
await runOne(ws, script, args, /* prefixOutput */ false, verbose);
|
|
187
|
+
await runOne(ws, script, args, /* prefixOutput */ false, verbose, exec);
|
|
126
188
|
}
|
|
127
189
|
}
|
|
128
|
-
async function runParallel(workspaces, script, args, concurrency, verbose) {
|
|
190
|
+
async function runParallel(workspaces, script, args, concurrency, verbose, exec) {
|
|
129
191
|
let cursor = 0;
|
|
130
192
|
const workers = [];
|
|
131
193
|
for (let w = 0; w < concurrency; w++) {
|
|
132
194
|
workers.push((async () => {
|
|
133
195
|
while (cursor < workspaces.length) {
|
|
134
196
|
const i = cursor++;
|
|
135
|
-
await runOne(workspaces[i], script, args, /* prefixOutput */ true, verbose);
|
|
197
|
+
await runOne(workspaces[i], script, args, /* prefixOutput */ true, verbose, exec);
|
|
136
198
|
}
|
|
137
199
|
})());
|
|
138
200
|
}
|
|
139
201
|
await Promise.all(workers);
|
|
140
202
|
}
|
|
141
|
-
async function runTopologicalParallel(workspaces, script, args, concurrency, verbose, includeDev) {
|
|
203
|
+
async function runTopologicalParallel(workspaces, script, args, concurrency, verbose, includeDev, exec) {
|
|
142
204
|
const selectedNames = new Set(workspaces.map((w) => w.name));
|
|
143
205
|
const remaining = new Map();
|
|
144
206
|
for (const ws of workspaces) {
|
|
@@ -179,7 +241,7 @@ async function runTopologicalParallel(workspaces, script, args, concurrency, ver
|
|
|
179
241
|
const next = ready.sort()[0];
|
|
180
242
|
remaining.delete(next);
|
|
181
243
|
inflight++;
|
|
182
|
-
runOne(byName.get(next), script, args, /* prefixOutput */ true, verbose)
|
|
244
|
+
runOne(byName.get(next), script, args, /* prefixOutput */ true, verbose, exec)
|
|
183
245
|
.then(() => {
|
|
184
246
|
inflight--;
|
|
185
247
|
done.add(next);
|
|
@@ -205,7 +267,18 @@ async function runTopologicalParallel(workspaces, script, args, concurrency, ver
|
|
|
205
267
|
pump();
|
|
206
268
|
});
|
|
207
269
|
}
|
|
208
|
-
async function runOne(ws, script, args, prefixOutput, verbose) {
|
|
270
|
+
async function runOne(ws, script, args, prefixOutput, verbose, exec) {
|
|
271
|
+
if (exec) {
|
|
272
|
+
// Arbitrary-command mode: spawn `<script> <args...>` directly
|
|
273
|
+
// (yarn `workspaces foreach exec`-equivalent). Used by callers
|
|
274
|
+
// that need to run binaries the workspace doesn't expose as a
|
|
275
|
+
// package.json script — e.g. `gjsify foreach --exec npm publish`.
|
|
276
|
+
if (verbose) {
|
|
277
|
+
console.error(`[${ws.name}] $ ${script} ${args.join(' ')}`);
|
|
278
|
+
}
|
|
279
|
+
await spawnPrefixed(script, args, ws.location, prefixOutput ? `[${ws.name}] ` : null);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
209
282
|
// Use the same package manager that invoked us — yarn under yarn,
|
|
210
283
|
// npm under npm, gjsify under gjsify. Default to `npm` for portability
|
|
211
284
|
// when nothing is detectable; the script-runner (D.5) will replace
|
package/lib/commands/install.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// Workspace-aware install (`gjsify install` in a monorepo root with a
|
|
15
15
|
// `"workspaces"` field) is Phase D.3 — for now we detect and surface a
|
|
16
16
|
// clear error pointing at the in-progress work.
|
|
17
|
-
import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from 'node:fs';
|
|
17
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
18
18
|
import { dirname, join, relative } from 'node:path';
|
|
19
19
|
import { spawn } from 'node:child_process';
|
|
20
20
|
import { discoverWorkspaces } from '@gjsify/workspace';
|
|
@@ -168,6 +168,34 @@ async function projectInstallNative(args) {
|
|
|
168
168
|
addDependencyEntry(pkg, name, finalRange, kind);
|
|
169
169
|
}
|
|
170
170
|
writePackageJson(pkgPath, pkg);
|
|
171
|
+
// Re-sync the lockfile's `requested` field with what
|
|
172
|
+
// `projectSpecsFromPackageJson()` will return on the next
|
|
173
|
+
// invocation. Without this, a `gjsify install foo` (bare name,
|
|
174
|
+
// lockfile records `"foo"`) followed by `gjsify install
|
|
175
|
+
// --immutable` (reads package.json → spec `"foo@^1.2.3"`) would
|
|
176
|
+
// surface a spurious drift error.
|
|
177
|
+
if (!args.immutable) {
|
|
178
|
+
syncLockfileRequested(cwd, projectSpecsFromPackageJson(pkg));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function syncLockfileRequested(cwd, specs) {
|
|
183
|
+
const lockPath = join(cwd, 'gjsify-lock.json');
|
|
184
|
+
if (!existsSync(lockPath))
|
|
185
|
+
return;
|
|
186
|
+
try {
|
|
187
|
+
const lock = JSON.parse(readFileSync(lockPath, 'utf-8'));
|
|
188
|
+
const sorted = [...specs].sort();
|
|
189
|
+
const current = [...(lock.requested ?? [])].sort();
|
|
190
|
+
if (sorted.length === current.length && sorted.every((s, i) => s === current[i])) {
|
|
191
|
+
return; // Already in sync; preserve byte-stability.
|
|
192
|
+
}
|
|
193
|
+
lock.requested = specs;
|
|
194
|
+
writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Best-effort sync; if the lockfile is malformed, the next
|
|
198
|
+
// non-immutable install will rewrite it from scratch.
|
|
171
199
|
}
|
|
172
200
|
}
|
|
173
201
|
/**
|
|
@@ -256,6 +284,156 @@ async function workspaceInstall(cwd, args) {
|
|
|
256
284
|
if (symlinks.length > 0) {
|
|
257
285
|
console.log(`gjsify install: wired ${symlinks.length} workspace symlink(s)`);
|
|
258
286
|
}
|
|
287
|
+
// Hoist EVERY workspace package to the repo root's `node_modules/` so
|
|
288
|
+
// transitive workspace deps are reachable from any descendant via
|
|
289
|
+
// standard Node parent-walk resolution. yarn's `nodeLinker: node-modules`
|
|
290
|
+
// does the same thing — the entire workspace graph is materialised at
|
|
291
|
+
// the root, which is how rolldown's resolver finds e.g.
|
|
292
|
+
// `@gjsify/abort-controller/register` injected from a deeply-nested
|
|
293
|
+
// package's `node_modules/.cache/gjsify/` cache file when the consumer
|
|
294
|
+
// didn't declare a direct dep on it (auto-globals injection at build
|
|
295
|
+
// time).
|
|
296
|
+
//
|
|
297
|
+
// Without this hoist, each workspace's `node_modules/` only contains
|
|
298
|
+
// its direct declared deps, and any auto-injected register import for
|
|
299
|
+
// a workspace package the consumer didn't list as a dep externalises
|
|
300
|
+
// and the bundle fails at runtime with `Module not found`.
|
|
301
|
+
const rootBinDir = join(cwd, 'node_modules');
|
|
302
|
+
let rootHoisted = 0;
|
|
303
|
+
for (const ws of workspaces) {
|
|
304
|
+
// Skip the root workspace itself (its location IS cwd; it can't
|
|
305
|
+
// symlink itself into its own node_modules).
|
|
306
|
+
if (ws.location === cwd)
|
|
307
|
+
continue;
|
|
308
|
+
if (!ws.name)
|
|
309
|
+
continue;
|
|
310
|
+
const linkPath = join(rootBinDir, ws.name);
|
|
311
|
+
// If a symlink already exists here (from the per-workspace loop
|
|
312
|
+
// above when the root workspace declared this dep directly), it
|
|
313
|
+
// already points at the right place — skip. We don't try to
|
|
314
|
+
// remove + recreate because under GJS's Gio-backed fs polyfill,
|
|
315
|
+
// `rmSync` on a symlink can race with `symlinkSync` and surface
|
|
316
|
+
// EEXIST. A real directory at this path is also left alone —
|
|
317
|
+
// someone else (npm, yarn) seeded it and we shouldn't clobber.
|
|
318
|
+
let existsHere = false;
|
|
319
|
+
try {
|
|
320
|
+
lstatSync(linkPath);
|
|
321
|
+
existsHere = true;
|
|
322
|
+
}
|
|
323
|
+
catch { /* ENOENT */ }
|
|
324
|
+
if (existsHere)
|
|
325
|
+
continue;
|
|
326
|
+
mkdirSync(dirname(linkPath), { recursive: true });
|
|
327
|
+
const relTarget = relative(dirname(linkPath), ws.location);
|
|
328
|
+
symlinkSync(relTarget, linkPath);
|
|
329
|
+
rootHoisted++;
|
|
330
|
+
}
|
|
331
|
+
if (rootHoisted > 0) {
|
|
332
|
+
console.log(`gjsify install: hoisted ${rootHoisted} workspace(s) to root node_modules/`);
|
|
333
|
+
}
|
|
334
|
+
// Link workspace bins into `node_modules/.bin/`. Without this,
|
|
335
|
+
// `npm run <script>` (or any `node_modules/.bin`-PATH consumer)
|
|
336
|
+
// cannot find the `gjsify` binary on a fresh checkout — yarn
|
|
337
|
+
// creates these shims at install time; we need to match.
|
|
338
|
+
//
|
|
339
|
+
// Each workspace's `bin` entry maps `<binName>` → `<relative-target>`.
|
|
340
|
+
// For GJS-runnable bins, `gjsify.bin` is preferred — its target is the
|
|
341
|
+
// committed `dist/cli.gjs.mjs` bundle that exists on a fresh checkout,
|
|
342
|
+
// versus the `bin` field which typically points at `lib/index.js`
|
|
343
|
+
// (a build artifact that may not yet exist). The shim wraps the
|
|
344
|
+
// target in a shell script that picks the right interpreter (`gjs -m`
|
|
345
|
+
// for `.mjs` bundles, `node` for `.js` files).
|
|
346
|
+
const wsBinDir = join(cwd, 'node_modules', '.bin');
|
|
347
|
+
let wsBinsCreated = 0;
|
|
348
|
+
for (const ws of workspaces) {
|
|
349
|
+
const m = ws.manifest;
|
|
350
|
+
const gjsifyBin = m.gjsify?.bin;
|
|
351
|
+
const nodeBin = m.bin;
|
|
352
|
+
// For each bin name, collect both the Node-target and GJS-target
|
|
353
|
+
// when they exist. The shim prefers Node at invocation time
|
|
354
|
+
// because Node's child_process is more reliable than GJS's
|
|
355
|
+
// Gio.Subprocess polyfill (parallel-spawn close-event delivery
|
|
356
|
+
// races under heavy concurrency); GJS is the fallback for fresh
|
|
357
|
+
// checkouts where the Node target hasn't been built yet.
|
|
358
|
+
const merged = mergeWorkspaceBins(ws.name, gjsifyBin, nodeBin);
|
|
359
|
+
if (merged.size === 0)
|
|
360
|
+
continue;
|
|
361
|
+
mkdirSync(wsBinDir, { recursive: true });
|
|
362
|
+
for (const [binName, { nodeTarget, gjsTarget }] of merged) {
|
|
363
|
+
const linkPath = join(wsBinDir, binName);
|
|
364
|
+
try {
|
|
365
|
+
rmSync(linkPath, { force: true });
|
|
366
|
+
}
|
|
367
|
+
catch { /* fine */ }
|
|
368
|
+
writeFileSync(linkPath, buildBinShim(ws.location, nodeTarget, gjsTarget), { mode: 0o755 });
|
|
369
|
+
chmodSync(linkPath, 0o755);
|
|
370
|
+
wsBinsCreated++;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (wsBinsCreated > 0) {
|
|
374
|
+
console.log(`gjsify install: linked ${wsBinsCreated} workspace bin(s) into node_modules/.bin/`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Build a shell shim that prefers Node when its target file exists at
|
|
379
|
+
* invocation time, falling back to GJS otherwise. The runtime check is
|
|
380
|
+
* per-invocation (not at install time) so the same shim works both
|
|
381
|
+
* before and after the workspace's `lib/` has been built — a fresh
|
|
382
|
+
* checkout only has the committed `dist/cli.gjs.mjs`, while every
|
|
383
|
+
* subsequent `npm run build` produces `lib/index.js`.
|
|
384
|
+
*
|
|
385
|
+
* Both targets are absolute paths so the shim is portable across the
|
|
386
|
+
* different cwds that consumers (`yarn run`, `npm run`, direct PATH
|
|
387
|
+
* invocation) call us from.
|
|
388
|
+
*/
|
|
389
|
+
function buildBinShim(wsLocation, nodeTarget, gjsTarget) {
|
|
390
|
+
const nodeAbs = nodeTarget ? join(wsLocation, nodeTarget) : null;
|
|
391
|
+
const gjsAbs = gjsTarget ? join(wsLocation, gjsTarget) : null;
|
|
392
|
+
if (nodeAbs && gjsAbs) {
|
|
393
|
+
return `#!/bin/sh\nif [ -f "${nodeAbs}" ]; then\n exec node "${nodeAbs}" "$@"\nfi\nexec gjs -m "${gjsAbs}" "$@"\n`;
|
|
394
|
+
}
|
|
395
|
+
if (nodeAbs)
|
|
396
|
+
return `#!/bin/sh\nexec node "${nodeAbs}" "$@"\n`;
|
|
397
|
+
if (gjsAbs)
|
|
398
|
+
return `#!/bin/sh\nexec gjs -m "${gjsAbs}" "$@"\n`;
|
|
399
|
+
throw new Error('buildBinShim: either nodeTarget or gjsTarget must be provided');
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Walk a workspace's `bin` (Node) + `gjsify.bin` (GJS) declarations
|
|
403
|
+
* into a unified `<binName> → {nodeTarget?, gjsTarget?}` map. The
|
|
404
|
+
* shim built from this picks Node at runtime when its target exists,
|
|
405
|
+
* GJS otherwise.
|
|
406
|
+
*/
|
|
407
|
+
function mergeWorkspaceBins(pkgName, gjsifyBin, nodeBin) {
|
|
408
|
+
const out = new Map();
|
|
409
|
+
const baseName = pkgName.startsWith('@') ? pkgName.slice(pkgName.indexOf('/') + 1) : pkgName;
|
|
410
|
+
const get = (key) => {
|
|
411
|
+
let entry = out.get(key);
|
|
412
|
+
if (!entry) {
|
|
413
|
+
entry = {};
|
|
414
|
+
out.set(key, entry);
|
|
415
|
+
}
|
|
416
|
+
return entry;
|
|
417
|
+
};
|
|
418
|
+
if (typeof nodeBin === 'string') {
|
|
419
|
+
get(baseName).nodeTarget = nodeBin;
|
|
420
|
+
}
|
|
421
|
+
else if (nodeBin && typeof nodeBin === 'object') {
|
|
422
|
+
for (const [k, v] of Object.entries(nodeBin)) {
|
|
423
|
+
if (typeof v === 'string' && v.length > 0)
|
|
424
|
+
get(k).nodeTarget = v;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (typeof gjsifyBin === 'string') {
|
|
428
|
+
get(baseName).gjsTarget = gjsifyBin;
|
|
429
|
+
}
|
|
430
|
+
else if (gjsifyBin && typeof gjsifyBin === 'object') {
|
|
431
|
+
for (const [k, v] of Object.entries(gjsifyBin)) {
|
|
432
|
+
if (typeof v === 'string' && v.length > 0)
|
|
433
|
+
get(k).gjsTarget = v;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return out;
|
|
259
437
|
}
|
|
260
438
|
async function projectInstallViaNpm(args) {
|
|
261
439
|
const npmArgs = ['install'];
|
package/lib/commands/run.js
CHANGED
|
@@ -18,7 +18,7 @@ import { delimiter, join, resolve } from 'node:path';
|
|
|
18
18
|
import { spawn } from 'node:child_process';
|
|
19
19
|
import { runGjsBundle } from '../utils/run-gjs.js';
|
|
20
20
|
import { readPackageJson } from '../utils/pkg-json-edit.js';
|
|
21
|
-
import {
|
|
21
|
+
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
22
22
|
export const runCommand = {
|
|
23
23
|
command: 'run <target> [args..]',
|
|
24
24
|
description: 'Run a script from package.json (yarn-run-style) or a GJS bundle file. If <target> resolves to a file on disk (or has a path-like prefix), it is launched via gjs with LD_LIBRARY_PATH + GI_TYPELIB_PATH set for native packages. Otherwise it is looked up in the current package.json `scripts`.',
|
|
@@ -94,8 +94,11 @@ async function runScript(script, extraArgs) {
|
|
|
94
94
|
const fullCmd = extraArgs.length > 0
|
|
95
95
|
? `${literal} ${extraArgs.map(shellEscape).join(' ')}`
|
|
96
96
|
: literal;
|
|
97
|
+
// ensureMainLoop() (called inside spawn) keeps GJS alive after the
|
|
98
|
+
// child exits — without an explicit process.exit() the success path
|
|
99
|
+
// would park the loop forever. The error path already exits.
|
|
97
100
|
await new Promise((resolveOk, reject) => {
|
|
98
|
-
const child = spawn(fullCmd, { cwd, env, stdio: 'inherit', shell: true });
|
|
101
|
+
const child = spawn(fullCmd, [], { cwd, env, stdio: 'inherit', shell: true });
|
|
99
102
|
child.on('close', (code) => {
|
|
100
103
|
if (code === 0)
|
|
101
104
|
resolveOk();
|
|
@@ -107,30 +110,7 @@ async function runScript(script, extraArgs) {
|
|
|
107
110
|
console.error(err.message);
|
|
108
111
|
process.exit(1);
|
|
109
112
|
});
|
|
110
|
-
|
|
111
|
-
function findWorkspaceRoot(start) {
|
|
112
|
-
let dir = start;
|
|
113
|
-
for (let i = 0; i < 12; i++) {
|
|
114
|
-
const pkgPath = join(dir, 'package.json');
|
|
115
|
-
if (existsSync(pkgPath)) {
|
|
116
|
-
const pkg = readPackageJson(pkgPath);
|
|
117
|
-
if (pkg?.workspaces !== undefined) {
|
|
118
|
-
try {
|
|
119
|
-
// Sanity-check that cwd is reachable as a workspace —
|
|
120
|
-
// otherwise we'd pick an unrelated grand-parent monorepo.
|
|
121
|
-
const ws = discoverWorkspaces(dir);
|
|
122
|
-
if (dir === start || ws.some((w) => w.location === start))
|
|
123
|
-
return dir;
|
|
124
|
-
}
|
|
125
|
-
catch { /* not a usable workspace root */ }
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
const parent = resolve(dir, '..');
|
|
129
|
-
if (parent === dir)
|
|
130
|
-
break;
|
|
131
|
-
dir = parent;
|
|
132
|
-
}
|
|
133
|
-
return null;
|
|
113
|
+
process.exit(0);
|
|
134
114
|
}
|
|
135
115
|
function shellEscape(arg) {
|
|
136
116
|
if (/^[a-zA-Z0-9_\-./=:@,]+$/.test(arg))
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// extensively in gjsify's own root `package.json` (17 call sites).
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { discoverWorkspaces } from '@gjsify/workspace';
|
|
8
|
+
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
8
9
|
export const workspaceCommand = {
|
|
9
10
|
command: 'workspace <name> <script> [args..]',
|
|
10
11
|
description: 'Run a workspace script (`yarn workspace <name> run <script>` equivalent).',
|
|
@@ -25,7 +26,12 @@ export const workspaceCommand = {
|
|
|
25
26
|
array: true,
|
|
26
27
|
}),
|
|
27
28
|
handler: async (args) => {
|
|
28
|
-
|
|
29
|
+
// Walk up to the monorepo root — `gjsify workspace` is often
|
|
30
|
+
// invoked from a child workspace's script (e.g. website's
|
|
31
|
+
// `build:deps` calls `gjsify workspace @gjsify/adwaita-web …`),
|
|
32
|
+
// where process.cwd() is the child workspace, not the monorepo root.
|
|
33
|
+
const root = findWorkspaceRoot(process.cwd()) ?? process.cwd();
|
|
34
|
+
const workspaces = discoverWorkspaces(root);
|
|
29
35
|
const target = workspaces.find((w) => w.name === args.name);
|
|
30
36
|
if (!target) {
|
|
31
37
|
console.error(`gjsify workspace: no workspace named "${args.name}" — discovered ${workspaces.length} workspace(s)`);
|
|
@@ -57,6 +63,10 @@ export const workspaceCommand = {
|
|
|
57
63
|
console.error(err.message);
|
|
58
64
|
process.exit(1);
|
|
59
65
|
});
|
|
66
|
+
// ensureMainLoop() (called inside spawn) keeps GJS alive after the
|
|
67
|
+
// child exits — without an explicit process.exit() the success path
|
|
68
|
+
// would park the loop forever.
|
|
69
|
+
process.exit(0);
|
|
60
70
|
},
|
|
61
71
|
};
|
|
62
72
|
function detectPackageManager() {
|