@gjsify/cli 0.41.0 → 0.43.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 +1068 -405
- package/lib/actions/build.d.ts +20 -0
- package/lib/actions/build.js +118 -15
- 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.d.ts +2 -0
- package/lib/commands/ship.js +285 -88
- 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 +59 -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/gjs-bundle-guard.d.ts +57 -3
- package/lib/utils/gjs-bundle-guard.js +122 -13
- package/lib/utils/jsx-config.d.ts +45 -0
- package/lib/utils/jsx-config.js +98 -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/onboard-probe.d.ts +23 -0
- package/lib/utils/onboard-probe.js +65 -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/deb.d.ts +3 -3
- package/lib/utils/ship/deb.js +6 -2
- package/lib/utils/ship/depends.d.ts +14 -6
- package/lib/utils/ship/depends.js +45 -4
- package/lib/utils/ship/discover.js +178 -7
- package/lib/utils/ship/formats.js +9 -1
- package/lib/utils/ship/launcher.js +27 -4
- package/lib/utils/ship/mime.d.ts +19 -0
- package/lib/utils/ship/mime.js +89 -0
- package/lib/utils/ship/payload.d.ts +111 -0
- package/lib/utils/ship/payload.js +174 -0
- package/lib/utils/ship/plan.d.ts +30 -1
- package/lib/utils/ship/plan.js +74 -2
- package/lib/utils/ship/rpm.d.ts +3 -3
- package/lib/utils/ship/rpm.js +20 -3
- package/lib/utils/ship/scripts.d.ts +14 -4
- package/lib/utils/ship/scripts.js +26 -8
- package/lib/utils/ship/settings.d.ts +8 -1
- package/lib/utils/ship/settings.js +26 -1
- package/lib/utils/ship/stage-manifest.d.ts +154 -0
- package/lib/utils/ship/stage-manifest.js +408 -0
- package/lib/utils/ship/stage-writer.d.ts +30 -3
- package/lib/utils/ship/stage-writer.js +76 -9
- package/lib/utils/ship/types.d.ts +110 -18
- package/lib/utils/ship/types.js +5 -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
|
@@ -72,6 +72,12 @@ export declare class BuildAction {
|
|
|
72
72
|
* natively, `@gjsify/rolldown-native` via `synthRolldownOutput`), and it
|
|
73
73
|
* cannot be fooled by a `node:` specifier quoted inside a string, which a
|
|
74
74
|
* text scan cannot tell from a statement.
|
|
75
|
+
*
|
|
76
|
+
* The same list also answers the GENERAL form of the question — any bare
|
|
77
|
+
* specifier GJS cannot resolve, `node:` or not — which is why the target's
|
|
78
|
+
* externals predicate and the emitted fileNames are handed over: they are
|
|
79
|
+
* what separates a declared external and a sibling chunk from a dependency
|
|
80
|
+
* that simply is not there.
|
|
75
81
|
*/
|
|
76
82
|
private assertGjsOutputLoadable;
|
|
77
83
|
/**
|
|
@@ -166,11 +172,25 @@ export declare class BuildAction {
|
|
|
166
172
|
*/
|
|
167
173
|
globals?: string;
|
|
168
174
|
excludeGlobals?: string[];
|
|
175
|
+
/**
|
|
176
|
+
* Allow a `@gjsify/*` this project cannot resolve to come from the copy
|
|
177
|
+
* installed beside the RUNNING CLI.
|
|
178
|
+
*
|
|
179
|
+
* OPT-IN PER CALLER, never inferred here, because this helper does NOT
|
|
180
|
+
* only bundle toolchain — the `node` shim `gjsify run` puts on the PATH
|
|
181
|
+
* routes any project's `node <file>` through `utils/node-script.ts` and
|
|
182
|
+
* into this same function, a consumer's `"start": "node ./src/main.mjs"`
|
|
183
|
+
* included. Each caller states what it is bundling and why that earns the
|
|
184
|
+
* fallback; `gjsify build` sets nothing, so a user's missing dependency
|
|
185
|
+
* stays fatal there.
|
|
186
|
+
*/
|
|
187
|
+
resolveFromToolchain?: boolean;
|
|
169
188
|
}): Promise<string>;
|
|
170
189
|
/** Application mode */
|
|
171
190
|
buildApp(app?: App, opts?: {
|
|
172
191
|
watch?: boolean;
|
|
173
192
|
preserveDefaultExport?: boolean;
|
|
193
|
+
toolchainAnchor?: string;
|
|
174
194
|
}): Promise<RolldownOutput[]>;
|
|
175
195
|
/**
|
|
176
196
|
* Drive `rolldown.watch(...)`: rebuild on source change, apply the
|
package/lib/actions/build.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { runBundle, runWatch, bundleToChunks } from '../bundler-pick.js';
|
|
2
|
-
import { gjsifyPlugin, textLoaderPlugin, resolveShebangLine, NODE_SHEBANG } from '@gjsify/rolldown-plugin-gjsify';
|
|
2
|
+
import { gjsifyPlugin, textLoaderPlugin, resolveShebangLine, NODE_SHEBANG, createGjsExternalsPredicate, globToEntryPoints, } from '@gjsify/rolldown-plugin-gjsify';
|
|
3
3
|
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
4
4
|
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
|
-
import { assertGjsBundleLoadable } from '../utils/gjs-bundle-guard.js';
|
|
13
|
+
import { assertGjsBundleLoadable, assertGjsBundleParses } from '../utils/gjs-bundle-guard.js';
|
|
14
|
+
import { collectEntryPaths, describeJsxConfig, findJsxEntryPoint, jsxConfigMissingError } from '../utils/jsx-config.js';
|
|
15
|
+
import { giSystemProbes } from '../utils/gi-runtime-paths.js';
|
|
14
16
|
import { escapeRawNulForGjs } from '../utils/gjs-source-escape.js';
|
|
15
17
|
import { assertNodeBundleGlobalsShimmed } from '../utils/node-bundle-guard.js';
|
|
16
18
|
const DEFAULT_GJS_SHEBANG = '#!/usr/bin/env -S gjs -m';
|
|
@@ -78,17 +80,41 @@ async function newestLockfileMtime(cwd) {
|
|
|
78
80
|
}
|
|
79
81
|
return newest;
|
|
80
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Absolute path of the CLI module that is running, or `null` when it did not come
|
|
85
|
+
* from a `file:` URL.
|
|
86
|
+
*
|
|
87
|
+
* The conversion happens ONCE, here, inside a catch. A CLI loaded from a non-`file:`
|
|
88
|
+
* origin — the bundled/Flatpak shape `commands/tsc.ts` names as the reason its second
|
|
89
|
+
* anchor exists — would otherwise make `fileURLToPath` throw on the path that loads
|
|
90
|
+
* every config file and every plugin, not merely lose the fallback.
|
|
91
|
+
*/
|
|
92
|
+
function runningCliFile() {
|
|
93
|
+
try {
|
|
94
|
+
return fileURLToPath(import.meta.url);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
81
100
|
/**
|
|
82
101
|
* Fresh when the cached GJS bundle is newer than BOTH the source file AND the
|
|
83
102
|
* project's lockfile (the dep-change signal). Missing bundle/source → not fresh
|
|
84
103
|
* (rebuild). Shared by the plugin + config GJS-bundle caches.
|
|
104
|
+
*
|
|
105
|
+
* `anchorFile` is the running CLI, and only set when this bundle was allowed the
|
|
106
|
+
* toolchain fallback. Such a bundle can carry code from the CLI's OWN install,
|
|
107
|
+
* which neither the entry mtime nor the project lockfile tracks — so after a
|
|
108
|
+
* `gjsify self-update` a cached config/plugin bundle holding the previous CLI's
|
|
109
|
+
* `@gjsify/*` would otherwise stay "fresh" forever.
|
|
85
110
|
*/
|
|
86
|
-
async function isBundleFresh(outfile, sourcePath, cwd) {
|
|
111
|
+
async function isBundleFresh(outfile, sourcePath, cwd, anchorFile) {
|
|
87
112
|
try {
|
|
88
113
|
const outStat = await stat(outfile);
|
|
89
114
|
const srcStat = await stat(sourcePath);
|
|
90
115
|
const depMtime = await newestLockfileMtime(cwd);
|
|
91
|
-
|
|
116
|
+
const anchorMtime = anchorFile === undefined ? 0 : (await stat(anchorFile)).mtimeMs;
|
|
117
|
+
return outStat.mtimeMs >= Math.max(srcStat.mtimeMs, depMtime, anchorMtime);
|
|
92
118
|
}
|
|
93
119
|
catch {
|
|
94
120
|
return false;
|
|
@@ -124,6 +150,21 @@ export class BuildAction {
|
|
|
124
150
|
const multipleBuilds = moduleOutdir && mainOutdir && moduleOutdir !== mainOutdir;
|
|
125
151
|
const pnp = await buildPnpPlugin();
|
|
126
152
|
const pnpPlugins = pnp ? [pnp] : [];
|
|
153
|
+
// The same chain `buildApp` assembles, and it was missing here entirely:
|
|
154
|
+
// a library build resolved NO user plugin and NO user text loader, then
|
|
155
|
+
// emitted the default transform at exit 0. Configuring
|
|
156
|
+
// `gjsify.bundler.plugins` and watching it do nothing is the repo's most
|
|
157
|
+
// expensive failure shape, and `@gjsify/gtk-host` itself builds with
|
|
158
|
+
// `--library` — which is where it was found.
|
|
159
|
+
const userTextLoader = textLoaderPlugin({ loaders: this.configData.loaders });
|
|
160
|
+
const userPlugins = userTextLoader ? [userTextLoader] : [];
|
|
161
|
+
if (userBundler.plugins?.length) {
|
|
162
|
+
// Under GJS a plugin module cannot always be imported directly — the
|
|
163
|
+
// native ESM loader does not follow `package.json#exports` — so the
|
|
164
|
+
// same bundle-then-import escape the app path uses is passed in here.
|
|
165
|
+
const resolved = await resolveUserPlugins(userBundler.plugins, process.cwd(), isGjs() ? { loadModule: (pth, name) => this.loadPluginViaGjsBundle(pth, name, verbose) } : {});
|
|
166
|
+
userPlugins.push(...resolved);
|
|
167
|
+
}
|
|
127
168
|
const results = [];
|
|
128
169
|
if (multipleBuilds) {
|
|
129
170
|
// Both outdirs are derived from module/main — always guard them
|
|
@@ -143,6 +184,7 @@ export class BuildAction {
|
|
|
143
184
|
output: { dir: moduleOutdir },
|
|
144
185
|
userAliases: aliases,
|
|
145
186
|
pnpPlugins,
|
|
187
|
+
userPlugins,
|
|
146
188
|
}));
|
|
147
189
|
const mainFormat = mainOutdir.includes('/cjs') || mainOutExt === '.cjs' ? 'cjs' : 'esm';
|
|
148
190
|
results.push(await runOneLibraryBuild({
|
|
@@ -157,6 +199,7 @@ export class BuildAction {
|
|
|
157
199
|
output: { dir: mainOutdir },
|
|
158
200
|
userAliases: aliases,
|
|
159
201
|
pnpPlugins,
|
|
202
|
+
userPlugins,
|
|
160
203
|
}));
|
|
161
204
|
}
|
|
162
205
|
else {
|
|
@@ -182,6 +225,7 @@ export class BuildAction {
|
|
|
182
225
|
output: { dir: outdir },
|
|
183
226
|
userAliases: aliases,
|
|
184
227
|
pnpPlugins,
|
|
228
|
+
userPlugins,
|
|
185
229
|
}));
|
|
186
230
|
}
|
|
187
231
|
return results;
|
|
@@ -350,10 +394,22 @@ export class BuildAction {
|
|
|
350
394
|
* natively, `@gjsify/rolldown-native` via `synthRolldownOutput`), and it
|
|
351
395
|
* cannot be fooled by a `node:` specifier quoted inside a string, which a
|
|
352
396
|
* text scan cannot tell from a statement.
|
|
397
|
+
*
|
|
398
|
+
* The same list also answers the GENERAL form of the question — any bare
|
|
399
|
+
* specifier GJS cannot resolve, `node:` or not — which is why the target's
|
|
400
|
+
* externals predicate and the emitted fileNames are handed over: they are
|
|
401
|
+
* what separates a declared external and a sibling chunk from a dependency
|
|
402
|
+
* that simply is not there.
|
|
353
403
|
*/
|
|
354
|
-
assertGjsOutputLoadable(result, outfile, outdir) {
|
|
404
|
+
assertGjsOutputLoadable(result, outfile, outdir, userExternal) {
|
|
355
405
|
const chunks = (result.output ?? []).filter((item) => item.type === 'chunk');
|
|
356
|
-
assertGjsBundleLoadable(chunks, outfile ?? outdir ?? 'the GJS bundle'
|
|
406
|
+
assertGjsBundleLoadable(chunks, outfile ?? outdir ?? 'the GJS bundle', {
|
|
407
|
+
// The target's OWN predicate, not a second list: `externalsPlugin` enforces
|
|
408
|
+
// this exact one, so a specifier the build let through as external cannot be
|
|
409
|
+
// reported as unresolvable here.
|
|
410
|
+
isExternal: createGjsExternalsPredicate(userExternal ?? []),
|
|
411
|
+
emitted: (result.output ?? []).map((item) => item.fileName),
|
|
412
|
+
});
|
|
357
413
|
}
|
|
358
414
|
/**
|
|
359
415
|
* Post-bundle gate for `--app node`: throw when the emitted bundle still
|
|
@@ -433,6 +489,9 @@ export class BuildAction {
|
|
|
433
489
|
label: pluginName,
|
|
434
490
|
verbose,
|
|
435
491
|
noCacheEnv: 'GJSIFY_NO_PLUGIN_CACHE',
|
|
492
|
+
// A bundler plugin runs inside the CLI process — toolchain by
|
|
493
|
+
// construction, not by where its file happens to sit.
|
|
494
|
+
resolveFromToolchain: true,
|
|
436
495
|
});
|
|
437
496
|
}
|
|
438
497
|
catch (err) {
|
|
@@ -471,9 +530,13 @@ export class BuildAction {
|
|
|
471
530
|
// policies is two artifacts, and the freshness check below (entry mtime + lockfile)
|
|
472
531
|
// cannot see the `package.json` edit that changed one. Callers without a policy keep
|
|
473
532
|
// their existing cache filenames.
|
|
474
|
-
|
|
533
|
+
// The anchor belongs in the key for the same reason: with it the artifact may
|
|
534
|
+
// contain code from a DIFFERENT install than without it, and two CLIs on one
|
|
535
|
+
// machine resolve to two different paths.
|
|
536
|
+
const anchorFile = opts.resolveFromToolchain === true ? runningCliFile() : null;
|
|
537
|
+
const cacheKey = opts.globals === undefined && opts.excludeGlobals === undefined && anchorFile === null
|
|
475
538
|
? inputPath
|
|
476
|
-
: `${inputPath} ${JSON.stringify([opts.globals ?? null, opts.excludeGlobals ?? null])}`;
|
|
539
|
+
: `${inputPath} ${JSON.stringify([opts.globals ?? null, opts.excludeGlobals ?? null, anchorFile])}`;
|
|
477
540
|
// THE HASH GOES IN THE DIRECTORY, THE FILE KEEPS THE SOURCE'S NAME. As
|
|
478
541
|
// `<name>-<hash>.mjs` it broke every script guarding its body with the standard
|
|
479
542
|
// is-entry test: `audit-runtimes.mjs` asks whether `process.argv[1]` ENDS WITH its
|
|
@@ -482,7 +545,7 @@ export class BuildAction {
|
|
|
482
545
|
// does — and a name is cheaper to keep right than an argv to rewrite.
|
|
483
546
|
const outfile = join(cacheDir, `${safeName}-${shortHash(cacheKey)}`, `${basename(inputPath).replace(/\.[cm]?[jt]sx?$/i, '')}.mjs`);
|
|
484
547
|
const cacheDisabled = opts.cache === false || (opts.noCacheEnv ? isTruthyEnv(process.env[opts.noCacheEnv]) : false);
|
|
485
|
-
if (!cacheDisabled && (await isBundleFresh(outfile, inputPath, cwd))) {
|
|
548
|
+
if (!cacheDisabled && (await isBundleFresh(outfile, inputPath, cwd, anchorFile ?? undefined))) {
|
|
486
549
|
if (opts.verbose)
|
|
487
550
|
console.debug(`[gjsify] reusing cached GJS bundle ${outfile}`);
|
|
488
551
|
return outfile;
|
|
@@ -499,13 +562,34 @@ export class BuildAction {
|
|
|
499
562
|
output: { file: outfile },
|
|
500
563
|
...(opts.define ? { transform: { define: opts.define } } : {}),
|
|
501
564
|
},
|
|
502
|
-
}).buildApp('gjs', {
|
|
565
|
+
}).buildApp('gjs', {
|
|
566
|
+
preserveDefaultExport: opts.preserveDefaultExport ?? true,
|
|
567
|
+
// See `opts.resolveFromToolchain`: the CALLER decides whether this input is
|
|
568
|
+
// toolchain, because this helper alone cannot tell a config file from a
|
|
569
|
+
// consumer's `node ./src/main.mjs`.
|
|
570
|
+
...(anchorFile === null ? {} : { toolchainAnchor: anchorFile }),
|
|
571
|
+
});
|
|
503
572
|
return outfile;
|
|
504
573
|
}
|
|
505
574
|
/** Application mode */
|
|
506
575
|
async buildApp(app = 'gjs', opts = {}) {
|
|
507
576
|
const { verbose, typescript, exclude, library: pkg, aliases, excludeGlobals } = this.configData;
|
|
508
577
|
const userBundler = normalizeBundlerOptions(this.configData);
|
|
578
|
+
// Refuse a JSX entry with no JSX policy BEFORE bundling anything — see
|
|
579
|
+
// `utils/jsx-config.ts` for what the unset default emits and why it can never be
|
|
580
|
+
// right for this target. The glob expansion is paid only on the failing branch:
|
|
581
|
+
// when JSX is configured there is nothing to name.
|
|
582
|
+
const jsxPolicy = describeJsxConfig({
|
|
583
|
+
transformJsx: userBundler.transform?.jsx,
|
|
584
|
+
tsconfigJsx: typescript?.compilerOptions?.jsx,
|
|
585
|
+
tsconfigJsxImportSource: typescript?.compilerOptions?.jsxImportSource,
|
|
586
|
+
});
|
|
587
|
+
if (app === 'gjs' && !jsxPolicy.configured) {
|
|
588
|
+
const entries = collectEntryPaths(await globToEntryPoints(userBundler.input, exclude));
|
|
589
|
+
const jsxEntry = findJsxEntryPoint(entries);
|
|
590
|
+
if (jsxEntry !== undefined)
|
|
591
|
+
throw jsxConfigMissingError(jsxEntry);
|
|
592
|
+
}
|
|
509
593
|
// --- `--app browser` HTML entry (Vite-style) ---
|
|
510
594
|
// When the entry is an `index.html`, bundle the module its
|
|
511
595
|
// `<script type="module" src>` references (rewrite `input` to that
|
|
@@ -558,6 +642,7 @@ export class BuildAction {
|
|
|
558
642
|
consoleShim,
|
|
559
643
|
...(aliases ? { aliases } : {}),
|
|
560
644
|
...(opts.preserveDefaultExport ? { preserveDefaultExport: true } : {}),
|
|
645
|
+
...(opts.toolchainAnchor !== undefined ? { toolchainAnchor: opts.toolchainAnchor } : {}),
|
|
561
646
|
};
|
|
562
647
|
const { autoMode, extras } = this.parseGlobalsValue(globals);
|
|
563
648
|
const pnp = await buildPnpPlugin();
|
|
@@ -682,6 +767,13 @@ export class BuildAction {
|
|
|
682
767
|
else if (extras) {
|
|
683
768
|
pluginOpts.autoGlobalsInject = await this.resolveGlobalsInject(app, extras, verbose);
|
|
684
769
|
}
|
|
770
|
+
// Fills the byte-1 GI prologue — built across #1152/#1160/#1026 and called by
|
|
771
|
+
// nothing, so every `--app gjs` bundle ever built shipped the empty string
|
|
772
|
+
// `giRuntimePathsStub` returns for an empty input. Only the FINAL build: the
|
|
773
|
+
// two analysis passes above bundle in memory to be PARSED for free globals,
|
|
774
|
+
// never written and never run, so a prologue there is work whose result is
|
|
775
|
+
// discarded.
|
|
776
|
+
const giProbes = app === 'gjs' ? giSystemProbes() : undefined;
|
|
685
777
|
// Final build: orchestrator → rolldown → write
|
|
686
778
|
const cfg = await gjsifyPlugin({
|
|
687
779
|
input: userBundler.input,
|
|
@@ -690,6 +782,7 @@ export class BuildAction {
|
|
|
690
782
|
userBanner,
|
|
691
783
|
userAliases: aliases,
|
|
692
784
|
shebang: this.configData.shebang,
|
|
785
|
+
giSystemProbes: giProbes,
|
|
693
786
|
}, pluginOpts);
|
|
694
787
|
const merged = mergeBundlerOptions(cfg.options, userBundler);
|
|
695
788
|
const finalOpts = {
|
|
@@ -699,7 +792,7 @@ export class BuildAction {
|
|
|
699
792
|
// and appended into `userPlugins` above. Re-emitting the raw
|
|
700
793
|
// entries (which may include `BundlerPluginByName` shapes
|
|
701
794
|
// Rolldown doesn't understand) would crash the build.
|
|
702
|
-
plugins: [...pnpPlugins, ...userPlugins, ...cfg.plugins],
|
|
795
|
+
plugins: [...pnpPlugins, ...cfg.prePlugins, ...userPlugins, ...cfg.plugins],
|
|
703
796
|
};
|
|
704
797
|
if (opts.watch) {
|
|
705
798
|
await this.runWatchLoop(finalOpts, app, outfile, outdir, verbose);
|
|
@@ -715,9 +808,15 @@ export class BuildAction {
|
|
|
715
808
|
if ((app === 'gjs' || app === 'node') && this.configData.shebang) {
|
|
716
809
|
await this.applyShebang(app, outfile, verbose);
|
|
717
810
|
}
|
|
718
|
-
// Refuse to hand back a `--app gjs` bundle stock GJS cannot even load
|
|
811
|
+
// Refuse to hand back a `--app gjs` bundle stock GJS cannot even load: one that
|
|
812
|
+
// imports what the runtime cannot resolve, or — when the transform was told to
|
|
813
|
+
// keep JSX — one that is not parseable JavaScript at all.
|
|
719
814
|
if (app === 'gjs') {
|
|
720
|
-
this.assertGjsOutputLoadable(writeResult, outfile, outdir);
|
|
815
|
+
this.assertGjsOutputLoadable(writeResult, outfile, outdir, userExternal);
|
|
816
|
+
if (jsxPolicy.preserves) {
|
|
817
|
+
const chunks = (writeResult.output ?? []).filter((item) => item.type === 'chunk');
|
|
818
|
+
assertGjsBundleParses(chunks, outfile ?? outdir ?? 'the GJS bundle');
|
|
819
|
+
}
|
|
721
820
|
}
|
|
722
821
|
// Refuse to hand back a `--app node` bundle that reaches for GJS ambient
|
|
723
822
|
// globals nothing seeds. Only meaningful when the shim was NOT injected:
|
|
@@ -835,7 +934,11 @@ async function runOneLibraryBuild(args) {
|
|
|
835
934
|
const merged = mergeBundlerOptions(cfg.options, args.userBundler);
|
|
836
935
|
const finalOpts = {
|
|
837
936
|
...merged,
|
|
838
|
-
|
|
937
|
+
// Same order as the app path: PnP resolves, then the user transforms,
|
|
938
|
+
// then gjsify's own chain. `merged.plugins` is deliberately NOT spread —
|
|
939
|
+
// the user's entries arrive RESOLVED, in `userPlugins`, exactly as
|
|
940
|
+
// `buildApp` passes them.
|
|
941
|
+
plugins: [...args.pnpPlugins, ...cfg.prePlugins, ...args.userPlugins, ...cfg.plugins],
|
|
839
942
|
};
|
|
840
943
|
return await runBundle(finalOpts);
|
|
841
944
|
}
|
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