@gjsify/rolldown-plugin-gjsify 0.3.20 → 0.4.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/lib/app/gjs.js +66 -2
- package/lib/plugins/alias.js +9 -1
- package/lib/plugins/css-as-string.js +98 -11
- package/lib/plugins/shebang.d.ts +2 -0
- package/lib/plugins/shebang.js +23 -0
- package/lib/shims/unicorn-magic.d.ts +14 -0
- package/lib/shims/unicorn-magic.js +68 -0
- package/lib/utils/auto-globals.d.ts +17 -2
- package/lib/utils/auto-globals.js +49 -27
- package/lib/utils/detect-free-globals.js +6 -0
- package/package.json +9 -5
- package/src/app/gjs.ts +63 -2
- package/src/plugins/alias.ts +9 -1
- package/src/plugins/css-as-string.ts +122 -11
- package/src/plugins/shebang.ts +24 -0
- package/src/shims/unicorn-magic.ts +75 -0
- package/src/utils/auto-globals.ts +62 -24
- package/src/utils/detect-free-globals.ts +6 -0
package/lib/app/gjs.js
CHANGED
|
@@ -18,8 +18,38 @@ import { globToEntryPoints } from '../utils/entry-points.js';
|
|
|
18
18
|
import { nodeModulesPathRewritePlugin, getBundleDirFromOutput, } from '../plugins/rewrite-node-modules-paths.js';
|
|
19
19
|
import { processStubPlugin } from '../plugins/process-stub.js';
|
|
20
20
|
import { cssAsStringPlugin } from '../plugins/css-as-string.js';
|
|
21
|
-
import { shebangPlugin, resolveShebangLine } from '../plugins/shebang.js';
|
|
21
|
+
import { shebangPlugin, resolveShebangLine, inputShebangStripPlugin } from '../plugins/shebang.js';
|
|
22
22
|
const _shimDir = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
function resolveConsoleShim() {
|
|
24
|
+
// Preferred: relative to this module's directory. Works under the
|
|
25
|
+
// normal Node consumer flow where `_shimDir` = `<pkg>/lib/app/`.
|
|
26
|
+
const relative = resolve(_shimDir, '../shims/console-gjs.js');
|
|
27
|
+
let fs = null;
|
|
28
|
+
try {
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
|
+
fs = require('node:fs');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return relative;
|
|
34
|
+
}
|
|
35
|
+
if (fs.existsSync(relative))
|
|
36
|
+
return relative;
|
|
37
|
+
// Fallback: when the orchestrator is bundled into a single .mjs
|
|
38
|
+
// (GJS-CLI self-host loop) `_shimDir` collapses to the bundle's
|
|
39
|
+
// own directory and the relative lookup misses. createRequire's
|
|
40
|
+
// resolver is `exports`-map-aware (Phase C), so the published
|
|
41
|
+
// subpath export `./shims/console-gjs` works under both Node and
|
|
42
|
+
// GJS without further walking.
|
|
43
|
+
try {
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
45
|
+
const Module = require('node:module');
|
|
46
|
+
const require_ = Module.createRequire(import.meta.url);
|
|
47
|
+
return require_.resolve('@gjsify/rolldown-plugin-gjsify/shims/console-gjs');
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return relative;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
23
53
|
export const setupForGjs = async (input) => {
|
|
24
54
|
const userExternal = input.userExternal ?? [];
|
|
25
55
|
// Rolldown's `external` array does not support glob patterns the way
|
|
@@ -37,17 +67,32 @@ export const setupForGjs = async (input) => {
|
|
|
37
67
|
const format = input.pluginOptions.format ?? 'esm';
|
|
38
68
|
const exclude = input.pluginOptions.exclude ?? [];
|
|
39
69
|
const entryPoints = await globToEntryPoints(input.input, exclude);
|
|
70
|
+
// unicorn-magic gates its full API behind the "node" conditional
|
|
71
|
+
// exports. We deliberately omit `node` from conditionNames (some
|
|
72
|
+
// packages ship genuinely Node-only code there — see comment
|
|
73
|
+
// around `conditionNames` below). Route the package to our
|
|
74
|
+
// bundled shim so the API is reachable under --app gjs without
|
|
75
|
+
// turning on the node condition globally.
|
|
76
|
+
const unicornMagicShim = resolve(_shimDir, '../shims/unicorn-magic.js');
|
|
40
77
|
const aliasMap = {
|
|
41
78
|
...getAliasesForGjs({ external }),
|
|
79
|
+
'unicorn-magic': unicornMagicShim,
|
|
42
80
|
...(input.pluginOptions.aliases ?? {}),
|
|
43
81
|
...(input.userAliases ?? {}),
|
|
44
82
|
};
|
|
45
83
|
// The console shim replaces all `console` references with print()/printerr()-
|
|
46
84
|
// based implementations that bypass GLib.log_structured() — no prefix,
|
|
47
85
|
// ANSI codes work. Disabled via `pluginOptions.consoleShim === false`.
|
|
86
|
+
//
|
|
87
|
+
// Path resolution: `resolve(_shimDir, '../shims/...')` works in normal
|
|
88
|
+
// Node consumption (_shimDir = `<pkg>/lib/app/`). When the CLI is
|
|
89
|
+
// bundled into a single .mjs (e.g. the GJS-CLI self-host loop),
|
|
90
|
+
// `import.meta.url` collapses to the bundle's path and the relative
|
|
91
|
+
// resolution lands at a non-existent location. Walk up via
|
|
92
|
+
// createRequire's node_modules-aware resolver as a fallback.
|
|
48
93
|
const consoleShimEnabled = input.pluginOptions.consoleShim !== false;
|
|
49
94
|
const consoleShimPath = consoleShimEnabled
|
|
50
|
-
?
|
|
95
|
+
? resolveConsoleShim()
|
|
51
96
|
: null;
|
|
52
97
|
// The auto-globals inject stub (when present) is side-effect-imported
|
|
53
98
|
// via a virtual entry — its register modules write to globalThis, so
|
|
@@ -72,6 +117,19 @@ export const setupForGjs = async (input) => {
|
|
|
72
117
|
mainFields: format === 'esm' ? ['browser', 'module', 'main'] : ['browser', 'main', 'module'],
|
|
73
118
|
// ESM: omit 'require' — packages listing 'require' before 'import'
|
|
74
119
|
// would silently route through their CJS entry.
|
|
120
|
+
//
|
|
121
|
+
// We deliberately do NOT add `'node'` here. Per Node's exports-map
|
|
122
|
+
// spec the resolver iterates keys in DECLARATION ORDER and picks
|
|
123
|
+
// the first one whose name is in `conditionNames` — the order of
|
|
124
|
+
// conditionNames itself is irrelevant. Packages like
|
|
125
|
+
// `cross-fetch-ponyfill` declare `"node"` first in their exports
|
|
126
|
+
// map and ship a Node-only entry that imports `blobFrom`/
|
|
127
|
+
// `fileFrom` (from native `node:fetch`). With `node` enabled,
|
|
128
|
+
// the resolver picks that branch over `browser` and the bundle
|
|
129
|
+
// breaks at link time. Packages that genuinely need their `node`
|
|
130
|
+
// export under GJS (rare — only one known case so far,
|
|
131
|
+
// `unicorn-magic`'s `traversePathUp`) are handled with explicit
|
|
132
|
+
// resolve aliases instead.
|
|
75
133
|
conditionNames: format === 'esm' ? ['browser', 'import'] : ['browser', 'require', 'import'],
|
|
76
134
|
},
|
|
77
135
|
transform: {
|
|
@@ -110,6 +168,12 @@ export const setupForGjs = async (input) => {
|
|
|
110
168
|
// Virtual-entry plugin runs FIRST so its resolveId/load match the
|
|
111
169
|
// synthetic input ids that `wrapInputWithSideEffects` produces.
|
|
112
170
|
...(virtualEntries.plugin ? [virtualEntries.plugin] : []),
|
|
171
|
+
// Strip leading #! from any input module BEFORE bundling — otherwise
|
|
172
|
+
// a shebang in e.g. the CLI's own entry file ends up embedded
|
|
173
|
+
// mid-chunk after our process-stub banner, and acorn (auto-globals
|
|
174
|
+
// detector) rejects the `#` byte. Final-output shebang is composed
|
|
175
|
+
// by shebangPlugin's renderChunk hook.
|
|
176
|
+
inputShebangStripPlugin(),
|
|
113
177
|
// random-access-file's 'browser' field maps to a throwing stub; force
|
|
114
178
|
// the fs-backed Node entry. Implemented via the gjsify alias plugin
|
|
115
179
|
// as a direct entry-table override.
|
package/lib/plugins/alias.js
CHANGED
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
// - exact string match (no prefix-aware semantics needed at this layer)
|
|
14
14
|
// - `node:<name>` specifiers map to the same target as `<name>`
|
|
15
15
|
// (handled in the alias-builder helpers, not here).
|
|
16
|
+
//
|
|
17
|
+
// `extraOptions.kind` is forwarded to `this.resolve()` so package.json
|
|
18
|
+
// `exports` conditions ("import" / "require") match the original call site.
|
|
19
|
+
// Without this, a CJS `require('stream')` in a bundled npm package would
|
|
20
|
+
// resolve through the "import" condition (Rolldown's default), bypassing the
|
|
21
|
+
// `cjs-compat.cjs` shim that unwraps named-export ESM modules to their
|
|
22
|
+
// constructor — breaking `util.inherits(Child, Stream)` patterns.
|
|
16
23
|
export function aliasPlugin(options) {
|
|
17
24
|
const entries = options.entries;
|
|
18
25
|
const keys = Object.keys(entries);
|
|
@@ -20,7 +27,7 @@ export function aliasPlugin(options) {
|
|
|
20
27
|
name: 'gjsify-alias',
|
|
21
28
|
resolveId: {
|
|
22
29
|
order: 'pre',
|
|
23
|
-
async handler(source, importer) {
|
|
30
|
+
async handler(source, importer, extraOptions) {
|
|
24
31
|
if (!Object.prototype.hasOwnProperty.call(entries, source)) {
|
|
25
32
|
return null;
|
|
26
33
|
}
|
|
@@ -31,6 +38,7 @@ export function aliasPlugin(options) {
|
|
|
31
38
|
return null;
|
|
32
39
|
const resolved = await this.resolve(target, importer, {
|
|
33
40
|
skipSelf: true,
|
|
41
|
+
kind: extraOptions?.kind,
|
|
34
42
|
});
|
|
35
43
|
if (resolved !== null) {
|
|
36
44
|
return resolved;
|
|
@@ -19,10 +19,100 @@
|
|
|
19
19
|
// flattened to GTK4-CSS-engine-compatible output. Targeting is opt-in —
|
|
20
20
|
// a missing `targets` keeps the source pristine.
|
|
21
21
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
22
|
+
// Backend selection (Phase D-2 decision matrix in
|
|
23
|
+
// `docs/poc/lightningcss-decision.md`):
|
|
24
|
+
//
|
|
25
|
+
// 1. `@gjsify/lightningcss-native` when its prebuild is loadable on
|
|
26
|
+
// the running architecture (3-5× faster than the WASM track,
|
|
27
|
+
// ~960× faster cold init). Only relevant when `gjsify build`
|
|
28
|
+
// itself runs under GJS (Phase D-3).
|
|
29
|
+
// 2. npm `lightningcss` for everything else (Node, unsupported
|
|
30
|
+
// arches, dev machines without the prebuild). Existing behavior;
|
|
31
|
+
// keeps the regular dependency on this package.
|
|
32
|
+
//
|
|
33
|
+
// Selection is lazy and silent — the first `.css` load probes for
|
|
34
|
+
// the native bridge once, caches the answer, and routes the rest of
|
|
35
|
+
// the build through the chosen backend. Set the env var
|
|
36
|
+
// `GJSIFY_CSS_BACKEND={native|npm}` to force a specific backend
|
|
37
|
+
// (mainly useful for benchmarking + the integration suite).
|
|
25
38
|
import { readFile } from 'node:fs/promises';
|
|
39
|
+
let _bundlerPromise = null;
|
|
40
|
+
async function pickBundler() {
|
|
41
|
+
const forced = globalThis.process?.env?.GJSIFY_CSS_BACKEND;
|
|
42
|
+
if (forced === 'npm')
|
|
43
|
+
return loadNpmBundler();
|
|
44
|
+
if (forced === 'native') {
|
|
45
|
+
const native = await tryLoadNativeBundler();
|
|
46
|
+
if (!native)
|
|
47
|
+
throw new Error('GJSIFY_CSS_BACKEND=native but @gjsify/lightningcss-native is not loadable');
|
|
48
|
+
return native;
|
|
49
|
+
}
|
|
50
|
+
const native = await tryLoadNativeBundler();
|
|
51
|
+
return native ?? loadNpmBundler();
|
|
52
|
+
}
|
|
53
|
+
async function tryLoadNativeBundler() {
|
|
54
|
+
// The native bridge only exists under GJS — `imports.gi` marker. Skip
|
|
55
|
+
// the dynamic import entirely on Node so it doesn't even register as a
|
|
56
|
+
// resolved dep, which would inflate the CLI's bundled output.
|
|
57
|
+
const isGjs = typeof globalThis.imports?.gi !== 'undefined';
|
|
58
|
+
if (!isGjs)
|
|
59
|
+
return null;
|
|
60
|
+
try {
|
|
61
|
+
// Indirect specifier so tsc + Rolldown don't try to resolve the
|
|
62
|
+
// optional peer dep at build time. Resolution happens only at
|
|
63
|
+
// runtime under GJS (where the prebuild is installed).
|
|
64
|
+
const specifier = '@gjsify/lightningcss-native';
|
|
65
|
+
const mod = (await import(/* @vite-ignore */ specifier));
|
|
66
|
+
if (!mod.hasNativeLightningcss())
|
|
67
|
+
return null;
|
|
68
|
+
return async (filename, targets) => {
|
|
69
|
+
// The native shim accepts a browserslist string; the npm
|
|
70
|
+
// `lightningcss` Targets struct is bitfield-encoded
|
|
71
|
+
// (`firefox: 60 << 16` etc). Convert by extracting major
|
|
72
|
+
// version per browser key and re-emitting as the equivalent
|
|
73
|
+
// browserslist query.
|
|
74
|
+
const query = targetsToBrowserslist(targets);
|
|
75
|
+
return mod.bundle({
|
|
76
|
+
filename,
|
|
77
|
+
targets: query,
|
|
78
|
+
minify: false,
|
|
79
|
+
sourceMap: false,
|
|
80
|
+
errorRecovery: true,
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function loadNpmBundler() {
|
|
89
|
+
const { bundleAsync } = await import('lightningcss');
|
|
90
|
+
return async (filename, targets) => {
|
|
91
|
+
const result = await bundleAsync({
|
|
92
|
+
filename,
|
|
93
|
+
targets,
|
|
94
|
+
minify: false,
|
|
95
|
+
errorRecovery: true,
|
|
96
|
+
});
|
|
97
|
+
return { code: result.code };
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function targetsToBrowserslist(targets) {
|
|
101
|
+
if (!targets)
|
|
102
|
+
return undefined;
|
|
103
|
+
const parts = [];
|
|
104
|
+
for (const [browser, encoded] of Object.entries(targets)) {
|
|
105
|
+
if (typeof encoded !== 'number')
|
|
106
|
+
continue;
|
|
107
|
+
// npm lightningcss encodes versions as `(major << 16) | (minor << 8) | patch`.
|
|
108
|
+
const major = (encoded >>> 16) & 0xff;
|
|
109
|
+
if (major === 0)
|
|
110
|
+
continue;
|
|
111
|
+
const name = browser === 'ios_saf' ? 'ios' : browser;
|
|
112
|
+
parts.push(`${name} >= ${major}`);
|
|
113
|
+
}
|
|
114
|
+
return parts.length ? parts.join(', ') : undefined;
|
|
115
|
+
}
|
|
26
116
|
export function cssAsStringPlugin(options = {}) {
|
|
27
117
|
const { targets, bundle = true } = options;
|
|
28
118
|
return {
|
|
@@ -42,12 +132,9 @@ export function cssAsStringPlugin(options = {}) {
|
|
|
42
132
|
};
|
|
43
133
|
}
|
|
44
134
|
async function loadAndBundleCss(filename, targets) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
errorRecovery: true,
|
|
51
|
-
});
|
|
52
|
-
return result.code;
|
|
135
|
+
if (!_bundlerPromise)
|
|
136
|
+
_bundlerPromise = pickBundler();
|
|
137
|
+
const bundler = await _bundlerPromise;
|
|
138
|
+
const { code } = await bundler(filename, targets);
|
|
139
|
+
return code;
|
|
53
140
|
}
|
package/lib/plugins/shebang.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface ShebangPluginOptions {
|
|
|
5
5
|
/** Override the shebang line. Defaults to `GJS_SHEBANG`. */
|
|
6
6
|
line?: string;
|
|
7
7
|
}
|
|
8
|
+
/** Always-on plugin half: strips input shebangs regardless of output options. */
|
|
9
|
+
export declare function inputShebangStripPlugin(): Plugin;
|
|
8
10
|
export declare function shebangPlugin(options?: ShebangPluginOptions): Plugin | null;
|
|
9
11
|
/**
|
|
10
12
|
* Expand `${env:NAME}` and `${env:NAME:-default}` placeholders against
|
package/lib/plugins/shebang.js
CHANGED
|
@@ -6,6 +6,29 @@
|
|
|
6
6
|
// the `#` character is only valid as the very first byte of the file under
|
|
7
7
|
// SpiderMonkey 128+.
|
|
8
8
|
export const GJS_SHEBANG = '#!/usr/bin/env -S gjs -m';
|
|
9
|
+
/**
|
|
10
|
+
* Strip a leading `#!…\n` from a source module. Rolldown preserves input
|
|
11
|
+
* shebangs verbatim, which ends up embedded mid-chunk after our process-stub
|
|
12
|
+
* banner — acorn (used by the auto-globals detector) then rejects `#` because
|
|
13
|
+
* it's not at byte 0 anymore. Stripping at the transform stage cleans both
|
|
14
|
+
* the analysis bundle and the final bundle; the gjsify-shebang renderChunk
|
|
15
|
+
* step then injects the correct line for the output target.
|
|
16
|
+
*/
|
|
17
|
+
const SHEBANG_RE = /^#![^\n]*\n/;
|
|
18
|
+
/** Always-on plugin half: strips input shebangs regardless of output options. */
|
|
19
|
+
export function inputShebangStripPlugin() {
|
|
20
|
+
return {
|
|
21
|
+
name: 'gjsify-input-shebang-strip',
|
|
22
|
+
transform: {
|
|
23
|
+
order: 'pre',
|
|
24
|
+
handler(code) {
|
|
25
|
+
if (!code.startsWith('#!'))
|
|
26
|
+
return null;
|
|
27
|
+
return { code: code.replace(SHEBANG_RE, ''), map: null };
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
9
32
|
export function shebangPlugin(options = {}) {
|
|
10
33
|
if (!options.enabled)
|
|
11
34
|
return null;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function toPath(urlOrPath: any): any;
|
|
2
|
+
export declare function rootDirectory(pathInput: any): string;
|
|
3
|
+
export declare function traversePathUp(startPath: any): {
|
|
4
|
+
[Symbol.iterator](): Generator<string, void, unknown>;
|
|
5
|
+
};
|
|
6
|
+
export declare function execFile(file: any, arguments_: any, options?: {}): Promise<{
|
|
7
|
+
stdout: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function execFileSync(file: any, arguments_?: any[], options?: {}): NonSharedBuffer;
|
|
11
|
+
export declare function delay(opts?: {
|
|
12
|
+
seconds?: number;
|
|
13
|
+
milliseconds?: number;
|
|
14
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Shim for `unicorn-magic` under --app gjs.
|
|
2
|
+
//
|
|
3
|
+
// The upstream package gates the full API (`toPath`, `traversePathUp`,
|
|
4
|
+
// `rootDirectory`, `execFile`, `execFileSync`) behind the `"node"`
|
|
5
|
+
// conditional exports entry. Under --app gjs we intentionally omit
|
|
6
|
+
// the `node` resolve-condition (cross-fetch-ponyfill ships
|
|
7
|
+
// Node-only code under that key — see `app/gjs.ts` conditionNames
|
|
8
|
+
// comment), so a bare `import { toPath } from 'unicorn-magic'` falls
|
|
9
|
+
// back to `default.js` which only exposes `delay`.
|
|
10
|
+
//
|
|
11
|
+
// This shim mirrors the node.js entry verbatim — the underlying
|
|
12
|
+
// `node:url`/`node:path`/`node:child_process`/`node:util` imports
|
|
13
|
+
// route through `@gjsify/{url,path,child_process,util}` under GJS
|
|
14
|
+
// and through real Node-internals under Node. The aliasPlugin
|
|
15
|
+
// points `unicorn-magic` here for --app gjs builds.
|
|
16
|
+
//
|
|
17
|
+
// Source-of-truth: refs/unicorn-magic/node.js (when added — for
|
|
18
|
+
// now mirrored from node_modules/unicorn-magic@0.3.0).
|
|
19
|
+
import { promisify } from 'node:util';
|
|
20
|
+
import { execFile as execFileCallback, execFileSync as execFileSyncOriginal } from 'node:child_process';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
const execFileOriginal = promisify(execFileCallback);
|
|
24
|
+
export function toPath(urlOrPath) {
|
|
25
|
+
return urlOrPath instanceof URL ? fileURLToPath(urlOrPath) : urlOrPath;
|
|
26
|
+
}
|
|
27
|
+
export function rootDirectory(pathInput) {
|
|
28
|
+
return path.parse(toPath(pathInput)).root;
|
|
29
|
+
}
|
|
30
|
+
export function traversePathUp(startPath) {
|
|
31
|
+
return {
|
|
32
|
+
*[Symbol.iterator]() {
|
|
33
|
+
let currentPath = path.resolve(toPath(startPath));
|
|
34
|
+
let previousPath;
|
|
35
|
+
while (previousPath !== currentPath) {
|
|
36
|
+
yield currentPath;
|
|
37
|
+
previousPath = currentPath;
|
|
38
|
+
currentPath = path.resolve(currentPath, '..');
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const TEN_MEGABYTES_IN_BYTES = 10 * 1024 * 1024;
|
|
44
|
+
export async function execFile(file, arguments_, options = {}) {
|
|
45
|
+
return execFileOriginal(file, arguments_, {
|
|
46
|
+
maxBuffer: TEN_MEGABYTES_IN_BYTES,
|
|
47
|
+
...options,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export function execFileSync(file, arguments_ = [], options = {}) {
|
|
51
|
+
return execFileSyncOriginal(file, arguments_, {
|
|
52
|
+
maxBuffer: TEN_MEGABYTES_IN_BYTES,
|
|
53
|
+
...options,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// Re-export from default.js so the union API (delay + node helpers)
|
|
57
|
+
// stays intact for callers that import both.
|
|
58
|
+
export async function delay(opts = {}) {
|
|
59
|
+
const { seconds, milliseconds } = opts;
|
|
60
|
+
let duration;
|
|
61
|
+
if (typeof seconds === 'number')
|
|
62
|
+
duration = seconds * 1000;
|
|
63
|
+
else if (typeof milliseconds === 'number')
|
|
64
|
+
duration = milliseconds;
|
|
65
|
+
else
|
|
66
|
+
throw new TypeError('Expected an object with either `seconds` or `milliseconds`.');
|
|
67
|
+
return new Promise((resolveFn) => setTimeout(resolveFn, duration));
|
|
68
|
+
}
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { InputOptions, RolldownPluginOption, TransformOptions } from 'rolldown';
|
|
2
|
+
/**
|
|
3
|
+
* In-memory bundle function — returns the per-entry chunk code strings.
|
|
4
|
+
* Implementations: npm rolldown (Node default), `@gjsify/rolldown-native`
|
|
5
|
+
* (GJS). Pulled out so auto-globals can run under either engine without
|
|
6
|
+
* hardcoding npm rolldown (which can't load under GJS — the Rust prebuild's
|
|
7
|
+
* init code uses `require('node:fs')` synchronously).
|
|
8
|
+
*
|
|
9
|
+
* The default impl below dynamically imports npm rolldown; the CLI
|
|
10
|
+
* overrides this from `actions/build.ts` to route via the same engine the
|
|
11
|
+
* final build uses.
|
|
12
|
+
*/
|
|
13
|
+
export type AnalysisBundler = (input: {
|
|
14
|
+
rolldownInput: InputOptions;
|
|
15
|
+
format: 'esm' | 'cjs' | 'iife';
|
|
16
|
+
}) => Promise<string[]>;
|
|
2
17
|
import type { PluginOptions } from '../types/plugin-options.js';
|
|
3
18
|
export interface AutoGlobalsResult {
|
|
4
19
|
/** Global identifiers detected in the bundle */
|
|
@@ -68,5 +83,5 @@ type GjsifyPluginFactory = (options: PluginOptions) => RolldownPluginOption | Pr
|
|
|
68
83
|
* @param verbose Emit per-iteration debug output to console.
|
|
69
84
|
* @param options Optional `extraGlobalsList` / `excludeGlobals`.
|
|
70
85
|
*/
|
|
71
|
-
export declare function detectAutoGlobals(analysisOptions: AnalysisOptions, pluginOptions: Omit<PluginOptions, 'autoGlobalsInject'>, gjsifyPluginFactory: GjsifyPluginFactory, verbose?: boolean, options?: DetectAutoGlobalsOptions): Promise<AutoGlobalsResult>;
|
|
86
|
+
export declare function detectAutoGlobals(analysisOptions: AnalysisOptions, pluginOptions: Omit<PluginOptions, 'autoGlobalsInject'>, gjsifyPluginFactory: GjsifyPluginFactory, verbose?: boolean, options?: DetectAutoGlobalsOptions, bundler?: AnalysisBundler): Promise<AutoGlobalsResult>;
|
|
72
87
|
export {};
|
|
@@ -17,7 +17,25 @@
|
|
|
17
17
|
// We deliberately do NOT minify the analysis builds: minification can
|
|
18
18
|
// alias `globalThis` to a short variable and defeat MemberExpression
|
|
19
19
|
// detection in detect-free-globals.ts.
|
|
20
|
-
|
|
20
|
+
const defaultBundler = async ({ rolldownInput, format }) => {
|
|
21
|
+
// Indirect specifier so the GJS bundle doesn't pull npm rolldown in
|
|
22
|
+
// statically. Only reached when the caller doesn't override (Node).
|
|
23
|
+
const specifier = 'rolldown';
|
|
24
|
+
const mod = (await import(/* @vite-ignore */ specifier));
|
|
25
|
+
const build = await mod.rolldown(rolldownInput);
|
|
26
|
+
try {
|
|
27
|
+
const result = await build.generate({ format, minify: false, sourcemap: false });
|
|
28
|
+
const codes = [];
|
|
29
|
+
for (const entry of result.output) {
|
|
30
|
+
if (entry.type === 'chunk')
|
|
31
|
+
codes.push(entry.code);
|
|
32
|
+
}
|
|
33
|
+
return codes;
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await build.close();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
21
39
|
import { detectFreeGlobals } from './detect-free-globals.js';
|
|
22
40
|
import { resolveGlobalsList, writeRegisterInjectFile } from './scan-globals.js';
|
|
23
41
|
import { GJS_GLOBALS_MAP } from '@gjsify/resolve-npm/globals-map';
|
|
@@ -69,7 +87,7 @@ function detectedToRegisterPaths(detected) {
|
|
|
69
87
|
* @param verbose Emit per-iteration debug output to console.
|
|
70
88
|
* @param options Optional `extraGlobalsList` / `excludeGlobals`.
|
|
71
89
|
*/
|
|
72
|
-
export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPluginFactory, verbose, options = {}) {
|
|
90
|
+
export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPluginFactory, verbose, options = {}, bundler = defaultBundler) {
|
|
73
91
|
const extraRegisterPaths = options.extraGlobalsList
|
|
74
92
|
? resolveGlobalsList(options.extraGlobalsList)
|
|
75
93
|
: new Set();
|
|
@@ -101,29 +119,17 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
|
|
|
101
119
|
const inputWithInject = currentInject
|
|
102
120
|
? appendInjectAsEntry(analysisOptions.input, currentInject)
|
|
103
121
|
: analysisOptions.input;
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
const chunkCodes = await bundler({
|
|
123
|
+
rolldownInput: {
|
|
124
|
+
input: inputWithInject,
|
|
125
|
+
external: analysisOptions.external,
|
|
126
|
+
resolve: analysisOptions.resolve,
|
|
127
|
+
transform: analysisOptions.transform,
|
|
128
|
+
plugins: [...callerPlugins, gjsifyInstance],
|
|
129
|
+
logLevel: 'silent',
|
|
130
|
+
},
|
|
131
|
+
format: analysisOptions.format ?? 'esm',
|
|
111
132
|
});
|
|
112
|
-
const chunkCodes = [];
|
|
113
|
-
try {
|
|
114
|
-
const result = await build.generate({
|
|
115
|
-
format: analysisOptions.format ?? 'esm',
|
|
116
|
-
minify: false,
|
|
117
|
-
sourcemap: false,
|
|
118
|
-
});
|
|
119
|
-
for (const entry of result.output) {
|
|
120
|
-
if (entry.type === 'chunk')
|
|
121
|
-
chunkCodes.push(entry.code);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
finally {
|
|
125
|
-
await build.close();
|
|
126
|
-
}
|
|
127
133
|
if (chunkCodes.length === 0) {
|
|
128
134
|
return { detected: new Set(), injectPath: currentInject };
|
|
129
135
|
}
|
|
@@ -133,9 +139,25 @@ export async function detectAutoGlobals(analysisOptions, pluginOptions, gjsifyPl
|
|
|
133
139
|
// top-level declarations: `File`, `Buffer`, …) that acorn can't
|
|
134
140
|
// parse. Per-chunk parsing keeps each chunk's lexical scope intact.
|
|
135
141
|
const newDetected = new Set();
|
|
136
|
-
for (
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
for (let i = 0; i < chunkCodes.length; i++) {
|
|
143
|
+
const code = chunkCodes[i] ?? '';
|
|
144
|
+
try {
|
|
145
|
+
for (const id of detectFreeGlobals(code))
|
|
146
|
+
newDetected.add(id);
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
if (globalThis.process?.env?.GJSIFY_DEBUG_AUTO_GLOBALS) {
|
|
150
|
+
const path = `/tmp/gjsify-auto-globals-failed-chunk-${i}.mjs`;
|
|
151
|
+
try {
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
153
|
+
const fs = await import('node:fs');
|
|
154
|
+
fs.writeFileSync(path, code);
|
|
155
|
+
console.error(`[gjsify-auto-globals] parse failed on chunk #${i} — wrote ${path} for inspection`);
|
|
156
|
+
}
|
|
157
|
+
catch { /* ignore */ }
|
|
158
|
+
}
|
|
159
|
+
throw e;
|
|
160
|
+
}
|
|
139
161
|
}
|
|
140
162
|
// Apply excludeGlobals BEFORE writing the next iteration's inject file.
|
|
141
163
|
// Otherwise an excluded identifier would still appear in the inject
|
|
@@ -93,6 +93,12 @@ export function detectFreeGlobals(code) {
|
|
|
93
93
|
const ast = acorn.parse(code, {
|
|
94
94
|
ecmaVersion: 'latest',
|
|
95
95
|
sourceType: 'module',
|
|
96
|
+
// Some bundled chunks carry an embedded `#!shebang` line —
|
|
97
|
+
// notably any project bundling its own CLI gets the
|
|
98
|
+
// `#!/usr/bin/env -S gjs -m` shebang hoisted to byte 0.
|
|
99
|
+
// Acorn rejects shebangs by default; allow them so the
|
|
100
|
+
// free-globals analyzer doesn't choke on its own input.
|
|
101
|
+
allowHashBang: true,
|
|
96
102
|
});
|
|
97
103
|
// --- Pass 1: collect all declared names across the entire module ---
|
|
98
104
|
const declaredNames = new Set();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/rolldown-plugin-gjsify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Rolldown / Rollup / Vite plugin orchestrator for GJS, Node, and Browser targets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
],
|
|
43
43
|
"license": "MIT",
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@gjsify/resolve-npm": "^0.
|
|
46
|
-
"@gjsify/rolldown-plugin-deepkit": "^0.
|
|
47
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
48
|
-
"@gjsify/vite-plugin-blueprint": "^0.
|
|
45
|
+
"@gjsify/resolve-npm": "^0.4.0",
|
|
46
|
+
"@gjsify/rolldown-plugin-deepkit": "^0.4.0",
|
|
47
|
+
"@gjsify/rolldown-plugin-pnp": "^0.4.0",
|
|
48
|
+
"@gjsify/vite-plugin-blueprint": "^0.4.0",
|
|
49
49
|
"@rollup/pluginutils": "^5.3.0",
|
|
50
50
|
"acorn": "^8.16.0",
|
|
51
51
|
"acorn-walk": "^8.3.5",
|
|
@@ -53,9 +53,13 @@
|
|
|
53
53
|
"lightningcss": "^1.32.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
+
"@gjsify/lightningcss-native": "^0.4.0",
|
|
56
57
|
"rolldown": "^1.0.0-rc.18"
|
|
57
58
|
},
|
|
58
59
|
"peerDependenciesMeta": {
|
|
60
|
+
"@gjsify/lightningcss-native": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
59
63
|
"rolldown": {
|
|
60
64
|
"optional": true
|
|
61
65
|
}
|
package/src/app/gjs.ts
CHANGED
|
@@ -26,10 +26,36 @@ import {
|
|
|
26
26
|
} from '../plugins/rewrite-node-modules-paths.js';
|
|
27
27
|
import { processStubPlugin } from '../plugins/process-stub.js';
|
|
28
28
|
import { cssAsStringPlugin } from '../plugins/css-as-string.js';
|
|
29
|
-
import { shebangPlugin, resolveShebangLine } from '../plugins/shebang.js';
|
|
29
|
+
import { shebangPlugin, resolveShebangLine, inputShebangStripPlugin } from '../plugins/shebang.js';
|
|
30
30
|
|
|
31
31
|
const _shimDir = dirname(fileURLToPath(import.meta.url));
|
|
32
32
|
|
|
33
|
+
function resolveConsoleShim(): string {
|
|
34
|
+
// Preferred: relative to this module's directory. Works under the
|
|
35
|
+
// normal Node consumer flow where `_shimDir` = `<pkg>/lib/app/`.
|
|
36
|
+
const relative = resolve(_shimDir, '../shims/console-gjs.js');
|
|
37
|
+
let fs: typeof import('node:fs') | null = null;
|
|
38
|
+
try {
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
40
|
+
fs = require('node:fs') as typeof import('node:fs');
|
|
41
|
+
} catch { return relative; }
|
|
42
|
+
if (fs.existsSync(relative)) return relative;
|
|
43
|
+
// Fallback: when the orchestrator is bundled into a single .mjs
|
|
44
|
+
// (GJS-CLI self-host loop) `_shimDir` collapses to the bundle's
|
|
45
|
+
// own directory and the relative lookup misses. createRequire's
|
|
46
|
+
// resolver is `exports`-map-aware (Phase C), so the published
|
|
47
|
+
// subpath export `./shims/console-gjs` works under both Node and
|
|
48
|
+
// GJS without further walking.
|
|
49
|
+
try {
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
51
|
+
const Module = require('node:module') as typeof import('node:module');
|
|
52
|
+
const require_ = Module.createRequire(import.meta.url);
|
|
53
|
+
return require_.resolve('@gjsify/rolldown-plugin-gjsify/shims/console-gjs');
|
|
54
|
+
} catch {
|
|
55
|
+
return relative;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
33
59
|
/** Resolved Rolldown configuration template + plugins for `--app gjs`. */
|
|
34
60
|
export interface GjsBuildConfig {
|
|
35
61
|
options: RolldownOptions;
|
|
@@ -75,8 +101,17 @@ export const setupForGjs = async (input: GjsFactoryInput): Promise<GjsBuildConfi
|
|
|
75
101
|
const exclude = input.pluginOptions.exclude ?? [];
|
|
76
102
|
const entryPoints = await globToEntryPoints(input.input, exclude);
|
|
77
103
|
|
|
104
|
+
// unicorn-magic gates its full API behind the "node" conditional
|
|
105
|
+
// exports. We deliberately omit `node` from conditionNames (some
|
|
106
|
+
// packages ship genuinely Node-only code there — see comment
|
|
107
|
+
// around `conditionNames` below). Route the package to our
|
|
108
|
+
// bundled shim so the API is reachable under --app gjs without
|
|
109
|
+
// turning on the node condition globally.
|
|
110
|
+
const unicornMagicShim = resolve(_shimDir, '../shims/unicorn-magic.js');
|
|
111
|
+
|
|
78
112
|
const aliasMap = {
|
|
79
113
|
...getAliasesForGjs({ external }),
|
|
114
|
+
'unicorn-magic': unicornMagicShim,
|
|
80
115
|
...(input.pluginOptions.aliases ?? {}),
|
|
81
116
|
...(input.userAliases ?? {}),
|
|
82
117
|
};
|
|
@@ -84,9 +119,16 @@ export const setupForGjs = async (input: GjsFactoryInput): Promise<GjsBuildConfi
|
|
|
84
119
|
// The console shim replaces all `console` references with print()/printerr()-
|
|
85
120
|
// based implementations that bypass GLib.log_structured() — no prefix,
|
|
86
121
|
// ANSI codes work. Disabled via `pluginOptions.consoleShim === false`.
|
|
122
|
+
//
|
|
123
|
+
// Path resolution: `resolve(_shimDir, '../shims/...')` works in normal
|
|
124
|
+
// Node consumption (_shimDir = `<pkg>/lib/app/`). When the CLI is
|
|
125
|
+
// bundled into a single .mjs (e.g. the GJS-CLI self-host loop),
|
|
126
|
+
// `import.meta.url` collapses to the bundle's path and the relative
|
|
127
|
+
// resolution lands at a non-existent location. Walk up via
|
|
128
|
+
// createRequire's node_modules-aware resolver as a fallback.
|
|
87
129
|
const consoleShimEnabled = input.pluginOptions.consoleShim !== false;
|
|
88
130
|
const consoleShimPath = consoleShimEnabled
|
|
89
|
-
?
|
|
131
|
+
? resolveConsoleShim()
|
|
90
132
|
: null;
|
|
91
133
|
|
|
92
134
|
// The auto-globals inject stub (when present) is side-effect-imported
|
|
@@ -113,6 +155,19 @@ export const setupForGjs = async (input: GjsFactoryInput): Promise<GjsBuildConfi
|
|
|
113
155
|
mainFields: format === 'esm' ? ['browser', 'module', 'main'] : ['browser', 'main', 'module'],
|
|
114
156
|
// ESM: omit 'require' — packages listing 'require' before 'import'
|
|
115
157
|
// would silently route through their CJS entry.
|
|
158
|
+
//
|
|
159
|
+
// We deliberately do NOT add `'node'` here. Per Node's exports-map
|
|
160
|
+
// spec the resolver iterates keys in DECLARATION ORDER and picks
|
|
161
|
+
// the first one whose name is in `conditionNames` — the order of
|
|
162
|
+
// conditionNames itself is irrelevant. Packages like
|
|
163
|
+
// `cross-fetch-ponyfill` declare `"node"` first in their exports
|
|
164
|
+
// map and ship a Node-only entry that imports `blobFrom`/
|
|
165
|
+
// `fileFrom` (from native `node:fetch`). With `node` enabled,
|
|
166
|
+
// the resolver picks that branch over `browser` and the bundle
|
|
167
|
+
// breaks at link time. Packages that genuinely need their `node`
|
|
168
|
+
// export under GJS (rare — only one known case so far,
|
|
169
|
+
// `unicorn-magic`'s `traversePathUp`) are handled with explicit
|
|
170
|
+
// resolve aliases instead.
|
|
116
171
|
conditionNames: format === 'esm' ? ['browser', 'import'] : ['browser', 'require', 'import'],
|
|
117
172
|
},
|
|
118
173
|
transform: {
|
|
@@ -153,6 +208,12 @@ export const setupForGjs = async (input: GjsFactoryInput): Promise<GjsBuildConfi
|
|
|
153
208
|
// Virtual-entry plugin runs FIRST so its resolveId/load match the
|
|
154
209
|
// synthetic input ids that `wrapInputWithSideEffects` produces.
|
|
155
210
|
...(virtualEntries.plugin ? [virtualEntries.plugin] : []),
|
|
211
|
+
// Strip leading #! from any input module BEFORE bundling — otherwise
|
|
212
|
+
// a shebang in e.g. the CLI's own entry file ends up embedded
|
|
213
|
+
// mid-chunk after our process-stub banner, and acorn (auto-globals
|
|
214
|
+
// detector) rejects the `#` byte. Final-output shebang is composed
|
|
215
|
+
// by shebangPlugin's renderChunk hook.
|
|
216
|
+
inputShebangStripPlugin(),
|
|
156
217
|
// random-access-file's 'browser' field maps to a throwing stub; force
|
|
157
218
|
// the fs-backed Node entry. Implemented via the gjsify alias plugin
|
|
158
219
|
// as a direct entry-table override.
|
package/src/plugins/alias.ts
CHANGED
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
// - exact string match (no prefix-aware semantics needed at this layer)
|
|
14
14
|
// - `node:<name>` specifiers map to the same target as `<name>`
|
|
15
15
|
// (handled in the alias-builder helpers, not here).
|
|
16
|
+
//
|
|
17
|
+
// `extraOptions.kind` is forwarded to `this.resolve()` so package.json
|
|
18
|
+
// `exports` conditions ("import" / "require") match the original call site.
|
|
19
|
+
// Without this, a CJS `require('stream')` in a bundled npm package would
|
|
20
|
+
// resolve through the "import" condition (Rolldown's default), bypassing the
|
|
21
|
+
// `cjs-compat.cjs` shim that unwraps named-export ESM modules to their
|
|
22
|
+
// constructor — breaking `util.inherits(Child, Stream)` patterns.
|
|
16
23
|
|
|
17
24
|
import type { Plugin } from 'rolldown';
|
|
18
25
|
|
|
@@ -28,7 +35,7 @@ export function aliasPlugin(options: AliasPluginOptions): Plugin {
|
|
|
28
35
|
name: 'gjsify-alias',
|
|
29
36
|
resolveId: {
|
|
30
37
|
order: 'pre' as const,
|
|
31
|
-
async handler(source, importer) {
|
|
38
|
+
async handler(source, importer, extraOptions) {
|
|
32
39
|
if (!Object.prototype.hasOwnProperty.call(entries, source)) {
|
|
33
40
|
return null;
|
|
34
41
|
}
|
|
@@ -39,6 +46,7 @@ export function aliasPlugin(options: AliasPluginOptions): Plugin {
|
|
|
39
46
|
|
|
40
47
|
const resolved = await this.resolve(target, importer, {
|
|
41
48
|
skipSelf: true,
|
|
49
|
+
kind: extraOptions?.kind,
|
|
42
50
|
});
|
|
43
51
|
if (resolved !== null) {
|
|
44
52
|
return resolved;
|
|
@@ -19,9 +19,22 @@
|
|
|
19
19
|
// flattened to GTK4-CSS-engine-compatible output. Targeting is opt-in —
|
|
20
20
|
// a missing `targets` keeps the source pristine.
|
|
21
21
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
22
|
+
// Backend selection (Phase D-2 decision matrix in
|
|
23
|
+
// `docs/poc/lightningcss-decision.md`):
|
|
24
|
+
//
|
|
25
|
+
// 1. `@gjsify/lightningcss-native` when its prebuild is loadable on
|
|
26
|
+
// the running architecture (3-5× faster than the WASM track,
|
|
27
|
+
// ~960× faster cold init). Only relevant when `gjsify build`
|
|
28
|
+
// itself runs under GJS (Phase D-3).
|
|
29
|
+
// 2. npm `lightningcss` for everything else (Node, unsupported
|
|
30
|
+
// arches, dev machines without the prebuild). Existing behavior;
|
|
31
|
+
// keeps the regular dependency on this package.
|
|
32
|
+
//
|
|
33
|
+
// Selection is lazy and silent — the first `.css` load probes for
|
|
34
|
+
// the native bridge once, caches the answer, and routes the rest of
|
|
35
|
+
// the build through the chosen backend. Set the env var
|
|
36
|
+
// `GJSIFY_CSS_BACKEND={native|npm}` to force a specific backend
|
|
37
|
+
// (mainly useful for benchmarking + the integration suite).
|
|
25
38
|
|
|
26
39
|
import { readFile } from 'node:fs/promises';
|
|
27
40
|
import type { Plugin } from 'rolldown';
|
|
@@ -44,6 +57,108 @@ export interface CssAsStringOptions {
|
|
|
44
57
|
bundle?: boolean;
|
|
45
58
|
}
|
|
46
59
|
|
|
60
|
+
interface BundleResult {
|
|
61
|
+
code: Uint8Array;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type Bundler = (filename: string, targets: import('lightningcss').Targets | undefined) => Promise<BundleResult>;
|
|
65
|
+
|
|
66
|
+
let _bundlerPromise: Promise<Bundler> | null = null;
|
|
67
|
+
|
|
68
|
+
async function pickBundler(): Promise<Bundler> {
|
|
69
|
+
const forced = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.GJSIFY_CSS_BACKEND;
|
|
70
|
+
|
|
71
|
+
if (forced === 'npm') return loadNpmBundler();
|
|
72
|
+
if (forced === 'native') {
|
|
73
|
+
const native = await tryLoadNativeBundler();
|
|
74
|
+
if (!native) throw new Error('GJSIFY_CSS_BACKEND=native but @gjsify/lightningcss-native is not loadable');
|
|
75
|
+
return native;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const native = await tryLoadNativeBundler();
|
|
79
|
+
return native ?? loadNpmBundler();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Local mirror of the @gjsify/lightningcss-native surface we touch. We
|
|
83
|
+
// can't rely on the published types here because the package is an
|
|
84
|
+
// OPTIONAL peer dep — under Node it's not installed, so `import type`
|
|
85
|
+
// from it would break tsc on every Node consumer. Local interface
|
|
86
|
+
// keeps the type narrow + decouples the plugin's typecheck from
|
|
87
|
+
// whether the prebuild package is installed.
|
|
88
|
+
interface NativeLightningcssSurface {
|
|
89
|
+
hasNativeLightningcss(): boolean;
|
|
90
|
+
bundle(input: {
|
|
91
|
+
filename: string;
|
|
92
|
+
targets?: string;
|
|
93
|
+
minify?: boolean;
|
|
94
|
+
sourceMap?: boolean;
|
|
95
|
+
errorRecovery?: boolean;
|
|
96
|
+
}): { code: Uint8Array; map?: Uint8Array };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function tryLoadNativeBundler(): Promise<Bundler | null> {
|
|
100
|
+
// The native bridge only exists under GJS — `imports.gi` marker. Skip
|
|
101
|
+
// the dynamic import entirely on Node so it doesn't even register as a
|
|
102
|
+
// resolved dep, which would inflate the CLI's bundled output.
|
|
103
|
+
const isGjs = typeof (globalThis as { imports?: { gi?: unknown } }).imports?.gi !== 'undefined';
|
|
104
|
+
if (!isGjs) return null;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// Indirect specifier so tsc + Rolldown don't try to resolve the
|
|
108
|
+
// optional peer dep at build time. Resolution happens only at
|
|
109
|
+
// runtime under GJS (where the prebuild is installed).
|
|
110
|
+
const specifier = '@gjsify/lightningcss-native';
|
|
111
|
+
const mod = (await import(/* @vite-ignore */ specifier)) as NativeLightningcssSurface;
|
|
112
|
+
if (!mod.hasNativeLightningcss()) return null;
|
|
113
|
+
return async (filename, targets) => {
|
|
114
|
+
// The native shim accepts a browserslist string; the npm
|
|
115
|
+
// `lightningcss` Targets struct is bitfield-encoded
|
|
116
|
+
// (`firefox: 60 << 16` etc). Convert by extracting major
|
|
117
|
+
// version per browser key and re-emitting as the equivalent
|
|
118
|
+
// browserslist query.
|
|
119
|
+
const query = targetsToBrowserslist(targets);
|
|
120
|
+
return mod.bundle({
|
|
121
|
+
filename,
|
|
122
|
+
targets: query,
|
|
123
|
+
minify: false,
|
|
124
|
+
sourceMap: false,
|
|
125
|
+
errorRecovery: true,
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function loadNpmBundler(): Promise<Bundler> {
|
|
134
|
+
const { bundleAsync } = await import('lightningcss');
|
|
135
|
+
return async (filename, targets) => {
|
|
136
|
+
const result = await bundleAsync({
|
|
137
|
+
filename,
|
|
138
|
+
targets,
|
|
139
|
+
minify: false,
|
|
140
|
+
errorRecovery: true,
|
|
141
|
+
});
|
|
142
|
+
return { code: result.code };
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function targetsToBrowserslist(
|
|
147
|
+
targets: import('lightningcss').Targets | undefined,
|
|
148
|
+
): string | undefined {
|
|
149
|
+
if (!targets) return undefined;
|
|
150
|
+
const parts: string[] = [];
|
|
151
|
+
for (const [browser, encoded] of Object.entries(targets) as [string, number | undefined][]) {
|
|
152
|
+
if (typeof encoded !== 'number') continue;
|
|
153
|
+
// npm lightningcss encodes versions as `(major << 16) | (minor << 8) | patch`.
|
|
154
|
+
const major = (encoded >>> 16) & 0xff;
|
|
155
|
+
if (major === 0) continue;
|
|
156
|
+
const name = browser === 'ios_saf' ? 'ios' : browser;
|
|
157
|
+
parts.push(`${name} >= ${major}`);
|
|
158
|
+
}
|
|
159
|
+
return parts.length ? parts.join(', ') : undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
47
162
|
export function cssAsStringPlugin(options: CssAsStringOptions = {}): Plugin {
|
|
48
163
|
const { targets, bundle = true } = options;
|
|
49
164
|
return {
|
|
@@ -67,12 +182,8 @@ async function loadAndBundleCss(
|
|
|
67
182
|
filename: string,
|
|
68
183
|
targets: import('lightningcss').Targets | undefined,
|
|
69
184
|
): Promise<Uint8Array> {
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
minify: false,
|
|
75
|
-
errorRecovery: true,
|
|
76
|
-
});
|
|
77
|
-
return result.code;
|
|
185
|
+
if (!_bundlerPromise) _bundlerPromise = pickBundler();
|
|
186
|
+
const bundler = await _bundlerPromise;
|
|
187
|
+
const { code } = await bundler(filename, targets);
|
|
188
|
+
return code;
|
|
78
189
|
}
|
package/src/plugins/shebang.ts
CHANGED
|
@@ -16,6 +16,30 @@ export interface ShebangPluginOptions {
|
|
|
16
16
|
line?: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Strip a leading `#!…\n` from a source module. Rolldown preserves input
|
|
21
|
+
* shebangs verbatim, which ends up embedded mid-chunk after our process-stub
|
|
22
|
+
* banner — acorn (used by the auto-globals detector) then rejects `#` because
|
|
23
|
+
* it's not at byte 0 anymore. Stripping at the transform stage cleans both
|
|
24
|
+
* the analysis bundle and the final bundle; the gjsify-shebang renderChunk
|
|
25
|
+
* step then injects the correct line for the output target.
|
|
26
|
+
*/
|
|
27
|
+
const SHEBANG_RE = /^#![^\n]*\n/;
|
|
28
|
+
|
|
29
|
+
/** Always-on plugin half: strips input shebangs regardless of output options. */
|
|
30
|
+
export function inputShebangStripPlugin(): Plugin {
|
|
31
|
+
return {
|
|
32
|
+
name: 'gjsify-input-shebang-strip',
|
|
33
|
+
transform: {
|
|
34
|
+
order: 'pre' as const,
|
|
35
|
+
handler(code) {
|
|
36
|
+
if (!code.startsWith('#!')) return null;
|
|
37
|
+
return { code: code.replace(SHEBANG_RE, ''), map: null };
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
19
43
|
export function shebangPlugin(options: ShebangPluginOptions = {}): Plugin | null {
|
|
20
44
|
if (!options.enabled) return null;
|
|
21
45
|
const line = options.line ?? GJS_SHEBANG;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Shim for `unicorn-magic` under --app gjs.
|
|
2
|
+
//
|
|
3
|
+
// The upstream package gates the full API (`toPath`, `traversePathUp`,
|
|
4
|
+
// `rootDirectory`, `execFile`, `execFileSync`) behind the `"node"`
|
|
5
|
+
// conditional exports entry. Under --app gjs we intentionally omit
|
|
6
|
+
// the `node` resolve-condition (cross-fetch-ponyfill ships
|
|
7
|
+
// Node-only code under that key — see `app/gjs.ts` conditionNames
|
|
8
|
+
// comment), so a bare `import { toPath } from 'unicorn-magic'` falls
|
|
9
|
+
// back to `default.js` which only exposes `delay`.
|
|
10
|
+
//
|
|
11
|
+
// This shim mirrors the node.js entry verbatim — the underlying
|
|
12
|
+
// `node:url`/`node:path`/`node:child_process`/`node:util` imports
|
|
13
|
+
// route through `@gjsify/{url,path,child_process,util}` under GJS
|
|
14
|
+
// and through real Node-internals under Node. The aliasPlugin
|
|
15
|
+
// points `unicorn-magic` here for --app gjs builds.
|
|
16
|
+
//
|
|
17
|
+
// Source-of-truth: refs/unicorn-magic/node.js (when added — for
|
|
18
|
+
// now mirrored from node_modules/unicorn-magic@0.3.0).
|
|
19
|
+
|
|
20
|
+
import { promisify } from 'node:util';
|
|
21
|
+
import { execFile as execFileCallback, execFileSync as execFileSyncOriginal } from 'node:child_process';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const execFileOriginal = promisify(execFileCallback);
|
|
26
|
+
|
|
27
|
+
export function toPath(urlOrPath) {
|
|
28
|
+
return urlOrPath instanceof URL ? fileURLToPath(urlOrPath) : urlOrPath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function rootDirectory(pathInput) {
|
|
32
|
+
return path.parse(toPath(pathInput)).root;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function traversePathUp(startPath) {
|
|
36
|
+
return {
|
|
37
|
+
*[Symbol.iterator]() {
|
|
38
|
+
let currentPath = path.resolve(toPath(startPath));
|
|
39
|
+
let previousPath;
|
|
40
|
+
|
|
41
|
+
while (previousPath !== currentPath) {
|
|
42
|
+
yield currentPath;
|
|
43
|
+
previousPath = currentPath;
|
|
44
|
+
currentPath = path.resolve(currentPath, '..');
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const TEN_MEGABYTES_IN_BYTES = 10 * 1024 * 1024;
|
|
51
|
+
|
|
52
|
+
export async function execFile(file, arguments_, options = {}) {
|
|
53
|
+
return execFileOriginal(file, arguments_, {
|
|
54
|
+
maxBuffer: TEN_MEGABYTES_IN_BYTES,
|
|
55
|
+
...options,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function execFileSync(file, arguments_ = [], options = {}) {
|
|
60
|
+
return execFileSyncOriginal(file, arguments_, {
|
|
61
|
+
maxBuffer: TEN_MEGABYTES_IN_BYTES,
|
|
62
|
+
...options,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Re-export from default.js so the union API (delay + node helpers)
|
|
67
|
+
// stays intact for callers that import both.
|
|
68
|
+
export async function delay(opts: { seconds?: number; milliseconds?: number } = {}): Promise<void> {
|
|
69
|
+
const { seconds, milliseconds } = opts;
|
|
70
|
+
let duration: number;
|
|
71
|
+
if (typeof seconds === 'number') duration = seconds * 1000;
|
|
72
|
+
else if (typeof milliseconds === 'number') duration = milliseconds;
|
|
73
|
+
else throw new TypeError('Expected an object with either `seconds` or `milliseconds`.');
|
|
74
|
+
return new Promise<void>((resolveFn) => setTimeout(resolveFn, duration));
|
|
75
|
+
}
|
|
@@ -18,7 +18,41 @@
|
|
|
18
18
|
// alias `globalThis` to a short variable and defeat MemberExpression
|
|
19
19
|
// detection in detect-free-globals.ts.
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import type { InputOptions, RolldownPluginOption, TransformOptions } from 'rolldown';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* In-memory bundle function — returns the per-entry chunk code strings.
|
|
25
|
+
* Implementations: npm rolldown (Node default), `@gjsify/rolldown-native`
|
|
26
|
+
* (GJS). Pulled out so auto-globals can run under either engine without
|
|
27
|
+
* hardcoding npm rolldown (which can't load under GJS — the Rust prebuild's
|
|
28
|
+
* init code uses `require('node:fs')` synchronously).
|
|
29
|
+
*
|
|
30
|
+
* The default impl below dynamically imports npm rolldown; the CLI
|
|
31
|
+
* overrides this from `actions/build.ts` to route via the same engine the
|
|
32
|
+
* final build uses.
|
|
33
|
+
*/
|
|
34
|
+
export type AnalysisBundler = (input: {
|
|
35
|
+
rolldownInput: InputOptions;
|
|
36
|
+
format: 'esm' | 'cjs' | 'iife';
|
|
37
|
+
}) => Promise<string[]>;
|
|
38
|
+
|
|
39
|
+
const defaultBundler: AnalysisBundler = async ({ rolldownInput, format }) => {
|
|
40
|
+
// Indirect specifier so the GJS bundle doesn't pull npm rolldown in
|
|
41
|
+
// statically. Only reached when the caller doesn't override (Node).
|
|
42
|
+
const specifier = 'rolldown';
|
|
43
|
+
const mod = (await import(/* @vite-ignore */ specifier)) as typeof import('rolldown');
|
|
44
|
+
const build = await mod.rolldown(rolldownInput);
|
|
45
|
+
try {
|
|
46
|
+
const result = await build.generate({ format, minify: false, sourcemap: false });
|
|
47
|
+
const codes: string[] = [];
|
|
48
|
+
for (const entry of result.output) {
|
|
49
|
+
if (entry.type === 'chunk') codes.push(entry.code);
|
|
50
|
+
}
|
|
51
|
+
return codes;
|
|
52
|
+
} finally {
|
|
53
|
+
await build.close();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
22
56
|
import { detectFreeGlobals } from './detect-free-globals.js';
|
|
23
57
|
import { resolveGlobalsList, writeRegisterInjectFile } from './scan-globals.js';
|
|
24
58
|
import { GJS_GLOBALS_MAP } from '@gjsify/resolve-npm/globals-map';
|
|
@@ -139,6 +173,7 @@ export async function detectAutoGlobals(
|
|
|
139
173
|
gjsifyPluginFactory: GjsifyPluginFactory,
|
|
140
174
|
verbose?: boolean,
|
|
141
175
|
options: DetectAutoGlobalsOptions = {},
|
|
176
|
+
bundler: AnalysisBundler = defaultBundler,
|
|
142
177
|
): Promise<AutoGlobalsResult> {
|
|
143
178
|
const extraRegisterPaths = options.extraGlobalsList
|
|
144
179
|
? resolveGlobalsList(options.extraGlobalsList)
|
|
@@ -178,29 +213,18 @@ export async function detectAutoGlobals(
|
|
|
178
213
|
? appendInjectAsEntry(analysisOptions.input, currentInject)
|
|
179
214
|
: analysisOptions.input;
|
|
180
215
|
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
216
|
+
const chunkCodes = await bundler({
|
|
217
|
+
rolldownInput: {
|
|
218
|
+
input: inputWithInject,
|
|
219
|
+
external: analysisOptions.external,
|
|
220
|
+
resolve: analysisOptions.resolve,
|
|
221
|
+
transform: analysisOptions.transform,
|
|
222
|
+
plugins: [...callerPlugins, gjsifyInstance],
|
|
223
|
+
logLevel: 'silent',
|
|
224
|
+
},
|
|
225
|
+
format: analysisOptions.format ?? 'esm',
|
|
188
226
|
});
|
|
189
227
|
|
|
190
|
-
const chunkCodes: string[] = [];
|
|
191
|
-
try {
|
|
192
|
-
const result = await build.generate({
|
|
193
|
-
format: analysisOptions.format ?? 'esm',
|
|
194
|
-
minify: false,
|
|
195
|
-
sourcemap: false,
|
|
196
|
-
});
|
|
197
|
-
for (const entry of result.output) {
|
|
198
|
-
if (entry.type === 'chunk') chunkCodes.push((entry as OutputChunk).code);
|
|
199
|
-
}
|
|
200
|
-
} finally {
|
|
201
|
-
await build.close();
|
|
202
|
-
}
|
|
203
|
-
|
|
204
228
|
if (chunkCodes.length === 0) {
|
|
205
229
|
return { detected: new Set(), injectPath: currentInject };
|
|
206
230
|
}
|
|
@@ -211,8 +235,22 @@ export async function detectAutoGlobals(
|
|
|
211
235
|
// top-level declarations: `File`, `Buffer`, …) that acorn can't
|
|
212
236
|
// parse. Per-chunk parsing keeps each chunk's lexical scope intact.
|
|
213
237
|
const newDetected = new Set<string>();
|
|
214
|
-
for (
|
|
215
|
-
|
|
238
|
+
for (let i = 0; i < chunkCodes.length; i++) {
|
|
239
|
+
const code = chunkCodes[i] ?? '';
|
|
240
|
+
try {
|
|
241
|
+
for (const id of detectFreeGlobals(code)) newDetected.add(id);
|
|
242
|
+
} catch (e) {
|
|
243
|
+
if ((globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.GJSIFY_DEBUG_AUTO_GLOBALS) {
|
|
244
|
+
const path = `/tmp/gjsify-auto-globals-failed-chunk-${i}.mjs`;
|
|
245
|
+
try {
|
|
246
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
247
|
+
const fs = await import('node:fs');
|
|
248
|
+
fs.writeFileSync(path, code);
|
|
249
|
+
console.error(`[gjsify-auto-globals] parse failed on chunk #${i} — wrote ${path} for inspection`);
|
|
250
|
+
} catch { /* ignore */ }
|
|
251
|
+
}
|
|
252
|
+
throw e;
|
|
253
|
+
}
|
|
216
254
|
}
|
|
217
255
|
|
|
218
256
|
// Apply excludeGlobals BEFORE writing the next iteration's inject file.
|
|
@@ -101,6 +101,12 @@ export function detectFreeGlobals(code: string): Set<string> {
|
|
|
101
101
|
const ast = acorn.parse(code, {
|
|
102
102
|
ecmaVersion: 'latest',
|
|
103
103
|
sourceType: 'module',
|
|
104
|
+
// Some bundled chunks carry an embedded `#!shebang` line —
|
|
105
|
+
// notably any project bundling its own CLI gets the
|
|
106
|
+
// `#!/usr/bin/env -S gjs -m` shebang hoisted to byte 0.
|
|
107
|
+
// Acorn rejects shebangs by default; allow them so the
|
|
108
|
+
// free-globals analyzer doesn't choke on its own input.
|
|
109
|
+
allowHashBang: true,
|
|
104
110
|
});
|
|
105
111
|
|
|
106
112
|
// --- Pass 1: collect all declared names across the entire module ---
|