@gjsify/cli 0.40.0 → 0.42.0
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/README.md +79 -33
- package/dist/cli.gjs.mjs +1043 -404
- package/lib/actions/build.d.ts +14 -0
- package/lib/actions/build.js +54 -7
- package/lib/bundler-pick.js +10 -2
- package/lib/cli-app.js +3 -1
- package/lib/commands/browse.js +1 -1
- package/lib/commands/create.d.ts +3 -0
- package/lib/commands/create.js +49 -6
- package/lib/commands/debug.js +1 -1
- package/lib/commands/dev.d.ts +13 -0
- package/lib/commands/dev.js +132 -0
- package/lib/commands/dlx.js +1 -1
- 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 +32 -1
- package/lib/commands/prune.d.ts +11 -0
- package/lib/commands/prune.js +84 -0
- package/lib/commands/run.js +38 -85
- package/lib/commands/self-update.d.ts +1 -0
- package/lib/commands/self-update.js +55 -0
- package/lib/commands/ship.js +6 -1
- package/lib/commands/showcase.js +1 -1
- package/lib/commands/storybook.js +17 -74
- package/lib/commands/test.js +12 -16
- package/lib/commands/tsc.js +96 -45
- package/lib/config.js +4 -0
- package/lib/templates/install.mjs.tmpl +8 -3
- package/lib/types/config-data.d.ts +27 -0
- package/lib/utils/app-metadata.js +27 -6
- package/lib/utils/check-system-deps.d.ts +2 -2
- package/lib/utils/check-system-deps.js +92 -13
- package/lib/utils/daemon-command.d.ts +7 -0
- package/lib/utils/daemon-command.js +30 -0
- package/lib/utils/dev-plan.d.ts +52 -0
- package/lib/utils/dev-plan.js +117 -0
- package/lib/utils/gi-runtime-paths.d.ts +27 -0
- package/lib/utils/gi-runtime-paths.js +84 -0
- package/lib/utils/gi-typelib.d.ts +49 -0
- package/lib/utils/gi-typelib.js +160 -0
- package/lib/utils/install-provenance.d.ts +44 -0
- package/lib/utils/install-provenance.js +106 -0
- package/lib/utils/node-script.d.ts +4 -1
- package/lib/utils/node-script.js +27 -3
- package/lib/utils/npm-oidc.js +8 -5
- package/lib/utils/prune-prefix.d.ts +83 -0
- package/lib/utils/prune-prefix.js +333 -0
- package/lib/utils/run-gjs.d.ts +12 -1
- package/lib/utils/run-gjs.js +24 -31
- package/lib/utils/run-node.d.ts +9 -4
- package/lib/utils/run-node.js +34 -41
- package/lib/utils/ship/depends.d.ts +5 -0
- package/lib/utils/ship/depends.js +38 -0
- package/lib/utils/ship/discover.js +106 -1
- package/lib/utils/ship/launcher.js +27 -4
- package/lib/utils/ship/payload.d.ts +35 -0
- package/lib/utils/ship/payload.js +84 -0
- package/lib/utils/ship/plan.js +20 -0
- package/lib/utils/ship/settings.d.ts +8 -1
- package/lib/utils/ship/settings.js +13 -1
- package/lib/utils/ship/types.d.ts +19 -0
- package/lib/utils/simple-command.d.ts +16 -0
- package/lib/utils/simple-command.js +77 -0
- package/lib/utils/spawn.d.ts +7 -1
- package/lib/utils/spawn.js +34 -7
- package/lib/utils/system-gi.d.ts +24 -0
- package/lib/utils/system-gi.js +14 -3
- package/lib/utils/watch-loop.d.ts +91 -0
- package/lib/utils/watch-loop.js +207 -0
- package/package.json +27 -18
- package/lib/templates/app/desktop.tmpl +0 -10
package/lib/actions/build.d.ts
CHANGED
|
@@ -166,11 +166,25 @@ export declare class BuildAction {
|
|
|
166
166
|
*/
|
|
167
167
|
globals?: string;
|
|
168
168
|
excludeGlobals?: string[];
|
|
169
|
+
/**
|
|
170
|
+
* Allow a `@gjsify/*` this project cannot resolve to come from the copy
|
|
171
|
+
* installed beside the RUNNING CLI.
|
|
172
|
+
*
|
|
173
|
+
* OPT-IN PER CALLER, never inferred here, because this helper does NOT
|
|
174
|
+
* only bundle toolchain — the `node` shim `gjsify run` puts on the PATH
|
|
175
|
+
* routes any project's `node <file>` through `utils/node-script.ts` and
|
|
176
|
+
* into this same function, a consumer's `"start": "node ./src/main.mjs"`
|
|
177
|
+
* included. Each caller states what it is bundling and why that earns the
|
|
178
|
+
* fallback; `gjsify build` sets nothing, so a user's missing dependency
|
|
179
|
+
* stays fatal there.
|
|
180
|
+
*/
|
|
181
|
+
resolveFromToolchain?: boolean;
|
|
169
182
|
}): Promise<string>;
|
|
170
183
|
/** Application mode */
|
|
171
184
|
buildApp(app?: App, opts?: {
|
|
172
185
|
watch?: boolean;
|
|
173
186
|
preserveDefaultExport?: boolean;
|
|
187
|
+
toolchainAnchor?: string;
|
|
174
188
|
}): Promise<RolldownOutput[]>;
|
|
175
189
|
/**
|
|
176
190
|
* Drive `rolldown.watch(...)`: rebuild on source change, apply the
|
package/lib/actions/build.js
CHANGED
|
@@ -5,12 +5,13 @@ import { resolveUserPlugins } from '../utils/resolve-plugin-by-name.js';
|
|
|
5
5
|
import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals, detectNodeGiGlobals, } from '@gjsify/rolldown-plugin-gjsify/globals';
|
|
6
6
|
import { pnpPlugin } from '@gjsify/rolldown-plugin-pnp';
|
|
7
7
|
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
8
|
-
import { pathToFileURL } from 'node:url';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
9
|
import { chmod, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
10
10
|
import { normalizeBundlerOptions, mergeBundlerOptions } from '../utils/normalize-bundler-options.js';
|
|
11
11
|
import { inputSourceDirs, isOutdirInsideSource, libraryOutputLeakError } from '../utils/library-output.js';
|
|
12
12
|
import { detectHtmlEntry, parseHtmlEntry, emitBrowserHtml, htmlOutPathFor } from '../utils/html-entry.js';
|
|
13
13
|
import { assertGjsBundleLoadable } from '../utils/gjs-bundle-guard.js';
|
|
14
|
+
import { giSystemProbes } from '../utils/gi-runtime-paths.js';
|
|
14
15
|
import { escapeRawNulForGjs } from '../utils/gjs-source-escape.js';
|
|
15
16
|
import { assertNodeBundleGlobalsShimmed } from '../utils/node-bundle-guard.js';
|
|
16
17
|
const DEFAULT_GJS_SHEBANG = '#!/usr/bin/env -S gjs -m';
|
|
@@ -78,17 +79,41 @@ async function newestLockfileMtime(cwd) {
|
|
|
78
79
|
}
|
|
79
80
|
return newest;
|
|
80
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Absolute path of the CLI module that is running, or `null` when it did not come
|
|
84
|
+
* from a `file:` URL.
|
|
85
|
+
*
|
|
86
|
+
* The conversion happens ONCE, here, inside a catch. A CLI loaded from a non-`file:`
|
|
87
|
+
* origin — the bundled/Flatpak shape `commands/tsc.ts` names as the reason its second
|
|
88
|
+
* anchor exists — would otherwise make `fileURLToPath` throw on the path that loads
|
|
89
|
+
* every config file and every plugin, not merely lose the fallback.
|
|
90
|
+
*/
|
|
91
|
+
function runningCliFile() {
|
|
92
|
+
try {
|
|
93
|
+
return fileURLToPath(import.meta.url);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
81
99
|
/**
|
|
82
100
|
* Fresh when the cached GJS bundle is newer than BOTH the source file AND the
|
|
83
101
|
* project's lockfile (the dep-change signal). Missing bundle/source → not fresh
|
|
84
102
|
* (rebuild). Shared by the plugin + config GJS-bundle caches.
|
|
103
|
+
*
|
|
104
|
+
* `anchorFile` is the running CLI, and only set when this bundle was allowed the
|
|
105
|
+
* toolchain fallback. Such a bundle can carry code from the CLI's OWN install,
|
|
106
|
+
* which neither the entry mtime nor the project lockfile tracks — so after a
|
|
107
|
+
* `gjsify self-update` a cached config/plugin bundle holding the previous CLI's
|
|
108
|
+
* `@gjsify/*` would otherwise stay "fresh" forever.
|
|
85
109
|
*/
|
|
86
|
-
async function isBundleFresh(outfile, sourcePath, cwd) {
|
|
110
|
+
async function isBundleFresh(outfile, sourcePath, cwd, anchorFile) {
|
|
87
111
|
try {
|
|
88
112
|
const outStat = await stat(outfile);
|
|
89
113
|
const srcStat = await stat(sourcePath);
|
|
90
114
|
const depMtime = await newestLockfileMtime(cwd);
|
|
91
|
-
|
|
115
|
+
const anchorMtime = anchorFile === undefined ? 0 : (await stat(anchorFile)).mtimeMs;
|
|
116
|
+
return outStat.mtimeMs >= Math.max(srcStat.mtimeMs, depMtime, anchorMtime);
|
|
92
117
|
}
|
|
93
118
|
catch {
|
|
94
119
|
return false;
|
|
@@ -433,6 +458,9 @@ export class BuildAction {
|
|
|
433
458
|
label: pluginName,
|
|
434
459
|
verbose,
|
|
435
460
|
noCacheEnv: 'GJSIFY_NO_PLUGIN_CACHE',
|
|
461
|
+
// A bundler plugin runs inside the CLI process — toolchain by
|
|
462
|
+
// construction, not by where its file happens to sit.
|
|
463
|
+
resolveFromToolchain: true,
|
|
436
464
|
});
|
|
437
465
|
}
|
|
438
466
|
catch (err) {
|
|
@@ -471,9 +499,13 @@ export class BuildAction {
|
|
|
471
499
|
// policies is two artifacts, and the freshness check below (entry mtime + lockfile)
|
|
472
500
|
// cannot see the `package.json` edit that changed one. Callers without a policy keep
|
|
473
501
|
// their existing cache filenames.
|
|
474
|
-
|
|
502
|
+
// The anchor belongs in the key for the same reason: with it the artifact may
|
|
503
|
+
// contain code from a DIFFERENT install than without it, and two CLIs on one
|
|
504
|
+
// machine resolve to two different paths.
|
|
505
|
+
const anchorFile = opts.resolveFromToolchain === true ? runningCliFile() : null;
|
|
506
|
+
const cacheKey = opts.globals === undefined && opts.excludeGlobals === undefined && anchorFile === null
|
|
475
507
|
? inputPath
|
|
476
|
-
: `${inputPath} ${JSON.stringify([opts.globals ?? null, opts.excludeGlobals ?? null])}`;
|
|
508
|
+
: `${inputPath} ${JSON.stringify([opts.globals ?? null, opts.excludeGlobals ?? null, anchorFile])}`;
|
|
477
509
|
// THE HASH GOES IN THE DIRECTORY, THE FILE KEEPS THE SOURCE'S NAME. As
|
|
478
510
|
// `<name>-<hash>.mjs` it broke every script guarding its body with the standard
|
|
479
511
|
// is-entry test: `audit-runtimes.mjs` asks whether `process.argv[1]` ENDS WITH its
|
|
@@ -482,7 +514,7 @@ export class BuildAction {
|
|
|
482
514
|
// does — and a name is cheaper to keep right than an argv to rewrite.
|
|
483
515
|
const outfile = join(cacheDir, `${safeName}-${shortHash(cacheKey)}`, `${basename(inputPath).replace(/\.[cm]?[jt]sx?$/i, '')}.mjs`);
|
|
484
516
|
const cacheDisabled = opts.cache === false || (opts.noCacheEnv ? isTruthyEnv(process.env[opts.noCacheEnv]) : false);
|
|
485
|
-
if (!cacheDisabled && (await isBundleFresh(outfile, inputPath, cwd))) {
|
|
517
|
+
if (!cacheDisabled && (await isBundleFresh(outfile, inputPath, cwd, anchorFile ?? undefined))) {
|
|
486
518
|
if (opts.verbose)
|
|
487
519
|
console.debug(`[gjsify] reusing cached GJS bundle ${outfile}`);
|
|
488
520
|
return outfile;
|
|
@@ -499,7 +531,13 @@ export class BuildAction {
|
|
|
499
531
|
output: { file: outfile },
|
|
500
532
|
...(opts.define ? { transform: { define: opts.define } } : {}),
|
|
501
533
|
},
|
|
502
|
-
}).buildApp('gjs', {
|
|
534
|
+
}).buildApp('gjs', {
|
|
535
|
+
preserveDefaultExport: opts.preserveDefaultExport ?? true,
|
|
536
|
+
// See `opts.resolveFromToolchain`: the CALLER decides whether this input is
|
|
537
|
+
// toolchain, because this helper alone cannot tell a config file from a
|
|
538
|
+
// consumer's `node ./src/main.mjs`.
|
|
539
|
+
...(anchorFile === null ? {} : { toolchainAnchor: anchorFile }),
|
|
540
|
+
});
|
|
503
541
|
return outfile;
|
|
504
542
|
}
|
|
505
543
|
/** Application mode */
|
|
@@ -558,6 +596,7 @@ export class BuildAction {
|
|
|
558
596
|
consoleShim,
|
|
559
597
|
...(aliases ? { aliases } : {}),
|
|
560
598
|
...(opts.preserveDefaultExport ? { preserveDefaultExport: true } : {}),
|
|
599
|
+
...(opts.toolchainAnchor !== undefined ? { toolchainAnchor: opts.toolchainAnchor } : {}),
|
|
561
600
|
};
|
|
562
601
|
const { autoMode, extras } = this.parseGlobalsValue(globals);
|
|
563
602
|
const pnp = await buildPnpPlugin();
|
|
@@ -682,6 +721,13 @@ export class BuildAction {
|
|
|
682
721
|
else if (extras) {
|
|
683
722
|
pluginOpts.autoGlobalsInject = await this.resolveGlobalsInject(app, extras, verbose);
|
|
684
723
|
}
|
|
724
|
+
// Fills the byte-1 GI prologue — built across #1152/#1160/#1026 and called by
|
|
725
|
+
// nothing, so every `--app gjs` bundle ever built shipped the empty string
|
|
726
|
+
// `giRuntimePathsStub` returns for an empty input. Only the FINAL build: the
|
|
727
|
+
// two analysis passes above bundle in memory to be PARSED for free globals,
|
|
728
|
+
// never written and never run, so a prologue there is work whose result is
|
|
729
|
+
// discarded.
|
|
730
|
+
const giProbes = app === 'gjs' ? giSystemProbes() : undefined;
|
|
685
731
|
// Final build: orchestrator → rolldown → write
|
|
686
732
|
const cfg = await gjsifyPlugin({
|
|
687
733
|
input: userBundler.input,
|
|
@@ -690,6 +736,7 @@ export class BuildAction {
|
|
|
690
736
|
userBanner,
|
|
691
737
|
userAliases: aliases,
|
|
692
738
|
shebang: this.configData.shebang,
|
|
739
|
+
giSystemProbes: giProbes,
|
|
693
740
|
}, pluginOpts);
|
|
694
741
|
const merged = mergeBundlerOptions(cfg.options, userBundler);
|
|
695
742
|
const finalOpts = {
|
package/lib/bundler-pick.js
CHANGED
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
// npm `rolldown(opts).write(opts.output)`; GJS gets `@gjsify/rolldown-native` (the
|
|
3
3
|
// Vala/Rust prebuild) via `bundleWithPlugins()`, with `runNativeBundle` replicating
|
|
4
4
|
// npm rolldown's `.write()` including nested chunk/asset subdirectories. The native
|
|
5
|
-
// engine is the GJS DEFAULT with no env var, because npm `rolldown`
|
|
6
|
-
//
|
|
5
|
+
// engine is the GJS DEFAULT with no env var, because npm `rolldown` does not run
|
|
6
|
+
// under GJS. Where it fails matters: NOT in the N-API addon, but in the JS entry
|
|
7
|
+
// above it. `rolldown/dist/shared/binding-*.mjs` evaluates
|
|
8
|
+
// `createRequire(import.meta.url)` at module scope and its platform detection then
|
|
9
|
+
// calls `__require('node:fs')` / `__require('node:child_process')`; GJS refuses a
|
|
10
|
+
// synchronous require of a builtin, so the load dies before any `.node` is opened.
|
|
11
|
+
// The distinction is not pedantry — it says an N-API host alone would not be
|
|
12
|
+
// enough; the wrapper has to be bypassed too, which is exactly what
|
|
13
|
+
// `napiNodeAddonPlugin` does for napi-rs packages. See `status/open-todos.md`
|
|
14
|
+
// § "Can `@gjsify/napi` retire the hand-written `-native` bridges?".
|
|
7
15
|
//
|
|
8
16
|
// `GJSIFY_BUNDLER=native|npm` overrides that: `native` throws when the prebuild is
|
|
9
17
|
// not loadable for the running architecture instead of silently switching engines;
|
package/lib/cli-app.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import yargs from 'yargs';
|
|
19
19
|
import { classifyCliFailure } from './cli-fail.js';
|
|
20
20
|
import { cliVersion } from './utils/publish-headers.js';
|
|
21
|
-
import { buildCommand as build, clearCommand as clear, copyCommand as copy, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, shipCommand as ship, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
|
|
21
|
+
import { buildCommand as build, clearCommand as clear, copyCommand as copy, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, shipCommand as ship, dlxCommand as dlx, installCommand as install, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, pruneCommand as prune, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, devCommand as dev, debugCommand as debug, browseCommand as browse, } from './commands/index.js';
|
|
22
22
|
import { APP_NAME } from './constants.js';
|
|
23
23
|
import { isBun, isDeno, isNode, gjsSystemVersion } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
24
24
|
import { ensureGjsifyShimOnPath } from './utils/gjsify-shim.js';
|
|
@@ -120,6 +120,7 @@ export async function runCli(argv) {
|
|
|
120
120
|
.command(selfUpdate.command, selfUpdate.description, selfUpdate.builder, selfUpdate.handler)
|
|
121
121
|
.command(generateInstaller.command, generateInstaller.description, generateInstaller.builder, generateInstaller.handler)
|
|
122
122
|
.command(uninstall.command, uninstall.description, uninstall.builder, uninstall.handler)
|
|
123
|
+
.command(prune.command, prune.description, prune.builder, prune.handler)
|
|
123
124
|
.command(upgrade.command, upgrade.description, upgrade.builder, upgrade.handler)
|
|
124
125
|
.command(format.command, format.description, format.builder, format.handler)
|
|
125
126
|
.command(lint.command, lint.description, lint.builder, lint.handler)
|
|
@@ -128,6 +129,7 @@ export async function runCli(argv) {
|
|
|
128
129
|
.command(tsc.command, tsc.description, tsc.builder, tsc.handler)
|
|
129
130
|
.command(affected.command, affected.description, affected.builder, affected.handler)
|
|
130
131
|
.command(storybook.command, storybook.description, storybook.builder, storybook.handler)
|
|
132
|
+
.command(dev.command, dev.description, dev.builder, dev.handler)
|
|
131
133
|
.command(debug.command, debug.description, debug.builder, debug.handler)
|
|
132
134
|
.command(browse.command, browse.description, browse.builder, browse.handler)
|
|
133
135
|
.demandCommand(1)
|
package/lib/commands/browse.js
CHANGED
|
@@ -112,6 +112,6 @@ export const browseCommand = {
|
|
|
112
112
|
process.env.WEBKIT_INSPECTOR_HTTP_SERVER = `127.0.0.1:${inspectorPort}`;
|
|
113
113
|
}
|
|
114
114
|
const { runGjsBundle } = await import('../utils/run-gjs.js');
|
|
115
|
-
await runGjsBundle(outPath, [], { exitOnSuccess: true });
|
|
115
|
+
await runGjsBundle(outPath, [], { completion: 'exit', exitOnSuccess: true });
|
|
116
116
|
},
|
|
117
117
|
};
|
package/lib/commands/create.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import type { PackageManager } from '@gjsify/create-app';
|
|
1
2
|
import type { Command } from '../types/index.js';
|
|
2
3
|
interface CreateOptions {
|
|
3
4
|
'project-name': string;
|
|
4
5
|
template?: string;
|
|
6
|
+
runtime?: string;
|
|
7
|
+
'package-manager'?: PackageManager;
|
|
5
8
|
force: boolean;
|
|
6
9
|
install: boolean;
|
|
7
10
|
}
|
package/lib/commands/create.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { createProject, discoverTemplates } from '@gjsify/create-app';
|
|
2
|
-
import { promptTemplate } from '@gjsify/create-app/prompt';
|
|
1
|
+
import { createProject, discoverTemplates, PACKAGE_MANAGERS, supportedRuntimes } from '@gjsify/create-app';
|
|
2
|
+
import { isCancelled, promptSetup, promptTemplate } from '@gjsify/create-app/prompt';
|
|
3
3
|
export const createCommand = {
|
|
4
4
|
command: 'create [project-name]',
|
|
5
5
|
description: 'Scaffold a new Gjsify project in a new directory.',
|
|
6
6
|
builder: (yargs) => {
|
|
7
7
|
const templates = discoverTemplates();
|
|
8
8
|
const templateChoices = templates.map((t) => t.name);
|
|
9
|
+
// DERIVED from the shipped templates, never a hand-written list:
|
|
10
|
+
// accepting `--runtime deno` is a claim that some template runs there,
|
|
11
|
+
// and only the templates can make it.
|
|
12
|
+
const runtimeChoices = [...new Set(templates.flatMap((t) => supportedRuntimes(t)))];
|
|
9
13
|
return yargs
|
|
10
14
|
.positional('project-name', {
|
|
11
15
|
describe: 'Name of the project directory to create',
|
|
@@ -17,6 +21,12 @@ export const createCommand = {
|
|
|
17
21
|
describe: 'Template to scaffold from',
|
|
18
22
|
type: 'string',
|
|
19
23
|
choices: templateChoices.length > 0 ? templateChoices : undefined,
|
|
24
|
+
})
|
|
25
|
+
.option('runtime', {
|
|
26
|
+
alias: 'r',
|
|
27
|
+
describe: 'Runtime to set the project up for. Decides which package managers are offered and which start script the next steps name.',
|
|
28
|
+
type: 'string',
|
|
29
|
+
choices: runtimeChoices.length > 0 ? runtimeChoices : undefined,
|
|
20
30
|
})
|
|
21
31
|
.option('force', {
|
|
22
32
|
alias: 'f',
|
|
@@ -25,30 +35,63 @@ export const createCommand = {
|
|
|
25
35
|
default: false,
|
|
26
36
|
})
|
|
27
37
|
.option('install', {
|
|
28
|
-
describe: '
|
|
38
|
+
describe: 'Install dependencies after scaffolding',
|
|
29
39
|
type: 'boolean',
|
|
30
40
|
default: false,
|
|
41
|
+
})
|
|
42
|
+
.option('package-manager', {
|
|
43
|
+
alias: 'p',
|
|
44
|
+
describe: 'Package manager to install with, and to name in the printed next steps. Must be one the chosen runtime can install for. `gjsify` is the only one that works on a host with no Node.js.',
|
|
45
|
+
type: 'string',
|
|
46
|
+
choices: PACKAGE_MANAGERS,
|
|
31
47
|
});
|
|
32
48
|
},
|
|
33
49
|
handler: async (args) => {
|
|
50
|
+
const interactive = Boolean(process.stdin.isTTY);
|
|
51
|
+
const templates = discoverTemplates();
|
|
34
52
|
let template = args.template;
|
|
35
53
|
if (!template) {
|
|
36
|
-
|
|
37
|
-
if (!process.stdin.isTTY) {
|
|
54
|
+
if (!interactive) {
|
|
38
55
|
const list = templates.map((t) => t.name).join(', ');
|
|
39
56
|
console.error(`Error: --template is required in non-interactive mode. Available templates: ${list || '(none)'}`);
|
|
40
57
|
// `return` — the deferred GJS exit otherwise fell through into
|
|
41
58
|
// the interactive template prompt on a non-TTY.
|
|
42
59
|
return process.exit(1);
|
|
43
60
|
}
|
|
44
|
-
|
|
61
|
+
// Ctrl+C is an abort, not a crash.
|
|
62
|
+
const picked = await promptTemplate(templates).catch((error) => {
|
|
63
|
+
if (!isCancelled(error))
|
|
64
|
+
throw error;
|
|
65
|
+
console.error('Cancelled.');
|
|
66
|
+
return undefined;
|
|
67
|
+
});
|
|
68
|
+
if (picked === undefined)
|
|
69
|
+
return process.exit(1);
|
|
45
70
|
template = picked.name;
|
|
46
71
|
}
|
|
72
|
+
// The SAME decision layer `npm create @gjsify/app` runs, not a second
|
|
73
|
+
// one. Wired late: this command used to pass neither runtime nor package
|
|
74
|
+
// manager, so `createProject` fell back to the host runtime and the first
|
|
75
|
+
// manager for it — meaning `gjsify create --install` on a Node host ran
|
|
76
|
+
// `npm install` without ever saying so, the one default that reaches the
|
|
77
|
+
// user's disk and the one the standalone bin refuses to guess.
|
|
78
|
+
const info = templates.find((t) => t.name === template);
|
|
79
|
+
const setup = await promptSetup({
|
|
80
|
+
offered: info ? supportedRuntimes(info) : [],
|
|
81
|
+
runtime: args.runtime,
|
|
82
|
+
packageManager: args['package-manager'],
|
|
83
|
+
install: args.install,
|
|
84
|
+
interactive,
|
|
85
|
+
});
|
|
86
|
+
if (setup === undefined)
|
|
87
|
+
return process.exit(1);
|
|
47
88
|
await createProject({
|
|
48
89
|
projectName: args['project-name'],
|
|
49
90
|
template,
|
|
91
|
+
runtime: setup.runtime,
|
|
50
92
|
force: args.force,
|
|
51
93
|
install: args.install,
|
|
94
|
+
packageManager: setup.packageManager,
|
|
52
95
|
});
|
|
53
96
|
},
|
|
54
97
|
};
|
package/lib/commands/debug.js
CHANGED
|
@@ -158,6 +158,6 @@ export const debugCommand = {
|
|
|
158
158
|
return;
|
|
159
159
|
}
|
|
160
160
|
const { runGjsBundle } = await import('../utils/run-gjs.js');
|
|
161
|
-
await runGjsBundle(outPath, [], { exitOnSuccess: true });
|
|
161
|
+
await runGjsBundle(outPath, [], { completion: 'exit', exitOnSuccess: true });
|
|
162
162
|
},
|
|
163
163
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Command } from '../types/index.js';
|
|
2
|
+
interface DevCliOptions {
|
|
3
|
+
entry?: string;
|
|
4
|
+
runtime?: string;
|
|
5
|
+
script?: string;
|
|
6
|
+
globals?: string;
|
|
7
|
+
outfile?: string;
|
|
8
|
+
watchDir?: string;
|
|
9
|
+
debounce: number;
|
|
10
|
+
buildOnly: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const devCommand: Command<unknown, DevCliOptions>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// `gjsify dev` — the inner development loop: watch, rebuild, relaunch.
|
|
2
|
+
//
|
|
3
|
+
// It exists because `gjsify build --watch` cannot serve it on the host that
|
|
4
|
+
// needs it most. That flag drives rolldown's watcher API, which only the npm
|
|
5
|
+
// engine exposes (`bundler-pick.ts` refuses under `@gjsify/rolldown-native`), so
|
|
6
|
+
// on a Node-free GJS host the loop was manual: edit, Ctrl-C, rebuild, relaunch.
|
|
7
|
+
// `gjsify storybook --watch` had already shown the way around it — watch with
|
|
8
|
+
// `fs.watch` and rebuild by re-entering the ordinary build command, which needs
|
|
9
|
+
// no watcher API and is runtime-agnostic. That loop is now shared
|
|
10
|
+
// (`utils/watch-loop.ts`) and this command is its second caller.
|
|
11
|
+
//
|
|
12
|
+
// WHAT to build is not re-declared here: it is read out of the project's own
|
|
13
|
+
// `build:gjs` / `build:node` script (`utils/dev-plan.ts`), so the dev loop and
|
|
14
|
+
// `gjsify run build` cannot drift into building different bundles.
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { join, relative, resolve } from 'node:path';
|
|
17
|
+
import { noBundleError, planDev } from '../utils/dev-plan.js';
|
|
18
|
+
import { buildAppForRuntime, EXAMPLE_RUNTIMES } from '../utils/runtimes.js';
|
|
19
|
+
import { hostRuntime } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
20
|
+
function readPackage(cwd) {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// A missing or malformed package.json is not this command's error to
|
|
26
|
+
// report: `planDev` refuses right after, naming the flags that make
|
|
27
|
+
// `gjsify dev` work in a directory with no manifest at all.
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export const devCommand = {
|
|
32
|
+
command: 'dev [entry]',
|
|
33
|
+
description: 'Watch this project, rebuild on change and relaunch the app.',
|
|
34
|
+
builder: (yargs) => yargs
|
|
35
|
+
.positional('entry', {
|
|
36
|
+
description: 'Entry point to build (default: the one the build script names, e.g. src/index.ts from `build:gjs`).',
|
|
37
|
+
type: 'string',
|
|
38
|
+
})
|
|
39
|
+
.option('runtime', {
|
|
40
|
+
description: 'Runtime to build for and launch on: gjs | node | bun | deno (default: the host runtime). ' +
|
|
41
|
+
'node/bun/deno build the same `--app node` bundle.',
|
|
42
|
+
type: 'string',
|
|
43
|
+
choices: EXAMPLE_RUNTIMES,
|
|
44
|
+
// No yargs `default` — the host default is applied in the
|
|
45
|
+
// handler, so `hostRuntime()` is evaluated where it can be seen.
|
|
46
|
+
defaultDescription: 'host runtime (gjs on gjs, else node/bun/deno)',
|
|
47
|
+
})
|
|
48
|
+
.option('script', {
|
|
49
|
+
description: 'package.json script the build flags are read from (default: build:gjs, or build:node for node/bun/deno).',
|
|
50
|
+
type: 'string',
|
|
51
|
+
})
|
|
52
|
+
.option('globals', { description: 'Override the build script `--globals` value.', type: 'string' })
|
|
53
|
+
.option('outfile', { description: 'Override the build script `--outfile` path.', type: 'string' })
|
|
54
|
+
.option('watch-dir', {
|
|
55
|
+
description: 'Directory watched recursively (default: the directory of the entry point).',
|
|
56
|
+
type: 'string',
|
|
57
|
+
})
|
|
58
|
+
.option('debounce', {
|
|
59
|
+
description: 'Quiet window in ms after a change before rebuilding.',
|
|
60
|
+
type: 'number',
|
|
61
|
+
default: 200,
|
|
62
|
+
})
|
|
63
|
+
.option('build-only', {
|
|
64
|
+
description: 'Rebuild on every change but never launch the app.',
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
default: false,
|
|
67
|
+
}),
|
|
68
|
+
handler: async (args) => {
|
|
69
|
+
const cwd = process.cwd();
|
|
70
|
+
const pkg = readPackage(cwd);
|
|
71
|
+
// The cast, not a guard: yargs' `choices` above already rejects anything
|
|
72
|
+
// outside the four, and `hostRuntime()` returns exactly that union — a
|
|
73
|
+
// second check here would be validating our own vocabulary.
|
|
74
|
+
const runtime = (args.runtime ?? hostRuntime());
|
|
75
|
+
const app = buildAppForRuntime(runtime);
|
|
76
|
+
const scriptName = args.script ?? (app === 'gjs' ? 'build:gjs' : 'build:node');
|
|
77
|
+
let plan;
|
|
78
|
+
try {
|
|
79
|
+
plan = planDev({
|
|
80
|
+
scripts: pkg.scripts ?? {},
|
|
81
|
+
scriptName,
|
|
82
|
+
app,
|
|
83
|
+
declaredBundle: app === 'gjs' ? pkg.gjsify?.main : pkg.gjsify?.example?.node,
|
|
84
|
+
entry: args.entry,
|
|
85
|
+
globals: args.globals,
|
|
86
|
+
outfile: args.outfile,
|
|
87
|
+
watchDir: args.watchDir,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
console.error(err.message);
|
|
92
|
+
// `return process.exit()`: under GJS a bare `process.exit()` only
|
|
93
|
+
// SCHEDULES the exit and returns, so execution would fall through
|
|
94
|
+
// into the watch loop and the deferred exit would never carry its
|
|
95
|
+
// code. Same trap as `showcase.ts` / `storybook.ts`.
|
|
96
|
+
return process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
const bundlePath = plan.bundle === undefined ? undefined : resolve(cwd, plan.bundle);
|
|
99
|
+
if (bundlePath === undefined && !args.buildOnly) {
|
|
100
|
+
console.error(noBundleError(scriptName, app).message);
|
|
101
|
+
return process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const watchDir = resolve(cwd, plan.watchDir);
|
|
104
|
+
if (!existsSync(watchDir)) {
|
|
105
|
+
// `fs.watch` on a missing directory throws a bare ENOENT with the
|
|
106
|
+
// path buried in a stack — say which directory was DERIVED and how
|
|
107
|
+
// to override it instead.
|
|
108
|
+
console.error(`gjsify dev: nothing to watch — ${relative(cwd, watchDir) || watchDir} does not exist.\n` +
|
|
109
|
+
` Point the loop at your sources: gjsify dev --watch-dir <dir>`);
|
|
110
|
+
return process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
const target = bundlePath === undefined ? `\`${scriptName}\` output` : relative(cwd, bundlePath);
|
|
113
|
+
console.log(`Watching ${relative(cwd, watchDir) || '.'} → ${target} (runtime: ${runtime})`);
|
|
114
|
+
// In-process build — runtime-agnostic (Node uses npm rolldown, GJS the
|
|
115
|
+
// @gjsify/rolldown-native facade), no re-spawn of the CLI binary.
|
|
116
|
+
const { runCli } = await import('../cli-app.js');
|
|
117
|
+
const { runWatchLoop, spawnBundleSupervised } = await import('../utils/watch-loop.js');
|
|
118
|
+
await runWatchLoop({
|
|
119
|
+
dir: watchDir,
|
|
120
|
+
dirLabel: relative(cwd, watchDir) || '.',
|
|
121
|
+
label: 'dev',
|
|
122
|
+
build: () => runCli(plan.buildArgv),
|
|
123
|
+
spawnChild: args.buildOnly || bundlePath === undefined
|
|
124
|
+
? null
|
|
125
|
+
: () => spawnBundleSupervised(runtime, bundlePath, cwd, 'this project'),
|
|
126
|
+
// Passed even under `--build-only`: what makes the loop feed itself
|
|
127
|
+
// is the WRITE, not the launch.
|
|
128
|
+
output: bundlePath,
|
|
129
|
+
debounceMs: args.debounce,
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
};
|
package/lib/commands/dlx.js
CHANGED
|
@@ -100,7 +100,7 @@ export const dlxCommand = {
|
|
|
100
100
|
}
|
|
101
101
|
// Terminal call — exit on success, or the GJS main loop parks this
|
|
102
102
|
// process forever (see RunGjsBundleOptions.exitOnSuccess).
|
|
103
|
-
await runGjsBundle(entry.bundlePath, extraArgs, { exitOnSuccess: true });
|
|
103
|
+
await runGjsBundle(entry.bundlePath, extraArgs, { completion: 'exit', exitOnSuccess: true });
|
|
104
104
|
},
|
|
105
105
|
};
|
|
106
106
|
/**
|
package/lib/commands/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export * from './trust.js';
|
|
|
26
26
|
export * from './onboard.js';
|
|
27
27
|
export * from './self-update.js';
|
|
28
28
|
export * from './generate-installer.js';
|
|
29
|
+
export * from './prune.js';
|
|
29
30
|
export * from './uninstall.js';
|
|
30
31
|
export * from './format.js';
|
|
31
32
|
export * from './lint.js';
|
|
@@ -35,5 +36,6 @@ export * from './barrels.js';
|
|
|
35
36
|
export * from './tsc.js';
|
|
36
37
|
export * from './affected.js';
|
|
37
38
|
export * from './storybook.js';
|
|
39
|
+
export * from './dev.js';
|
|
38
40
|
export * from './debug.js';
|
|
39
41
|
export * from './browse.js';
|
package/lib/commands/index.js
CHANGED
|
@@ -26,6 +26,7 @@ export * from './trust.js';
|
|
|
26
26
|
export * from './onboard.js';
|
|
27
27
|
export * from './self-update.js';
|
|
28
28
|
export * from './generate-installer.js';
|
|
29
|
+
export * from './prune.js';
|
|
29
30
|
export * from './uninstall.js';
|
|
30
31
|
export * from './format.js';
|
|
31
32
|
export * from './lint.js';
|
|
@@ -35,5 +36,6 @@ export * from './barrels.js';
|
|
|
35
36
|
export * from './tsc.js';
|
|
36
37
|
export * from './affected.js';
|
|
37
38
|
export * from './storybook.js';
|
|
39
|
+
export * from './dev.js';
|
|
38
40
|
export * from './debug.js';
|
|
39
41
|
export * from './browse.js';
|
package/lib/commands/install.js
CHANGED
|
@@ -32,6 +32,8 @@ import { nodeBinary } from '../utils/run-node.js';
|
|
|
32
32
|
import { spawnToCompletion } from '../utils/spawn.js';
|
|
33
33
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
34
34
|
import { binDirOnPath, BUNDLER_ENGINE_PACKAGE, defaultGlobalLayout, hasBundlerEngineInstalled, installGjsEnginePackages, linkGlobalBins, specToPackageName, } from '../utils/install-global.js';
|
|
35
|
+
import { resolveHostPlatform } from '../utils/platform-check.js';
|
|
36
|
+
import { pruneAfterInstall } from '../utils/prune-prefix.js';
|
|
35
37
|
import { addDependencyEntry, defaultRangeFromVersion, parseSpec, projectSpecsFromPackageJson, readPackageJson, writePackageJson, } from '../utils/pkg-json-edit.js';
|
|
36
38
|
/**
|
|
37
39
|
* Link type for the workspace↔workspace directory links.
|
|
@@ -125,6 +127,11 @@ export const installCommand = {
|
|
|
125
127
|
description: 'Install a REQUIRED dependency even when its os/cpu/libc excludes the target, instead of failing with EBADPLATFORM (npm config key `force`). Incompatible OPTIONAL dependencies stay skipped — npm ignores --force for those too, and a binary that cannot load is not worth downloading.',
|
|
126
128
|
type: 'boolean',
|
|
127
129
|
default: false,
|
|
130
|
+
})
|
|
131
|
+
.option('prune', {
|
|
132
|
+
description: 'After installing, remove packages an earlier install left behind that this host cannot use (foreign os/cpu/libc). Use --no-prune to disable. Skipped automatically under --immutable, and whenever --os/--cpu/--libc is given: an install must never delete against a target the user typed.',
|
|
133
|
+
type: 'boolean',
|
|
134
|
+
default: true,
|
|
128
135
|
}),
|
|
129
136
|
handler: async (args) => {
|
|
130
137
|
// Must run before anything resolves — `--os/--cpu/--libc/--force` are npm
|
|
@@ -161,7 +168,7 @@ export const installCommand = {
|
|
|
161
168
|
console.warn(`gjsify install --global ignores --${flag}: global installs do not modify a project package.json.`);
|
|
162
169
|
}
|
|
163
170
|
}
|
|
164
|
-
await installGlobalAndLink(args.packages, { verbose: args.verbose });
|
|
171
|
+
await installGlobalAndLink(args.packages, { verbose: args.verbose, prune: args.prune });
|
|
165
172
|
return;
|
|
166
173
|
}
|
|
167
174
|
// Before the heavy install, so the skew is visible even if a later phase
|
|
@@ -170,6 +177,7 @@ export const installCommand = {
|
|
|
170
177
|
const backend = args.backend ?? process.env.GJSIFY_INSTALL_BACKEND ?? 'native';
|
|
171
178
|
if (backend === 'npm') {
|
|
172
179
|
await projectInstallViaNpm(args);
|
|
180
|
+
pruneProjectPrefix(args);
|
|
173
181
|
await runPostInstallChecks(args);
|
|
174
182
|
return;
|
|
175
183
|
}
|
|
@@ -217,6 +225,7 @@ export const installCommand = {
|
|
|
217
225
|
hardExitTimerId?.unref?.();
|
|
218
226
|
try {
|
|
219
227
|
await projectInstallNative(args, overallController?.signal);
|
|
228
|
+
pruneProjectPrefix(args);
|
|
220
229
|
await runPostInstallChecks(args);
|
|
221
230
|
}
|
|
222
231
|
catch (err) {
|
|
@@ -1198,6 +1207,14 @@ async function installGlobalAndLink(specs, opts) {
|
|
|
1198
1207
|
const cliVersion = result.installed.find((r) => r.name === '@gjsify/cli')?.version ?? 'latest';
|
|
1199
1208
|
await installGjsEnginePackages(layout.prefix, cliVersion, { verbose: opts.verbose });
|
|
1200
1209
|
}
|
|
1210
|
+
// Before the LINK on purpose: `linkGlobalBins`' launchers bake the resolved
|
|
1211
|
+
// prebuild directories into GI_TYPELIB_PATH/LD_LIBRARY_PATH, and must not name
|
|
1212
|
+
// a directory this pass is about to delete.
|
|
1213
|
+
if (opts.prune !== false) {
|
|
1214
|
+
pruneAfterInstall(layout.prefix, resolveHostPlatform({ env: {} }), {
|
|
1215
|
+
hint: 'gjsify prune -g --dry-run',
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1201
1218
|
const created = linkGlobalBins(packageNames, layout);
|
|
1202
1219
|
reportLinkedBins(created, layout.binDir);
|
|
1203
1220
|
}
|
|
@@ -1205,6 +1222,20 @@ async function installGlobalAndLink(specs, opts) {
|
|
|
1205
1222
|
lock.release();
|
|
1206
1223
|
}
|
|
1207
1224
|
}
|
|
1225
|
+
/**
|
|
1226
|
+
* The automatic pass for a PROJECT prefix.
|
|
1227
|
+
*
|
|
1228
|
+
* Hooked at the command level rather than inside `installPackages`, so `dlx`'s
|
|
1229
|
+
* throwaway cache prefix — created, used once and dropped — pays nothing for it.
|
|
1230
|
+
*/
|
|
1231
|
+
function pruneProjectPrefix(args) {
|
|
1232
|
+
if (args.prune === false)
|
|
1233
|
+
return;
|
|
1234
|
+
pruneAfterInstall(process.cwd(), resolveHostPlatform({ env: {} }), {
|
|
1235
|
+
immutable: args.immutable === true,
|
|
1236
|
+
hint: 'gjsify prune --dry-run',
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1208
1239
|
function reportLinkedBins(created, binDir) {
|
|
1209
1240
|
if (created.length === 0) {
|
|
1210
1241
|
console.warn('\nNo bins declared (neither `gjsify.bin` nor `bin` in package.json) — nothing was symlinked.');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Command } from '../types/index.js';
|
|
2
|
+
interface PruneOptions {
|
|
3
|
+
global: boolean;
|
|
4
|
+
dryRun: boolean;
|
|
5
|
+
verbose: boolean;
|
|
6
|
+
os?: string;
|
|
7
|
+
cpu?: string;
|
|
8
|
+
libc?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const pruneCommand: Command<unknown, PruneOptions>;
|
|
11
|
+
export {};
|