@gjsify/cli 0.41.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 +1040 -405
- 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 +2 -1
- package/lib/commands/browse.js +1 -1
- 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 +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/run.js +38 -85
- 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/types/config-data.d.ts +27 -0
- package/lib/utils/app-metadata.js +27 -6
- package/lib/utils/check-system-deps.js +49 -7
- 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/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/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 +20 -1
- 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 +19 -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, 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, 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';
|
|
@@ -129,6 +129,7 @@ export async function runCli(argv) {
|
|
|
129
129
|
.command(tsc.command, tsc.description, tsc.builder, tsc.handler)
|
|
130
130
|
.command(affected.command, affected.description, affected.builder, affected.handler)
|
|
131
131
|
.command(storybook.command, storybook.description, storybook.builder, storybook.handler)
|
|
132
|
+
.command(dev.command, dev.description, dev.builder, dev.handler)
|
|
132
133
|
.command(debug.command, debug.description, debug.builder, debug.handler)
|
|
133
134
|
.command(browse.command, browse.description, browse.builder, browse.handler)
|
|
134
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/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
package/lib/commands/index.js
CHANGED
package/lib/commands/run.js
CHANGED
|
@@ -15,13 +15,15 @@
|
|
|
15
15
|
// to disambiguate can pass `./<file>` explicitly.
|
|
16
16
|
import { readFileSync, statSync } from 'node:fs';
|
|
17
17
|
import { basename, delimiter, join, resolve } from 'node:path';
|
|
18
|
-
import {
|
|
18
|
+
import { describeExit, spawnToCompletion } from '../utils/spawn.js';
|
|
19
19
|
import { runGjsBundle } from '../utils/run-gjs.js';
|
|
20
20
|
import { runRuntimeBundle } from '../utils/run-node.js';
|
|
21
21
|
import { readPackageJson } from '../utils/pkg-json-edit.js';
|
|
22
22
|
import { runNodeScript } from '../utils/node-script.js';
|
|
23
23
|
import { nodeShimDir } from '../utils/gjsify-shim.js';
|
|
24
24
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
25
|
+
import { gjsifyCommandArgv } from '../utils/simple-command.js';
|
|
26
|
+
import { isDaemonCommand } from '../utils/daemon-command.js';
|
|
25
27
|
import { discoverWorkspaces } from '@gjsify/workspace';
|
|
26
28
|
import { isGjs, hostRuntime } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
27
29
|
import { EXAMPLE_RUNTIMES, isExampleRuntime, readDeclaredRuntimes, checkRuntimeSupported, } from '../utils/runtimes.js';
|
|
@@ -124,7 +126,7 @@ export const runCommand = {
|
|
|
124
126
|
console.error(`gjsify run: --node-script cannot be combined with ${clash}.`);
|
|
125
127
|
return process.exit(1);
|
|
126
128
|
}
|
|
127
|
-
await runNodeScript(target, extraArgs, { exitOnSuccess: true });
|
|
129
|
+
await runNodeScript(target, extraArgs, { completion: 'exit', exitOnSuccess: true });
|
|
128
130
|
return;
|
|
129
131
|
}
|
|
130
132
|
// `--runtime <gjs|node|bun|deno>` (explicit) → run a BUNDLE FILE on the
|
|
@@ -174,10 +176,10 @@ export const runCommand = {
|
|
|
174
176
|
// Terminal calls — exit on success, or the GJS main loop parks
|
|
175
177
|
// this process forever (see RunGjsBundleOptions.exitOnSuccess).
|
|
176
178
|
if (runtime === 'gjs') {
|
|
177
|
-
await runGjsBundle(file, extraArgs, { exitOnSuccess: true });
|
|
179
|
+
await runGjsBundle(file, extraArgs, { completion: 'exit', exitOnSuccess: true });
|
|
178
180
|
}
|
|
179
181
|
else {
|
|
180
|
-
await runRuntimeBundle(runtime, file, extraArgs, { exitOnSuccess: true });
|
|
182
|
+
await runRuntimeBundle(runtime, file, extraArgs, { completion: 'exit', exitOnSuccess: true });
|
|
181
183
|
}
|
|
182
184
|
return;
|
|
183
185
|
}
|
|
@@ -209,10 +211,10 @@ async function runTargetOnRuntime(runtime, target, extraArgs) {
|
|
|
209
211
|
const file = resolve(target);
|
|
210
212
|
// Terminal call — exit on success (the GJS/GLib loop would otherwise park).
|
|
211
213
|
if (runtime === 'gjs') {
|
|
212
|
-
await runGjsBundle(file, extraArgs, { exitOnSuccess: true });
|
|
214
|
+
await runGjsBundle(file, extraArgs, { completion: 'exit', exitOnSuccess: true });
|
|
213
215
|
}
|
|
214
216
|
else {
|
|
215
|
-
await runRuntimeBundle(runtime, file, extraArgs, { exitOnSuccess: true });
|
|
217
|
+
await runRuntimeBundle(runtime, file, extraArgs, { completion: 'exit', exitOnSuccess: true });
|
|
216
218
|
}
|
|
217
219
|
}
|
|
218
220
|
/**
|
|
@@ -420,6 +422,16 @@ async function runScript(script, extraArgs, cwd = process.cwd()) {
|
|
|
420
422
|
console.error(err.message);
|
|
421
423
|
dispatchFailed = true;
|
|
422
424
|
}
|
|
425
|
+
// A watch loop RESOLVES as soon as it is armed and then keeps running,
|
|
426
|
+
// so its resolution is not an ending to exit on: `gjsify run dev` used
|
|
427
|
+
// to take down the loop it had just started, the instant the first
|
|
428
|
+
// build+launch finished. Node never saw it — there the same script takes
|
|
429
|
+
// the shell-spawn path below — and it is the DOCUMENTED entry point:
|
|
430
|
+
// `gjsify create` prints `gjsify run dev`, and every template's `dev`
|
|
431
|
+
// script is a bare `gjsify dev`. Returning leaves the process on the
|
|
432
|
+
// main loop `runWatchLoop` holds, where Ctrl+C ends it.
|
|
433
|
+
if (!dispatchFailed && isDaemonCommand())
|
|
434
|
+
return;
|
|
423
435
|
// A thrown command wins over `process.exitCode`; otherwise honour a
|
|
424
436
|
// non-zero code the handler set instead of throwing (e.g. a failing
|
|
425
437
|
// check) — a bare exit(0) here masked those as success too.
|
|
@@ -436,23 +448,23 @@ async function runScript(script, extraArgs, cwd = process.cwd()) {
|
|
|
436
448
|
return process.exit(dispatchCode);
|
|
437
449
|
}
|
|
438
450
|
const fullCmd = extraArgs.length > 0 ? `${literal} ${extraArgs.map(shellEscape).join(' ')}` : literal;
|
|
439
|
-
// ensureMainLoop() (called inside spawn) keeps
|
|
440
|
-
// child exits
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
reject(new Error(`script "${script}" exited with code ${code}`));
|
|
449
|
-
});
|
|
450
|
-
child.on('error', reject);
|
|
451
|
-
}).catch((err) => {
|
|
451
|
+
// `completion: 'exit'` — ensureMainLoop() (called inside the async spawn) keeps
|
|
452
|
+
// GJS alive after the child exits, and both paths below end in `process.exit`,
|
|
453
|
+
// which is what tears it down. `shell: true` deliberately skips the win32
|
|
454
|
+
// rewrite: this is a command LINE, and Node routes it through %COMSPEC%.
|
|
455
|
+
let result;
|
|
456
|
+
try {
|
|
457
|
+
result = await spawnToCompletion(fullCmd, [], { completion: 'exit', cwd, env, shell: true });
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
452
460
|
console.error(err.message);
|
|
453
|
-
process.exit(1);
|
|
454
|
-
}
|
|
455
|
-
|
|
461
|
+
return process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
if (result.code !== 0) {
|
|
464
|
+
console.error(`script "${script}" exited with ${describeExit(result)}`);
|
|
465
|
+
return process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
return process.exit(0);
|
|
456
468
|
}
|
|
457
469
|
function shellEscape(arg) {
|
|
458
470
|
if (/^[a-zA-Z0-9_\-./=:@,]+$/.test(arg))
|
|
@@ -463,70 +475,11 @@ function shellEscape(arg) {
|
|
|
463
475
|
* If `literal` is a SINGLE `gjsify <subcommand> …` command with no shell
|
|
464
476
|
* operators / substitutions / unquoted globs, return the argv to feed `runCli`
|
|
465
477
|
* (the subcommand + its args, with `extraArgs` appended). Otherwise null — the
|
|
466
|
-
* caller falls back to the shell spawn path.
|
|
467
|
-
* glob (`'src/**'`) survives as a single literal token, exactly as the shell
|
|
468
|
-
* would hand it to `gjsify` (which does its own glob expansion).
|
|
478
|
+
* caller falls back to the shell spawn path.
|
|
469
479
|
*/
|
|
470
480
|
function gjsifyInProcessArgv(literal, extraArgs) {
|
|
471
|
-
const
|
|
472
|
-
if (!
|
|
481
|
+
const argv = gjsifyCommandArgv(literal);
|
|
482
|
+
if (!argv)
|
|
473
483
|
return null;
|
|
474
|
-
return [...
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* Quote-aware tokenizer for a SIMPLE command line (no shell features beyond
|
|
478
|
-
* single/double quoting). Returns the tokens, or null if the string contains
|
|
479
|
-
* anything the shell would treat specially — operators (`&& | ; < > &`),
|
|
480
|
-
* substitutions (`$(...)` / backticks / `$VAR`), unquoted globs/expansions
|
|
481
|
-
* (`* ? { } [ ] ~`), comments (`#`), or an unterminated quote — in which case
|
|
482
|
-
* the caller MUST use the real shell instead.
|
|
483
|
-
*/
|
|
484
|
-
function tokenizeSimpleCommand(cmd) {
|
|
485
|
-
const tokens = [];
|
|
486
|
-
let cur = '';
|
|
487
|
-
let has = false;
|
|
488
|
-
let quote = null;
|
|
489
|
-
for (let i = 0; i < cmd.length; i++) {
|
|
490
|
-
const c = cmd[i];
|
|
491
|
-
if (quote === "'") {
|
|
492
|
-
if (c === "'")
|
|
493
|
-
quote = null;
|
|
494
|
-
else
|
|
495
|
-
cur += c;
|
|
496
|
-
continue;
|
|
497
|
-
}
|
|
498
|
-
if (quote === '"') {
|
|
499
|
-
if (c === '"')
|
|
500
|
-
quote = null;
|
|
501
|
-
else if (c === '\\' && (cmd[i + 1] === '"' || cmd[i + 1] === '\\'))
|
|
502
|
-
cur += cmd[++i];
|
|
503
|
-
else if (c === '$' || c === '`')
|
|
504
|
-
return null; // substitution inside "…"
|
|
505
|
-
else
|
|
506
|
-
cur += c;
|
|
507
|
-
continue;
|
|
508
|
-
}
|
|
509
|
-
if (c === "'" || c === '"') {
|
|
510
|
-
quote = c;
|
|
511
|
-
has = true;
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
if (c === ' ' || c === '\t') {
|
|
515
|
-
if (has) {
|
|
516
|
-
tokens.push(cur);
|
|
517
|
-
cur = '';
|
|
518
|
-
has = false;
|
|
519
|
-
}
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
if ('|&;<>`$()\\\n\r*?{}[]~#!'.includes(c))
|
|
523
|
-
return null;
|
|
524
|
-
cur += c;
|
|
525
|
-
has = true;
|
|
526
|
-
}
|
|
527
|
-
if (quote)
|
|
528
|
-
return null; // unterminated quote
|
|
529
|
-
if (has)
|
|
530
|
-
tokens.push(cur);
|
|
531
|
-
return tokens;
|
|
484
|
+
return [...argv, ...extraArgs];
|
|
532
485
|
}
|
package/lib/commands/ship.js
CHANGED
|
@@ -20,7 +20,7 @@ import { deriveDepends, warnAboutGjsFloor } from '../utils/ship/depends.js';
|
|
|
20
20
|
import { discoverPayload } from '../utils/ship/discover.js';
|
|
21
21
|
import { resolveFormats, FORMAT_IDS } from '../utils/ship/formats.js';
|
|
22
22
|
import { scanGiNamespaces } from '../utils/ship/gi-namespaces.js';
|
|
23
|
-
import { buildTimestamp, isArchIndependent } from '../utils/ship/payload.js';
|
|
23
|
+
import { assertPayloadMatchesArch, buildTimestamp, isArchIndependent } from '../utils/ship/payload.js';
|
|
24
24
|
import { planOverlay, planStage } from '../utils/ship/plan.js';
|
|
25
25
|
import { renderLauncher } from '../utils/ship/launcher.js';
|
|
26
26
|
import { buildRpm } from '../utils/ship/rpm.js';
|
|
@@ -151,10 +151,15 @@ async function packOne(input) {
|
|
|
151
151
|
hasSchemas: settings.schemaFiles.length > 0,
|
|
152
152
|
extra: settings.extraDepends[format.id],
|
|
153
153
|
typelibPackages: settings.typelibPackages,
|
|
154
|
+
bundledTypelibs: settings.typelibFiles,
|
|
154
155
|
minGjsVersion: settings.minGjsVersion,
|
|
155
156
|
});
|
|
156
157
|
for (const warning of warnAboutGjsFloor(format.id, settings.minGjsVersion))
|
|
157
158
|
console.warn(`${LOG} ${warning}`);
|
|
159
|
+
// Before the label is chosen, not after: `archName` collapses an
|
|
160
|
+
// arch-independent payload to `all`/`noarch`, and a mismatch only matters
|
|
161
|
+
// for a payload that IS architecture-specific.
|
|
162
|
+
assertPayloadMatchesArch(payload, settings.arch);
|
|
158
163
|
const archLabel = format.archName(settings.arch, isArchIndependent(payload));
|
|
159
164
|
const common = { settings, payload, prefix: format.prefix, depends, archLabel, mtime };
|
|
160
165
|
const bytes = format.id === 'deb' ? await buildDeb(common) : await buildRpm(common);
|
package/lib/commands/showcase.js
CHANGED
|
@@ -411,7 +411,7 @@ async function runShowcaseOnRuntime(showcase, runtime, cliVersion) {
|
|
|
411
411
|
// thrown: an exception escaping the handler makes yargs dump the command's
|
|
412
412
|
// full `--help` above the message, which buries it.
|
|
413
413
|
try {
|
|
414
|
-
await runRuntimeBundle(runtime, entry, [], { exitOnSuccess: true });
|
|
414
|
+
await runRuntimeBundle(runtime, entry, [], { completion: 'exit', exitOnSuccess: true });
|
|
415
415
|
}
|
|
416
416
|
catch (err) {
|
|
417
417
|
console.error(`${err.message}\n\n` +
|