@gjsify/cli 0.4.28 → 0.4.30
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 +132 -132
- package/lib/actions/barrels-generate.js +1 -5
- package/lib/actions/build.d.ts +3 -3
- package/lib/actions/build.js +56 -64
- package/lib/bundler-pick.d.ts +3 -3
- package/lib/bundler-pick.js +5 -6
- package/lib/commands/build.d.ts +1 -1
- package/lib/commands/build.js +37 -31
- package/lib/commands/check.js +3 -3
- package/lib/commands/create.d.ts +1 -1
- package/lib/commands/dlx.d.ts +1 -1
- package/lib/commands/fix.js +33 -23
- package/lib/commands/flatpak/build.js +6 -2
- package/lib/commands/flatpak/check.js +9 -3
- package/lib/commands/flatpak/ci.js +1 -2
- package/lib/commands/flatpak/deps.js +1 -2
- package/lib/commands/flatpak/diff.js +2 -6
- package/lib/commands/flatpak/init.js +19 -19
- package/lib/commands/flatpak/release.js +2 -2
- package/lib/commands/flatpak/scaffold.js +3 -11
- package/lib/commands/flatpak/sync-flathub.js +5 -9
- package/lib/commands/flatpak/utils.js +1 -6
- package/lib/commands/foreach.d.ts +1 -1
- package/lib/commands/foreach.js +5 -14
- package/lib/commands/format.js +54 -41
- package/lib/commands/generate-installer.d.ts +1 -1
- package/lib/commands/gettext.d.ts +1 -1
- package/lib/commands/gettext.js +8 -15
- package/lib/commands/gresource.d.ts +1 -1
- package/lib/commands/gresource.js +8 -13
- package/lib/commands/gsettings.d.ts +1 -1
- package/lib/commands/gsettings.js +7 -8
- package/lib/commands/info.d.ts +1 -1
- package/lib/commands/install.d.ts +1 -1
- package/lib/commands/install.js +45 -13
- package/lib/commands/lint.d.ts +1 -1
- package/lib/commands/lint.js +22 -22
- package/lib/commands/pack.d.ts +1 -1
- package/lib/commands/pack.js +29 -17
- package/lib/commands/publish.d.ts +1 -1
- package/lib/commands/publish.js +17 -18
- package/lib/commands/run.d.ts +1 -1
- package/lib/commands/run.js +2 -6
- package/lib/commands/self-update.d.ts +1 -1
- package/lib/commands/self-update.js +1 -3
- package/lib/commands/showcase.d.ts +1 -1
- package/lib/commands/showcase.js +1 -1
- package/lib/commands/system-check.d.ts +1 -1
- package/lib/commands/system-check.js +8 -11
- package/lib/commands/test.js +12 -8
- package/lib/commands/uninstall.d.ts +1 -1
- package/lib/commands/uninstall.js +1 -3
- package/lib/commands/upgrade.d.ts +1 -1
- package/lib/commands/upgrade.js +109 -120
- package/lib/commands/workspace.d.ts +1 -1
- package/lib/commands/workspace.js +1 -3
- package/lib/config.js +18 -13
- package/lib/index.js +3 -1
- package/lib/templates/install.mjs.tmpl +20 -14
- package/lib/templates/oxfmtrc.tmpl +54 -0
- package/lib/templates/oxlintrc.json.tmpl +35 -0
- package/lib/types/command.d.ts +1 -1
- package/lib/types/config-data.d.ts +23 -13
- package/lib/types/cosmiconfig-result.d.ts +1 -1
- package/lib/utils/check-system-deps.js +10 -4
- package/lib/utils/detect-native-packages.js +1 -1
- package/lib/utils/dlx-cache.js +2 -7
- package/lib/utils/install-backend-native.d.ts +2 -2
- package/lib/utils/install-backend-native.js +112 -58
- package/lib/utils/install-backend.js +2 -1
- package/lib/utils/install-global.js +1 -3
- package/lib/utils/normalize-bundler-options.js +52 -17
- package/lib/utils/oxc-resolve.d.ts +63 -0
- package/lib/utils/oxc-resolve.js +264 -0
- package/lib/utils/pkg-json-edit.js +1 -6
- package/lib/utils/run-gjs.js +1 -4
- package/lib/utils/run-lifecycle-script.js +3 -7
- package/lib/utils/workspace-root.js +3 -1
- package/package.json +17 -17
- package/lib/templates/biome.json.tmpl +0 -79
- package/lib/utils/biome-resolve.d.ts +0 -47
- package/lib/utils/biome-resolve.js +0 -204
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
//
|
|
14
14
|
// Out of scope (still deferred): peerDependencies validation,
|
|
15
15
|
// lifecycle scripts, git/file specs.
|
|
16
|
-
import * as fs from
|
|
17
|
-
import * as path from
|
|
18
|
-
import * as os from
|
|
19
|
-
import { Range, SemVer, maxSatisfying, satisfies
|
|
20
|
-
import { DEFAULT_REGISTRY, fetchPackument, fetchTarball, parseNpmrc, } from
|
|
21
|
-
import { extractTarball } from
|
|
22
|
-
const DEFAULT_CONCURRENCY = Number(process.env.GJSIFY_INSTALL_CONCURRENCY ??
|
|
23
|
-
const LOCKFILE_NAME =
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import * as os from 'node:os';
|
|
19
|
+
import { Range, SemVer, maxSatisfying, satisfies } from '@gjsify/semver';
|
|
20
|
+
import { DEFAULT_REGISTRY, fetchPackument, fetchTarball, parseNpmrc, } from '@gjsify/npm-registry';
|
|
21
|
+
import { extractTarball } from '@gjsify/tar';
|
|
22
|
+
const DEFAULT_CONCURRENCY = Number(process.env.GJSIFY_INSTALL_CONCURRENCY ?? '8') || 8;
|
|
23
|
+
const LOCKFILE_NAME = 'gjsify-lock.json';
|
|
24
24
|
const LOCKFILE_VERSION = 2;
|
|
25
25
|
export async function installPackagesNative(opts) {
|
|
26
26
|
if (opts.specs.length === 0) {
|
|
27
|
-
throw new Error(
|
|
27
|
+
throw new Error('installPackagesNative: empty specs list');
|
|
28
28
|
}
|
|
29
29
|
fs.mkdirSync(opts.prefix, { recursive: true });
|
|
30
30
|
const npmrc = await loadNpmrc(opts);
|
|
@@ -47,25 +47,25 @@ export async function installPackagesNative(opts) {
|
|
|
47
47
|
throw new Error(`install: --immutable but ${lockfilePath} is stale.\n${drift}\n` +
|
|
48
48
|
`Re-run \`gjsify install\` (without --immutable) to refresh the lockfile.`);
|
|
49
49
|
}
|
|
50
|
-
log(
|
|
50
|
+
log('install: --immutable, using lockfile (%d package(s))', Object.keys(existingLock.packages).length);
|
|
51
51
|
nodes = lockfileToNodes(existingLock);
|
|
52
52
|
}
|
|
53
53
|
else if (existingLock && lockfileMatchesRequest(existingLock, opts.specs)) {
|
|
54
|
-
log(
|
|
54
|
+
log('install: using lockfile (%d package(s))', Object.keys(existingLock.packages).length);
|
|
55
55
|
nodes = lockfileToNodes(existingLock);
|
|
56
56
|
}
|
|
57
57
|
else {
|
|
58
|
-
log(
|
|
58
|
+
log('install: resolving %d top-level spec(s) → %s', opts.specs.length, opts.prefix);
|
|
59
59
|
nodes = await resolveDeps(opts.specs, npmrc, log, opts.overrides, opts.skipDeps);
|
|
60
60
|
if (opts.lockfile) {
|
|
61
61
|
writeLockfile(lockfilePath, opts.specs, nodes);
|
|
62
|
-
log(
|
|
62
|
+
log('install: wrote %s (%d entries)', LOCKFILE_NAME, nodes.length);
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
log(
|
|
65
|
+
log('install: downloading %d tarball(s)', nodes.length);
|
|
66
66
|
await downloadAndExtractAll(nodes, opts.prefix, npmrc, log);
|
|
67
67
|
await linkBins(nodes, opts.prefix, log);
|
|
68
|
-
log(
|
|
68
|
+
log('install: done');
|
|
69
69
|
// Surface the top-level requested packages so callers can update
|
|
70
70
|
// package.json with the resolved version (mirrors `npm install --save`
|
|
71
71
|
// behavior). Sub-deps are not included.
|
|
@@ -94,14 +94,14 @@ function topLevelResolutions(specs, nodes) {
|
|
|
94
94
|
return out;
|
|
95
95
|
}
|
|
96
96
|
function parseSpecName(spec) {
|
|
97
|
-
if (spec.startsWith(
|
|
98
|
-
const slash = spec.indexOf(
|
|
97
|
+
if (spec.startsWith('@')) {
|
|
98
|
+
const slash = spec.indexOf('/');
|
|
99
99
|
if (slash === -1)
|
|
100
100
|
return spec;
|
|
101
|
-
const at = spec.indexOf(
|
|
101
|
+
const at = spec.indexOf('@', slash + 1);
|
|
102
102
|
return at === -1 ? spec : spec.slice(0, at);
|
|
103
103
|
}
|
|
104
|
-
const at = spec.indexOf(
|
|
104
|
+
const at = spec.indexOf('@');
|
|
105
105
|
return at === -1 ? spec : spec.slice(0, at);
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
@@ -128,7 +128,7 @@ async function resolveDeps(specs, npmrc, log, overrides, skipDeps) {
|
|
|
128
128
|
return range;
|
|
129
129
|
if (override === range)
|
|
130
130
|
return range;
|
|
131
|
-
log(
|
|
131
|
+
log('install: override %s %s → %s', name, range, override);
|
|
132
132
|
return override;
|
|
133
133
|
};
|
|
134
134
|
const packumentCache = new Map();
|
|
@@ -139,7 +139,7 @@ async function resolveDeps(specs, npmrc, log, overrides, skipDeps) {
|
|
|
139
139
|
const fresh = fetchPackument(name, {
|
|
140
140
|
npmrc,
|
|
141
141
|
onRetry: ({ attempt, error, delayMs }) => {
|
|
142
|
-
log(
|
|
142
|
+
log('packument %s: retry %d after %dms (%s)', name, attempt, delayMs, errMsg(error));
|
|
143
143
|
},
|
|
144
144
|
});
|
|
145
145
|
packumentCache.set(name, fresh);
|
|
@@ -200,13 +200,23 @@ async function resolveDeps(specs, npmrc, log, overrides, skipDeps) {
|
|
|
200
200
|
if (installPath === `node_modules/${edge.name}`) {
|
|
201
201
|
root.set(edge.name, node);
|
|
202
202
|
}
|
|
203
|
-
log(
|
|
203
|
+
log('resolve: %s@%s ← %s (at %s)', edge.name, version, edge.range, installPath);
|
|
204
204
|
if (!skipDeps) {
|
|
205
205
|
for (const [depName, depRange] of Object.entries(node.dependencies)) {
|
|
206
|
-
queue.push({
|
|
206
|
+
queue.push({
|
|
207
|
+
from: installPath,
|
|
208
|
+
name: depName,
|
|
209
|
+
range: applyOverride(depName, depRange),
|
|
210
|
+
required: true,
|
|
211
|
+
});
|
|
207
212
|
}
|
|
208
213
|
for (const [depName, depRange] of Object.entries(node.optionalDependencies)) {
|
|
209
|
-
queue.push({
|
|
214
|
+
queue.push({
|
|
215
|
+
from: installPath,
|
|
216
|
+
name: depName,
|
|
217
|
+
range: applyOverride(depName, depRange),
|
|
218
|
+
required: false,
|
|
219
|
+
});
|
|
210
220
|
}
|
|
211
221
|
}
|
|
212
222
|
}
|
|
@@ -214,7 +224,7 @@ async function resolveDeps(specs, npmrc, log, overrides, skipDeps) {
|
|
|
214
224
|
// Optional deps that fail to resolve are skipped — yarn/npm
|
|
215
225
|
// behavior. Required deps re-throw.
|
|
216
226
|
if (!edge.required) {
|
|
217
|
-
log(
|
|
227
|
+
log('resolve: optional dep %s@%s skipped (%s)', edge.name, edge.range, e.message);
|
|
218
228
|
continue;
|
|
219
229
|
}
|
|
220
230
|
throw e;
|
|
@@ -245,12 +255,12 @@ function findVisible(requesterPath, name, byPath) {
|
|
|
245
255
|
// eslint-disable-next-line no-constant-condition
|
|
246
256
|
while (true) {
|
|
247
257
|
// Find the deepest `/node_modules/<pkg>` in `p`, strip it.
|
|
248
|
-
const idx = p.lastIndexOf(
|
|
258
|
+
const idx = p.lastIndexOf('/node_modules/');
|
|
249
259
|
if (idx < 0)
|
|
250
260
|
break;
|
|
251
261
|
p = p.slice(0, idx);
|
|
252
262
|
candidates.push(`${p}/node_modules/${name}`);
|
|
253
|
-
if (p ===
|
|
263
|
+
if (p === '')
|
|
254
264
|
break;
|
|
255
265
|
}
|
|
256
266
|
}
|
|
@@ -301,10 +311,10 @@ function readLockfile(lockfilePath) {
|
|
|
301
311
|
if (!fs.existsSync(lockfilePath))
|
|
302
312
|
return null;
|
|
303
313
|
try {
|
|
304
|
-
const parsed = JSON.parse(fs.readFileSync(lockfilePath,
|
|
314
|
+
const parsed = JSON.parse(fs.readFileSync(lockfilePath, 'utf-8'));
|
|
305
315
|
if (parsed.lockfileVersion !== LOCKFILE_VERSION)
|
|
306
316
|
return null;
|
|
307
|
-
if (!parsed.packages || typeof parsed.packages !==
|
|
317
|
+
if (!parsed.packages || typeof parsed.packages !== 'object')
|
|
308
318
|
return null;
|
|
309
319
|
return parsed;
|
|
310
320
|
}
|
|
@@ -330,7 +340,7 @@ function writeLockfile(lockfilePath, specs, nodes) {
|
|
|
330
340
|
requested: [...specs],
|
|
331
341
|
packages,
|
|
332
342
|
};
|
|
333
|
-
fs.writeFileSync(lockfilePath, JSON.stringify(lockfile, null, 2) +
|
|
343
|
+
fs.writeFileSync(lockfilePath, JSON.stringify(lockfile, null, 2) + '\n');
|
|
334
344
|
}
|
|
335
345
|
function lockfileToNodes(lockfile) {
|
|
336
346
|
return Object.entries(lockfile.packages).map(([installPath, entry]) => ({
|
|
@@ -349,8 +359,8 @@ function lockfileToNodes(lockfile) {
|
|
|
349
359
|
function nameFromInstallPath(installPath) {
|
|
350
360
|
// Last `node_modules/` boundary, then the rest is the package name
|
|
351
361
|
// (single segment unscoped, or `@scope/pkg` scoped).
|
|
352
|
-
const idx = installPath.lastIndexOf(
|
|
353
|
-
const after = idx < 0 ? installPath.replace(/^node_modules\//,
|
|
362
|
+
const idx = installPath.lastIndexOf('/node_modules/');
|
|
363
|
+
const after = idx < 0 ? installPath.replace(/^node_modules\//, '') : installPath.slice(idx + '/node_modules/'.length);
|
|
354
364
|
return after;
|
|
355
365
|
}
|
|
356
366
|
function lockfileMatchesRequest(lockfile, specs) {
|
|
@@ -381,10 +391,10 @@ function describeLockfileDrift(lockfile, specs) {
|
|
|
381
391
|
return null;
|
|
382
392
|
const lines = [];
|
|
383
393
|
if (added.length > 0)
|
|
384
|
-
lines.push(` + ${added.sort().join(
|
|
394
|
+
lines.push(` + ${added.sort().join('\n + ')}`);
|
|
385
395
|
if (removed.length > 0)
|
|
386
|
-
lines.push(` - ${removed.sort().join(
|
|
387
|
-
return lines.join(
|
|
396
|
+
lines.push(` - ${removed.sort().join('\n - ')}`);
|
|
397
|
+
return lines.join('\n');
|
|
388
398
|
}
|
|
389
399
|
// Exported for unit-testing — keep the function name + signature
|
|
390
400
|
// stable, the install-backend itself still calls it via the local
|
|
@@ -400,25 +410,25 @@ export function parseSpec(raw) {
|
|
|
400
410
|
// shipped only prereleases (4.0.0-rc.17 is the `latest` tag, no
|
|
401
411
|
// stable 4.x yet) and `*` was selecting the abandoned 3.3.0
|
|
402
412
|
// instead.
|
|
403
|
-
if (raw.startsWith(
|
|
404
|
-
const slash = raw.indexOf(
|
|
413
|
+
if (raw.startsWith('@')) {
|
|
414
|
+
const slash = raw.indexOf('/');
|
|
405
415
|
if (slash < 0)
|
|
406
416
|
throw new Error(`Invalid spec (scoped name without slash): ${raw}`);
|
|
407
|
-
const at = raw.indexOf(
|
|
417
|
+
const at = raw.indexOf('@', slash);
|
|
408
418
|
if (at < 0)
|
|
409
|
-
return { name: raw, range:
|
|
410
|
-
return { name: raw.slice(0, at), range: raw.slice(at + 1) ||
|
|
419
|
+
return { name: raw, range: 'latest' };
|
|
420
|
+
return { name: raw.slice(0, at), range: raw.slice(at + 1) || 'latest' };
|
|
411
421
|
}
|
|
412
|
-
const at = raw.indexOf(
|
|
422
|
+
const at = raw.indexOf('@');
|
|
413
423
|
if (at < 0)
|
|
414
|
-
return { name: raw, range:
|
|
415
|
-
return { name: raw.slice(0, at), range: raw.slice(at + 1) ||
|
|
424
|
+
return { name: raw, range: 'latest' };
|
|
425
|
+
return { name: raw.slice(0, at), range: raw.slice(at + 1) || 'latest' };
|
|
416
426
|
}
|
|
417
427
|
// Exported for unit-testing. Internal API.
|
|
418
428
|
export function pickVersion(packument, range) {
|
|
419
429
|
// dist-tag fast path: `latest`, `next`, ...
|
|
420
|
-
if (packument[
|
|
421
|
-
return packument[
|
|
430
|
+
if (packument['dist-tags'][range])
|
|
431
|
+
return packument['dist-tags'][range];
|
|
422
432
|
// Validate range early so a typo fails loudly.
|
|
423
433
|
let parsedRange;
|
|
424
434
|
try {
|
|
@@ -442,8 +452,7 @@ async function downloadAndExtractAll(nodes, prefix, npmrc, log) {
|
|
|
442
452
|
// Sort by install-path depth ascending so parents extract before
|
|
443
453
|
// children. Extracting a parent on top of an existing child would
|
|
444
454
|
// wipe out the child.
|
|
445
|
-
const queue = [...nodes].sort((a, b) => depth(a.installPath) - depth(b.installPath) ||
|
|
446
|
-
(a.installPath < b.installPath ? -1 : 1));
|
|
455
|
+
const queue = [...nodes].sort((a, b) => depth(a.installPath) - depth(b.installPath) || (a.installPath < b.installPath ? -1 : 1));
|
|
447
456
|
const workers = [];
|
|
448
457
|
const concurrency = Math.max(1, Math.min(DEFAULT_CONCURRENCY, queue.length));
|
|
449
458
|
// Parents (depth 1) are extracted serially first to avoid concurrent
|
|
@@ -477,22 +486,69 @@ async function downloadAndExtractAll(nodes, prefix, npmrc, log) {
|
|
|
477
486
|
}
|
|
478
487
|
async function extractOne(node, prefix, npmrc, log) {
|
|
479
488
|
const dest = path.join(prefix, node.installPath);
|
|
480
|
-
|
|
489
|
+
// Defense-in-depth against the workspace-source-wipe data-loss bug:
|
|
490
|
+
// every extractable node MUST land inside a `node_modules/` directory.
|
|
491
|
+
// The resolver only ever produces `installPath`s of that shape, so this
|
|
492
|
+
// can only fail if a workspace package leaked into the fetch/extract
|
|
493
|
+
// queue (the root cause fixed in `workspaceInstall`). Refusing here means
|
|
494
|
+
// a regression in the resolver can never again `rmSync` a working-tree
|
|
495
|
+
// source dir — the realpath check additionally rejects a `dest` that
|
|
496
|
+
// resolves THROUGH a symlink into a directory outside node_modules.
|
|
497
|
+
assertNodeModulesDest(dest, node);
|
|
498
|
+
log('fetch: %s@%s ← %s (→ %s)', node.name, node.version, node.tarballUrl, node.installPath);
|
|
481
499
|
const bytes = await fetchTarball(node.tarballUrl, {
|
|
482
500
|
npmrc,
|
|
483
501
|
integrity: node.integrity,
|
|
484
502
|
onRetry: ({ attempt, error, delayMs }) => {
|
|
485
|
-
log(
|
|
503
|
+
log('tarball %s@%s: retry %d after %dms (%s)', node.name, node.version, attempt, delayMs, errMsg(error));
|
|
486
504
|
},
|
|
487
505
|
});
|
|
488
506
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
489
507
|
fs.mkdirSync(dest, { recursive: true });
|
|
490
508
|
await extractTarball(bytes, dest);
|
|
491
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* Guard: a tarball may only be extracted into a `node_modules/` directory.
|
|
512
|
+
*
|
|
513
|
+
* Two checks, both belt-and-suspenders against ever wiping a working-tree
|
|
514
|
+
* source dir (the install-deletes-workspace-sources data-loss bug):
|
|
515
|
+
*
|
|
516
|
+
* 1. The logical `installPath` must contain a `node_modules` path segment.
|
|
517
|
+
* The resolver always produces such paths; a workspace package that
|
|
518
|
+
* slipped into the queue would not.
|
|
519
|
+
* 2. If `dest` already exists and resolves (via symlink) to a directory
|
|
520
|
+
* whose REAL path is not under a `node_modules/` segment, refuse. This
|
|
521
|
+
* catches the case where `node_modules/<name>` is a symlink to a
|
|
522
|
+
* workspace's source tree — `rmSync(dest, { recursive: true })` would
|
|
523
|
+
* then delete the link's target contents.
|
|
524
|
+
*/
|
|
525
|
+
function assertNodeModulesDest(dest, node) {
|
|
526
|
+
const segments = dest.split(path.sep);
|
|
527
|
+
if (!segments.includes('node_modules')) {
|
|
528
|
+
throw new Error(`gjsify install: refusing to extract ${node.name}@${node.version} into ${dest} — ` +
|
|
529
|
+
`target is not inside a node_modules/ directory. This would overwrite working-tree files. ` +
|
|
530
|
+
`A workspace package likely leaked into the fetch queue (it must be symlinked, not fetched).`);
|
|
531
|
+
}
|
|
532
|
+
let real;
|
|
533
|
+
try {
|
|
534
|
+
real = fs.realpathSync(dest);
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
// `dest` doesn't exist yet (fresh install) — nothing to resolve, the
|
|
538
|
+
// logical-path check above is sufficient.
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const realSegments = real.split(path.sep);
|
|
542
|
+
if (!realSegments.includes('node_modules')) {
|
|
543
|
+
throw new Error(`gjsify install: refusing to extract ${node.name}@${node.version} — ${dest} resolves to ${real}, ` +
|
|
544
|
+
`which is outside any node_modules/ directory (likely a symlink to a workspace source tree). ` +
|
|
545
|
+
`Extracting here would delete working-tree source files.`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
492
548
|
function depth(installPath) {
|
|
493
549
|
// Count `node_modules/` segments to know nesting depth.
|
|
494
550
|
// `node_modules/foo` = 1, `node_modules/foo/node_modules/bar` = 2, etc.
|
|
495
|
-
return installPath.split(
|
|
551
|
+
return installPath.split('/node_modules/').length;
|
|
496
552
|
}
|
|
497
553
|
async function linkBins(nodes, prefix, log) {
|
|
498
554
|
// Only root-level packages publish bins into the top-level
|
|
@@ -500,7 +556,7 @@ async function linkBins(nodes, prefix, log) {
|
|
|
500
556
|
// direct dependents through the nested .bin (npm matches this) — we
|
|
501
557
|
// omit nested-bin linking for now since no consumer of the install
|
|
502
558
|
// backend depends on it (gjsify's own use cases all hit root bins).
|
|
503
|
-
const binDir = path.join(prefix,
|
|
559
|
+
const binDir = path.join(prefix, 'node_modules', '.bin');
|
|
504
560
|
let created = 0;
|
|
505
561
|
for (const node of nodes) {
|
|
506
562
|
if (!node.bin)
|
|
@@ -536,15 +592,13 @@ async function linkBins(nodes, prefix, log) {
|
|
|
536
592
|
}
|
|
537
593
|
}
|
|
538
594
|
if (created > 0)
|
|
539
|
-
log(
|
|
595
|
+
log('bin: linked %d entry(ies) under .bin/', created);
|
|
540
596
|
}
|
|
541
597
|
function normalizeBin(pkgName, bin) {
|
|
542
598
|
const out = new Map();
|
|
543
|
-
if (typeof bin ===
|
|
599
|
+
if (typeof bin === 'string') {
|
|
544
600
|
// String form is shorthand for `{ <last-segment-of-pkgName>: <bin> }`.
|
|
545
|
-
const baseName = pkgName.startsWith(
|
|
546
|
-
? pkgName.slice(pkgName.indexOf("/") + 1)
|
|
547
|
-
: pkgName;
|
|
601
|
+
const baseName = pkgName.startsWith('@') ? pkgName.slice(pkgName.indexOf('/') + 1) : pkgName;
|
|
548
602
|
out.set(baseName, bin);
|
|
549
603
|
return out;
|
|
550
604
|
}
|
|
@@ -565,11 +619,11 @@ async function loadNpmrc(opts) {
|
|
|
565
619
|
// workspace-root one too; the gjsify project-local case is what users
|
|
566
620
|
// hit most often (mock-registry tests, scoped-registry overrides), so
|
|
567
621
|
// we cover that explicitly.
|
|
568
|
-
for (const candidate of [path.join(home,
|
|
622
|
+
for (const candidate of [path.join(home, '.npmrc'), path.join(opts.prefix, '.npmrc')]) {
|
|
569
623
|
if (!fs.existsSync(candidate))
|
|
570
624
|
continue;
|
|
571
625
|
try {
|
|
572
|
-
const projectParsed = parseNpmrc(fs.readFileSync(candidate,
|
|
626
|
+
const projectParsed = parseNpmrc(fs.readFileSync(candidate, 'utf-8'));
|
|
573
627
|
parsed = { ...parsed, ...projectParsed, scopes: { ...parsed.scopes, ...projectParsed.scopes } };
|
|
574
628
|
}
|
|
575
629
|
catch (e) {
|
|
@@ -40,7 +40,8 @@ async function installViaNpm({ prefix, specs, verbose, registry }) {
|
|
|
40
40
|
'--no-package-lock',
|
|
41
41
|
'--no-audit',
|
|
42
42
|
'--no-fund',
|
|
43
|
-
'--prefix',
|
|
43
|
+
'--prefix',
|
|
44
|
+
prefix,
|
|
44
45
|
...(verbose ? ['--loglevel', 'verbose'] : ['--loglevel', 'warn']),
|
|
45
46
|
...specs,
|
|
46
47
|
];
|
|
@@ -161,9 +161,7 @@ function pickBinMap(pkgName, pkgJson) {
|
|
|
161
161
|
function normalizeBin(pkgName, bin) {
|
|
162
162
|
const out = new Map();
|
|
163
163
|
if (typeof bin === 'string') {
|
|
164
|
-
const baseName = pkgName.startsWith('@')
|
|
165
|
-
? pkgName.slice(pkgName.indexOf('/') + 1)
|
|
166
|
-
: pkgName;
|
|
164
|
+
const baseName = pkgName.startsWith('@') ? pkgName.slice(pkgName.indexOf('/') + 1) : pkgName;
|
|
167
165
|
out.set(baseName, bin);
|
|
168
166
|
return out;
|
|
169
167
|
}
|
|
@@ -2,10 +2,46 @@
|
|
|
2
2
|
// field on `.gjsifyrc.js` / `package.json#gjsify` into the equivalent
|
|
3
3
|
// `bundler?: RolldownOptions` shape. Logs a single warning per build.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// Also handles the top-level `bundler.define` alias: Rolldown reads
|
|
6
|
+
// `transform.define`, not a top-level `define`. A user who flat-renames
|
|
7
|
+
// `esbuild: { define: {...} }` → `bundler: { define: {...} }` puts `define`
|
|
8
|
+
// at the top level where Rolldown silently ignores it, causing
|
|
9
|
+
// `ReferenceError: <TOKEN> is not defined` at GJS load time. We auto-map
|
|
10
|
+
// top-level `bundler.define` into `bundler.transform.define` and emit a
|
|
11
|
+
// one-time warning so the user can correct their config.
|
|
12
|
+
//
|
|
13
|
+
// Drop `esbuild` shim in 0.5.0; the `bundler.define` alias is permanent.
|
|
6
14
|
let warnedOnce = false;
|
|
15
|
+
let warnedDefineOnce = false;
|
|
7
16
|
export function normalizeBundlerOptions(configData) {
|
|
8
|
-
const
|
|
17
|
+
const raw = (configData.bundler ?? {});
|
|
18
|
+
// --- Top-level bundler.define alias ------------------------------------------
|
|
19
|
+
// Rolldown only reads `transform.define`; a top-level `define` key is silently
|
|
20
|
+
// ignored. Detect it, warn once, and move it into `transform.define` so the
|
|
21
|
+
// user's flat esbuild→bundler rename of `define` Just Works.
|
|
22
|
+
let fromBundler = raw;
|
|
23
|
+
if (typeof raw['define'] === 'object' &&
|
|
24
|
+
raw['define'] !== null) {
|
|
25
|
+
if (!warnedDefineOnce) {
|
|
26
|
+
warnedDefineOnce = true;
|
|
27
|
+
// eslint-disable-next-line no-console
|
|
28
|
+
console.warn("[gjsify] WARNING: 'bundler.define' is not a valid Rolldown option and would be " +
|
|
29
|
+
"silently ignored — it has been auto-mapped to 'bundler.transform.define'. " +
|
|
30
|
+
"Move 'define' under 'bundler.transform.define' in your config to suppress " +
|
|
31
|
+
'this warning and avoid a ReferenceError at GJS load time.');
|
|
32
|
+
}
|
|
33
|
+
const { define: topLevelDefine, ...rest } = raw;
|
|
34
|
+
fromBundler = {
|
|
35
|
+
...rest,
|
|
36
|
+
transform: {
|
|
37
|
+
...rest.transform,
|
|
38
|
+
define: {
|
|
39
|
+
...topLevelDefine,
|
|
40
|
+
...rest.transform?.define,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
9
45
|
if (!configData.esbuild)
|
|
10
46
|
return fromBundler;
|
|
11
47
|
if (!warnedOnce) {
|
|
@@ -22,19 +58,19 @@ export function normalizeBundlerOptions(configData) {
|
|
|
22
58
|
// user-provided config and `input` must survive the merge.
|
|
23
59
|
const out = { ...fromEsbuild, ...fromBundler };
|
|
24
60
|
if (fromEsbuild.output || fromBundler.output) {
|
|
25
|
-
out.output = { ...
|
|
61
|
+
out.output = { ...fromEsbuild.output, ...fromBundler.output };
|
|
26
62
|
}
|
|
27
63
|
if (fromEsbuild.transform || fromBundler.transform) {
|
|
28
|
-
out.transform = { ...
|
|
64
|
+
out.transform = { ...fromEsbuild.transform, ...fromBundler.transform };
|
|
29
65
|
if (fromEsbuild.transform?.define || fromBundler.transform?.define) {
|
|
30
66
|
out.transform.define = {
|
|
31
|
-
...
|
|
32
|
-
...
|
|
67
|
+
...fromEsbuild.transform?.define,
|
|
68
|
+
...fromBundler.transform?.define,
|
|
33
69
|
};
|
|
34
70
|
}
|
|
35
71
|
}
|
|
36
72
|
if (fromEsbuild.resolve || fromBundler.resolve) {
|
|
37
|
-
out.resolve = { ...
|
|
73
|
+
out.resolve = { ...fromEsbuild.resolve, ...fromBundler.resolve };
|
|
38
74
|
}
|
|
39
75
|
return out;
|
|
40
76
|
}
|
|
@@ -54,10 +90,7 @@ function legacyEsbuildToRolldown(esb) {
|
|
|
54
90
|
output.minify = esb.minify;
|
|
55
91
|
if (esb.sourcemap !== undefined) {
|
|
56
92
|
// esbuild has 'external' / 'both' which Rolldown doesn't — coerce to boolean.
|
|
57
|
-
output.sourcemap =
|
|
58
|
-
esb.sourcemap === 'inline'
|
|
59
|
-
? 'inline'
|
|
60
|
-
: Boolean(esb.sourcemap);
|
|
93
|
+
output.sourcemap = esb.sourcemap === 'inline' ? 'inline' : Boolean(esb.sourcemap);
|
|
61
94
|
}
|
|
62
95
|
if (esb.banner?.js !== undefined)
|
|
63
96
|
output.banner = esb.banner.js;
|
|
@@ -80,10 +113,12 @@ function legacyEsbuildToRolldown(esb) {
|
|
|
80
113
|
out.transform = transform;
|
|
81
114
|
if (Object.keys(resolve).length > 0)
|
|
82
115
|
out.resolve = resolve;
|
|
83
|
-
// Discarded
|
|
116
|
+
// Discarded (handled elsewhere):
|
|
84
117
|
// esb.inject — esbuild's array-of-side-effect-files; surfaced at the
|
|
85
118
|
// CLI layer instead, via input expansion.
|
|
86
|
-
// esb.loader —
|
|
119
|
+
// esb.loader — replaced by top-level `gjsify.loaders` (see ConfigData);
|
|
120
|
+
// migration: `esbuild.loader: { '.png': 'dataurl', '.glsl': 'text' }`
|
|
121
|
+
// → `loaders: { '.png': 'dataurl', '.glsl': 'text' }`.
|
|
87
122
|
return out;
|
|
88
123
|
}
|
|
89
124
|
/**
|
|
@@ -108,16 +143,16 @@ export function mergeBundlerOptions(base, overrides) {
|
|
|
108
143
|
const { input: _ignoredInput, external: _ignoredExternal, ...overridesRest } = overrides;
|
|
109
144
|
const out = { ...base, ...overridesRest };
|
|
110
145
|
if (base.output || overrides.output) {
|
|
111
|
-
out.output = { ...
|
|
146
|
+
out.output = { ...base.output, ...overrides.output };
|
|
112
147
|
}
|
|
113
148
|
if (base.transform || overrides.transform) {
|
|
114
|
-
out.transform = { ...
|
|
149
|
+
out.transform = { ...base.transform, ...overrides.transform };
|
|
115
150
|
if (base.transform?.define || overrides.transform?.define) {
|
|
116
|
-
out.transform.define = { ...
|
|
151
|
+
out.transform.define = { ...base.transform?.define, ...overrides.transform?.define };
|
|
117
152
|
}
|
|
118
153
|
}
|
|
119
154
|
if (base.resolve || overrides.resolve) {
|
|
120
|
-
out.resolve = { ...
|
|
155
|
+
out.resolve = { ...base.resolve, ...overrides.resolve };
|
|
121
156
|
}
|
|
122
157
|
return out;
|
|
123
158
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export type OxcTool = 'oxlint' | 'oxfmt';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the absolute path to a tool's Node ESM launcher
|
|
4
|
+
* (`node_modules/<tool>/bin/<tool>`). Walks cwd → workspace-root → parents.
|
|
5
|
+
*
|
|
6
|
+
* Throws {@link OxcNotFoundError} with a clear install hint when not found.
|
|
7
|
+
*/
|
|
8
|
+
export declare function findOxcLauncher(tool: OxcTool, cwd?: string): string;
|
|
9
|
+
export declare class OxcNotFoundError extends Error {
|
|
10
|
+
tool: OxcTool;
|
|
11
|
+
cwd: string;
|
|
12
|
+
constructor(tool: OxcTool, cwd: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Walk up from a starting directory to find the nearest oxlint config
|
|
16
|
+
* (`.oxlintrc.json`). Returns absolute path or null.
|
|
17
|
+
*/
|
|
18
|
+
export declare function findOxlintConfig(cwd?: string): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* Walk up from a starting directory to find the nearest oxfmt config
|
|
21
|
+
* (`.oxfmtrc` / `.oxfmtrc.json`). Returns absolute path or null.
|
|
22
|
+
*/
|
|
23
|
+
export declare function findOxfmtConfig(cwd?: string): string | null;
|
|
24
|
+
export interface RunOxcOptions {
|
|
25
|
+
cwd?: string;
|
|
26
|
+
verbose?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Spawn an oxc tool (oxlint / oxfmt) via its Node ESM launcher. Inherits
|
|
30
|
+
* stdio so the tool's own output (diagnostics, reformatted files, summary
|
|
31
|
+
* lines) reaches the user.
|
|
32
|
+
*
|
|
33
|
+
* The launcher is run with the current Node executable (`process.execPath`)
|
|
34
|
+
* so the JS plugin host is available — required for oxlint's `jsPlugins`.
|
|
35
|
+
*
|
|
36
|
+
* Returns the exit code; never throws on non-zero exit (callers check it).
|
|
37
|
+
*/
|
|
38
|
+
export declare function runOxc(tool: OxcTool, args: string[], opts?: RunOxcOptions): Promise<number>;
|
|
39
|
+
/** Convenience wrappers. */
|
|
40
|
+
export declare function runOxlint(args: string[], opts?: RunOxcOptions): Promise<number>;
|
|
41
|
+
export declare function runOxfmt(args: string[], opts?: RunOxcOptions): Promise<number>;
|
|
42
|
+
/**
|
|
43
|
+
* Lazy-load the embedded `.oxlintrc.json` scaffold template. The
|
|
44
|
+
* static-read-inliner matches this `readFileSync(new URL(<lit>,
|
|
45
|
+
* import.meta.url), 'utf-8')` shape at build time and inlines the file into
|
|
46
|
+
* the GJS bundle, so the template is available without runtime file I/O
|
|
47
|
+
* against the install dir. (Same mechanism the old `loadBiomeTemplate()`
|
|
48
|
+
* relied on — the shape must stay exactly this for the inliner to fire.)
|
|
49
|
+
*/
|
|
50
|
+
export declare function loadOxlintTemplate(): string;
|
|
51
|
+
/** Lazy-load the embedded `.oxfmtrc` scaffold template (same inliner shape). */
|
|
52
|
+
export declare function loadOxfmtTemplate(): string;
|
|
53
|
+
/** Helper for callers to surface the install hint to the user cleanly. */
|
|
54
|
+
export declare function printOxcNotFound(err: OxcNotFoundError): void;
|
|
55
|
+
/**
|
|
56
|
+
* Has the given oxc tool's npm package (or its companion) been declared in the
|
|
57
|
+
* project's dependencies? Useful as a cheap pre-flight check — `gjsify flatpak
|
|
58
|
+
* init`'s post-format hook uses this to decide whether to auto-format outputs.
|
|
59
|
+
*
|
|
60
|
+
* Checks for `oxfmt` by default (the formatter), since that is what the
|
|
61
|
+
* post-format hook would invoke.
|
|
62
|
+
*/
|
|
63
|
+
export declare function hasOxcDevDep(cwd?: string, tool?: OxcTool): boolean;
|