@gjsify/rolldown-plugin-gjsify 0.3.14

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 (89) hide show
  1. package/lib/app/browser.d.ts +17 -0
  2. package/lib/app/browser.js +77 -0
  3. package/lib/app/gjs.d.ts +27 -0
  4. package/lib/app/gjs.js +211 -0
  5. package/lib/app/index.d.ts +6 -0
  6. package/lib/app/index.js +3 -0
  7. package/lib/app/node.d.ts +17 -0
  8. package/lib/app/node.js +102 -0
  9. package/lib/globals.d.ts +4 -0
  10. package/lib/globals.js +9 -0
  11. package/lib/index.d.ts +17 -0
  12. package/lib/index.js +15 -0
  13. package/lib/library/index.d.ts +2 -0
  14. package/lib/library/index.js +1 -0
  15. package/lib/library/lib.d.ts +16 -0
  16. package/lib/library/lib.js +118 -0
  17. package/lib/plugin.d.ts +25 -0
  18. package/lib/plugin.js +67 -0
  19. package/lib/plugins/alias.d.ts +5 -0
  20. package/lib/plugins/alias.js +45 -0
  21. package/lib/plugins/css-as-string.d.ts +2 -0
  22. package/lib/plugins/css-as-string.js +34 -0
  23. package/lib/plugins/gjs-imports-empty.d.ts +2 -0
  24. package/lib/plugins/gjs-imports-empty.js +26 -0
  25. package/lib/plugins/process-stub.d.ts +28 -0
  26. package/lib/plugins/process-stub.js +60 -0
  27. package/lib/plugins/rewrite-node-modules-paths.d.ts +38 -0
  28. package/lib/plugins/rewrite-node-modules-paths.js +132 -0
  29. package/lib/plugins/shebang.d.ts +8 -0
  30. package/lib/plugins/shebang.js +26 -0
  31. package/lib/shims/console-gjs.d.ts +24 -0
  32. package/lib/shims/console-gjs.js +24 -0
  33. package/lib/types/app.d.ts +1 -0
  34. package/lib/types/app.js +1 -0
  35. package/lib/types/index.d.ts +3 -0
  36. package/lib/types/index.js +3 -0
  37. package/lib/types/plugin-options.d.ts +46 -0
  38. package/lib/types/plugin-options.js +1 -0
  39. package/lib/types/resolve-alias-options.d.ts +2 -0
  40. package/lib/types/resolve-alias-options.js +1 -0
  41. package/lib/utils/alias.d.ts +12 -0
  42. package/lib/utils/alias.js +29 -0
  43. package/lib/utils/auto-globals.d.ts +72 -0
  44. package/lib/utils/auto-globals.js +193 -0
  45. package/lib/utils/detect-free-globals.d.ts +18 -0
  46. package/lib/utils/detect-free-globals.js +268 -0
  47. package/lib/utils/entry-points.d.ts +2 -0
  48. package/lib/utils/entry-points.js +38 -0
  49. package/lib/utils/extension.d.ts +1 -0
  50. package/lib/utils/extension.js +7 -0
  51. package/lib/utils/index.d.ts +7 -0
  52. package/lib/utils/index.js +7 -0
  53. package/lib/utils/inline-static-reads.d.ts +11 -0
  54. package/lib/utils/inline-static-reads.js +549 -0
  55. package/lib/utils/merge.d.ts +2 -0
  56. package/lib/utils/merge.js +23 -0
  57. package/lib/utils/scan-globals.d.ts +32 -0
  58. package/lib/utils/scan-globals.js +85 -0
  59. package/package.json +68 -0
  60. package/src/app/browser.ts +102 -0
  61. package/src/app/gjs.ts +260 -0
  62. package/src/app/index.ts +6 -0
  63. package/src/app/node.ts +128 -0
  64. package/src/globals.ts +11 -0
  65. package/src/index.ts +32 -0
  66. package/src/library/index.ts +2 -0
  67. package/src/library/lib.ts +142 -0
  68. package/src/plugin.ts +91 -0
  69. package/src/plugins/alias.ts +53 -0
  70. package/src/plugins/css-as-string.ts +37 -0
  71. package/src/plugins/gjs-imports-empty.ts +29 -0
  72. package/src/plugins/process-stub.ts +91 -0
  73. package/src/plugins/rewrite-node-modules-paths.ts +169 -0
  74. package/src/plugins/shebang.ts +33 -0
  75. package/src/shims/console-gjs.ts +25 -0
  76. package/src/types/app.ts +1 -0
  77. package/src/types/index.ts +3 -0
  78. package/src/types/plugin-options.ts +48 -0
  79. package/src/types/resolve-alias-options.ts +1 -0
  80. package/src/utils/alias.ts +46 -0
  81. package/src/utils/auto-globals.ts +283 -0
  82. package/src/utils/detect-free-globals.ts +278 -0
  83. package/src/utils/entry-points.ts +48 -0
  84. package/src/utils/extension.ts +7 -0
  85. package/src/utils/index.ts +7 -0
  86. package/src/utils/inline-static-reads.ts +541 -0
  87. package/src/utils/merge.ts +22 -0
  88. package/src/utils/scan-globals.ts +91 -0
  89. package/tsconfig.json +16 -0
@@ -0,0 +1,85 @@
1
+ // Explicit `--globals` CLI flag support.
2
+ //
3
+ // This module resolves a user-provided comma-separated list of global
4
+ // identifiers (e.g. `fetch,Buffer,process,URL,crypto`) into the
5
+ // corresponding set of `@gjsify/<pkg>/register` subpaths and writes an
6
+ // ESM stub file that the esbuild plugin injects via its
7
+ // `autoGlobalsInject` option.
8
+ //
9
+ // gjsify does NOT scan user code to guess which globals are needed —
10
+ // the user declares them explicitly via `gjsify build --globals <list>`
11
+ // (or via the default script scaffolded by `@gjsify/create-app`). See
12
+ // the "Tree-shakeable Globals" section in AGENTS.md for the rationale.
13
+ import { writeFile, mkdir } from 'node:fs/promises';
14
+ import { createHash } from 'node:crypto';
15
+ import { join } from 'node:path';
16
+ import { GJS_GLOBALS_MAP, GJS_GLOBALS_GROUPS } from '@gjsify/resolve-npm/globals-map';
17
+ const GLOBALS_MAP = GJS_GLOBALS_MAP;
18
+ const GLOBALS_GROUPS = GJS_GLOBALS_GROUPS;
19
+ /**
20
+ * Resolve a `--globals` CLI argument into the set of `/register` subpaths
21
+ * that must be injected into the build.
22
+ *
23
+ * The argument is a comma-separated list of identifiers or group names.
24
+ * Group names (`node`, `web`, `dom`) expand to all identifiers in that group.
25
+ * Unknown tokens are silently ignored. Empty or whitespace-only input returns
26
+ * an empty set.
27
+ *
28
+ * Examples:
29
+ * resolveGlobalsList('fetch,Buffer,process')
30
+ * → Set { 'fetch/register', '@gjsify/buffer/register', '@gjsify/node-globals/register' }
31
+ *
32
+ * resolveGlobalsList('node,web')
33
+ * → Set { '@gjsify/buffer/register', '@gjsify/node-globals/register', 'fetch/register', … }
34
+ *
35
+ * resolveGlobalsList('')
36
+ * → Set { }
37
+ */
38
+ export function resolveGlobalsList(globalsArg) {
39
+ const result = new Set();
40
+ const trimmed = globalsArg.trim();
41
+ if (!trimmed)
42
+ return result;
43
+ for (const rawToken of trimmed.split(',')) {
44
+ const token = rawToken.trim();
45
+ if (!token)
46
+ continue;
47
+ const group = GLOBALS_GROUPS[token];
48
+ if (group) {
49
+ for (const id of group) {
50
+ const path = GLOBALS_MAP[id];
51
+ if (path)
52
+ result.add(path);
53
+ }
54
+ }
55
+ else {
56
+ const path = GLOBALS_MAP[token];
57
+ if (path)
58
+ result.add(path);
59
+ }
60
+ }
61
+ return result;
62
+ }
63
+ /**
64
+ * Write a stub ESM file with `import` statements for the given register
65
+ * paths and return its absolute path, suitable for passing to esbuild's
66
+ * `inject` option via the plugin's `autoGlobalsInject` field.
67
+ *
68
+ * The file lives inside `<cwd>/node_modules/.cache/gjsify/` so esbuild's
69
+ * module resolver can follow the bare specifiers in the generated imports.
70
+ *
71
+ * The file name is hashed by content so repeated builds with the same
72
+ * set reuse the same file (no churn, idempotent on disk).
73
+ */
74
+ export async function writeRegisterInjectFile(registerPaths, cwd = process.cwd()) {
75
+ if (registerPaths.size === 0)
76
+ return null;
77
+ const sorted = [...registerPaths].sort();
78
+ const content = sorted.map((p) => `import '${p}';`).join('\n') + '\n';
79
+ const hash = createHash('sha1').update(content).digest('hex').slice(0, 10);
80
+ const cacheDir = join(cwd, 'node_modules', '.cache', 'gjsify');
81
+ await mkdir(cacheDir, { recursive: true });
82
+ const path = join(cacheDir, `auto-globals-${hash}.mjs`);
83
+ await writeFile(path, content, 'utf-8');
84
+ return path;
85
+ }
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@gjsify/rolldown-plugin-gjsify",
3
+ "version": "0.3.14",
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
+ }
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.14",
46
+ "@gjsify/rolldown-plugin-deepkit": "^0.3.14",
47
+ "@gjsify/rolldown-plugin-pnp": "^0.3.14",
48
+ "@gjsify/vite-plugin-blueprint": "^0.3.14",
49
+ "@rollup/pluginutils": "^5.1.4",
50
+ "acorn": "^8.14.0",
51
+ "acorn-walk": "^8.3.4",
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
61
+ }
62
+ },
63
+ "devDependencies": {
64
+ "@types/node": "^25.6.0",
65
+ "rolldown": "^1.0.0-rc.18",
66
+ "typescript": "^6.0.3"
67
+ }
68
+ }
@@ -0,0 +1,102 @@
1
+ // `--app browser` Rolldown configuration factory.
2
+ //
3
+ // Browser builds redirect `@girs/*` and `gi://*` to an empty virtual module
4
+ // (they appear transitively via `@gjsify/unit` and similar packages with
5
+ // GJS-specific code paths). Standard Node.js → browser polyfill aliases
6
+ // for `process` and `assert` keep `@gjsify/unit`'s top-level imports
7
+ // resolvable in a browser bundle.
8
+
9
+ import { aliasPlugin } from '../plugins/alias.js';
10
+ import type { RolldownOptions, RolldownPluginOption } from 'rolldown';
11
+
12
+ import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
13
+ import blueprintPlugin from '@gjsify/vite-plugin-blueprint';
14
+
15
+ import type { PluginOptions } from '../types/plugin-options.js';
16
+ import { globToEntryPoints } from '../utils/entry-points.js';
17
+ import { gjsImportsEmptyPlugin } from '../plugins/gjs-imports-empty.js';
18
+ import { cssAsStringPlugin } from '../plugins/css-as-string.js';
19
+
20
+ export interface BrowserBuildConfig {
21
+ options: RolldownOptions;
22
+ plugins: RolldownPluginOption[];
23
+ }
24
+
25
+ export interface BrowserFactoryInput {
26
+ input?: RolldownOptions['input'];
27
+ output: { file?: string; dir?: string };
28
+ userExternal?: string[];
29
+ userAliases?: Record<string, string>;
30
+ pluginOptions: PluginOptions;
31
+ }
32
+
33
+ export const setupForBrowser = async (input: BrowserFactoryInput): Promise<BrowserBuildConfig> => {
34
+ const userExternal = input.userExternal ?? [];
35
+ const external = [...userExternal];
36
+
37
+ const exclude = input.pluginOptions.exclude ?? [];
38
+ const entryPoints = await globToEntryPoints(input.input, exclude);
39
+
40
+ // `@gjsify/unit` has `await import('process')` inside a try-catch that
41
+ // is unreachable in browser (typeof document check comes first), but
42
+ // Rolldown still resolves it statically. Map to `@gjsify/empty` so the
43
+ // build succeeds. `assert` → `@gjsify/assert` because `@gjsify/unit`
44
+ // imports `node:assert` at the top level.
45
+ const browserPolyfillAliases: Record<string, string> = {
46
+ process: '@gjsify/empty',
47
+ 'node:process': '@gjsify/empty',
48
+ assert: '@gjsify/assert',
49
+ 'node:assert': '@gjsify/assert',
50
+ };
51
+
52
+ const aliasMap: Record<string, string> = {
53
+ ...browserPolyfillAliases,
54
+ ...(input.pluginOptions.aliases ?? {}),
55
+ ...(input.userAliases ?? {}),
56
+ };
57
+
58
+ const options: RolldownOptions = {
59
+ input: entryPoints,
60
+ platform: 'browser',
61
+ external,
62
+ resolve: {
63
+ mainFields: ['browser', 'module', 'main'],
64
+ conditionNames: ['import', 'browser'],
65
+ },
66
+ transform: {
67
+ target: 'esnext',
68
+ define: {
69
+ global: 'globalThis',
70
+ window: 'globalThis',
71
+ },
72
+ },
73
+ output: {
74
+ ...input.output,
75
+ format: 'esm',
76
+ minify: false,
77
+ sourcemap: false,
78
+ // Single-bundle output. `codeSplitting: false` replaces the
79
+ // deprecated `inlineDynamicImports: true`.
80
+ codeSplitting: false,
81
+ },
82
+ treeshake: true,
83
+ };
84
+
85
+ const plugins: RolldownPluginOption[] = [
86
+ gjsImportsEmptyPlugin(),
87
+ aliasPlugin({ entries: flattenAliases(aliasMap) }),
88
+ blueprintPlugin() as RolldownPluginOption,
89
+ deepkitPlugin({ reflection: input.pluginOptions.reflection }),
90
+ cssAsStringPlugin(),
91
+ ];
92
+
93
+ return { options, plugins };
94
+ };
95
+
96
+ function flattenAliases(map: Record<string, string>): Record<string, string> {
97
+ const out: Record<string, string> = {};
98
+ for (const [from, to] of Object.entries(map)) {
99
+ if (to) out[from] = to;
100
+ }
101
+ return out;
102
+ }
package/src/app/gjs.ts ADDED
@@ -0,0 +1,260 @@
1
+ // `--app gjs` Rolldown configuration factory.
2
+ //
3
+ // Mirrors the esbuild predecessor's `setupForGjs` exactly in terms of the
4
+ // effective build behaviour: same externals, same alias map, same target
5
+ // (firefox128 for JS, firefox60 for CSS), same console-shim injection,
6
+ // same process-stub banner, same `random-access-file` fs-backed-fallback.
7
+ //
8
+ // Returns a partial `RolldownOptions` template plus the plugin array the
9
+ // caller should compose with their user-supplied options. Library mode is
10
+ // handled separately by `setupLib`.
11
+
12
+ import { fileURLToPath } from 'node:url';
13
+ import { dirname, resolve } from 'node:path';
14
+ import type { RolldownOptions, RolldownPluginOption } from 'rolldown';
15
+ import { aliasPlugin } from '../plugins/alias.js';
16
+
17
+ import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
18
+ import blueprintPlugin from '@gjsify/vite-plugin-blueprint';
19
+
20
+ import type { PluginOptions } from '../types/plugin-options.js';
21
+ import { getAliasesForGjs } from '../utils/alias.js';
22
+ import { globToEntryPoints } from '../utils/entry-points.js';
23
+ import {
24
+ nodeModulesPathRewritePlugin,
25
+ getBundleDirFromOutput,
26
+ } from '../plugins/rewrite-node-modules-paths.js';
27
+ import { processStubPlugin } from '../plugins/process-stub.js';
28
+ import { cssAsStringPlugin } from '../plugins/css-as-string.js';
29
+ import { shebangPlugin, GJS_SHEBANG } from '../plugins/shebang.js';
30
+
31
+ const _shimDir = dirname(fileURLToPath(import.meta.url));
32
+
33
+ /** Resolved Rolldown configuration template + plugins for `--app gjs`. */
34
+ export interface GjsBuildConfig {
35
+ options: RolldownOptions;
36
+ plugins: RolldownPluginOption[];
37
+ }
38
+
39
+ export interface GjsFactoryInput {
40
+ /** User entry points after CLI / config merging. */
41
+ input?: RolldownOptions['input'];
42
+ /** Output `file` or `dir` so `import.meta.url` rewriter knows the bundle path. */
43
+ output: { file?: string; dir?: string };
44
+ /** Caller-supplied externals (`gjsify build --external`). */
45
+ userExternal?: string[];
46
+ /** User-supplied banner string (may contain a leading `#!shebang`). */
47
+ userBanner?: string;
48
+ /** User-supplied resolve.alias overrides. */
49
+ userAliases?: Record<string, string>;
50
+ /** Whether to prepend the `#!/usr/bin/env -S gjs -m` shebang. */
51
+ shebang?: boolean;
52
+ /** Plugin options forwarded to sub-plugins (deepkit, css, …). */
53
+ pluginOptions: PluginOptions;
54
+ }
55
+
56
+ export const setupForGjs = async (input: GjsFactoryInput): Promise<GjsBuildConfig> => {
57
+ const userExternal = input.userExternal ?? [];
58
+ // Rolldown's `external` array does not support glob patterns the way
59
+ // esbuild's did (`gi://*`). We use a function predicate so any
60
+ // `gi://Foo?version=…` URI matches by prefix and the GJS-built-in
61
+ // string specifiers stay externalised by name.
62
+ const exactExternal = ['cairo', 'gettext', 'system', ...userExternal];
63
+ const external = (id: string): boolean => {
64
+ if (id.startsWith('gi://')) return true;
65
+ if (exactExternal.includes(id)) return true;
66
+ return false;
67
+ };
68
+ const format = input.pluginOptions.format ?? 'esm';
69
+
70
+ const exclude = input.pluginOptions.exclude ?? [];
71
+ const entryPoints = await globToEntryPoints(input.input, exclude);
72
+
73
+ const aliasMap = {
74
+ ...getAliasesForGjs({ external }),
75
+ ...(input.pluginOptions.aliases ?? {}),
76
+ ...(input.userAliases ?? {}),
77
+ };
78
+
79
+ // The console shim replaces all `console` references with print()/printerr()-
80
+ // based implementations that bypass GLib.log_structured() — no prefix,
81
+ // ANSI codes work. Disabled via `pluginOptions.consoleShim === false`.
82
+ const consoleShimEnabled = input.pluginOptions.consoleShim !== false;
83
+ const consoleShimPath = consoleShimEnabled
84
+ ? resolve(_shimDir, '../shims/console-gjs.js')
85
+ : null;
86
+
87
+ // The auto-globals inject stub (when present) is side-effect-imported
88
+ // via a virtual entry — its register modules write to globalThis, so
89
+ // the import chain matters but no name binding does. We can't use
90
+ // Rolldown's `inject` for this because the auto-globals invariant
91
+ // forbids source-AST rewrites for global identifiers (false positives
92
+ // from isomorphic guards / bracket access — see AGENTS.md).
93
+ const sideEffectImports: string[] = [];
94
+ if (input.pluginOptions.autoGlobalsInject) sideEffectImports.push(input.pluginOptions.autoGlobalsInject);
95
+
96
+ const virtualEntries = wrapInputWithSideEffects(entryPoints, sideEffectImports);
97
+ const finalInput = virtualEntries.input;
98
+
99
+ const options: RolldownOptions = {
100
+ input: finalInput,
101
+ platform: 'neutral',
102
+ external,
103
+ // 'browser' field is needed so packages like create-hash, create-hmac,
104
+ // randombytes use their pure-JS browser entry instead of index.js
105
+ // (which does require('crypto') and causes circular dependencies via
106
+ // the crypto → @gjsify/crypto alias).
107
+ resolve: {
108
+ mainFields: format === 'esm' ? ['browser', 'module', 'main'] : ['browser', 'main', 'module'],
109
+ // ESM: omit 'require' — packages listing 'require' before 'import'
110
+ // would silently route through their CJS entry.
111
+ conditionNames: format === 'esm' ? ['browser', 'import'] : ['browser', 'require', 'import'],
112
+ },
113
+ transform: {
114
+ // Compile target: GJS 1.86 / SpiderMonkey 128 ≈ firefox128.
115
+ target: 'firefox128',
116
+ define: {
117
+ global: 'globalThis',
118
+ window: 'globalThis',
119
+ 'process.env.READABLE_STREAM': '"disable"',
120
+ },
121
+ // Console shim: rewrite bare `console` references to a named
122
+ // import from our shim module. We use Rolldown's `inject`
123
+ // (Oxc-driven, lives under `transform`) because:
124
+ // 1. `globalThis.console` is non-configurable on SpiderMonkey
125
+ // 128 so a register-style global write throws.
126
+ // 2. We're replacing console unconditionally — there's no
127
+ // tree-shake-aware detection concern that motivated the
128
+ // auto-globals invariant.
129
+ ...(consoleShimPath ? { inject: { console: [consoleShimPath, 'console'] } } : {}),
130
+ },
131
+ output: {
132
+ ...input.output,
133
+ format,
134
+ minify: false,
135
+ sourcemap: false,
136
+ // App builds emit a single bundle file. Disable code-splitting
137
+ // so dynamic imports get inlined and the entire program lands
138
+ // in one chunk that matches `gjsify build --outfile foo.js`.
139
+ // (`codeSplitting: false` replaces the deprecated
140
+ // `inlineDynamicImports: true` in Rolldown ≥ 1.0-rc.18.)
141
+ codeSplitting: false,
142
+ },
143
+ treeshake: true,
144
+ };
145
+
146
+ const bundleDir = getBundleDirFromOutput(input.output);
147
+
148
+ const plugins: RolldownPluginOption[] = [
149
+ // Virtual-entry plugin runs FIRST so its resolveId/load match the
150
+ // synthetic input ids that `wrapInputWithSideEffects` produces.
151
+ ...(virtualEntries.plugin ? [virtualEntries.plugin] : []),
152
+ // random-access-file's 'browser' field maps to a throwing stub; force
153
+ // the fs-backed Node entry. Implemented via the gjsify alias plugin
154
+ // as a direct entry-table override.
155
+ aliasPlugin({
156
+ entries: {
157
+ 'random-access-file': 'random-access-file/index.js',
158
+ ...flattenAliases(aliasMap),
159
+ },
160
+ }),
161
+ blueprintPlugin() as RolldownPluginOption,
162
+ deepkitPlugin({ reflection: input.pluginOptions.reflection }),
163
+ cssAsStringPlugin(),
164
+ nodeModulesPathRewritePlugin({ bundleDir }),
165
+ processStubPlugin({ userBanner: input.userBanner }),
166
+ shebangPlugin({ enabled: input.shebang === true, line: GJS_SHEBANG }),
167
+ ];
168
+
169
+ return { options, plugins };
170
+ };
171
+
172
+ interface VirtualEntriesResult {
173
+ input: RolldownOptions['input'];
174
+ plugin: RolldownPluginOption | null;
175
+ }
176
+
177
+ /**
178
+ * If there are side-effect imports to land alongside the user's entry,
179
+ * wrap each entry in a virtual module that imports them first then
180
+ * re-exports the entry. Returns the rewritten `input` plus the resolveId/load
181
+ * plugin that resolves the virtual ids.
182
+ *
183
+ * Single-input case: `'src/index.ts'` → `'\0gjsify-entry:src/index.ts'`.
184
+ * Array-input case: each element gets the same wrapper id.
185
+ * Record-input case: values get wrapped, keys preserved.
186
+ *
187
+ * `\0`-prefixed ids are Rollup's convention for synthetic modules — Rolldown
188
+ * recognises and treats them as not-from-disk, skipping the default loader.
189
+ */
190
+ function wrapInputWithSideEffects(
191
+ input: RolldownOptions['input'],
192
+ sideEffects: string[],
193
+ ): VirtualEntriesResult {
194
+ if (sideEffects.length === 0 || input === undefined) {
195
+ return { input, plugin: null };
196
+ }
197
+
198
+ const userEntries = new Map<string, string>(); // virtualId → realPath
199
+ const PREFIX = '\0gjsify-entry:';
200
+
201
+ function wrap(realPath: string): string {
202
+ const id = PREFIX + realPath;
203
+ userEntries.set(id, realPath);
204
+ return id;
205
+ }
206
+
207
+ let wrappedInput: RolldownOptions['input'];
208
+ if (typeof input === 'string') {
209
+ wrappedInput = wrap(input);
210
+ } else if (Array.isArray(input)) {
211
+ wrappedInput = input.map(wrap);
212
+ } else {
213
+ const out: Record<string, string> = {};
214
+ for (const [name, path] of Object.entries(input)) {
215
+ out[name] = wrap(path);
216
+ }
217
+ wrappedInput = out;
218
+ }
219
+
220
+ const sideEffectImports = sideEffects
221
+ .map((p) => `import ${JSON.stringify(p)};`)
222
+ .join('\n');
223
+
224
+ const plugin: RolldownPluginOption = {
225
+ name: 'gjsify-virtual-entry',
226
+ async resolveId(source, importer) {
227
+ if (source.startsWith(PREFIX)) return source;
228
+ return null;
229
+ },
230
+ async load(id) {
231
+ if (!id.startsWith(PREFIX)) return null;
232
+ const realPath = userEntries.get(id);
233
+ if (!realPath) return null;
234
+ // Resolve the user-provided entry path through the full
235
+ // resolver chain so the re-export targets a real on-disk
236
+ // module — otherwise Rolldown treats `src/foo.ts` as a bare
237
+ // specifier and emits it as an external import.
238
+ const resolved = await this.resolve(realPath, undefined, { skipSelf: true });
239
+ const target = resolved?.id ?? realPath;
240
+ return {
241
+ code: `${sideEffectImports}\nexport * from ${JSON.stringify(target)};\n`,
242
+ moduleSideEffects: true,
243
+ };
244
+ },
245
+ };
246
+
247
+ return { input: wrappedInput, plugin };
248
+ }
249
+
250
+ /**
251
+ * Flatten the legacy `Record<string, string>` alias map into the
252
+ * `@rollup/plugin-alias` `entries` array shape, dropping empty values.
253
+ */
254
+ function flattenAliases(map: Record<string, string>): Record<string, string> {
255
+ const out: Record<string, string> = {};
256
+ for (const [from, to] of Object.entries(map)) {
257
+ if (to) out[from] = to;
258
+ }
259
+ return out;
260
+ }
@@ -0,0 +1,6 @@
1
+ export { setupForGjs } from './gjs.js';
2
+ export type { GjsBuildConfig, GjsFactoryInput } from './gjs.js';
3
+ export { setupForNode } from './node.js';
4
+ export type { NodeBuildConfig, NodeFactoryInput } from './node.js';
5
+ export { setupForBrowser } from './browser.js';
6
+ export type { BrowserBuildConfig, BrowserFactoryInput } from './browser.js';
@@ -0,0 +1,128 @@
1
+ // `--app node` Rolldown configuration factory.
2
+ //
3
+ // Same external set + alias map as the esbuild predecessor. The
4
+ // `createRequire` banner that esbuild needed for ESM-output CJS interop
5
+ // translates to Rolldown's `output.banner` directly — Rolldown itself does
6
+ // not synthesise a `require()` shim for ESM consumers of bundled CJS code.
7
+
8
+ import { aliasPlugin } from '../plugins/alias.js';
9
+ import type { RolldownOptions, RolldownPluginOption } from 'rolldown';
10
+
11
+ import { deepkitPlugin } from '@gjsify/rolldown-plugin-deepkit';
12
+ import { EXTERNALS_NODE } from '@gjsify/resolve-npm';
13
+
14
+ import type { PluginOptions } from '../types/plugin-options.js';
15
+ import { getAliasesForNode } from '../utils/alias.js';
16
+ import { globToEntryPoints } from '../utils/entry-points.js';
17
+ import {
18
+ nodeModulesPathRewritePlugin,
19
+ getBundleDirFromOutput,
20
+ } from '../plugins/rewrite-node-modules-paths.js';
21
+ import { cssAsStringPlugin } from '../plugins/css-as-string.js';
22
+
23
+ export interface NodeBuildConfig {
24
+ options: RolldownOptions;
25
+ plugins: RolldownPluginOption[];
26
+ }
27
+
28
+ export interface NodeFactoryInput {
29
+ input?: RolldownOptions['input'];
30
+ output: { file?: string; dir?: string };
31
+ userExternal?: string[];
32
+ userAliases?: Record<string, string>;
33
+ pluginOptions: PluginOptions;
34
+ }
35
+
36
+ export const setupForNode = async (input: NodeFactoryInput): Promise<NodeBuildConfig> => {
37
+ const userExternal = input.userExternal ?? [];
38
+ // node-datachannel is a native C++ addon that cannot be bundled — its
39
+ // `require('../build/Release/node_datachannel.node')` must resolve at
40
+ // runtime against the real node_modules tree.
41
+ //
42
+ // Note: Rolldown's `external` array does NOT support glob patterns the
43
+ // way esbuild's did (`gi://*`, `@girs/*`). We use a function predicate
44
+ // instead so the gi:// URI scheme and the @girs/ namespace are matched
45
+ // by prefix.
46
+ const exactExternal = [
47
+ ...EXTERNALS_NODE as string[],
48
+ 'node-datachannel',
49
+ ...userExternal,
50
+ ];
51
+ const external = (id: string): boolean => {
52
+ if (id.startsWith('gi://')) return true;
53
+ if (id.startsWith('@girs/')) return true;
54
+ if (exactExternal.includes(id)) return true;
55
+ return false;
56
+ };
57
+ const format = input.pluginOptions.format ?? 'esm';
58
+
59
+ const exclude = input.pluginOptions.exclude ?? [];
60
+ const entryPoints = await globToEntryPoints(input.input, exclude);
61
+
62
+ const aliasMap = {
63
+ ...getAliasesForNode({ external }),
64
+ ...(input.pluginOptions.aliases ?? {}),
65
+ ...(input.userAliases ?? {}),
66
+ };
67
+
68
+ const bundleDir = getBundleDirFromOutput(input.output);
69
+
70
+ // Rolldown's CJS interop wraps bundled CJS via `__commonJSMin` and
71
+ // routes external Node-builtin `require()` through `__require` —
72
+ // both injected internally. Unlike esbuild we therefore don't need a
73
+ // top-of-bundle `const require = createRequire(...)` shim. Keeping
74
+ // one collides with bundled CJS sources that declare their own
75
+ // `const require = createRequire(...)` (e.g. yargs's ESM platform
76
+ // shim) — `SyntaxError: Identifier 'require' has already been
77
+ // declared`.
78
+ const banner: string | undefined = undefined;
79
+
80
+ const options: RolldownOptions = {
81
+ input: entryPoints,
82
+ platform: 'node',
83
+ external,
84
+ resolve: {
85
+ mainFields: format === 'esm' ? ['module', 'main', 'browser'] : ['main', 'module', 'browser'],
86
+ // CJS-priority conditions for Node bundles. Rolldown uses the first
87
+ // matching key, so including 'import' would route packages like ws
88
+ // v8 (whose exports map lists 'import' before 'require') through
89
+ // their incomplete ESM wrapper.
90
+ conditionNames: format === 'esm' ? ['require', 'node', 'module'] : ['require'],
91
+ },
92
+ transform: {
93
+ target: 'node24',
94
+ define: {
95
+ global: 'globalThis',
96
+ window: 'globalThis',
97
+ },
98
+ },
99
+ output: {
100
+ ...input.output,
101
+ format,
102
+ minify: false,
103
+ sourcemap: false,
104
+ banner,
105
+ // Single-bundle output. `codeSplitting: false` replaces the
106
+ // deprecated `inlineDynamicImports: true`.
107
+ codeSplitting: false,
108
+ },
109
+ treeshake: true,
110
+ };
111
+
112
+ const plugins: RolldownPluginOption[] = [
113
+ aliasPlugin({ entries: flattenAliases(aliasMap) }),
114
+ deepkitPlugin({ reflection: input.pluginOptions.reflection }),
115
+ cssAsStringPlugin(),
116
+ nodeModulesPathRewritePlugin({ bundleDir }),
117
+ ];
118
+
119
+ return { options, plugins };
120
+ };
121
+
122
+ function flattenAliases(map: Record<string, string>): Record<string, string> {
123
+ const out: Record<string, string> = {};
124
+ for (const [from, to] of Object.entries(map)) {
125
+ if (to) out[from] = to;
126
+ }
127
+ return out;
128
+ }
package/src/globals.ts ADDED
@@ -0,0 +1,11 @@
1
+ // Public subpath export for the `--globals` CLI support.
2
+ //
3
+ // Consumed by `@gjsify/cli` to resolve the user's explicit `--globals` list
4
+ // (or auto-detect via the iterative multi-pass build) and write the inject
5
+ // stub that the plugin picks up via its `autoGlobalsInject` option. See the
6
+ // "Tree-shakeable Globals" section in AGENTS.md for the architecture.
7
+
8
+ export { resolveGlobalsList, writeRegisterInjectFile } from './utils/scan-globals.js';
9
+ export { detectFreeGlobals } from './utils/detect-free-globals.js';
10
+ export { detectAutoGlobals } from './utils/auto-globals.js';
11
+ export type { AutoGlobalsResult, DetectAutoGlobalsOptions, AnalysisOptions } from './utils/auto-globals.js';