@gjsify/cli 0.3.21 → 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 +791 -0
- package/lib/actions/build.js +4 -17
- package/lib/bundler-pick.d.ts +79 -0
- package/lib/bundler-pick.js +436 -0
- package/lib/commands/foreach.d.ts +17 -0
- package/lib/commands/foreach.js +341 -0
- package/lib/commands/index.d.ts +2 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/install.d.ts +1 -0
- package/lib/commands/install.js +401 -27
- package/lib/commands/run.d.ts +1 -1
- package/lib/commands/run.js +113 -20
- package/lib/commands/workspace.d.ts +8 -0
- package/lib/commands/workspace.js +79 -0
- package/lib/config.js +12 -1
- package/lib/index.js +11 -3
- package/lib/types/config-data.d.ts +10 -1
- package/lib/utils/install-backend-native.d.ts +5 -1
- package/lib/utils/install-backend-native.js +329 -70
- package/lib/utils/install-backend.d.ts +11 -1
- package/lib/utils/install-backend.js +4 -2
- package/lib/utils/pkg-json-edit.d.ts +47 -0
- package/lib/utils/pkg-json-edit.js +108 -0
- package/lib/utils/workspace-root.d.ts +1 -0
- package/lib/utils/workspace-root.js +46 -0
- package/package.json +70 -44
- package/src/actions/build.ts +0 -431
- package/src/actions/index.ts +0 -1
- package/src/commands/build.ts +0 -146
- package/src/commands/check.ts +0 -87
- package/src/commands/create.ts +0 -63
- package/src/commands/dlx.ts +0 -195
- package/src/commands/flatpak/build.ts +0 -225
- package/src/commands/flatpak/ci.ts +0 -173
- package/src/commands/flatpak/deps.ts +0 -120
- package/src/commands/flatpak/index.ts +0 -53
- package/src/commands/flatpak/init.ts +0 -191
- package/src/commands/flatpak/utils.ts +0 -76
- package/src/commands/gettext.ts +0 -258
- package/src/commands/gresource.ts +0 -97
- package/src/commands/gsettings.ts +0 -87
- package/src/commands/index.ts +0 -12
- package/src/commands/info.ts +0 -70
- package/src/commands/install.ts +0 -195
- package/src/commands/run.ts +0 -33
- package/src/commands/showcase.ts +0 -149
- package/src/config.ts +0 -304
- package/src/constants.ts +0 -1
- package/src/index.ts +0 -37
- package/src/types/cli-build-options.ts +0 -100
- package/src/types/command.ts +0 -10
- package/src/types/config-data-library.ts +0 -5
- package/src/types/config-data-typescript.ts +0 -6
- package/src/types/config-data.ts +0 -225
- package/src/types/cosmiconfig-result.ts +0 -5
- package/src/types/index.ts +0 -6
- package/src/utils/check-system-deps.ts +0 -480
- package/src/utils/detect-native-packages.ts +0 -153
- package/src/utils/discover-showcases.ts +0 -75
- package/src/utils/dlx-cache.ts +0 -135
- package/src/utils/install-backend-native.ts +0 -363
- package/src/utils/install-backend.ts +0 -88
- package/src/utils/install-global.ts +0 -182
- package/src/utils/normalize-bundler-options.ts +0 -129
- package/src/utils/parse-spec.ts +0 -48
- package/src/utils/resolve-gjs-entry.ts +0 -96
- package/src/utils/resolve-plugin-by-name.ts +0 -106
- package/src/utils/run-gjs.ts +0 -90
- package/tsconfig.json +0 -16
package/lib/commands/install.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
// `gjsify install [pkg...]` — install packages with gjsify-aware post-checks.
|
|
2
2
|
//
|
|
3
3
|
// Modes:
|
|
4
|
-
// gjsify install → project install (
|
|
5
|
-
// gjsify install <pkg> [<pkg>...] → add package(s) to project (
|
|
4
|
+
// gjsify install → project install (native, reads pkg.json)
|
|
5
|
+
// gjsify install <pkg> [<pkg>...] → add package(s) to project (native)
|
|
6
6
|
// gjsify install -g <pkg> [...] → user-global install (XDG, GJS-runnable bin)
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// All three modes route through `@gjsify/{semver,npm-registry,tar}` via
|
|
9
|
+
// `installPackagesNative` — no Node/npm required at runtime. Set
|
|
10
|
+
// `GJSIFY_INSTALL_BACKEND=npm` to opt back into the legacy `npm install`
|
|
11
|
+
// subprocess flow (useful as escape-hatch for projects that hit a
|
|
12
|
+
// missing native-backend feature).
|
|
11
13
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// extracted package, so package-relative assets like `@ts-for-gir/cli`'s
|
|
18
|
-
// `dist-templates/` are found by ordinary `__dirname/..` resolution — no
|
|
19
|
-
// embedded asset stores, no separate release tarballs.
|
|
14
|
+
// Workspace-aware install (`gjsify install` in a monorepo root with a
|
|
15
|
+
// `"workspaces"` field) is Phase D.3 — for now we detect and surface a
|
|
16
|
+
// clear error pointing at the in-progress work.
|
|
17
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { dirname, join, relative } from 'node:path';
|
|
20
19
|
import { spawn } from 'node:child_process';
|
|
21
|
-
import {
|
|
20
|
+
import { discoverWorkspaces } from '@gjsify/workspace';
|
|
22
21
|
import { buildInstallCommand, detectPackageManager, runMinimalChecks, } from '../utils/check-system-deps.js';
|
|
23
22
|
import { detectNativePackages } from '../utils/detect-native-packages.js';
|
|
24
23
|
import { installPackages } from '../utils/install-backend.js';
|
|
25
24
|
import { binDirOnPath, defaultGlobalLayout, linkGlobalBins, specToPackageName, } from '../utils/install-global.js';
|
|
25
|
+
import { addDependencyEntry, defaultRangeFromVersion, parseSpec, projectSpecsFromPackageJson, readPackageJson, writePackageJson, } from '../utils/pkg-json-edit.js';
|
|
26
26
|
export const installCommand = {
|
|
27
27
|
command: 'install [packages..]',
|
|
28
28
|
description: 'Install npm dependencies in the current project (or globally with -g), then run gjsify-aware post-checks.',
|
|
@@ -41,12 +41,31 @@ export const installCommand = {
|
|
|
41
41
|
.option('save-dev', { type: 'boolean', alias: 'D' })
|
|
42
42
|
.option('save-peer', { type: 'boolean' })
|
|
43
43
|
.option('save-optional', { type: 'boolean', alias: 'O' })
|
|
44
|
+
.option('immutable', {
|
|
45
|
+
description: 'CI mode: install strictly from gjsify-lock.json, fail if the lockfile is missing or stale. Equivalent to yarn --immutable / npm ci --frozen-lockfile.',
|
|
46
|
+
type: 'boolean',
|
|
47
|
+
default: false,
|
|
48
|
+
})
|
|
44
49
|
.option('verbose', {
|
|
45
50
|
description: 'Verbose install logging.',
|
|
46
51
|
type: 'boolean',
|
|
47
52
|
default: false,
|
|
48
53
|
}),
|
|
49
54
|
handler: async (args) => {
|
|
55
|
+
// --immutable is incompatible with explicit `<pkg>` adds and with
|
|
56
|
+
// `--global` (which has no lockfile concept). Matches yarn's
|
|
57
|
+
// behavior: `yarn add --immutable` is a hard error.
|
|
58
|
+
if (args.immutable) {
|
|
59
|
+
if (args.packages && args.packages.length > 0) {
|
|
60
|
+
console.error('gjsify install --immutable does not accept package arguments. ' +
|
|
61
|
+
'Remove the package names or drop --immutable.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
if (args.global) {
|
|
65
|
+
console.error('gjsify install --immutable is incompatible with --global.');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
50
69
|
if (args.global) {
|
|
51
70
|
if (!args.packages || args.packages.length === 0) {
|
|
52
71
|
console.error('gjsify install --global requires at least one <pkg> argument.');
|
|
@@ -60,22 +79,377 @@ export const installCommand = {
|
|
|
60
79
|
await installGlobalAndLink(args.packages, { verbose: args.verbose });
|
|
61
80
|
return;
|
|
62
81
|
}
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (args.verbose)
|
|
71
|
-
npmArgs.push('--loglevel', 'verbose');
|
|
72
|
-
if (args.packages && args.packages.length > 0) {
|
|
73
|
-
npmArgs.push(...args.packages);
|
|
74
|
-
}
|
|
75
|
-
await spawnNpm(npmArgs);
|
|
82
|
+
// Escape-hatch: legacy npm subprocess flow.
|
|
83
|
+
if (process.env.GJSIFY_INSTALL_BACKEND === 'npm') {
|
|
84
|
+
await projectInstallViaNpm(args);
|
|
85
|
+
await runPostInstallChecks();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await projectInstallNative(args);
|
|
76
89
|
await runPostInstallChecks();
|
|
77
90
|
},
|
|
78
91
|
};
|
|
92
|
+
function isWorkspaceRoot(cwd) {
|
|
93
|
+
const pkgPath = join(cwd, 'package.json');
|
|
94
|
+
const pkg = readPackageJson(pkgPath);
|
|
95
|
+
if (!pkg)
|
|
96
|
+
return false;
|
|
97
|
+
return pkg.workspaces !== undefined;
|
|
98
|
+
}
|
|
99
|
+
function depKindFromArgs(args) {
|
|
100
|
+
if (args['save-dev'])
|
|
101
|
+
return 'devDependencies';
|
|
102
|
+
if (args['save-peer'])
|
|
103
|
+
return 'peerDependencies';
|
|
104
|
+
if (args['save-optional'])
|
|
105
|
+
return 'optionalDependencies';
|
|
106
|
+
return 'dependencies';
|
|
107
|
+
}
|
|
108
|
+
async function projectInstallNative(args) {
|
|
109
|
+
const cwd = process.cwd();
|
|
110
|
+
const pkgPath = join(cwd, 'package.json');
|
|
111
|
+
// Yarn-Berry / PnP detection: fall back to yarn with a clear warning
|
|
112
|
+
// rather than producing a half-working node_modules tree.
|
|
113
|
+
if (existsSync(join(cwd, '.pnp.cjs')) || existsSync(join(cwd, '.pnp.loader.mjs'))) {
|
|
114
|
+
throw new Error('gjsify install: detected Yarn PnP (.pnp.cjs) — native install is ' +
|
|
115
|
+
'not PnP-aware yet. Use `yarn install` or set ' +
|
|
116
|
+
'GJSIFY_INSTALL_BACKEND=npm.');
|
|
117
|
+
}
|
|
118
|
+
// Workspace install (no args, root pkg.json has `workspaces`) — Phase D.3.
|
|
119
|
+
// Project-local `gjsify install <pkg>` inside a workspace child still
|
|
120
|
+
// goes through the single-package code path below (this branch only
|
|
121
|
+
// fires for the root no-args case, which is the `yarn install`
|
|
122
|
+
// equivalent).
|
|
123
|
+
if ((!args.packages || args.packages.length === 0) && isWorkspaceRoot(cwd)) {
|
|
124
|
+
await workspaceInstall(cwd, args);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
let specs;
|
|
128
|
+
const pkg = readPackageJson(pkgPath);
|
|
129
|
+
const existingSpecs = pkg ? projectSpecsFromPackageJson(pkg) : [];
|
|
130
|
+
if (args.packages && args.packages.length > 0) {
|
|
131
|
+
// Combine new specs with existing manifest deps so a single
|
|
132
|
+
// `gjsify install <new>` doesn't churn the lockfile (would drop
|
|
133
|
+
// every previously-pinned entry otherwise). New specs with the
|
|
134
|
+
// same name as an existing dep override.
|
|
135
|
+
const newNames = new Set(args.packages.map((s) => parseSpec(s).name));
|
|
136
|
+
const carryover = existingSpecs.filter((s) => !newNames.has(parseSpec(s).name));
|
|
137
|
+
specs = [...carryover, ...args.packages];
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
if (!pkg) {
|
|
141
|
+
throw new Error(`gjsify install: no package.json in ${cwd}`);
|
|
142
|
+
}
|
|
143
|
+
specs = existingSpecs;
|
|
144
|
+
if (specs.length === 0) {
|
|
145
|
+
console.log('gjsify install: no dependencies declared in package.json — nothing to do.');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
mkdirSync(cwd, { recursive: true });
|
|
150
|
+
const result = await installPackages({
|
|
151
|
+
prefix: cwd,
|
|
152
|
+
specs,
|
|
153
|
+
verbose: args.verbose,
|
|
154
|
+
// --immutable consumes the lockfile verbatim and must NOT rewrite
|
|
155
|
+
// it (the whole point is byte-stability under CI).
|
|
156
|
+
lockfile: !args.immutable,
|
|
157
|
+
frozen: args.immutable,
|
|
158
|
+
});
|
|
159
|
+
// Update package.json only when the user passed explicit packages
|
|
160
|
+
// (the `gjsify install <pkg>...` add-a-dep flow). The no-args refresh
|
|
161
|
+
// flow doesn't mutate manifest entries.
|
|
162
|
+
if (args.packages && args.packages.length > 0 && pkg) {
|
|
163
|
+
const kind = depKindFromArgs(args);
|
|
164
|
+
for (const spec of args.packages) {
|
|
165
|
+
const { name, range } = parseSpec(spec);
|
|
166
|
+
const installed = result.installed.find((r) => r.name === name);
|
|
167
|
+
const finalRange = range ?? (installed ? defaultRangeFromVersion(installed.version) : 'latest');
|
|
168
|
+
addDependencyEntry(pkg, name, finalRange, kind);
|
|
169
|
+
}
|
|
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.
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Phase D.3 — workspace-aware install. Mirrors what `yarn install` does
|
|
203
|
+
* at a monorepo root:
|
|
204
|
+
* 1. Discover every workspace under the root.
|
|
205
|
+
* 2. Aggregate the union of their external (non-`workspace:`) deps.
|
|
206
|
+
* 3. Run the native install backend ONCE at the root prefix so all
|
|
207
|
+
* externals land in a single `node_modules/` (poor-man's hoisting —
|
|
208
|
+
* we don't deduplicate version-range conflicts yet, the BFS resolver
|
|
209
|
+
* picks first-match).
|
|
210
|
+
* 4. For every `workspace:` reference, symlink the target workspace's
|
|
211
|
+
* directory into the requesting workspace's `node_modules/<dep>`
|
|
212
|
+
* so `import '@gjsify/utils'` resolves to the local source.
|
|
213
|
+
*
|
|
214
|
+
* Hoisting strategy is intentionally minimal — D.3 ships the working
|
|
215
|
+
* baseline; per-workspace dedup + nested `node_modules/` for version
|
|
216
|
+
* conflicts are tracked as a follow-up in STATUS.md "Open TODOs".
|
|
217
|
+
*/
|
|
218
|
+
async function workspaceInstall(cwd, args) {
|
|
219
|
+
const workspaces = discoverWorkspaces(cwd, { includeRoot: true });
|
|
220
|
+
if (workspaces.length === 0) {
|
|
221
|
+
throw new Error(`gjsify install: ${cwd} has a "workspaces" field but no workspaces were discovered`);
|
|
222
|
+
}
|
|
223
|
+
const byName = new Map(workspaces.map((w) => [w.name, w]));
|
|
224
|
+
const externalSpecs = new Set();
|
|
225
|
+
const symlinks = [];
|
|
226
|
+
for (const ws of workspaces) {
|
|
227
|
+
const m = ws.manifest;
|
|
228
|
+
for (const kind of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
229
|
+
const block = m[kind];
|
|
230
|
+
if (!block)
|
|
231
|
+
continue;
|
|
232
|
+
for (const [depName, spec] of Object.entries(block)) {
|
|
233
|
+
if (typeof spec !== 'string')
|
|
234
|
+
continue;
|
|
235
|
+
if (spec.startsWith('workspace:')) {
|
|
236
|
+
const target = byName.get(depName);
|
|
237
|
+
if (!target) {
|
|
238
|
+
throw new Error(`gjsify install: ${ws.name} declares "${depName}: ${spec}" but ` +
|
|
239
|
+
`no workspace with that name exists`);
|
|
240
|
+
}
|
|
241
|
+
symlinks.push({ fromWorkspaceName: ws.name, depName, targetLocation: target.location });
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (/^(link|file|portal|git\+|https?):/.test(spec))
|
|
245
|
+
continue;
|
|
246
|
+
externalSpecs.add(`${depName}@${spec}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
console.log(`gjsify install: ${workspaces.length} workspace(s), ${externalSpecs.size} external dep spec(s), ${symlinks.length} workspace symlink(s)`);
|
|
251
|
+
if (externalSpecs.size > 0) {
|
|
252
|
+
await installPackages({
|
|
253
|
+
prefix: cwd,
|
|
254
|
+
specs: [...externalSpecs],
|
|
255
|
+
verbose: args.verbose,
|
|
256
|
+
lockfile: !args.immutable,
|
|
257
|
+
frozen: args.immutable,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
else if (args.verbose) {
|
|
261
|
+
console.log('gjsify install: no external deps to fetch');
|
|
262
|
+
}
|
|
263
|
+
for (const link of symlinks) {
|
|
264
|
+
const target = byName.get(link.fromWorkspaceName);
|
|
265
|
+
if (!target)
|
|
266
|
+
continue;
|
|
267
|
+
const linkPath = join(target.location, 'node_modules', link.depName);
|
|
268
|
+
mkdirSync(dirname(linkPath), { recursive: true });
|
|
269
|
+
// Remove any prior entry (regular dir, broken symlink, file).
|
|
270
|
+
try {
|
|
271
|
+
const stat = lstatSync(linkPath);
|
|
272
|
+
if (stat.isSymbolicLink() || stat.isFile()) {
|
|
273
|
+
rmSync(linkPath, { force: true });
|
|
274
|
+
}
|
|
275
|
+
else if (stat.isDirectory()) {
|
|
276
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch { /* ENOENT — fine, nothing to remove */ }
|
|
280
|
+
// Relative symlink so the repo is portable across checkout paths.
|
|
281
|
+
const relTarget = relative(dirname(linkPath), link.targetLocation);
|
|
282
|
+
symlinkSync(relTarget, linkPath);
|
|
283
|
+
}
|
|
284
|
+
if (symlinks.length > 0) {
|
|
285
|
+
console.log(`gjsify install: wired ${symlinks.length} workspace symlink(s)`);
|
|
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;
|
|
437
|
+
}
|
|
438
|
+
async function projectInstallViaNpm(args) {
|
|
439
|
+
const npmArgs = ['install'];
|
|
440
|
+
if (args['save-dev'])
|
|
441
|
+
npmArgs.push('--save-dev');
|
|
442
|
+
if (args['save-peer'])
|
|
443
|
+
npmArgs.push('--save-peer');
|
|
444
|
+
if (args['save-optional'])
|
|
445
|
+
npmArgs.push('--save-optional');
|
|
446
|
+
if (args.verbose)
|
|
447
|
+
npmArgs.push('--loglevel', 'verbose');
|
|
448
|
+
if (args.packages && args.packages.length > 0) {
|
|
449
|
+
npmArgs.push(...args.packages);
|
|
450
|
+
}
|
|
451
|
+
await spawnNpm(npmArgs);
|
|
452
|
+
}
|
|
79
453
|
async function spawnNpm(npmArgs) {
|
|
80
454
|
return new Promise((resolve, reject) => {
|
|
81
455
|
const child = spawn('npm', npmArgs, { stdio: 'inherit' });
|
package/lib/commands/run.d.ts
CHANGED
package/lib/commands/run.js
CHANGED
|
@@ -1,26 +1,119 @@
|
|
|
1
|
-
|
|
1
|
+
// `gjsify run <target> [args..]` — dual-mode runner.
|
|
2
|
+
//
|
|
3
|
+
// gjsify run <file> → existing behavior: run a GJS bundle file
|
|
4
|
+
// via `gjs -m`, with LD_LIBRARY_PATH +
|
|
5
|
+
// GI_TYPELIB_PATH set for native packages.
|
|
6
|
+
// gjsify run <script> → yarn-run-style: look up `<script>` in the
|
|
7
|
+
// current workspace's package.json `scripts`
|
|
8
|
+
// and execute it with `node_modules/.bin` on
|
|
9
|
+
// PATH (workspace + monorepo root).
|
|
10
|
+
//
|
|
11
|
+
// Phase D.5 added the script-runner side. The two modes coexist via a
|
|
12
|
+
// `looksLikeFile()` heuristic: anything with a path separator, JS-ish
|
|
13
|
+
// extension, or that resolves to an existing path on disk is treated
|
|
14
|
+
// as a bundle file. Everything else is a script name. Users who want
|
|
15
|
+
// to disambiguate can pass `./<file>` explicitly.
|
|
16
|
+
import { existsSync } from 'node:fs';
|
|
17
|
+
import { delimiter, join, resolve } from 'node:path';
|
|
18
|
+
import { spawn } from 'node:child_process';
|
|
2
19
|
import { runGjsBundle } from '../utils/run-gjs.js';
|
|
20
|
+
import { readPackageJson } from '../utils/pkg-json-edit.js';
|
|
21
|
+
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
3
22
|
export const runCommand = {
|
|
4
|
-
command: 'run <
|
|
5
|
-
description: 'Run a GJS bundle,
|
|
6
|
-
builder: (yargs) =>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
default: [],
|
|
19
|
-
});
|
|
20
|
-
},
|
|
23
|
+
command: 'run <target> [args..]',
|
|
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`.',
|
|
25
|
+
builder: (yargs) => yargs
|
|
26
|
+
.positional('target', {
|
|
27
|
+
description: 'Either a script name (looked up in package.json `scripts`) or a path to a GJS bundle (e.g. dist/gjs.js).',
|
|
28
|
+
type: 'string',
|
|
29
|
+
demandOption: true,
|
|
30
|
+
})
|
|
31
|
+
.positional('args', {
|
|
32
|
+
description: 'Extra arguments passed through to the script / gjs.',
|
|
33
|
+
type: 'string',
|
|
34
|
+
array: true,
|
|
35
|
+
default: [],
|
|
36
|
+
}),
|
|
21
37
|
handler: async (args) => {
|
|
22
|
-
const
|
|
38
|
+
const target = args.target;
|
|
23
39
|
const extraArgs = args.args ?? [];
|
|
24
|
-
|
|
40
|
+
if (looksLikeFile(target)) {
|
|
41
|
+
const file = resolve(target);
|
|
42
|
+
await runGjsBundle(file, extraArgs);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
await runScript(target, extraArgs);
|
|
25
46
|
},
|
|
26
47
|
};
|
|
48
|
+
function looksLikeFile(target) {
|
|
49
|
+
if (target.startsWith('./') || target.startsWith('../') || target.startsWith('/'))
|
|
50
|
+
return true;
|
|
51
|
+
if (target.includes('/') || target.includes('\\'))
|
|
52
|
+
return true;
|
|
53
|
+
if (/\.(c?js|mjs|cjs|gjs)$/.test(target))
|
|
54
|
+
return true;
|
|
55
|
+
return existsSync(target);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run a script declared in the current workspace's `package.json#scripts`.
|
|
59
|
+
* Mirrors `yarn run <script>` semantics:
|
|
60
|
+
* - PATH prepended with `<workspace>/node_modules/.bin` AND the
|
|
61
|
+
* monorepo-root `node_modules/.bin` (covers locally-installed bins
|
|
62
|
+
* and hoisted bins)
|
|
63
|
+
* - extra args appended after the script's literal command, shell-escaped
|
|
64
|
+
* - executed through `shell: true` so `&&` / `|` / env-var refs work
|
|
65
|
+
* exactly as in package.json scripts (matches npm/yarn)
|
|
66
|
+
*/
|
|
67
|
+
async function runScript(script, extraArgs) {
|
|
68
|
+
const cwd = process.cwd();
|
|
69
|
+
const pkgPath = join(cwd, 'package.json');
|
|
70
|
+
const pkg = readPackageJson(pkgPath);
|
|
71
|
+
if (!pkg) {
|
|
72
|
+
console.error(`gjsify run: no package.json in ${cwd}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const scripts = pkg.scripts ?? {};
|
|
76
|
+
const literal = scripts[script];
|
|
77
|
+
if (typeof literal !== 'string') {
|
|
78
|
+
const available = Object.keys(scripts).join(', ') || '<none>';
|
|
79
|
+
console.error(`gjsify run: no script "${script}" in ${pkgPath} (available: ${available})`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const monorepoRoot = findWorkspaceRoot(cwd);
|
|
83
|
+
const binDirs = [join(cwd, 'node_modules', '.bin')];
|
|
84
|
+
if (monorepoRoot && monorepoRoot !== cwd) {
|
|
85
|
+
binDirs.push(join(monorepoRoot, 'node_modules', '.bin'));
|
|
86
|
+
}
|
|
87
|
+
const env = {
|
|
88
|
+
...process.env,
|
|
89
|
+
PATH: [...binDirs, process.env.PATH ?? ''].filter(Boolean).join(delimiter),
|
|
90
|
+
npm_lifecycle_event: script,
|
|
91
|
+
npm_package_name: pkg.name ?? '',
|
|
92
|
+
npm_package_version: pkg.version ?? '',
|
|
93
|
+
};
|
|
94
|
+
const fullCmd = extraArgs.length > 0
|
|
95
|
+
? `${literal} ${extraArgs.map(shellEscape).join(' ')}`
|
|
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.
|
|
100
|
+
await new Promise((resolveOk, reject) => {
|
|
101
|
+
const child = spawn(fullCmd, [], { cwd, env, stdio: 'inherit', shell: true });
|
|
102
|
+
child.on('close', (code) => {
|
|
103
|
+
if (code === 0)
|
|
104
|
+
resolveOk();
|
|
105
|
+
else
|
|
106
|
+
reject(new Error(`script "${script}" exited with code ${code}`));
|
|
107
|
+
});
|
|
108
|
+
child.on('error', reject);
|
|
109
|
+
}).catch((err) => {
|
|
110
|
+
console.error(err.message);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
});
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
function shellEscape(arg) {
|
|
116
|
+
if (/^[a-zA-Z0-9_\-./=:@,]+$/.test(arg))
|
|
117
|
+
return arg;
|
|
118
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
119
|
+
}
|