@gjsify/rolldown-plugin-gjsify 0.4.0 → 0.4.4

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/src/app/node.ts DELETED
@@ -1,127 +0,0 @@
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
- sourcemap: false,
103
- banner,
104
- // Single-bundle output. `codeSplitting: false` replaces the
105
- // deprecated `inlineDynamicImports: true`.
106
- codeSplitting: false,
107
- },
108
- treeshake: true,
109
- };
110
-
111
- const plugins: RolldownPluginOption[] = [
112
- aliasPlugin({ entries: flattenAliases(aliasMap) }),
113
- deepkitPlugin({ reflection: input.pluginOptions.reflection }),
114
- cssAsStringPlugin(),
115
- nodeModulesPathRewritePlugin({ bundleDir }),
116
- ];
117
-
118
- return { options, plugins };
119
- };
120
-
121
- function flattenAliases(map: Record<string, string>): Record<string, string> {
122
- const out: Record<string, string> = {};
123
- for (const [from, to] of Object.entries(map)) {
124
- if (to) out[from] = to;
125
- }
126
- return out;
127
- }
package/src/globals.ts DELETED
@@ -1,11 +0,0 @@
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';
package/src/index.ts DELETED
@@ -1,34 +0,0 @@
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 { textLoaderPlugin } from './plugins/text-loader.js';
24
- export type { TextLoaderPluginOptions } from './plugins/text-loader.js';
25
- export { shebangPlugin, GJS_SHEBANG, expandEnvTemplate, resolveShebangLine } from './plugins/shebang.js';
26
- export type { ShebangPluginOptions } from './plugins/shebang.js';
27
- export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
28
-
29
- export * from './plugin.js';
30
- import { gjsifyPlugin } from './plugin.js';
31
- export { gjsifyPlugin };
32
- export default gjsifyPlugin;
33
-
34
- export * from '@gjsify/resolve-npm';
@@ -1,2 +0,0 @@
1
- export { setupLib } from './lib.js';
2
- export type { LibBuildConfig, LibFactoryInput } from './lib.js';
@@ -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,189 +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
- // Backend selection (Phase D-2 decision matrix in
23
- // `docs/poc/lightningcss-decision.md`):
24
- //
25
- // 1. `@gjsify/lightningcss-native` when its prebuild is loadable on
26
- // the running architecture (3-5× faster than the WASM track,
27
- // ~960× faster cold init). Only relevant when `gjsify build`
28
- // itself runs under GJS (Phase D-3).
29
- // 2. npm `lightningcss` for everything else (Node, unsupported
30
- // arches, dev machines without the prebuild). Existing behavior;
31
- // keeps the regular dependency on this package.
32
- //
33
- // Selection is lazy and silent — the first `.css` load probes for
34
- // the native bridge once, caches the answer, and routes the rest of
35
- // the build through the chosen backend. Set the env var
36
- // `GJSIFY_CSS_BACKEND={native|npm}` to force a specific backend
37
- // (mainly useful for benchmarking + the integration suite).
38
-
39
- import { readFile } from 'node:fs/promises';
40
- import type { Plugin } from 'rolldown';
41
-
42
- export interface CssAsStringOptions {
43
- /**
44
- * lightningcss browser targets passed to `bundleAsync`. When set,
45
- * nesting + modern syntax are lowered for the given engines. The
46
- * GJS orchestrator defaults this to `{ firefox: 60 << 16 }` to
47
- * match the GTK4 CSS parser. Omit or leave undefined to skip
48
- * lowering (output stays as-authored except for `@import` inlining).
49
- */
50
- targets?: import('lightningcss').Targets;
51
- /**
52
- * When true (default), `@import` statements are resolved by
53
- * lightningcss `bundleAsync`. Set false to fall back to a plain
54
- * `readFile` — useful only when you want to keep `@import` strings
55
- * verbatim in the bundled JS (rare).
56
- */
57
- bundle?: boolean;
58
- }
59
-
60
- interface BundleResult {
61
- code: Uint8Array;
62
- }
63
-
64
- type Bundler = (filename: string, targets: import('lightningcss').Targets | undefined) => Promise<BundleResult>;
65
-
66
- let _bundlerPromise: Promise<Bundler> | null = null;
67
-
68
- async function pickBundler(): Promise<Bundler> {
69
- const forced = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.GJSIFY_CSS_BACKEND;
70
-
71
- if (forced === 'npm') return loadNpmBundler();
72
- if (forced === 'native') {
73
- const native = await tryLoadNativeBundler();
74
- if (!native) throw new Error('GJSIFY_CSS_BACKEND=native but @gjsify/lightningcss-native is not loadable');
75
- return native;
76
- }
77
-
78
- const native = await tryLoadNativeBundler();
79
- return native ?? loadNpmBundler();
80
- }
81
-
82
- // Local mirror of the @gjsify/lightningcss-native surface we touch. We
83
- // can't rely on the published types here because the package is an
84
- // OPTIONAL peer dep — under Node it's not installed, so `import type`
85
- // from it would break tsc on every Node consumer. Local interface
86
- // keeps the type narrow + decouples the plugin's typecheck from
87
- // whether the prebuild package is installed.
88
- interface NativeLightningcssSurface {
89
- hasNativeLightningcss(): boolean;
90
- bundle(input: {
91
- filename: string;
92
- targets?: string;
93
- minify?: boolean;
94
- sourceMap?: boolean;
95
- errorRecovery?: boolean;
96
- }): { code: Uint8Array; map?: Uint8Array };
97
- }
98
-
99
- async function tryLoadNativeBundler(): Promise<Bundler | null> {
100
- // The native bridge only exists under GJS — `imports.gi` marker. Skip
101
- // the dynamic import entirely on Node so it doesn't even register as a
102
- // resolved dep, which would inflate the CLI's bundled output.
103
- const isGjs = typeof (globalThis as { imports?: { gi?: unknown } }).imports?.gi !== 'undefined';
104
- if (!isGjs) return null;
105
-
106
- try {
107
- // Indirect specifier so tsc + Rolldown don't try to resolve the
108
- // optional peer dep at build time. Resolution happens only at
109
- // runtime under GJS (where the prebuild is installed).
110
- const specifier = '@gjsify/lightningcss-native';
111
- const mod = (await import(/* @vite-ignore */ specifier)) as NativeLightningcssSurface;
112
- if (!mod.hasNativeLightningcss()) return null;
113
- return async (filename, targets) => {
114
- // The native shim accepts a browserslist string; the npm
115
- // `lightningcss` Targets struct is bitfield-encoded
116
- // (`firefox: 60 << 16` etc). Convert by extracting major
117
- // version per browser key and re-emitting as the equivalent
118
- // browserslist query.
119
- const query = targetsToBrowserslist(targets);
120
- return mod.bundle({
121
- filename,
122
- targets: query,
123
- minify: false,
124
- sourceMap: false,
125
- errorRecovery: true,
126
- });
127
- };
128
- } catch {
129
- return null;
130
- }
131
- }
132
-
133
- async function loadNpmBundler(): Promise<Bundler> {
134
- const { bundleAsync } = await import('lightningcss');
135
- return async (filename, targets) => {
136
- const result = await bundleAsync({
137
- filename,
138
- targets,
139
- minify: false,
140
- errorRecovery: true,
141
- });
142
- return { code: result.code };
143
- };
144
- }
145
-
146
- function targetsToBrowserslist(
147
- targets: import('lightningcss').Targets | undefined,
148
- ): string | undefined {
149
- if (!targets) return undefined;
150
- const parts: string[] = [];
151
- for (const [browser, encoded] of Object.entries(targets) as [string, number | undefined][]) {
152
- if (typeof encoded !== 'number') continue;
153
- // npm lightningcss encodes versions as `(major << 16) | (minor << 8) | patch`.
154
- const major = (encoded >>> 16) & 0xff;
155
- if (major === 0) continue;
156
- const name = browser === 'ios_saf' ? 'ios' : browser;
157
- parts.push(`${name} >= ${major}`);
158
- }
159
- return parts.length ? parts.join(', ') : undefined;
160
- }
161
-
162
- export function cssAsStringPlugin(options: CssAsStringOptions = {}): Plugin {
163
- const { targets, bundle = true } = options;
164
- return {
165
- name: 'gjsify-css-as-string',
166
- load: {
167
- filter: { id: /\.css$/ },
168
- async handler(id: string) {
169
- const code = bundle
170
- ? new TextDecoder('utf-8').decode(await loadAndBundleCss(id, targets))
171
- : await readFile(id, 'utf8');
172
- return {
173
- code: `export default ${JSON.stringify(code)};`,
174
- moduleType: 'js' as const,
175
- };
176
- },
177
- },
178
- };
179
- }
180
-
181
- async function loadAndBundleCss(
182
- filename: string,
183
- targets: import('lightningcss').Targets | undefined,
184
- ): Promise<Uint8Array> {
185
- if (!_bundlerPromise) _bundlerPromise = pickBundler();
186
- const bundler = await _bundlerPromise;
187
- const { code } = await bundler(filename, targets);
188
- return code;
189
- }