@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
@@ -1,141 +0,0 @@
1
- // Library mode — multi-entry, unbundled output for republication on npm.
2
- //
3
- // Equivalent to esbuild's `bundle: false`: every input file is emitted
4
- // 1:1 with its imports preserved as-is (resolved by Rolldown). The user
5
- // alias map is applied so `node:fs` → `fs/promises` style remappings still
6
- // work, and the JS extension table mirrors the esbuild predecessor.
7
- //
8
- // Targeting `esnext` and `platform: 'neutral'` matches the original
9
- // library config: consumers downstream (downstream apps using `gjsify
10
- // build --app gjs|node|browser`) re-bundle and apply their own target
11
- // lowering. Library output stays maximally portable.
12
-
13
- import { aliasPlugin } from '../plugins/alias.js';
14
- import { cssAsStringPlugin } from '../plugins/css-as-string.js';
15
- import type { RolldownOptions, RolldownPluginOption } from 'rolldown';
16
-
17
- import type { PluginOptions } from '../types/plugin-options.js';
18
- import { globToEntryPoints } from '../utils/entry-points.js';
19
-
20
- export interface LibBuildConfig {
21
- options: RolldownOptions;
22
- plugins: RolldownPluginOption[];
23
- }
24
-
25
- export interface LibFactoryInput {
26
- input?: RolldownOptions['input'];
27
- output: { file?: string; dir?: string };
28
- userAliases?: Record<string, string>;
29
- pluginOptions: PluginOptions;
30
- }
31
-
32
- export const setupLib = async (input: LibFactoryInput): Promise<LibBuildConfig> => {
33
- // Derive output format from `library: 'esm' | 'cjs'` when the caller
34
- // didn't pass `format` explicitly. The library type and the emitted
35
- // module format are inseparable: a CJS-library build that emits ESM
36
- // (or vice versa) is broken by definition.
37
- const format = input.pluginOptions.format ?? input.pluginOptions.library ?? 'esm';
38
-
39
- const exclude = input.pluginOptions.exclude ?? [];
40
- const entryPoints = await globToEntryPoints(input.input, exclude);
41
-
42
- // Derive `preserveModulesRoot` from the common ancestor of every
43
- // resolved entry. Workspaces use various roots (`src/`, `src/ts/`,
44
- // `lib/`); without stripping the right one, Rolldown emits paths
45
- // like `lib/esm/<root>/<file>.js` instead of `lib/esm/<file>.js`,
46
- // which doesn't match the package.json `exports` map.
47
- const preserveModulesRoot = computeCommonRoot(entryPoints);
48
-
49
- const aliasMap = {
50
- ...(input.pluginOptions.aliases ?? {}),
51
- ...(input.userAliases ?? {}),
52
- };
53
-
54
- // Library mode keeps all third-party / workspace imports as-is so the
55
- // emitted package re-exports its dep tree by reference. Rolldown's
56
- // default behaviour would inline workspace packages into the output
57
- // directory; we mark anything not starting with `./` or `/` as external.
58
- const external = (id: string): boolean => {
59
- if (id.startsWith('./') || id.startsWith('../') || id.startsWith('/')) return false;
60
- return true;
61
- };
62
-
63
- const options: RolldownOptions = {
64
- input: entryPoints,
65
- platform: 'neutral',
66
- external,
67
- resolve: {
68
- mainFields: format === 'esm' ? ['module', 'main'] : ['main'],
69
- conditionNames: format === 'esm' ? ['module', 'import'] : ['require'],
70
- },
71
- transform: { target: 'esnext' },
72
- output: {
73
- ...input.output,
74
- format,
75
- // Library mode = preserve module structure (multi-file output,
76
- // imports resolved but not bundled).
77
- preserveModules: true,
78
- // Strip the source root from the emitted paths. Without this,
79
- // Rolldown keeps the full project-relative path. The root is
80
- // computed from the common ancestor of resolved entries.
81
- preserveModulesRoot,
82
- sourcemap: false,
83
- },
84
- treeshake: false,
85
- };
86
-
87
- const plugins: RolldownPluginOption[] = [
88
- aliasPlugin({ entries: flattenAliases(aliasMap) }),
89
- // Rolldown removed experimental CSS bundling — `.css` files would
90
- // error at the bundler level. Library-mode packages that bundle
91
- // CSS as a string (e.g. `@gjsify/adwaita-fonts/index.css`) need
92
- // the same `load` hook the app factories install. The result is a
93
- // tiny JS module re-exporting the CSS source, which preserveModules
94
- // emits 1:1.
95
- cssAsStringPlugin(),
96
- ];
97
-
98
- return { options, plugins };
99
- };
100
-
101
- /**
102
- * Compute the common-ancestor directory of a set of entry paths so
103
- * Rolldown's `preserveModulesRoot` strips the right prefix from emitted
104
- * file paths. Falls back to `'src'` when there are no entries or the
105
- * entries don't share a meaningful prefix.
106
- */
107
- function computeCommonRoot(
108
- entries: ReturnType<typeof globToEntryPoints> extends Promise<infer T> ? T : never,
109
- ): string {
110
- const paths: string[] = entries === undefined
111
- ? []
112
- : typeof entries === 'string'
113
- ? [entries]
114
- : Array.isArray(entries)
115
- ? entries
116
- : Object.values(entries);
117
- if (paths.length === 0) return 'src';
118
-
119
- const split = paths.map((p) => p.split('/').filter(Boolean));
120
- const head = split[0];
121
- let i = 0;
122
- for (; i < head.length; i++) {
123
- const seg = head[i];
124
- if (!split.every((parts) => parts[i] === seg)) break;
125
- }
126
- if (i === 0) return 'src';
127
- // Drop the basename if the common prefix points at a single file.
128
- const commonParts = head.slice(0, i);
129
- // Heuristic: treat the prefix as a directory when at least one path
130
- // has more segments after it.
131
- const hasMoreAfter = split.some((parts) => parts.length > i);
132
- return hasMoreAfter ? commonParts.join('/') : commonParts.slice(0, -1).join('/') || 'src';
133
- }
134
-
135
- function flattenAliases(map: Record<string, string>): Record<string, string> {
136
- const out: Record<string, string> = {};
137
- for (const [from, to] of Object.entries(map)) {
138
- if (to) out[from] = to;
139
- }
140
- return out;
141
- }
package/src/plugin.ts DELETED
@@ -1,96 +0,0 @@
1
- // `gjsifyPlugin` — orchestrator entry point.
2
- //
3
- // Picks the platform-specific factory based on `pluginOptions.app`
4
- // (or `library` for library mode) and returns the resolved Rolldown
5
- // configuration. Unlike the esbuild predecessor, this is NOT a single
6
- // `Plugin` object — it returns a *config bundle* the caller composes into
7
- // `rolldown(opts)`, because Rolldown does not have esbuild's `setup(build)`
8
- // hook through which a single plugin can mutate `build.initialOptions`.
9
- //
10
- // The CLI consumer (`@gjsify/cli`) calls `gjsifyPlugin(...)` to get back
11
- // `{ options, plugins }`, then calls `rolldown({ ...options, plugins:
12
- // [...userPlugins, ...plugins] })`.
13
-
14
- import type { RolldownOptions, RolldownPluginOption } from 'rolldown';
15
- import type { PluginOptions } from './types/plugin-options.js';
16
- import { setupForGjs, setupForNode, setupForBrowser } from './app/index.js';
17
- import { setupLib } from './library/index.js';
18
-
19
- export interface GjsifyConfig {
20
- options: RolldownOptions;
21
- plugins: RolldownPluginOption[];
22
- }
23
-
24
- export interface GjsifyPluginInput {
25
- input?: RolldownOptions['input'];
26
- output: { file?: string; dir?: string };
27
- userExternal?: string[];
28
- userBanner?: string;
29
- userAliases?: Record<string, string>;
30
- /**
31
- * Shebang to prepend to the GJS bundle.
32
- * `true` → default `#!/usr/bin/env -S gjs -m`
33
- * `false` → no shebang
34
- * `"…"` → custom line, supports `${env:NAME[:-default]}` placeholders
35
- */
36
- shebang?: boolean | string;
37
- }
38
-
39
- /**
40
- * Build the Rolldown configuration template + plugin array for the given
41
- * pluginOptions. The caller composes the returned `options.plugins` with
42
- * its own user plugins and passes the merged options to `rolldown(...)`.
43
- */
44
- export const gjsifyPlugin = async (
45
- input: GjsifyPluginInput,
46
- pluginOptions: PluginOptions = {},
47
- ): Promise<GjsifyConfig> => {
48
- if (pluginOptions.library) {
49
- switch (pluginOptions.library) {
50
- case 'esm':
51
- case 'cjs':
52
- return await setupLib({
53
- input: input.input,
54
- output: input.output,
55
- userAliases: input.userAliases,
56
- pluginOptions,
57
- });
58
- default:
59
- throw new TypeError('Unknown library type: ' + pluginOptions.library);
60
- }
61
- }
62
-
63
- const app = pluginOptions.app ?? 'gjs';
64
- switch (app) {
65
- case 'gjs':
66
- return await setupForGjs({
67
- input: input.input,
68
- output: input.output,
69
- userExternal: input.userExternal,
70
- userBanner: input.userBanner,
71
- userAliases: input.userAliases,
72
- shebang: input.shebang,
73
- pluginOptions,
74
- });
75
- case 'node':
76
- return await setupForNode({
77
- input: input.input,
78
- output: input.output,
79
- userExternal: input.userExternal,
80
- userAliases: input.userAliases,
81
- pluginOptions,
82
- });
83
- case 'browser':
84
- return await setupForBrowser({
85
- input: input.input,
86
- output: input.output,
87
- userExternal: input.userExternal,
88
- userAliases: input.userAliases,
89
- pluginOptions,
90
- });
91
- default:
92
- throw new TypeError('Unknown app platform: ' + app);
93
- }
94
- };
95
-
96
- export default gjsifyPlugin;
@@ -1,61 +0,0 @@
1
- // Custom alias plugin — `@rollup/plugin-alias` returns the rewritten
2
- // specifier as the resolved id, then leaves Rolldown's default resolver to
3
- // load it. That fails for workspace package targets (`@gjsify/crypto` etc.)
4
- // when the importer's package.json doesn't list them as direct deps —
5
- // Rolldown rejects packages that aren't declared in the importer's manifest
6
- // even when they exist in a hoisted workspace `node_modules`.
7
- //
8
- // This plugin instead calls `this.resolve(target, importer, { skipSelf: true })`
9
- // which goes through the full plugin chain (including any subsequent resolvers)
10
- // and resolves to a real file path the default loader can read.
11
- //
12
- // Behaviour preserved from the esbuild predecessor's `aliasPlugin`:
13
- // - exact string match (no prefix-aware semantics needed at this layer)
14
- // - `node:<name>` specifiers map to the same target as `<name>`
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.
23
-
24
- import type { Plugin } from 'rolldown';
25
-
26
- export interface AliasPluginOptions {
27
- entries: Record<string, string>;
28
- }
29
-
30
- export function aliasPlugin(options: AliasPluginOptions): Plugin {
31
- const entries = options.entries;
32
- const keys = Object.keys(entries);
33
-
34
- return {
35
- name: 'gjsify-alias',
36
- resolveId: {
37
- order: 'pre' as const,
38
- async handler(source, importer, extraOptions) {
39
- if (!Object.prototype.hasOwnProperty.call(entries, source)) {
40
- return null;
41
- }
42
- const target = entries[source];
43
- // Self-reference guard: if a user maps a specifier to itself
44
- // (rare but legal), skip to avoid infinite loops.
45
- if (target === source) return null;
46
-
47
- const resolved = await this.resolve(target, importer, {
48
- skipSelf: true,
49
- kind: extraOptions?.kind,
50
- });
51
- if (resolved !== null) {
52
- return resolved;
53
- }
54
- // Fall through to other resolvers if we couldn't load it
55
- // ourselves. `null` from a `pre`-order resolveId lets the
56
- // default chain continue.
57
- return null;
58
- },
59
- },
60
- };
61
- }
@@ -1,78 +0,0 @@
1
- // Wrap `.css` imports as JS string default exports.
2
- //
3
- // Rolldown's experimental CSS bundling was removed (see
4
- // https://github.com/rolldown/rolldown/issues/4271) — when the bundler
5
- // sees a `.css` extension it errors out unless something else loads the
6
- // file first. We hook into `load` (with a path filter) BEFORE Rolldown's
7
- // CSS classification fires and emit a JS module whose default export is
8
- // the CSS source as a string. Consumers can then do:
9
- //
10
- // import css from './app.css';
11
- // provider.load_from_string(css);
12
- //
13
- // — the canonical pattern for `Gtk.CssProvider` under GJS.
14
- //
15
- // `@import` resolution + nesting/modern-syntax lowering are handled via
16
- // lightningcss `bundleAsync`. The defaults work for the common case
17
- // (resolve `@import`s, no targeting); the `--app gjs` orchestrator passes
18
- // `targets: { firefox: 60 << 16 }` so nesting + modern selectors get
19
- // flattened to GTK4-CSS-engine-compatible output. Targeting is opt-in —
20
- // a missing `targets` keeps the source pristine.
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.
25
-
26
- import { readFile } from 'node:fs/promises';
27
- import type { Plugin } from 'rolldown';
28
-
29
- export interface CssAsStringOptions {
30
- /**
31
- * lightningcss browser targets passed to `bundleAsync`. When set,
32
- * nesting + modern syntax are lowered for the given engines. The
33
- * GJS orchestrator defaults this to `{ firefox: 60 << 16 }` to
34
- * match the GTK4 CSS parser. Omit or leave undefined to skip
35
- * lowering (output stays as-authored except for `@import` inlining).
36
- */
37
- targets?: import('lightningcss').Targets;
38
- /**
39
- * When true (default), `@import` statements are resolved by
40
- * lightningcss `bundleAsync`. Set false to fall back to a plain
41
- * `readFile` — useful only when you want to keep `@import` strings
42
- * verbatim in the bundled JS (rare).
43
- */
44
- bundle?: boolean;
45
- }
46
-
47
- export function cssAsStringPlugin(options: CssAsStringOptions = {}): Plugin {
48
- const { targets, bundle = true } = options;
49
- return {
50
- name: 'gjsify-css-as-string',
51
- load: {
52
- filter: { id: /\.css$/ },
53
- async handler(id: string) {
54
- const code = bundle
55
- ? new TextDecoder('utf-8').decode(await loadAndBundleCss(id, targets))
56
- : await readFile(id, 'utf8');
57
- return {
58
- code: `export default ${JSON.stringify(code)};`,
59
- moduleType: 'js' as const,
60
- };
61
- },
62
- },
63
- };
64
- }
65
-
66
- async function loadAndBundleCss(
67
- filename: string,
68
- targets: import('lightningcss').Targets | undefined,
69
- ): Promise<Uint8Array> {
70
- const { bundleAsync } = await import('lightningcss');
71
- const result = await bundleAsync({
72
- filename,
73
- targets,
74
- minify: false,
75
- errorRecovery: true,
76
- });
77
- return result.code;
78
- }
@@ -1,29 +0,0 @@
1
- // For `--app browser`: redirect `@girs/*` and `gi://*` imports to an empty
2
- // module. These are GJS-specific (GObject introspection bindings / GI
3
- // protocol) with no browser equivalent. They appear transitively via
4
- // `@gjsify/unit` and similar packages that have GJS-specific code paths.
5
- //
6
- // Marking them external would leave bare specifiers in the bundle that the
7
- // browser cannot resolve at runtime; instead we resolve them to a virtual
8
- // empty ESM module so the bundle is self-contained.
9
-
10
- import type { Plugin } from 'rolldown';
11
-
12
- const GJSIMPORTS_VIRTUAL_ID = '\0gjsify-empty-gjs-import';
13
-
14
- export function gjsImportsEmptyPlugin(): Plugin {
15
- return {
16
- name: 'gjsify-gjs-imports-empty',
17
- resolveId: {
18
- order: 'pre' as const,
19
- filter: { id: /^(@girs\/|gi:\/\/)/ },
20
- handler(_source) {
21
- return { id: GJSIMPORTS_VIRTUAL_ID };
22
- },
23
- },
24
- load(id) {
25
- if (id !== GJSIMPORTS_VIRTUAL_ID) return null;
26
- return { code: 'export {}; export default {};', moduleSideEffects: false };
27
- },
28
- };
29
- }
@@ -1,91 +0,0 @@
1
- // Synchronous `globalThis.process` stub injected as a GJS bundle banner.
2
- //
3
- // Some npm packages (glob, path-scurry, readable-stream, …) access
4
- // `globalThis.process.platform` at their top-level during lazy `__esm`
5
- // initialisation — BEFORE any `import`-triggered side effects fire. A
6
- // banner runs before everything, including bundler helpers and all
7
- // bundled module code, making it the only reliable injection point for
8
- // a synchronous global that must exist from byte 1 of execution.
9
- //
10
- // Only installed if `process` is absent; the full @gjsify/process
11
- // implementation (with EventEmitter, real streams, etc.) is wired up
12
- // later via `--globals auto` (which injects @gjsify/node-globals/register/process).
13
- //
14
- // Kept as a single line: the banner runs before any source-map-aware
15
- // machinery, so newlines here would shift every line number by one. Single
16
- // line = zero source-map drift for the actual bundle code below.
17
- import type { Plugin } from 'rolldown';
18
-
19
- export const GJS_PROCESS_STUB =
20
- 'if(typeof globalThis.process==="undefined"){' +
21
- 'const _s=imports.system,_G=imports.gi.GLib;' +
22
- 'globalThis.process={' +
23
- 'platform:"linux",arch:"x64",version:"v20.0.0",' +
24
- 'env:new Proxy({},{' +
25
- 'get(_,p){return typeof p==="string"?(_G.getenv(p)??undefined):undefined},' +
26
- 'set(_,p,v){if(typeof p==="string")_G.setenv(p,String(v),true);return true},' +
27
- 'has(_,p){return typeof p==="string"&&_G.getenv(p)!==null},' +
28
- 'deleteProperty(_,p){if(typeof p==="string")_G.unsetenv(p);return true},' +
29
- 'ownKeys(){return _G.listenv()??[]},' +
30
- 'getOwnPropertyDescriptor(_,p){const v=_G.getenv(p);return v!==null?{value:v,writable:true,enumerable:true,configurable:true}:undefined}' +
31
- '}),' +
32
- 'argv:_s?.programArgs?["gjs",_s.programInvocationName||"",..._s.programArgs]:["gjs"],' +
33
- 'versions:{},config:{},' +
34
- 'cwd(){return _G.get_current_dir()||"/"},' +
35
- 'exit(c){_s.exit(c??0)},' +
36
- 'stderr:{write(s){printerr(s)}},stdout:{write(s){print(s)}},stdin:null,' +
37
- 'exitCode:undefined,' +
38
- 'nextTick(fn,...a){Promise.resolve().then(()=>fn(...a))},' +
39
- 'hrtime(t){return t?[0,0]:[0,0]},' +
40
- '};' +
41
- '}';
42
-
43
- /**
44
- * Compose the GJS process stub with the user-supplied banner so the result
45
- * is valid syntax for `gjs -m`. A leading `#!shebang` line in the user
46
- * banner is hoisted to byte 0 of the output. Any `#` character that appears
47
- * anywhere except byte 0 is a fatal SyntaxError under SpiderMonkey 128+ —
48
- * putting our process stub before the user's shebang would break the bundle.
49
- *
50
- * Output shape:
51
- * [#!shebang\n][<process-stub>\n<rest-of-user-banner>]
52
- *
53
- * Either side of the bracket may be empty; the result is always concatenated
54
- * without leading whitespace.
55
- */
56
- export function composeBanner(stub: string, userBanner: string): string {
57
- if (!userBanner) return stub;
58
- const shebangMatch = userBanner.match(/^#![^\n]*\n/);
59
- if (!shebangMatch) {
60
- return stub + '\n' + userBanner;
61
- }
62
- const shebang = shebangMatch[0];
63
- const rest = userBanner.slice(shebang.length);
64
- return shebang + stub + (rest ? '\n' + rest : '');
65
- }
66
-
67
- /**
68
- * Build a Rolldown plugin that injects the GJS process stub as a chunk
69
- * banner. Runs with `enforce: 'post'`-equivalent ordering so the stub
70
- * lands *after* any user `output.banner` value, except when the user
71
- * banner starts with a `#!shebang` line — which is hoisted to byte 0
72
- * by `composeBanner`.
73
- */
74
- export interface ProcessStubPluginOptions {
75
- /** User-supplied banner string. May contain a leading `#!shebang`. */
76
- userBanner?: string;
77
- }
78
-
79
- export function processStubPlugin(options: ProcessStubPluginOptions = {}): Plugin {
80
- const banner = composeBanner(GJS_PROCESS_STUB, options.userBanner ?? '');
81
- return {
82
- name: 'gjsify-process-stub',
83
- renderChunk: {
84
- order: 'post' as const,
85
- handler(code, chunk) {
86
- if (!chunk.isEntry) return null;
87
- return { code: banner + '\n' + code, map: null };
88
- },
89
- },
90
- };
91
- }
@@ -1,169 +0,0 @@
1
- // Per-source rewriter for node_modules files that reference
2
- // `import.meta.url`, `__dirname`, or `__filename`. Mirrors the esbuild
3
- // predecessor's logic — body is identical because the rewrite is purely
4
- // a string transform on already-loaded source. The only delta is the
5
- // host: a Rolldown `transform(code, id)` plugin instead of an esbuild
6
- // `onLoad` registered inside the PnP plugin.
7
- //
8
- // Why a separate plugin and not nested in the PnP loader, like esbuild?
9
- // Rolldown / Rollup's `transform` hooks all run in sequence on every
10
- // loaded module — there is no first-onLoad-wins race. So the PnP loader
11
- // (`@gjsify/rolldown-plugin-pnp`) is solely responsible for reading
12
- // zip-resident bytes; this plugin runs as a separate `transform` step
13
- // after the bytes have been loaded, regardless of which loader produced
14
- // them. No more F5-bug folklore.
15
-
16
- import { dirname, join, relative, resolve } from 'node:path';
17
- import type { Plugin } from 'rolldown';
18
-
19
- import { inlineStaticReads } from '../utils/inline-static-reads.js';
20
-
21
- export const REWRITE_FILTER = /\.(m?js|cjs|[cm]?tsx?)$/;
22
- const DIRNAME_DECL_RE = /(?:var|let|const)\s+__dirname\b|export\s+(?:var|let|const)\s+__dirname\b/;
23
- const FILENAME_DECL_RE = /(?:var|let|const)\s+__filename\b|export\s+(?:var|let|const)\s+__filename\b/;
24
-
25
- /** True when the rewriter wants to look at this path — node_modules + supported ext. */
26
- export function shouldRewrite(path: string): boolean {
27
- return path.includes('node_modules') && REWRITE_FILTER.test(path);
28
- }
29
-
30
- /**
31
- * Compute the directory the bundle's outfile lives in.
32
- *
33
- * For `import.meta.url` rewriting we emit a relative URL whose base is the
34
- * bundle's `import.meta.url` — so we need to know where the bundle will be
35
- * written. Both `output.file` and `output.dir` are accepted.
36
- */
37
- export function getBundleDirFromOutput(opts: { file?: string; dir?: string }): string {
38
- const outFile = opts.file ?? join(opts.dir ?? '.', 'bundle.mjs');
39
- return dirname(resolve(outFile));
40
- }
41
-
42
- /** Pick the per-file loader Rolldown should re-parse with. */
43
- function moduleTypeForPath(path: string): 'ts' | 'js' {
44
- const ext = path.split('.').pop() ?? 'js';
45
- return ['ts', 'mts', 'cts', 'tsx'].includes(ext) ? 'ts' : 'js';
46
- }
47
-
48
- interface PreambleArgs {
49
- needDirname: boolean;
50
- needFilename: boolean;
51
- dirnameDeclared: boolean;
52
- filenameDeclared: boolean;
53
- /** kind of rewrite: 'esm-relative' | 'esm-zip' | 'cjs-absolute' */
54
- kind: 'esm-relative' | 'esm-zip' | 'cjs-absolute';
55
- sourcePath: string;
56
- sourceDir: string;
57
- relDirWithSlash: string;
58
- relPath: string;
59
- }
60
-
61
- function buildDirFilenamePreamble(args: PreambleArgs): string[] {
62
- const lines: string[] = [];
63
- if (args.needDirname && !args.dirnameDeclared) {
64
- if (args.kind === 'esm-zip') {
65
- lines.push(`var __dirname = new URL(".", import.meta.url).pathname.replace(/\\/$/, "");`);
66
- } else if (args.kind === 'esm-relative') {
67
- lines.push(`var __dirname = new URL(${JSON.stringify(args.relDirWithSlash)}, import.meta.url).pathname.replace(/\\/$/, "");`);
68
- } else {
69
- lines.push(`var __dirname = ${JSON.stringify(args.sourceDir)};`);
70
- }
71
- }
72
- if (args.needFilename && !args.filenameDeclared) {
73
- if (args.kind === 'esm-zip') {
74
- lines.push(`var __filename = new URL(import.meta.url).pathname;`);
75
- } else if (args.kind === 'esm-relative') {
76
- lines.push(`var __filename = new URL(${JSON.stringify(args.relPath)}, import.meta.url).pathname;`);
77
- } else {
78
- lines.push(`var __filename = ${JSON.stringify(args.sourcePath)};`);
79
- }
80
- }
81
- return lines;
82
- }
83
-
84
- export interface RewriteResult {
85
- code: string;
86
- moduleType?: 'ts' | 'js';
87
- map?: null;
88
- }
89
-
90
- /**
91
- * Pure rewriter — same body as the esbuild predecessor. Returns the rewritten
92
- * code (and module type for re-parsing) or `null` if the file doesn't reference
93
- * any of the patterns we care about.
94
- */
95
- export function rewriteContents(
96
- args: { path: string },
97
- srcInput: string,
98
- bundleDir: string,
99
- ): RewriteResult | null {
100
- if (!shouldRewrite(args.path)) return null;
101
-
102
- // Step 1: inline statically-resolvable filesystem reads.
103
- const inlined = inlineStaticReads(srcInput, args.path);
104
- const src = inlined.contents;
105
-
106
- const hasMetaUrl = src.includes('import.meta.url');
107
- const hasDirname = src.includes('__dirname');
108
- const hasFilename = src.includes('__filename');
109
-
110
- if (!hasMetaUrl && !hasDirname && !hasFilename) {
111
- if (inlined.inlined === 0) return null;
112
- return { code: src, moduleType: moduleTypeForPath(args.path) };
113
- }
114
-
115
- // Step 2: classify rewrite kind.
116
- const dir = dirname(args.path);
117
- const relPath = hasMetaUrl ? relative(bundleDir, args.path) : '';
118
- const isZipResident = hasMetaUrl && relPath.includes('.zip/');
119
- const kind: 'esm-relative' | 'esm-zip' | 'cjs-absolute' =
120
- !hasMetaUrl ? 'cjs-absolute' : isZipResident ? 'esm-zip' : 'esm-relative';
121
-
122
- const preamble = buildDirFilenamePreamble({
123
- needDirname: hasDirname,
124
- needFilename: hasFilename,
125
- dirnameDeclared: DIRNAME_DECL_RE.test(src),
126
- filenameDeclared: FILENAME_DECL_RE.test(src),
127
- kind,
128
- sourcePath: args.path,
129
- sourceDir: dir,
130
- relPath,
131
- relDirWithSlash: (relative(bundleDir, dir) || '.') + '/',
132
- });
133
-
134
- // Step 3: rewrite import.meta.url for the regular esm-relative case.
135
- let code = src;
136
- if (kind === 'esm-relative') {
137
- const runtimeFileUrl = `new URL(${JSON.stringify(relPath)}, import.meta.url)`;
138
- code = code.replace(/\bimport\.meta\.url\b/g, `${runtimeFileUrl}.href`);
139
- }
140
- if (preamble.length > 0) code = preamble.join('\n') + '\n' + code;
141
-
142
- return { code, moduleType: moduleTypeForPath(args.path) };
143
- }
144
-
145
- export interface NodeModulesPathRewriteOptions {
146
- /** Bundle output directory, derived from `output.file` / `output.dir`. */
147
- bundleDir: string;
148
- }
149
-
150
- /**
151
- * Build a Rolldown plugin that runs the path rewriter as a `transform(code, id)`
152
- * hook with `order: 'post'` — runs after the deepkit/blueprint/css pre-transforms
153
- * but still during module loading, before chunking.
154
- */
155
- export function nodeModulesPathRewritePlugin(options: NodeModulesPathRewriteOptions): Plugin {
156
- return {
157
- name: 'gjsify-node-modules-path-rewrite',
158
- transform: {
159
- order: 'post' as const,
160
- filter: { id: REWRITE_FILTER },
161
- handler(code: string, id: string) {
162
- if (!id.includes('node_modules')) return null;
163
- const result = rewriteContents({ path: id }, code, options.bundleDir);
164
- if (!result) return null;
165
- return { code: result.code, map: null };
166
- },
167
- },
168
- };
169
- }