@gjsify/cli 0.16.1 → 0.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.gjs.mjs +45 -45
- package/lib/actions/build.d.ts +8 -0
- package/lib/actions/build.js +54 -1
- package/lib/commands/run.js +9 -0
- package/lib/utils/html-entry.d.ts +46 -0
- package/lib/utils/html-entry.js +89 -0
- package/package.json +17 -17
package/lib/actions/build.d.ts
CHANGED
|
@@ -37,6 +37,14 @@ export declare class BuildAction {
|
|
|
37
37
|
* there is no in-bundle plugin, so this hook is the sole injection point.
|
|
38
38
|
*/
|
|
39
39
|
private applyShebang;
|
|
40
|
+
/**
|
|
41
|
+
* Post-bundle step for a `--app browser` HTML entry: write the processed
|
|
42
|
+
* `index.html` beside the JS bundle, with the entry `<script>`'s `src`
|
|
43
|
+
* rewritten to point at the built bundle. Mirrors `applyShebang` — kept in
|
|
44
|
+
* the CLI (not a Rolldown plugin) because the native rolldown engine
|
|
45
|
+
* exposes no `emitFile`.
|
|
46
|
+
*/
|
|
47
|
+
private applyBrowserHtml;
|
|
40
48
|
/**
|
|
41
49
|
* GJS plugin loader — bundle the resolved plugin module to a single
|
|
42
50
|
* self-contained ESM file, then import that. Works around GJS's native
|
package/lib/actions/build.js
CHANGED
|
@@ -4,11 +4,12 @@ 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
|
-
import { dirname, extname, join } from 'node:path';
|
|
7
|
+
import { dirname, extname, join, resolve } from 'node:path';
|
|
8
8
|
import { pathToFileURL } from 'node:url';
|
|
9
9
|
import { chmod, mkdir, 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
|
+
import { detectHtmlEntry, parseHtmlEntry, emitBrowserHtml, htmlOutPathFor } from '../utils/html-entry.js';
|
|
12
13
|
const DEFAULT_GJS_SHEBANG = '#!/usr/bin/env -S gjs -m';
|
|
13
14
|
/**
|
|
14
15
|
* `true` when `path` points at a location that's unsafe to use as a build
|
|
@@ -241,6 +242,28 @@ export class BuildAction {
|
|
|
241
242
|
if (verbose)
|
|
242
243
|
console.debug(`[gjsify] --shebang: wrote ${line} + chmod 0o755 to ${outfile}`);
|
|
243
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Post-bundle step for a `--app browser` HTML entry: write the processed
|
|
247
|
+
* `index.html` beside the JS bundle, with the entry `<script>`'s `src`
|
|
248
|
+
* rewritten to point at the built bundle. Mirrors `applyShebang` — kept in
|
|
249
|
+
* the CLI (not a Rolldown plugin) because the native rolldown engine
|
|
250
|
+
* exposes no `emitFile`.
|
|
251
|
+
*/
|
|
252
|
+
async applyBrowserHtml(htmlEntry, outfile, verbose) {
|
|
253
|
+
const jsOutPath = resolve(outfile);
|
|
254
|
+
const outHtmlPath = htmlOutPathFor(jsOutPath);
|
|
255
|
+
const html = emitBrowserHtml({
|
|
256
|
+
htmlSource: htmlEntry.htmlSource,
|
|
257
|
+
scriptTag: htmlEntry.scriptTag,
|
|
258
|
+
scriptSrc: htmlEntry.scriptSrc,
|
|
259
|
+
jsOutPath,
|
|
260
|
+
outHtmlPath,
|
|
261
|
+
});
|
|
262
|
+
await mkdir(dirname(outHtmlPath), { recursive: true });
|
|
263
|
+
await writeFile(outHtmlPath, html);
|
|
264
|
+
if (verbose)
|
|
265
|
+
console.debug(`[gjsify] --app browser: wrote ${outHtmlPath}`);
|
|
266
|
+
}
|
|
244
267
|
/**
|
|
245
268
|
* GJS plugin loader — bundle the resolved plugin module to a single
|
|
246
269
|
* self-contained ESM file, then import that. Works around GJS's native
|
|
@@ -329,6 +352,23 @@ export class BuildAction {
|
|
|
329
352
|
async buildApp(app = 'gjs', opts = {}) {
|
|
330
353
|
const { verbose, typescript, exclude, library: pkg, aliases, excludeGlobals } = this.configData;
|
|
331
354
|
const userBundler = normalizeBundlerOptions(this.configData);
|
|
355
|
+
// --- `--app browser` HTML entry (Vite-style) ---
|
|
356
|
+
// When the entry is an `index.html`, bundle the module its
|
|
357
|
+
// `<script type="module" src>` references (rewrite `input` to that
|
|
358
|
+
// module so auto-globals detection + the bundle both see the real
|
|
359
|
+
// entry, NOT the html) and remember the page so a post-bundle step can
|
|
360
|
+
// emit a processed `index.html` next to the JS. Guarded on browser +
|
|
361
|
+
// an html input; every other build path is untouched.
|
|
362
|
+
let htmlEntry = null;
|
|
363
|
+
if (app === 'browser') {
|
|
364
|
+
const htmlPath = detectHtmlEntry(userBundler.input);
|
|
365
|
+
if (htmlPath) {
|
|
366
|
+
const htmlSource = await readFile(htmlPath, 'utf-8');
|
|
367
|
+
const { moduleEntry, scriptTag, scriptSrc } = parseHtmlEntry(htmlPath, htmlSource);
|
|
368
|
+
userBundler.input = moduleEntry;
|
|
369
|
+
htmlEntry = { htmlSource, scriptTag, scriptSrc };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
332
372
|
const formatRaw = userBundler.output?.format ??
|
|
333
373
|
(userBundler.output?.file?.endsWith('.cjs') ? 'cjs' : 'esm');
|
|
334
374
|
// The orchestrator only handles esm/cjs (iife is not a GJS / Node /
|
|
@@ -346,6 +386,12 @@ export class BuildAction {
|
|
|
346
386
|
}
|
|
347
387
|
outfile = candidate;
|
|
348
388
|
}
|
|
389
|
+
// An html entry emits its `index.html` next to the JS bundle, so it
|
|
390
|
+
// needs a concrete outfile to anchor that sibling path.
|
|
391
|
+
if (htmlEntry && !outfile) {
|
|
392
|
+
throw new Error('gjsify build: an .html entry needs an explicit --outfile ' +
|
|
393
|
+
'(e.g. --outfile dist-app/app.js); the emitted index.html is written beside it.');
|
|
394
|
+
}
|
|
349
395
|
const { consoleShim, globals } = this.configData;
|
|
350
396
|
const userExternal = Array.isArray(userBundler.external) ? userBundler.external : undefined;
|
|
351
397
|
const userBanner = typeof userBundler.output?.banner === 'string' ? userBundler.output.banner : undefined;
|
|
@@ -485,6 +531,13 @@ export class BuildAction {
|
|
|
485
531
|
if ((app === 'gjs' || app === 'node') && this.configData.shebang) {
|
|
486
532
|
await this.applyShebang(app, outfile, verbose);
|
|
487
533
|
}
|
|
534
|
+
// Browser HTML entry: emit the processed `index.html` beside the bundle
|
|
535
|
+
// (engine-portable post-step — the native rolldown engine has no
|
|
536
|
+
// `emitFile`, so this can't live in a plugin). `outfile` is guaranteed
|
|
537
|
+
// present here (guarded above when `htmlEntry` is set).
|
|
538
|
+
if (app === 'browser' && htmlEntry && outfile) {
|
|
539
|
+
await this.applyBrowserHtml(htmlEntry, outfile, verbose);
|
|
540
|
+
}
|
|
488
541
|
return [writeResult];
|
|
489
542
|
}
|
|
490
543
|
/**
|
package/lib/commands/run.js
CHANGED
|
@@ -188,6 +188,15 @@ async function runScript(script, extraArgs) {
|
|
|
188
188
|
// (e.g. a failing check that sets the code instead of exiting) —
|
|
189
189
|
// a bare exit(0) here masked such failures as success.
|
|
190
190
|
process.exit(process.exitCode != null ? Number(process.exitCode) : 0);
|
|
191
|
+
// `process.exit()` under GJS is NOT synchronous — with the GLib main
|
|
192
|
+
// loop armed (`ensureMainLoop`, e.g. by a bundle the in-process dispatch
|
|
193
|
+
// just ran) it defers the actual teardown, so execution CONTINUES past
|
|
194
|
+
// the call. Without this `return` the function falls through to the
|
|
195
|
+
// shell-spawn fallback below and runs the SAME command a SECOND time
|
|
196
|
+
// (the double-execution bug). Mirrors the file-mode path's `return`
|
|
197
|
+
// after `runGjsBundle`. Keep both — the return is the real guard, the
|
|
198
|
+
// exit sets the status code for when the loop finally unwinds.
|
|
199
|
+
return;
|
|
191
200
|
}
|
|
192
201
|
const fullCmd = extraArgs.length > 0 ? `${literal} ${extraArgs.map(shellEscape).join(' ')}` : literal;
|
|
193
202
|
// ensureMainLoop() (called inside spawn) keeps GJS alive after the
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { RolldownOptions } from 'rolldown';
|
|
2
|
+
export interface ParsedHtmlEntry {
|
|
3
|
+
/** Absolute path to the module the `<script type="module" src>` references. */
|
|
4
|
+
moduleEntry: string;
|
|
5
|
+
/** The full matched `<script …></script>`/`<script … />` tag text. */
|
|
6
|
+
scriptTag: string;
|
|
7
|
+
/** The raw `src` attribute value from that tag (as authored in the HTML). */
|
|
8
|
+
scriptSrc: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Return the `.html` path when `input` is a SINGLE html entry (a literal
|
|
12
|
+
* `.html` string, a 1-element array of one, or a 1-key record whose value is
|
|
13
|
+
* one) — else `null`. Multi-entry inputs and non-html entries (incl. globs
|
|
14
|
+
* like `src/**\/*.ts`) are never treated as html. The path is returned as
|
|
15
|
+
* authored (relative or absolute); the caller resolves + reads it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function detectHtmlEntry(input: RolldownOptions['input'] | undefined): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Find the FIRST `<script type="module" src="…">` whose `src` is a local
|
|
20
|
+
* (non-`http(s)://`, non-`//`) path and resolve the referenced module against
|
|
21
|
+
* the HTML file's directory. Throws when no such tag exists.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseHtmlEntry(htmlPath: string, htmlSource: string): ParsedHtmlEntry;
|
|
24
|
+
export interface EmitBrowserHtmlInput {
|
|
25
|
+
/** The original HTML source. */
|
|
26
|
+
htmlSource: string;
|
|
27
|
+
/** The `<script>` tag matched by {@link parseHtmlEntry}. */
|
|
28
|
+
scriptTag: string;
|
|
29
|
+
/** That tag's original `src` value. */
|
|
30
|
+
scriptSrc: string;
|
|
31
|
+
/** Absolute path to the built JS bundle the emitted page should load. */
|
|
32
|
+
jsOutPath: string;
|
|
33
|
+
/** Absolute path the emitted HTML will be written to. */
|
|
34
|
+
outHtmlPath: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Produce the emitted HTML: the original source with the entry script's `src`
|
|
38
|
+
* rewritten to point at the built JS bundle, relative to the output HTML's own
|
|
39
|
+
* directory (so the page is portable regardless of where `dist/` is served
|
|
40
|
+
* from). Everything else — `<title>`, meta tags, other markup — is preserved.
|
|
41
|
+
*/
|
|
42
|
+
export declare function emitBrowserHtml(input: EmitBrowserHtmlInput): string;
|
|
43
|
+
/** Convenience: the emitted HTML filename for a given JS outfile (sibling `index.html`). */
|
|
44
|
+
export declare function htmlOutPathFor(jsOutPath: string): string;
|
|
45
|
+
/** Derive a JS outfile from an outdir + the module entry basename (`<outdir>/<name>.js`). */
|
|
46
|
+
export declare function jsOutPathForOutdir(outdir: string, moduleEntry: string): string;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// HTML-entry support for `gjsify build --app browser`.
|
|
2
|
+
//
|
|
3
|
+
// Vite treats `index.html` as the build entry: it finds the
|
|
4
|
+
// `<script type="module" src="...">`, bundles that module, and emits a
|
|
5
|
+
// processed HTML with the script pointing at the built asset. `gjsify build`
|
|
6
|
+
// only ever took a `.ts`/`.js` entry + `--outfile`, so browser apps hand-wrote
|
|
7
|
+
// their shell page (the `write-app-html.mjs` pattern). This util brings the
|
|
8
|
+
// Vite behaviour to the production Rolldown build.
|
|
9
|
+
//
|
|
10
|
+
// HTML emission MUST run as a CLI post-bundle step (mirroring `applyShebang`),
|
|
11
|
+
// NOT as a Rolldown plugin `emitFile`: the native `@gjsify/rolldown-native`
|
|
12
|
+
// engine implements no `emitFile`, so a plugin-based emit would work on the
|
|
13
|
+
// npm engine (Node) but throw under the GJS-bundled CLI. Parsing is regex-based
|
|
14
|
+
// and tolerant on purpose — this replaces a ~25-line hand-written script, not a
|
|
15
|
+
// full HTML pipeline; CSS is NOT extracted here (it travels in the JS bundle as
|
|
16
|
+
// a string via css-as-string and is injected by the app — the unified pattern
|
|
17
|
+
// shared with the `--app gjs` target).
|
|
18
|
+
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
19
|
+
/**
|
|
20
|
+
* Return the `.html` path when `input` is a SINGLE html entry (a literal
|
|
21
|
+
* `.html` string, a 1-element array of one, or a 1-key record whose value is
|
|
22
|
+
* one) — else `null`. Multi-entry inputs and non-html entries (incl. globs
|
|
23
|
+
* like `src/**\/*.ts`) are never treated as html. The path is returned as
|
|
24
|
+
* authored (relative or absolute); the caller resolves + reads it.
|
|
25
|
+
*/
|
|
26
|
+
export function detectHtmlEntry(input) {
|
|
27
|
+
if (!input)
|
|
28
|
+
return null;
|
|
29
|
+
const isHtml = (v) => typeof v === 'string' && /\.html?$/i.test(v);
|
|
30
|
+
if (typeof input === 'string')
|
|
31
|
+
return isHtml(input) ? input : null;
|
|
32
|
+
if (Array.isArray(input)) {
|
|
33
|
+
return input.length === 1 && isHtml(input[0]) ? input[0] : null;
|
|
34
|
+
}
|
|
35
|
+
// Record<name, path> form.
|
|
36
|
+
if (typeof input === 'object') {
|
|
37
|
+
const values = Object.values(input);
|
|
38
|
+
return values.length === 1 && isHtml(values[0]) ? values[0] : null;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const SCRIPT_MODULE_RE = /<script\b[^>]*\btype\s*=\s*["']module["'][^>]*>(?:<\/script>)?/gi;
|
|
43
|
+
const SRC_ATTR_RE = /\bsrc\s*=\s*["']([^"']+)["']/i;
|
|
44
|
+
const EXTERNAL_SRC_RE = /^(?:https?:)?\/\//i;
|
|
45
|
+
/**
|
|
46
|
+
* Find the FIRST `<script type="module" src="…">` whose `src` is a local
|
|
47
|
+
* (non-`http(s)://`, non-`//`) path and resolve the referenced module against
|
|
48
|
+
* the HTML file's directory. Throws when no such tag exists.
|
|
49
|
+
*/
|
|
50
|
+
export function parseHtmlEntry(htmlPath, htmlSource) {
|
|
51
|
+
for (const match of htmlSource.matchAll(SCRIPT_MODULE_RE)) {
|
|
52
|
+
const scriptTag = match[0];
|
|
53
|
+
const srcMatch = SRC_ATTR_RE.exec(scriptTag);
|
|
54
|
+
if (!srcMatch)
|
|
55
|
+
continue; // inline module script — not an entry
|
|
56
|
+
const scriptSrc = srcMatch[1];
|
|
57
|
+
if (EXTERNAL_SRC_RE.test(scriptSrc))
|
|
58
|
+
continue; // externally hosted — leave alone
|
|
59
|
+
const moduleEntry = isAbsolute(scriptSrc) ? scriptSrc : resolve(dirname(htmlPath), scriptSrc);
|
|
60
|
+
return { moduleEntry, scriptTag, scriptSrc };
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`gjsify build: ${htmlPath} has no local <script type="module" src="…"> to bundle. ` +
|
|
63
|
+
`Add one pointing at your app entry (e.g. <script type="module" src="./main.ts">).`);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Produce the emitted HTML: the original source with the entry script's `src`
|
|
67
|
+
* rewritten to point at the built JS bundle, relative to the output HTML's own
|
|
68
|
+
* directory (so the page is portable regardless of where `dist/` is served
|
|
69
|
+
* from). Everything else — `<title>`, meta tags, other markup — is preserved.
|
|
70
|
+
*/
|
|
71
|
+
export function emitBrowserHtml(input) {
|
|
72
|
+
const { htmlSource, scriptTag, scriptSrc, jsOutPath, outHtmlPath } = input;
|
|
73
|
+
let rel = relative(dirname(outHtmlPath), jsOutPath).split(/[\\/]/).join('/');
|
|
74
|
+
if (!rel.startsWith('.'))
|
|
75
|
+
rel = `./${rel}`;
|
|
76
|
+
// Rewrite only the src value WITHIN the matched tag, then swap that tag
|
|
77
|
+
// back into the source — avoids touching any other `src=` in the document.
|
|
78
|
+
const newTag = scriptTag.replace(SRC_ATTR_RE, (whole) => whole.replace(scriptSrc, rel));
|
|
79
|
+
return htmlSource.replace(scriptTag, newTag);
|
|
80
|
+
}
|
|
81
|
+
/** Convenience: the emitted HTML filename for a given JS outfile (sibling `index.html`). */
|
|
82
|
+
export function htmlOutPathFor(jsOutPath) {
|
|
83
|
+
return resolve(dirname(jsOutPath), 'index.html');
|
|
84
|
+
}
|
|
85
|
+
/** Derive a JS outfile from an outdir + the module entry basename (`<outdir>/<name>.js`). */
|
|
86
|
+
export function jsOutPathForOutdir(outdir, moduleEntry) {
|
|
87
|
+
const name = basename(moduleEntry).replace(/\.[cm]?[jt]sx?$/i, '');
|
|
88
|
+
return resolve(outdir, `${name}.js`);
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -122,19 +122,19 @@
|
|
|
122
122
|
"cli"
|
|
123
123
|
],
|
|
124
124
|
"dependencies": {
|
|
125
|
-
"@gjsify/buffer": "^0.16.
|
|
126
|
-
"@gjsify/create-app": "^0.16.
|
|
127
|
-
"@gjsify/node-globals": "^0.16.
|
|
128
|
-
"@gjsify/node-polyfills": "^0.16.
|
|
129
|
-
"@gjsify/npm-registry": "^0.16.
|
|
130
|
-
"@gjsify/resolve-npm": "^0.16.
|
|
131
|
-
"@gjsify/rolldown-plugin-gjsify": "^0.16.
|
|
132
|
-
"@gjsify/rolldown-plugin-pnp": "^0.16.
|
|
133
|
-
"@gjsify/semver": "^0.16.
|
|
134
|
-
"@gjsify/tar": "^0.16.
|
|
135
|
-
"@gjsify/tsc": "^0.16.
|
|
136
|
-
"@gjsify/web-polyfills": "^0.16.
|
|
137
|
-
"@gjsify/workspace": "^0.16.
|
|
125
|
+
"@gjsify/buffer": "^0.16.3",
|
|
126
|
+
"@gjsify/create-app": "^0.16.3",
|
|
127
|
+
"@gjsify/node-globals": "^0.16.3",
|
|
128
|
+
"@gjsify/node-polyfills": "^0.16.3",
|
|
129
|
+
"@gjsify/npm-registry": "^0.16.3",
|
|
130
|
+
"@gjsify/resolve-npm": "^0.16.3",
|
|
131
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.16.3",
|
|
132
|
+
"@gjsify/rolldown-plugin-pnp": "^0.16.3",
|
|
133
|
+
"@gjsify/semver": "^0.16.3",
|
|
134
|
+
"@gjsify/tar": "^0.16.3",
|
|
135
|
+
"@gjsify/tsc": "^0.16.3",
|
|
136
|
+
"@gjsify/web-polyfills": "^0.16.3",
|
|
137
|
+
"@gjsify/workspace": "^0.16.3",
|
|
138
138
|
"cosmiconfig": "^9.0.2",
|
|
139
139
|
"get-tsconfig": "^4.14.0",
|
|
140
140
|
"pkg-types": "^2.3.1",
|
|
@@ -142,13 +142,13 @@
|
|
|
142
142
|
"yargs": "^18.0.0"
|
|
143
143
|
},
|
|
144
144
|
"devDependencies": {
|
|
145
|
-
"@gjsify/unit": "^0.16.
|
|
145
|
+
"@gjsify/unit": "^0.16.3",
|
|
146
146
|
"@types/yargs": "^17.0.35",
|
|
147
147
|
"typescript": "^6.0.3"
|
|
148
148
|
},
|
|
149
149
|
"peerDependencies": {
|
|
150
|
-
"@gjsify/oxfmt-native": "^0.16.
|
|
151
|
-
"@gjsify/rolldown-native": "^0.16.
|
|
150
|
+
"@gjsify/oxfmt-native": "^0.16.3",
|
|
151
|
+
"@gjsify/rolldown-native": "^0.16.3"
|
|
152
152
|
},
|
|
153
153
|
"peerDependenciesMeta": {
|
|
154
154
|
"@gjsify/oxfmt-native": {
|