@gjsify/rolldown-plugin-gjsify 0.3.21 → 0.4.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.
Files changed (41) hide show
  1. package/lib/app/gjs.js +66 -2
  2. package/lib/plugins/css-as-string.js +98 -11
  3. package/lib/plugins/shebang.d.ts +2 -0
  4. package/lib/plugins/shebang.js +23 -0
  5. package/lib/shims/unicorn-magic.d.ts +14 -0
  6. package/lib/shims/unicorn-magic.js +68 -0
  7. package/lib/utils/auto-globals.d.ts +17 -2
  8. package/lib/utils/auto-globals.js +49 -27
  9. package/lib/utils/detect-free-globals.js +6 -0
  10. package/package.json +72 -64
  11. package/src/app/browser.ts +0 -101
  12. package/src/app/gjs.ts +0 -273
  13. package/src/app/index.ts +0 -6
  14. package/src/app/node.ts +0 -127
  15. package/src/globals.ts +0 -11
  16. package/src/index.ts +0 -34
  17. package/src/library/index.ts +0 -2
  18. package/src/library/lib.ts +0 -141
  19. package/src/plugin.ts +0 -96
  20. package/src/plugins/alias.ts +0 -61
  21. package/src/plugins/css-as-string.ts +0 -78
  22. package/src/plugins/gjs-imports-empty.ts +0 -29
  23. package/src/plugins/process-stub.ts +0 -91
  24. package/src/plugins/rewrite-node-modules-paths.ts +0 -169
  25. package/src/plugins/shebang.ts +0 -69
  26. package/src/plugins/text-loader.ts +0 -54
  27. package/src/shims/console-gjs.ts +0 -25
  28. package/src/types/app.ts +0 -1
  29. package/src/types/index.ts +0 -3
  30. package/src/types/plugin-options.ts +0 -48
  31. package/src/types/resolve-alias-options.ts +0 -1
  32. package/src/utils/alias.ts +0 -46
  33. package/src/utils/auto-globals.ts +0 -283
  34. package/src/utils/detect-free-globals.ts +0 -278
  35. package/src/utils/entry-points.ts +0 -48
  36. package/src/utils/extension.ts +0 -7
  37. package/src/utils/index.ts +0 -7
  38. package/src/utils/inline-static-reads.ts +0 -541
  39. package/src/utils/merge.ts +0 -22
  40. package/src/utils/scan-globals.ts +0 -91
  41. package/tsconfig.json +0 -16
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
- ? resolve(_shimDir, '../shims/console-gjs.js')
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.
@@ -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
- // `lightningcss` is a regular dependency of this package; the plugin
23
- // imports it lazily so missing-arch installs surface the underlying
24
- // load error instead of crashing every gjsify build.
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
- const { bundleAsync } = await import('lightningcss');
46
- const result = await bundleAsync({
47
- filename,
48
- targets,
49
- minify: false,
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
  }
@@ -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
@@ -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 { type InputOptions, type RolldownPluginOption, type TransformOptions } from 'rolldown';
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
- import { rolldown } from 'rolldown';
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 build = await rolldown({
105
- input: inputWithInject,
106
- external: analysisOptions.external,
107
- resolve: analysisOptions.resolve,
108
- transform: analysisOptions.transform,
109
- plugins: [...callerPlugins, gjsifyInstance],
110
- logLevel: 'silent',
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 (const code of chunkCodes) {
137
- for (const id of detectFreeGlobals(code))
138
- newDetected.add(id);
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,68 +1,76 @@
1
1
  {
2
- "name": "@gjsify/rolldown-plugin-gjsify",
3
- "version": "0.3.21",
4
- "description": "Rolldown / Rollup / Vite plugin orchestrator for GJS, Node, and Browser targets",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "module": "lib/index.js",
8
- "types": "lib/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./lib/index.d.ts",
12
- "default": "./lib/index.js"
2
+ "name": "@gjsify/rolldown-plugin-gjsify",
3
+ "version": "0.4.3",
4
+ "description": "Rolldown / Rollup / Vite plugin orchestrator for GJS, Node, and Browser targets",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "module": "lib/index.js",
8
+ "types": "lib/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./lib/index.d.ts",
12
+ "default": "./lib/index.js"
13
+ },
14
+ "./globals": {
15
+ "types": "./lib/globals.d.ts",
16
+ "default": "./lib/globals.js"
17
+ },
18
+ "./shims/console-gjs": {
19
+ "default": "./lib/shims/console-gjs.js"
20
+ }
13
21
  },
14
- "./globals": {
15
- "types": "./lib/globals.d.ts",
16
- "default": "./lib/globals.js"
22
+ "files": [
23
+ "lib"
24
+ ],
25
+ "scripts": {
26
+ "clear": "rm -rf lib tsconfig.tsbuildinfo || exit 0",
27
+ "check": "tsc --noEmit",
28
+ "build": "tsc"
17
29
  },
18
- "./shims/console-gjs": {
19
- "default": "./lib/shims/console-gjs.js"
20
- }
21
- },
22
- "scripts": {
23
- "clear": "rm -rf lib tsconfig.tsbuildinfo || exit 0",
24
- "check": "tsc --noEmit",
25
- "build": "tsc"
26
- },
27
- "repository": {
28
- "type": "git",
29
- "url": "git+https://github.com/gjsify/gjsify.git"
30
- },
31
- "bugs": {
32
- "url": "https://github.com/gjsify/gjsify/issues"
33
- },
34
- "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/infra/rolldown-plugin-gjsify#readme",
35
- "keywords": [
36
- "gjs",
37
- "rolldown",
38
- "rollup",
39
- "vite",
40
- "plugin",
41
- "gjsify"
42
- ],
43
- "license": "MIT",
44
- "dependencies": {
45
- "@gjsify/resolve-npm": "^0.3.21",
46
- "@gjsify/rolldown-plugin-deepkit": "^0.3.21",
47
- "@gjsify/rolldown-plugin-pnp": "^0.3.21",
48
- "@gjsify/vite-plugin-blueprint": "^0.3.21",
49
- "@rollup/pluginutils": "^5.3.0",
50
- "acorn": "^8.16.0",
51
- "acorn-walk": "^8.3.5",
52
- "fast-glob": "^3.3.3",
53
- "lightningcss": "^1.32.0"
54
- },
55
- "peerDependencies": {
56
- "rolldown": "^1.0.0-rc.18"
57
- },
58
- "peerDependenciesMeta": {
59
- "rolldown": {
60
- "optional": true
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/gjsify/gjsify.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/gjsify/gjsify/issues"
36
+ },
37
+ "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/infra/rolldown-plugin-gjsify#readme",
38
+ "keywords": [
39
+ "gjs",
40
+ "rolldown",
41
+ "rollup",
42
+ "vite",
43
+ "plugin",
44
+ "gjsify"
45
+ ],
46
+ "license": "MIT",
47
+ "dependencies": {
48
+ "@gjsify/console": "workspace:^",
49
+ "@gjsify/resolve-npm": "workspace:^",
50
+ "@gjsify/rolldown-plugin-deepkit": "workspace:^",
51
+ "@gjsify/rolldown-plugin-pnp": "workspace:^",
52
+ "@gjsify/vite-plugin-blueprint": "workspace:^",
53
+ "@rollup/pluginutils": "^5.3.0",
54
+ "acorn": "^8.16.0",
55
+ "acorn-walk": "^8.3.5",
56
+ "fast-glob": "^3.3.3",
57
+ "lightningcss": "^1.32.0"
58
+ },
59
+ "peerDependencies": {
60
+ "@gjsify/lightningcss-native": "workspace:^",
61
+ "rolldown": "^1.0.0-rc.18"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "@gjsify/lightningcss-native": {
65
+ "optional": true
66
+ },
67
+ "rolldown": {
68
+ "optional": true
69
+ }
70
+ },
71
+ "devDependencies": {
72
+ "@types/node": "^25.6.2",
73
+ "rolldown": "^1.0.0",
74
+ "typescript": "^6.0.3"
61
75
  }
62
- },
63
- "devDependencies": {
64
- "@types/node": "^25.6.2",
65
- "rolldown": "^1.0.0",
66
- "typescript": "^6.0.3"
67
- }
68
- }
76
+ }