@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.
- package/lib/app/browser.d.ts +17 -0
- package/lib/app/browser.js +77 -0
- package/lib/app/gjs.d.ts +27 -0
- package/lib/app/gjs.js +211 -0
- package/lib/app/index.d.ts +6 -0
- package/lib/app/index.js +3 -0
- package/lib/app/node.d.ts +17 -0
- package/lib/app/node.js +102 -0
- package/lib/globals.d.ts +4 -0
- package/lib/globals.js +9 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +15 -0
- package/lib/library/index.d.ts +2 -0
- package/lib/library/index.js +1 -0
- package/lib/library/lib.d.ts +16 -0
- package/lib/library/lib.js +118 -0
- package/lib/plugin.d.ts +25 -0
- package/lib/plugin.js +67 -0
- package/lib/plugins/alias.d.ts +5 -0
- package/lib/plugins/alias.js +45 -0
- package/lib/plugins/css-as-string.d.ts +2 -0
- package/lib/plugins/css-as-string.js +34 -0
- package/lib/plugins/gjs-imports-empty.d.ts +2 -0
- package/lib/plugins/gjs-imports-empty.js +26 -0
- package/lib/plugins/process-stub.d.ts +28 -0
- package/lib/plugins/process-stub.js +60 -0
- package/lib/plugins/rewrite-node-modules-paths.d.ts +38 -0
- package/lib/plugins/rewrite-node-modules-paths.js +132 -0
- package/lib/plugins/shebang.d.ts +8 -0
- package/lib/plugins/shebang.js +26 -0
- package/lib/shims/console-gjs.d.ts +24 -0
- package/lib/shims/console-gjs.js +24 -0
- package/lib/types/app.d.ts +1 -0
- package/lib/types/app.js +1 -0
- package/lib/types/index.d.ts +3 -0
- package/lib/types/index.js +3 -0
- package/lib/types/plugin-options.d.ts +46 -0
- package/lib/types/plugin-options.js +1 -0
- package/lib/types/resolve-alias-options.d.ts +2 -0
- package/lib/types/resolve-alias-options.js +1 -0
- package/lib/utils/alias.d.ts +12 -0
- package/lib/utils/alias.js +29 -0
- package/lib/utils/auto-globals.d.ts +72 -0
- package/lib/utils/auto-globals.js +193 -0
- package/lib/utils/detect-free-globals.d.ts +18 -0
- package/lib/utils/detect-free-globals.js +268 -0
- package/lib/utils/entry-points.d.ts +2 -0
- package/lib/utils/entry-points.js +38 -0
- package/lib/utils/extension.d.ts +1 -0
- package/lib/utils/extension.js +7 -0
- package/lib/utils/index.d.ts +7 -0
- package/lib/utils/index.js +7 -0
- package/lib/utils/inline-static-reads.d.ts +11 -0
- package/lib/utils/inline-static-reads.js +549 -0
- package/lib/utils/merge.d.ts +2 -0
- package/lib/utils/merge.js +23 -0
- package/lib/utils/scan-globals.d.ts +32 -0
- package/lib/utils/scan-globals.js +85 -0
- package/package.json +68 -0
- package/src/app/browser.ts +102 -0
- package/src/app/gjs.ts +260 -0
- package/src/app/index.ts +6 -0
- package/src/app/node.ts +128 -0
- package/src/globals.ts +11 -0
- package/src/index.ts +32 -0
- package/src/library/index.ts +2 -0
- package/src/library/lib.ts +142 -0
- package/src/plugin.ts +91 -0
- package/src/plugins/alias.ts +53 -0
- package/src/plugins/css-as-string.ts +37 -0
- package/src/plugins/gjs-imports-empty.ts +29 -0
- package/src/plugins/process-stub.ts +91 -0
- package/src/plugins/rewrite-node-modules-paths.ts +169 -0
- package/src/plugins/shebang.ts +33 -0
- package/src/shims/console-gjs.ts +25 -0
- package/src/types/app.ts +1 -0
- package/src/types/index.ts +3 -0
- package/src/types/plugin-options.ts +48 -0
- package/src/types/resolve-alias-options.ts +1 -0
- package/src/utils/alias.ts +46 -0
- package/src/utils/auto-globals.ts +283 -0
- package/src/utils/detect-free-globals.ts +278 -0
- package/src/utils/entry-points.ts +48 -0
- package/src/utils/extension.ts +7 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/inline-static-reads.ts +541 -0
- package/src/utils/merge.ts +22 -0
- package/src/utils/scan-globals.ts +91 -0
- package/tsconfig.json +16 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Public re-exports for `@gjsify/rolldown-plugin-gjsify`.
|
|
2
|
+
|
|
3
|
+
export * from './types/index.js';
|
|
4
|
+
export * from './utils/index.js';
|
|
5
|
+
export * from './app/index.js';
|
|
6
|
+
export * from './library/index.js';
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
REWRITE_FILTER,
|
|
10
|
+
getBundleDirFromOutput,
|
|
11
|
+
rewriteContents,
|
|
12
|
+
shouldRewrite,
|
|
13
|
+
nodeModulesPathRewritePlugin,
|
|
14
|
+
} from './plugins/rewrite-node-modules-paths.js';
|
|
15
|
+
export type {
|
|
16
|
+
NodeModulesPathRewriteOptions,
|
|
17
|
+
RewriteResult,
|
|
18
|
+
} from './plugins/rewrite-node-modules-paths.js';
|
|
19
|
+
|
|
20
|
+
export { processStubPlugin, GJS_PROCESS_STUB, composeBanner } from './plugins/process-stub.js';
|
|
21
|
+
export type { ProcessStubPluginOptions } from './plugins/process-stub.js';
|
|
22
|
+
export { cssAsStringPlugin } from './plugins/css-as-string.js';
|
|
23
|
+
export { shebangPlugin, GJS_SHEBANG } from './plugins/shebang.js';
|
|
24
|
+
export type { ShebangPluginOptions } from './plugins/shebang.js';
|
|
25
|
+
export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
|
|
26
|
+
|
|
27
|
+
export * from './plugin.js';
|
|
28
|
+
import { gjsifyPlugin } from './plugin.js';
|
|
29
|
+
export { gjsifyPlugin };
|
|
30
|
+
export default gjsifyPlugin;
|
|
31
|
+
|
|
32
|
+
export * from '@gjsify/resolve-npm';
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
minify: false,
|
|
83
|
+
sourcemap: false,
|
|
84
|
+
},
|
|
85
|
+
treeshake: false,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const plugins: RolldownPluginOption[] = [
|
|
89
|
+
aliasPlugin({ entries: flattenAliases(aliasMap) }),
|
|
90
|
+
// Rolldown removed experimental CSS bundling — `.css` files would
|
|
91
|
+
// error at the bundler level. Library-mode packages that bundle
|
|
92
|
+
// CSS as a string (e.g. `@gjsify/adwaita-fonts/index.css`) need
|
|
93
|
+
// the same `load` hook the app factories install. The result is a
|
|
94
|
+
// tiny JS module re-exporting the CSS source, which preserveModules
|
|
95
|
+
// emits 1:1.
|
|
96
|
+
cssAsStringPlugin(),
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
return { options, plugins };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Compute the common-ancestor directory of a set of entry paths so
|
|
104
|
+
* Rolldown's `preserveModulesRoot` strips the right prefix from emitted
|
|
105
|
+
* file paths. Falls back to `'src'` when there are no entries or the
|
|
106
|
+
* entries don't share a meaningful prefix.
|
|
107
|
+
*/
|
|
108
|
+
function computeCommonRoot(
|
|
109
|
+
entries: ReturnType<typeof globToEntryPoints> extends Promise<infer T> ? T : never,
|
|
110
|
+
): string {
|
|
111
|
+
const paths: string[] = entries === undefined
|
|
112
|
+
? []
|
|
113
|
+
: typeof entries === 'string'
|
|
114
|
+
? [entries]
|
|
115
|
+
: Array.isArray(entries)
|
|
116
|
+
? entries
|
|
117
|
+
: Object.values(entries);
|
|
118
|
+
if (paths.length === 0) return 'src';
|
|
119
|
+
|
|
120
|
+
const split = paths.map((p) => p.split('/').filter(Boolean));
|
|
121
|
+
const head = split[0];
|
|
122
|
+
let i = 0;
|
|
123
|
+
for (; i < head.length; i++) {
|
|
124
|
+
const seg = head[i];
|
|
125
|
+
if (!split.every((parts) => parts[i] === seg)) break;
|
|
126
|
+
}
|
|
127
|
+
if (i === 0) return 'src';
|
|
128
|
+
// Drop the basename if the common prefix points at a single file.
|
|
129
|
+
const commonParts = head.slice(0, i);
|
|
130
|
+
// Heuristic: treat the prefix as a directory when at least one path
|
|
131
|
+
// has more segments after it.
|
|
132
|
+
const hasMoreAfter = split.some((parts) => parts.length > i);
|
|
133
|
+
return hasMoreAfter ? commonParts.join('/') : commonParts.slice(0, -1).join('/') || 'src';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function flattenAliases(map: Record<string, string>): Record<string, string> {
|
|
137
|
+
const out: Record<string, string> = {};
|
|
138
|
+
for (const [from, to] of Object.entries(map)) {
|
|
139
|
+
if (to) out[from] = to;
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
/** Whether to prepend `#!/usr/bin/env -S gjs -m` to the GJS bundle. */
|
|
31
|
+
shebang?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build the Rolldown configuration template + plugin array for the given
|
|
36
|
+
* pluginOptions. The caller composes the returned `options.plugins` with
|
|
37
|
+
* its own user plugins and passes the merged options to `rolldown(...)`.
|
|
38
|
+
*/
|
|
39
|
+
export const gjsifyPlugin = async (
|
|
40
|
+
input: GjsifyPluginInput,
|
|
41
|
+
pluginOptions: PluginOptions = {},
|
|
42
|
+
): Promise<GjsifyConfig> => {
|
|
43
|
+
if (pluginOptions.library) {
|
|
44
|
+
switch (pluginOptions.library) {
|
|
45
|
+
case 'esm':
|
|
46
|
+
case 'cjs':
|
|
47
|
+
return await setupLib({
|
|
48
|
+
input: input.input,
|
|
49
|
+
output: input.output,
|
|
50
|
+
userAliases: input.userAliases,
|
|
51
|
+
pluginOptions,
|
|
52
|
+
});
|
|
53
|
+
default:
|
|
54
|
+
throw new TypeError('Unknown library type: ' + pluginOptions.library);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const app = pluginOptions.app ?? 'gjs';
|
|
59
|
+
switch (app) {
|
|
60
|
+
case 'gjs':
|
|
61
|
+
return await setupForGjs({
|
|
62
|
+
input: input.input,
|
|
63
|
+
output: input.output,
|
|
64
|
+
userExternal: input.userExternal,
|
|
65
|
+
userBanner: input.userBanner,
|
|
66
|
+
userAliases: input.userAliases,
|
|
67
|
+
shebang: input.shebang,
|
|
68
|
+
pluginOptions,
|
|
69
|
+
});
|
|
70
|
+
case 'node':
|
|
71
|
+
return await setupForNode({
|
|
72
|
+
input: input.input,
|
|
73
|
+
output: input.output,
|
|
74
|
+
userExternal: input.userExternal,
|
|
75
|
+
userAliases: input.userAliases,
|
|
76
|
+
pluginOptions,
|
|
77
|
+
});
|
|
78
|
+
case 'browser':
|
|
79
|
+
return await setupForBrowser({
|
|
80
|
+
input: input.input,
|
|
81
|
+
output: input.output,
|
|
82
|
+
userExternal: input.userExternal,
|
|
83
|
+
userAliases: input.userAliases,
|
|
84
|
+
pluginOptions,
|
|
85
|
+
});
|
|
86
|
+
default:
|
|
87
|
+
throw new TypeError('Unknown app platform: ' + app);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default gjsifyPlugin;
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
import type { Plugin } from 'rolldown';
|
|
18
|
+
|
|
19
|
+
export interface AliasPluginOptions {
|
|
20
|
+
entries: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function aliasPlugin(options: AliasPluginOptions): Plugin {
|
|
24
|
+
const entries = options.entries;
|
|
25
|
+
const keys = Object.keys(entries);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
name: 'gjsify-alias',
|
|
29
|
+
resolveId: {
|
|
30
|
+
order: 'pre' as const,
|
|
31
|
+
async handler(source, importer) {
|
|
32
|
+
if (!Object.prototype.hasOwnProperty.call(entries, source)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const target = entries[source];
|
|
36
|
+
// Self-reference guard: if a user maps a specifier to itself
|
|
37
|
+
// (rare but legal), skip to avoid infinite loops.
|
|
38
|
+
if (target === source) return null;
|
|
39
|
+
|
|
40
|
+
const resolved = await this.resolve(target, importer, {
|
|
41
|
+
skipSelf: true,
|
|
42
|
+
});
|
|
43
|
+
if (resolved !== null) {
|
|
44
|
+
return resolved;
|
|
45
|
+
}
|
|
46
|
+
// Fall through to other resolvers if we couldn't load it
|
|
47
|
+
// ourselves. `null` from a `pre`-order resolveId lets the
|
|
48
|
+
// default chain continue.
|
|
49
|
+
return null;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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 is left to the user / CSS preprocessor. For simple
|
|
16
|
+
// app CSS this is fine; for @import-heavy CSS, run a preprocessor (e.g.
|
|
17
|
+
// sass / postcss) ahead of `gjsify build` so the input file is already
|
|
18
|
+
// flat.
|
|
19
|
+
|
|
20
|
+
import { readFile } from 'node:fs/promises';
|
|
21
|
+
import type { Plugin } from 'rolldown';
|
|
22
|
+
|
|
23
|
+
export function cssAsStringPlugin(): Plugin {
|
|
24
|
+
return {
|
|
25
|
+
name: 'gjsify-css-as-string',
|
|
26
|
+
load: {
|
|
27
|
+
filter: { id: /\.css$/ },
|
|
28
|
+
async handler(id: string) {
|
|
29
|
+
const code = await readFile(id, 'utf8');
|
|
30
|
+
return {
|
|
31
|
+
code: `export default ${JSON.stringify(code)};`,
|
|
32
|
+
moduleType: 'js' as const,
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
}
|