@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.
@@ -1,6 +1,10 @@
1
1
  import type { Command } from '../types/index.js';
2
2
  interface CheckOptions {
3
- json: boolean;
3
+ include?: string[];
4
+ exclude?: string[];
5
+ parallel?: boolean;
6
+ jobs?: number;
7
+ verbose?: boolean;
4
8
  }
5
- export declare const checkCommand: Command<any, CheckOptions>;
9
+ export declare const checkCommand: Command<unknown, CheckOptions>;
6
10
  export {};
@@ -1,72 +1,182 @@
1
- import { runAllChecks, detectPackageManager, buildInstallCommand } from '../utils/check-system-deps.js';
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: 'Check that required system dependencies (GJS, GTK4, libsoup3, ) are installed. Optional dependencies are detected only when their @gjsify/* package is in your project.',
5
- builder: (yargs) => {
6
- return yargs
7
- .option('json', {
8
- description: 'Output results as JSON',
9
- type: 'boolean',
10
- default: false,
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 results = runAllChecks(process.cwd());
15
- const pm = detectPackageManager();
16
- const missingRequired = results.filter(r => !r.found && r.severity === 'required');
17
- const missingOptional = results.filter(r => !r.found && r.severity === 'optional');
18
- const allMissing = [...missingRequired, ...missingOptional];
19
- if (args.json) {
20
- console.log(JSON.stringify({ packageManager: pm, deps: results }, null, 2));
21
- // Only required deps influence the exit code.
22
- process.exit(missingRequired.length > 0 ? 1 : 0);
23
- return;
24
- }
25
- console.log('System dependency check\n');
26
- const required = results.filter(r => r.severity === 'required');
27
- const optional = results.filter(r => r.severity === 'optional');
28
- if (required.length > 0) {
29
- console.log('Required:');
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
- if (optional.length > 0) {
37
- console.log('\nOptional:');
38
- for (const dep of optional) {
39
- // ⚠ for missing-but-needed-by-installed-packages, ○ for missing-but-not-needed (shouldn't appear in conditional mode)
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
- console.log(`\nPackage manager: ${pm}`);
49
- if (allMissing.length === 0) {
50
- console.log('\nAll dependencies found.');
51
- return;
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 (missingRequired.length > 0) {
54
- console.log(`\nMissing required: ${missingRequired.map(d => d.name).join(', ')}`);
136
+ if (args.verbose) {
137
+ console.log(`[check] root=${workspaceRoot} workspaces=${targets.length} parallel=${args.parallel ? 'yes' : 'no'}`);
55
138
  }
56
- if (missingOptional.length > 0) {
57
- console.log(`Missing optional: ${missingOptional.map(d => d.name).join(', ')}`);
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
- const cmd = buildInstallCommand(pm, allMissing);
60
- if (cmd) {
61
- console.log(`\nTo install:\n ${cmd}`);
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
- else {
64
- console.log('\nNo install command available for your package manager. Install manually.');
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
- // Exit non-zero ONLY if a required dependency is missing.
67
- // Optional deps that are missing but needed by an installed @gjsify/*
68
- // package generate a warning but keep exit code 0 — the user can still
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
  };
@@ -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
- const { binName, extraArgs } = splitBinAndArgs(pkgDir, args.binOrArg, args.extraArgs ?? []);
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.`);
@@ -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` (which verifies
6
- // system dependencies).
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.extraModules?.length)
143
- modules.push(...flatpak.extraModules);
144
- modules.push({
145
- name: deriveModuleName(appId),
146
- buildsystem: 'meson',
147
- sources: [{ type: 'dir', path: '.' }],
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) => {
@@ -2,6 +2,7 @@ export * from './build.js';
2
2
  export * from './test.js';
3
3
  export * from './run.js';
4
4
  export * from './info.js';
5
+ export * from './system-check.js';
5
6
  export * from './check.js';
6
7
  export * from './showcase.js';
7
8
  export * from './create.js';
@@ -2,6 +2,7 @@ export * from './build.js';
2
2
  export * from './test.js';
3
3
  export * from './run.js';
4
4
  export * from './info.js';
5
+ export * from './system-check.js';
5
6
  export * from './check.js';
6
7
  export * from './showcase.js';
7
8
  export * from './create.js';
@@ -7,6 +7,7 @@ interface InstallOptions {
7
7
  'save-optional'?: boolean;
8
8
  immutable?: boolean;
9
9
  verbose: boolean;
10
+ backend?: 'native' | 'npm';
10
11
  }
11
12
  export declare const installCommand: Command<any, InstallOptions>;
12
13
  export {};
@@ -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. 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).
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
- // Escape-hatch: legacy npm subprocess flow.
86
- if (process.env.GJSIFY_INSTALL_BACKEND === 'npm') {
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 (regular dir, broken symlink, file).
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
- const stat = lstatSync(linkPath);
282
- if (stat.isSymbolicLink() || stat.isFile()) {
283
- rmSync(linkPath, { force: true });
284
- }
285
- else if (stat.isDirectory()) {
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);
@@ -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`
@@ -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
- ? pkg
90
- : rewriteWorkspaceDeps(pkg, wsDir);
91
- const rewrittenSource = JSON.stringify(rewrittenPkg, null, indentOf(originalSource)) + '\n';
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
- const filesToPack = collectFiles(wsDir, pkg);
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;
@@ -4,6 +4,7 @@ interface PublishOptions {
4
4
  tag?: string;
5
5
  access?: string;
6
6
  'tolerate-republish'?: boolean;
7
+ 'tolerate-untrusted-new'?: boolean;
7
8
  provenance?: boolean;
8
9
  'dry-run'?: boolean;
9
10
  json?: boolean;