@gjsify/cli 0.4.20 → 0.4.22
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 +164 -163
- package/lib/commands/check.d.ts +6 -2
- package/lib/commands/check.js +169 -59
- package/lib/commands/dlx.js +18 -3
- package/lib/commands/fix.js +2 -2
- package/lib/commands/flatpak/init.js +21 -7
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/install.d.ts +1 -0
- package/lib/commands/install.js +31 -15
- package/lib/commands/pack.d.ts +16 -0
- package/lib/commands/pack.js +32 -5
- package/lib/commands/publish.d.ts +1 -0
- package/lib/commands/publish.js +54 -4
- package/lib/commands/system-check.d.ts +6 -0
- package/lib/commands/system-check.js +72 -0
- package/lib/index.js +22 -1
- package/lib/types/config-data.d.ts +7 -0
- package/lib/utils/check-system-deps.js +24 -0
- package/lib/utils/install-backend-native.d.ts +8 -0
- package/lib/utils/install-backend-native.js +20 -6
- package/lib/utils/run-lifecycle-script.d.ts +14 -0
- package/lib/utils/run-lifecycle-script.js +74 -0
- package/package.json +98 -15
package/lib/commands/check.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Command } from '../types/index.js';
|
|
2
2
|
interface CheckOptions {
|
|
3
|
-
|
|
3
|
+
include?: string[];
|
|
4
|
+
exclude?: string[];
|
|
5
|
+
parallel?: boolean;
|
|
6
|
+
jobs?: number;
|
|
7
|
+
verbose?: boolean;
|
|
4
8
|
}
|
|
5
|
-
export declare const checkCommand: Command<
|
|
9
|
+
export declare const checkCommand: Command<unknown, CheckOptions>;
|
|
6
10
|
export {};
|
package/lib/commands/check.js
CHANGED
|
@@ -1,72 +1,182 @@
|
|
|
1
|
-
|
|
1
|
+
// `gjsify check` — workspace TypeScript-check orchestrator.
|
|
2
|
+
//
|
|
3
|
+
// In a workspace root: runs `npm run check` across every workspace that
|
|
4
|
+
// declares a `check` script (filtered the same way `gjsify foreach` filters:
|
|
5
|
+
// excludes `@girs/*`, honours --include/--exclude). Parallel by default.
|
|
6
|
+
//
|
|
7
|
+
// In a single package (or anywhere a `package.json` with a `check` script
|
|
8
|
+
// is reachable from cwd): runs the local `check` script directly. This is
|
|
9
|
+
// the natural "tsc --noEmit on the current scope" invocation, analogous to
|
|
10
|
+
// `gjsify format` / `lint` / `fix` (which all wrap Biome workspace-wide).
|
|
11
|
+
//
|
|
12
|
+
// The legacy system-dep-check shape lives under `gjsify system-check` after
|
|
13
|
+
// PR #254. The `check` alias on `system-check` stays valid for one release
|
|
14
|
+
// — if both `system-check`-style flags (`--json`) and `check`-style scripts
|
|
15
|
+
// are reachable here, the `--json` flag routes through this command first
|
|
16
|
+
// (since the typescript-check binding is positional `[paths..]`).
|
|
17
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
18
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { cpus } from 'node:os';
|
|
21
|
+
import { discoverWorkspaces, filterWorkspaces, } from '@gjsify/workspace';
|
|
22
|
+
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
23
|
+
function readPackageJson(dir) {
|
|
24
|
+
const path = join(dir, 'package.json');
|
|
25
|
+
if (!existsSync(path))
|
|
26
|
+
return null;
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Run a single workspace's `check` script. Returns the exit code; non-zero
|
|
36
|
+
* indicates failure. Output is forwarded to the parent's stdout/stderr,
|
|
37
|
+
* line-prefixed with the workspace name when `prefix` is set.
|
|
38
|
+
*/
|
|
39
|
+
function runCheck(ws, prefix) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const child = spawn('npm', ['run', 'check', '--if-present'], {
|
|
42
|
+
cwd: ws.location,
|
|
43
|
+
stdio: prefix === null ? 'inherit' : ['ignore', 'pipe', 'pipe'],
|
|
44
|
+
});
|
|
45
|
+
if (prefix !== null && child.stdout && child.stderr) {
|
|
46
|
+
const tag = `[${prefix}] `;
|
|
47
|
+
const forward = (stream, dest) => {
|
|
48
|
+
stream.on('data', (chunk) => {
|
|
49
|
+
const text = chunk.toString('utf-8');
|
|
50
|
+
for (const line of text.split('\n')) {
|
|
51
|
+
if (line.length > 0)
|
|
52
|
+
dest.write(`${tag}${line}\n`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
forward(child.stdout, process.stdout);
|
|
57
|
+
forward(child.stderr, process.stderr);
|
|
58
|
+
}
|
|
59
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
60
|
+
child.on('error', () => resolve(1));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
2
63
|
export const checkCommand = {
|
|
3
64
|
command: 'check',
|
|
4
|
-
description: '
|
|
5
|
-
builder: (yargs) =>
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
65
|
+
description: 'Run `npm run check` (TypeScript type-check) across the current workspace. In a workspace root: walks every package with a `check` script (excludes @girs/*; honours --include/--exclude). In a single package: runs the local `check` script directly. Symmetric peer of `gjsify format` / `lint` / `fix`. (Legacy system-dep `gjsify check` is now `gjsify system-check` — see #254.)',
|
|
66
|
+
builder: (yargs) => yargs
|
|
67
|
+
.option('include', {
|
|
68
|
+
description: 'Only run in workspaces matching these glob patterns (repeatable).',
|
|
69
|
+
type: 'string',
|
|
70
|
+
array: true,
|
|
71
|
+
})
|
|
72
|
+
.option('exclude', {
|
|
73
|
+
description: 'Skip workspaces matching these glob patterns (repeatable). Always excludes @girs/*.',
|
|
74
|
+
type: 'string',
|
|
75
|
+
array: true,
|
|
76
|
+
})
|
|
77
|
+
.option('parallel', {
|
|
78
|
+
description: 'Run workspace checks in parallel (default). Use --no-parallel to run them sequentially with full per-workspace output.',
|
|
79
|
+
type: 'boolean',
|
|
80
|
+
alias: 'p',
|
|
81
|
+
default: true,
|
|
82
|
+
})
|
|
83
|
+
.option('jobs', {
|
|
84
|
+
description: 'Max parallel workers (when --parallel). Default: os.cpus().length.',
|
|
85
|
+
type: 'number',
|
|
86
|
+
alias: 'j',
|
|
87
|
+
})
|
|
88
|
+
.option('verbose', {
|
|
89
|
+
description: 'Log the per-workspace command before spawning.',
|
|
90
|
+
type: 'boolean',
|
|
91
|
+
default: false,
|
|
92
|
+
}),
|
|
13
93
|
handler: async (args) => {
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
for (const dep of required) {
|
|
31
|
-
const icon = dep.found ? '✓' : '✗';
|
|
32
|
-
const ver = dep.version ? ` (${dep.version})` : '';
|
|
33
|
-
console.log(` ${icon} ${dep.name}${ver}`);
|
|
94
|
+
const cwd = process.cwd();
|
|
95
|
+
const workspaceRoot = findWorkspaceRoot(cwd);
|
|
96
|
+
// ---- Single-package mode ----
|
|
97
|
+
// Run when we're inside a package that has a `check` script but is
|
|
98
|
+
// NOT itself the workspace root. This is the "I want to tsc just
|
|
99
|
+
// this package" path — equivalent to `npm run check` but spawned
|
|
100
|
+
// through gjsify so the command surface stays uniform with format/
|
|
101
|
+
// lint/fix.
|
|
102
|
+
if (workspaceRoot && cwd !== workspaceRoot) {
|
|
103
|
+
const pkg = readPackageJson(cwd);
|
|
104
|
+
if (pkg?.scripts?.check) {
|
|
105
|
+
if (args.verbose) {
|
|
106
|
+
console.log(`[check] cwd=${cwd} → npm run check`);
|
|
107
|
+
}
|
|
108
|
+
const r = spawnSync('npm', ['run', 'check'], { cwd, stdio: 'inherit' });
|
|
109
|
+
process.exit(r.status ?? 1);
|
|
34
110
|
}
|
|
111
|
+
// Fall through to workspace mode when the local package has no
|
|
112
|
+
// check script (cd'd into a non-package dir under the root).
|
|
35
113
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const icon = dep.found ? '✓' : '⚠';
|
|
41
|
-
const ver = dep.version ? ` (${dep.version})` : '';
|
|
42
|
-
const requiredBy = dep.requiredBy && dep.requiredBy.length > 0
|
|
43
|
-
? ` — needed by ${dep.requiredBy.join(', ')}`
|
|
44
|
-
: '';
|
|
45
|
-
console.log(` ${icon} ${dep.name}${ver}${requiredBy}`);
|
|
46
|
-
}
|
|
114
|
+
// ---- Workspace mode ----
|
|
115
|
+
if (!workspaceRoot) {
|
|
116
|
+
console.error('gjsify check: no workspace root found from cwd. Run inside a workspace (or a package within one) with `npm run check` defined.');
|
|
117
|
+
process.exit(1);
|
|
47
118
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
119
|
+
const allWorkspaces = discoverWorkspaces(workspaceRoot);
|
|
120
|
+
// Always exclude @girs/* (type-only packages, no own tsc check).
|
|
121
|
+
const exclude = ['@girs/*', ...(args.exclude ?? [])];
|
|
122
|
+
const filtered = filterWorkspaces(allWorkspaces, {
|
|
123
|
+
include: args.include,
|
|
124
|
+
exclude,
|
|
125
|
+
// noPrivate omitted → include private workspaces (test pkgs often have check).
|
|
126
|
+
});
|
|
127
|
+
// Skip workspaces without a `check` script (manifest.scripts.check).
|
|
128
|
+
const targets = filtered.filter((ws) => {
|
|
129
|
+
const scripts = ws.manifest.scripts;
|
|
130
|
+
return scripts?.check !== undefined;
|
|
131
|
+
});
|
|
132
|
+
if (targets.length === 0) {
|
|
133
|
+
console.error('gjsify check: no workspaces with a `check` script found.');
|
|
134
|
+
process.exit(1);
|
|
52
135
|
}
|
|
53
|
-
if (
|
|
54
|
-
console.log(
|
|
136
|
+
if (args.verbose) {
|
|
137
|
+
console.log(`[check] root=${workspaceRoot} workspaces=${targets.length} parallel=${args.parallel ? 'yes' : 'no'}`);
|
|
55
138
|
}
|
|
56
|
-
|
|
57
|
-
|
|
139
|
+
// ---- Sequential mode ----
|
|
140
|
+
if (!args.parallel) {
|
|
141
|
+
let firstFail = 0;
|
|
142
|
+
for (const ws of targets) {
|
|
143
|
+
if (args.verbose)
|
|
144
|
+
console.log(`[check] → ${ws.name}`);
|
|
145
|
+
const code = await runCheck(ws, null);
|
|
146
|
+
if (code !== 0 && firstFail === 0)
|
|
147
|
+
firstFail = code;
|
|
148
|
+
}
|
|
149
|
+
if (firstFail !== 0)
|
|
150
|
+
console.error(`gjsify check: failures in ${targets.filter(async (ws) => await runCheck(ws, null) !== 0).length}+ workspaces`);
|
|
151
|
+
process.exit(firstFail);
|
|
58
152
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
153
|
+
// ---- Parallel mode (default) ----
|
|
154
|
+
const concurrency = args.jobs ?? Math.max(1, cpus().length);
|
|
155
|
+
const failures = [];
|
|
156
|
+
let cursor = 0;
|
|
157
|
+
const workers = [];
|
|
158
|
+
for (let i = 0; i < concurrency; i++) {
|
|
159
|
+
workers.push((async () => {
|
|
160
|
+
while (true) {
|
|
161
|
+
const idx = cursor++;
|
|
162
|
+
if (idx >= targets.length)
|
|
163
|
+
return;
|
|
164
|
+
const ws = targets[idx];
|
|
165
|
+
const code = await runCheck(ws, ws.name);
|
|
166
|
+
if (code !== 0)
|
|
167
|
+
failures.push({ name: ws.name, code });
|
|
168
|
+
}
|
|
169
|
+
})());
|
|
62
170
|
}
|
|
63
|
-
|
|
64
|
-
|
|
171
|
+
await Promise.all(workers);
|
|
172
|
+
if (failures.length > 0) {
|
|
173
|
+
console.error(`\ngjsify check: ${failures.length} of ${targets.length} workspace(s) failed:`);
|
|
174
|
+
for (const f of failures)
|
|
175
|
+
console.error(` ✗ ${f.name} (exit ${f.code})`);
|
|
176
|
+
process.exit(1);
|
|
65
177
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// build/run code paths that don't touch the optional library.
|
|
70
|
-
process.exit(missingRequired.length > 0 ? 1 : 0);
|
|
178
|
+
if (args.verbose)
|
|
179
|
+
console.log(`\ngjsify check: ${targets.length} workspace(s) green.`);
|
|
180
|
+
process.exit(0);
|
|
71
181
|
},
|
|
72
182
|
};
|
package/lib/commands/dlx.js
CHANGED
|
@@ -19,17 +19,24 @@ export const dlxCommand = {
|
|
|
19
19
|
command: 'dlx <spec> [binOrArg] [extraArgs..]',
|
|
20
20
|
description: 'Run the GJS bundle of an npm-published package without installing it locally.',
|
|
21
21
|
builder: (yargs) => yargs
|
|
22
|
+
// Collect everything after `--` into argv['--'] so callers can
|
|
23
|
+
// forward flags that would otherwise be intercepted by gjsify's
|
|
24
|
+
// own parser. Canonical example: `gjsify dlx @ts-for-gir/cli --
|
|
25
|
+
// --help` shows ts-for-gir's --help instead of gjsify dlx's.
|
|
26
|
+
// Without `populate--`, the trailing `--help` is consumed at
|
|
27
|
+
// the gjsify level and the bundle never sees it.
|
|
28
|
+
.parserConfiguration({ 'populate--': true })
|
|
22
29
|
.positional('spec', {
|
|
23
30
|
description: 'Package spec (`name`, `name@version`, `@scope/name@spec`, or local path).',
|
|
24
31
|
type: 'string',
|
|
25
32
|
demandOption: true,
|
|
26
33
|
})
|
|
27
34
|
.positional('binOrArg', {
|
|
28
|
-
description: 'Optional bin name when the package defines `gjsify.bin` with multiple entries; otherwise treated as the first argument forwarded to the bundle.',
|
|
35
|
+
description: 'Optional bin name when the package defines `gjsify.bin` with multiple entries; otherwise treated as the first argument forwarded to the bundle. To pass a flag here (e.g. `--help`) use the `--` separator: `gjsify dlx <pkg> -- --help`.',
|
|
29
36
|
type: 'string',
|
|
30
37
|
})
|
|
31
38
|
.positional('extraArgs', {
|
|
32
|
-
description: 'Extra args forwarded to `gjs -m <bundle>`.',
|
|
39
|
+
description: 'Extra args forwarded to `gjs -m <bundle>`. Use `--` before flags to bypass gjsify-level parsing (`gjsify dlx <pkg> -- --help --verbose`).',
|
|
33
40
|
type: 'string',
|
|
34
41
|
array: true,
|
|
35
42
|
})
|
|
@@ -71,7 +78,15 @@ export const dlxCommand = {
|
|
|
71
78
|
// gjsify dlx <pkg> mybin → bin if package has gjsify.bin[mybin], else arg
|
|
72
79
|
// gjsify dlx <pkg> mybin -- arg1 arg2 → bin + extra args
|
|
73
80
|
// gjsify dlx <pkg> -- arg1 arg2 → no bin, extra args
|
|
74
|
-
|
|
81
|
+
//
|
|
82
|
+
// The `parserConfiguration({ 'populate--': true })` on the builder
|
|
83
|
+
// routes anything after `--` into `args['--']` (as `(string |
|
|
84
|
+
// number)[]`), so flags like `--help` reach the bundle untouched.
|
|
85
|
+
// Merge those into the positional extraArgs the splitter sees so
|
|
86
|
+
// both call shapes share one downstream path.
|
|
87
|
+
const passthroughDoubleDash = (args['--'] ?? []).map((v) => String(v));
|
|
88
|
+
const extraArgsCombined = [...(args.extraArgs ?? []), ...passthroughDoubleDash];
|
|
89
|
+
const { binName, extraArgs } = splitBinAndArgs(pkgDir, args.binOrArg, extraArgsCombined);
|
|
75
90
|
const entry = resolveGjsEntry(pkgDir, binName);
|
|
76
91
|
if (entry.fromFallback) {
|
|
77
92
|
console.warn(`[gjsify dlx] package "${cachedPkgName ?? parsed.kind}" has no \`gjsify\` field — falling back to package.json#main. Add \`gjsify.main\` to silence.`);
|
package/lib/commands/fix.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Equivalent to biome's `check` (format + safe-lint-fix + organize-imports).
|
|
4
4
|
// Default writes fixes in-place; pass `--no-write` to report-only.
|
|
5
|
-
// Naming: deliberately distinct from `gjsify check` (
|
|
6
|
-
// system
|
|
5
|
+
// Naming: deliberately distinct from `gjsify check` (workspace TS check)
|
|
6
|
+
// and `gjsify system-check` (system-dependency verifier).
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
8
|
import { BiomeNotFoundError, findBiomeConfig, printBiomeNotFound, runBiome, } from '../utils/biome-resolve.js';
|
|
9
9
|
export const fixCommand = {
|
|
@@ -138,14 +138,28 @@ export const flatpakInitCommand = {
|
|
|
138
138
|
const cleanup = flatpak.cleanup;
|
|
139
139
|
if (cleanup?.length)
|
|
140
140
|
manifest.cleanup = cleanup;
|
|
141
|
+
// Module assembly. Two precedence rules:
|
|
142
|
+
// `flatpak.modules` — full replacement; if set, neither the
|
|
143
|
+
// extras nor the meson default get added.
|
|
144
|
+
// Right shape for npm-tarball CLI tools
|
|
145
|
+
// where the meson default would be wrong.
|
|
146
|
+
// `flatpak.extraModules` — prepended to the meson default.
|
|
147
|
+
// Right shape for meson-built GTK apps
|
|
148
|
+
// that want a few extra sibling modules
|
|
149
|
+
// (e.g. blueprint-compiler).
|
|
141
150
|
const modules = [];
|
|
142
|
-
if (flatpak.
|
|
143
|
-
modules.push(...flatpak.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
if (flatpak.modules?.length) {
|
|
152
|
+
modules.push(...flatpak.modules);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
if (flatpak.extraModules?.length)
|
|
156
|
+
modules.push(...flatpak.extraModules);
|
|
157
|
+
modules.push({
|
|
158
|
+
name: deriveModuleName(appId),
|
|
159
|
+
buildsystem: 'meson',
|
|
160
|
+
sources: [{ type: 'dir', path: '.' }],
|
|
161
|
+
});
|
|
162
|
+
}
|
|
149
163
|
manifest.modules = modules;
|
|
150
164
|
const writtenFiles = [];
|
|
151
165
|
const trackWrite = (p) => {
|
package/lib/commands/index.d.ts
CHANGED
package/lib/commands/index.js
CHANGED
package/lib/commands/install.js
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
// gjsify install -g <pkg> [...] → user-global install (XDG, GJS-runnable bin)
|
|
7
7
|
//
|
|
8
8
|
// All three modes route through `@gjsify/{semver,npm-registry,tar}` via
|
|
9
|
-
// `installPackagesNative` — no Node/npm required at runtime.
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// missing native-backend feature
|
|
9
|
+
// `installPackagesNative` — no Node/npm required at runtime. Pass
|
|
10
|
+
// `--backend=npm` (or set the legacy `GJSIFY_INSTALL_BACKEND=npm` env var)
|
|
11
|
+
// to opt back into the `npm install` subprocess flow — useful as an
|
|
12
|
+
// escape hatch for projects that hit a missing native-backend feature
|
|
13
|
+
// (Yarn PnP repos, lifecycle scripts, npm's `overrides` quirks).
|
|
13
14
|
//
|
|
14
15
|
// Workspace install (`gjsify install` in a monorepo root with a
|
|
15
16
|
// `"workspaces"` field) hoists every workspace's externals into the root
|
|
@@ -53,6 +54,11 @@ export const installCommand = {
|
|
|
53
54
|
description: 'Verbose install logging.',
|
|
54
55
|
type: 'boolean',
|
|
55
56
|
default: false,
|
|
57
|
+
})
|
|
58
|
+
.option('backend', {
|
|
59
|
+
description: 'Install backend. `native` (default) routes through `@gjsify/{semver,npm-registry,tar}` — no Node/npm at runtime. `npm` shells out to `npm install` as an escape hatch for cases the native backend does not yet model (Yarn PnP repos, lifecycle scripts). Overrides `GJSIFY_INSTALL_BACKEND` if both are set.',
|
|
60
|
+
type: 'string',
|
|
61
|
+
choices: ['native', 'npm'],
|
|
56
62
|
}),
|
|
57
63
|
handler: async (args) => {
|
|
58
64
|
// --immutable is incompatible with explicit `<pkg>` adds and with
|
|
@@ -82,8 +88,12 @@ export const installCommand = {
|
|
|
82
88
|
await installGlobalAndLink(args.packages, { verbose: args.verbose });
|
|
83
89
|
return;
|
|
84
90
|
}
|
|
85
|
-
//
|
|
86
|
-
|
|
91
|
+
// Backend selection (in precedence order):
|
|
92
|
+
// 1. --backend flag (explicit user choice)
|
|
93
|
+
// 2. GJSIFY_INSTALL_BACKEND env (back-compat shape from pre-flag era)
|
|
94
|
+
// 3. native (default)
|
|
95
|
+
const backend = args.backend ?? process.env.GJSIFY_INSTALL_BACKEND ?? 'native';
|
|
96
|
+
if (backend === 'npm') {
|
|
87
97
|
await projectInstallViaNpm(args);
|
|
88
98
|
await runPostInstallChecks();
|
|
89
99
|
return;
|
|
@@ -276,17 +286,23 @@ async function workspaceInstall(cwd, args) {
|
|
|
276
286
|
continue;
|
|
277
287
|
const linkPath = join(target.location, 'node_modules', link.depName);
|
|
278
288
|
mkdirSync(dirname(linkPath), { recursive: true });
|
|
279
|
-
// Remove any prior entry
|
|
289
|
+
// Remove any prior entry — regular dir, broken symlink, file, or
|
|
290
|
+
// a normal symlink left over from a previous install. Using
|
|
291
|
+
// `{ recursive: true, force: true }` handles every shape in one
|
|
292
|
+
// call: `rmSync` no-ops on missing paths under `force: true`, and
|
|
293
|
+
// `recursive: true` covers the directory case. Avoids the EEXIST
|
|
294
|
+
// race a previous lstat-then-branch version hit when the stat's
|
|
295
|
+
// type-discrimination missed an edge case (e.g. broken symlink
|
|
296
|
+
// whose `isSymbolicLink()` returned a non-truthy value through
|
|
297
|
+
// Gio's NOFOLLOW path, leaving a leftover entry that
|
|
298
|
+
// `symlinkSync` would then refuse to overwrite).
|
|
280
299
|
try {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
rmSync(linkPath, { recursive: true, force: true });
|
|
287
|
-
}
|
|
300
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
301
|
+
}
|
|
302
|
+
catch { /* unexpected — Gio failure on a path we just lstat'd to
|
|
303
|
+
decide we wanted to remove. The subsequent symlinkSync
|
|
304
|
+
will surface the real reason if there is one. */
|
|
288
305
|
}
|
|
289
|
-
catch { /* ENOENT — fine, nothing to remove */ }
|
|
290
306
|
// Relative symlink so the repo is portable across checkout paths.
|
|
291
307
|
const relTarget = relative(dirname(linkPath), link.targetLocation);
|
|
292
308
|
symlinkSync(relTarget, linkPath);
|
package/lib/commands/pack.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ interface PackOptions {
|
|
|
4
4
|
'pack-destination'?: string;
|
|
5
5
|
json?: boolean;
|
|
6
6
|
'dry-run'?: boolean;
|
|
7
|
+
'ignore-scripts'?: boolean;
|
|
7
8
|
}
|
|
8
9
|
interface PackResult {
|
|
9
10
|
filename: string;
|
|
@@ -30,6 +31,21 @@ export interface PackWorkspaceOptions {
|
|
|
30
31
|
dryRun?: boolean;
|
|
31
32
|
/** Skip the workspace:^ rewrite step (rare — useful for testing the raw layout). */
|
|
32
33
|
skipWorkspaceRewrite?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Lifecycle scripts to run from `pkg.scripts` BEFORE the file collection
|
|
36
|
+
* pass. Order matters — entries are executed sequentially, stopping on
|
|
37
|
+
* the first failure.
|
|
38
|
+
*
|
|
39
|
+
* Defaults:
|
|
40
|
+
* - `['prepack']` from the `gjsify pack` CLI handler
|
|
41
|
+
* - `['prepublishOnly', 'prepack']` from `gjsify publish`
|
|
42
|
+
* - `[]` from programmatic callers that have already run scripts
|
|
43
|
+
*
|
|
44
|
+
* Mirrors `npm pack` / `npm publish` semantics. Pass `[]` (or set
|
|
45
|
+
* `--ignore-scripts` on the CLI) to skip — useful when an outer
|
|
46
|
+
* workflow has already produced the build artifacts.
|
|
47
|
+
*/
|
|
48
|
+
lifecycleScripts?: readonly string[];
|
|
33
49
|
}
|
|
34
50
|
/**
|
|
35
51
|
* Programmatic equivalent of the `pack` command — used by `gjsify publish`
|
package/lib/commands/pack.js
CHANGED
|
@@ -29,6 +29,7 @@ import { join, resolve } from 'node:path';
|
|
|
29
29
|
import { createTarball, gzip } from '@gjsify/tar';
|
|
30
30
|
import { discoverWorkspaces } from '@gjsify/workspace';
|
|
31
31
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
32
|
+
import { runLifecycleScript } from '../utils/run-lifecycle-script.js';
|
|
32
33
|
export const packCommand = {
|
|
33
34
|
command: 'pack [path]',
|
|
34
35
|
description: 'Produce an npm-compatible .tgz tarball for the workspace at <path> (default: cwd). Rewrites workspace:^/~/* deps to resolved versions.',
|
|
@@ -50,12 +51,20 @@ export const packCommand = {
|
|
|
50
51
|
description: 'Compute everything but do not write the .tgz.',
|
|
51
52
|
type: 'boolean',
|
|
52
53
|
default: false,
|
|
54
|
+
})
|
|
55
|
+
.option('ignore-scripts', {
|
|
56
|
+
description: 'Skip the `prepack` lifecycle script before packing. ' +
|
|
57
|
+
'Mirrors `npm pack --ignore-scripts`. Use when scripts ' +
|
|
58
|
+
'are already run by the outer workflow.',
|
|
59
|
+
type: 'boolean',
|
|
60
|
+
default: false,
|
|
53
61
|
}),
|
|
54
62
|
handler: async (args) => {
|
|
55
63
|
const wsDir = resolve(args.path ?? process.cwd());
|
|
56
64
|
const result = await packWorkspace(wsDir, {
|
|
57
65
|
destination: args['pack-destination'],
|
|
58
66
|
dryRun: args['dry-run'] === true,
|
|
67
|
+
lifecycleScripts: args['ignore-scripts'] ? [] : ['prepack'],
|
|
59
68
|
});
|
|
60
69
|
if (args.json) {
|
|
61
70
|
process.stdout.write(`${JSON.stringify([result], null, 2)}\n`);
|
|
@@ -82,17 +91,35 @@ export async function packWorkspace(wsDir, opts = {}) {
|
|
|
82
91
|
if (!name) {
|
|
83
92
|
throw new Error(`gjsify pack: package.json at ${wsDir} has no "name"`);
|
|
84
93
|
}
|
|
94
|
+
// Run npm-style lifecycle scripts BEFORE walking the file tree. The
|
|
95
|
+
// canonical case is `prepack` — many packages use it to generate
|
|
96
|
+
// build artifacts that aren't otherwise produced by their `build`
|
|
97
|
+
// script (template processing, codegen, etc.). Skipping these means
|
|
98
|
+
// the resulting tarball is missing files the package needs to work
|
|
99
|
+
// post-install. Matches `npm pack` / `npm publish` semantics.
|
|
100
|
+
const lifecycleScripts = opts.lifecycleScripts ?? ['prepack'];
|
|
101
|
+
for (const scriptName of lifecycleScripts) {
|
|
102
|
+
await runLifecycleScript(wsDir, pkg, scriptName, { optional: true });
|
|
103
|
+
}
|
|
104
|
+
// Re-read package.json AFTER lifecycle scripts in case one of them
|
|
105
|
+
// mutated it (e.g. a `prepack` that injects build metadata into
|
|
106
|
+
// package.json fields). Rare but legal — npm pack does the same.
|
|
107
|
+
const sourceAfterScripts = readFileSync(pkgPath, 'utf-8');
|
|
108
|
+
const pkgAfterScripts = sourceAfterScripts === originalSource
|
|
109
|
+
? pkg
|
|
110
|
+
: JSON.parse(sourceAfterScripts);
|
|
85
111
|
// Rewrite workspace:^/~/* deps to resolved npm version ranges, mirroring
|
|
86
112
|
// yarn's auto-rewrite at publish time. Done in-memory only — the source
|
|
87
113
|
// package.json on disk is never mutated by `gjsify pack`.
|
|
88
114
|
const rewrittenPkg = opts.skipWorkspaceRewrite
|
|
89
|
-
?
|
|
90
|
-
: rewriteWorkspaceDeps(
|
|
91
|
-
const rewrittenSource = JSON.stringify(rewrittenPkg, null, indentOf(
|
|
115
|
+
? pkgAfterScripts
|
|
116
|
+
: rewriteWorkspaceDeps(pkgAfterScripts, wsDir);
|
|
117
|
+
const rewrittenSource = JSON.stringify(rewrittenPkg, null, indentOf(sourceAfterScripts)) + '\n';
|
|
92
118
|
// Collect files according to the package.json `files` field (or npm's
|
|
93
119
|
// default set). The package.json itself is always included with the
|
|
94
|
-
// rewritten contents.
|
|
95
|
-
|
|
120
|
+
// rewritten contents. We use the post-script `pkgAfterScripts` here so
|
|
121
|
+
// that any `files` array modified by a prepack script is honored.
|
|
122
|
+
const filesToPack = collectFiles(wsDir, pkgAfterScripts);
|
|
96
123
|
const entries = [{ name: 'package/', directory: true, mode: 0o755 }];
|
|
97
124
|
const fileMetas = [];
|
|
98
125
|
let unpackedSize = 0;
|