@gjsify/rolldown-plugin-gjsify 0.3.14 → 0.3.16

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/gjs.d.ts CHANGED
@@ -19,8 +19,13 @@ export interface GjsFactoryInput {
19
19
  userBanner?: string;
20
20
  /** User-supplied resolve.alias overrides. */
21
21
  userAliases?: Record<string, string>;
22
- /** Whether to prepend the `#!/usr/bin/env -S gjs -m` shebang. */
23
- shebang?: boolean;
22
+ /**
23
+ * Shebang to prepend to the output bundle.
24
+ * `true` → default `#!/usr/bin/env -S gjs -m`
25
+ * `false` → no shebang
26
+ * `"…"` → custom line, supports `${env:NAME[:-default]}` placeholders
27
+ */
28
+ shebang?: boolean | string;
24
29
  /** Plugin options forwarded to sub-plugins (deepkit, css, …). */
25
30
  pluginOptions: PluginOptions;
26
31
  }
package/lib/app/gjs.js CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Mirrors the esbuild predecessor's `setupForGjs` exactly in terms of the
4
4
  // effective build behaviour: same externals, same alias map, same target
5
- // (firefox128 for JS, firefox60 for CSS), same console-shim injection,
5
+ // (firefox140 for JS, firefox60 for CSS), same console-shim injection,
6
6
  // same process-stub banner, same `random-access-file` fs-backed-fallback.
7
7
  //
8
8
  // Returns a partial `RolldownOptions` template plus the plugin array the
@@ -18,7 +18,7 @@ import { globToEntryPoints } from '../utils/entry-points.js';
18
18
  import { nodeModulesPathRewritePlugin, getBundleDirFromOutput, } from '../plugins/rewrite-node-modules-paths.js';
19
19
  import { processStubPlugin } from '../plugins/process-stub.js';
20
20
  import { cssAsStringPlugin } from '../plugins/css-as-string.js';
21
- import { shebangPlugin, GJS_SHEBANG } from '../plugins/shebang.js';
21
+ import { shebangPlugin, resolveShebangLine } from '../plugins/shebang.js';
22
22
  const _shimDir = dirname(fileURLToPath(import.meta.url));
23
23
  export const setupForGjs = async (input) => {
24
24
  const userExternal = input.userExternal ?? [];
@@ -75,8 +75,8 @@ export const setupForGjs = async (input) => {
75
75
  conditionNames: format === 'esm' ? ['browser', 'import'] : ['browser', 'require', 'import'],
76
76
  },
77
77
  transform: {
78
- // Compile target: GJS 1.86 / SpiderMonkey 128firefox128.
79
- target: 'firefox128',
78
+ // Compile target: GJS 1.86 / SpiderMonkey 140firefox140.
79
+ target: 'firefox140',
80
80
  define: {
81
81
  global: 'globalThis',
82
82
  window: 'globalThis',
@@ -122,10 +122,19 @@ export const setupForGjs = async (input) => {
122
122
  }),
123
123
  blueprintPlugin(),
124
124
  deepkitPlugin({ reflection: input.pluginOptions.reflection }),
125
- cssAsStringPlugin(),
125
+ // GTK4's CSS engine is much older than browser engines — its
126
+ // parser predates nesting + many modern selectors. Targeting
127
+ // `firefox: 60 << 16` makes lightningcss flatten the source
128
+ // into the subset GTK4 understands.
129
+ cssAsStringPlugin({ targets: { firefox: 60 << 16 } }),
126
130
  nodeModulesPathRewritePlugin({ bundleDir }),
127
131
  processStubPlugin({ userBanner: input.userBanner }),
128
- shebangPlugin({ enabled: input.shebang === true, line: GJS_SHEBANG }),
132
+ // resolveShebangLine returns null when disabled (false/undefined) and
133
+ // the resolved line otherwise — also handles `${env:…}` expansion.
134
+ (() => {
135
+ const line = resolveShebangLine(input.shebang);
136
+ return shebangPlugin({ enabled: line !== null, line: line ?? undefined });
137
+ })(),
129
138
  ];
130
139
  return { options, plugins };
131
140
  };
package/lib/index.d.ts CHANGED
@@ -7,7 +7,9 @@ export type { NodeModulesPathRewriteOptions, RewriteResult, } from './plugins/re
7
7
  export { processStubPlugin, GJS_PROCESS_STUB, composeBanner } from './plugins/process-stub.js';
8
8
  export type { ProcessStubPluginOptions } from './plugins/process-stub.js';
9
9
  export { cssAsStringPlugin } from './plugins/css-as-string.js';
10
- export { shebangPlugin, GJS_SHEBANG } from './plugins/shebang.js';
10
+ export { textLoaderPlugin } from './plugins/text-loader.js';
11
+ export type { TextLoaderPluginOptions } from './plugins/text-loader.js';
12
+ export { shebangPlugin, GJS_SHEBANG, expandEnvTemplate, resolveShebangLine } from './plugins/shebang.js';
11
13
  export type { ShebangPluginOptions } from './plugins/shebang.js';
12
14
  export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
13
15
  export * from './plugin.js';
package/lib/index.js CHANGED
@@ -6,7 +6,8 @@ export * from './library/index.js';
6
6
  export { REWRITE_FILTER, getBundleDirFromOutput, rewriteContents, shouldRewrite, nodeModulesPathRewritePlugin, } from './plugins/rewrite-node-modules-paths.js';
7
7
  export { processStubPlugin, GJS_PROCESS_STUB, composeBanner } from './plugins/process-stub.js';
8
8
  export { cssAsStringPlugin } from './plugins/css-as-string.js';
9
- export { shebangPlugin, GJS_SHEBANG } from './plugins/shebang.js';
9
+ export { textLoaderPlugin } from './plugins/text-loader.js';
10
+ export { shebangPlugin, GJS_SHEBANG, expandEnvTemplate, resolveShebangLine } from './plugins/shebang.js';
10
11
  export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
11
12
  export * from './plugin.js';
12
13
  import { gjsifyPlugin } from './plugin.js';
package/lib/plugin.d.ts CHANGED
@@ -13,8 +13,13 @@ export interface GjsifyPluginInput {
13
13
  userExternal?: string[];
14
14
  userBanner?: string;
15
15
  userAliases?: Record<string, string>;
16
- /** Whether to prepend `#!/usr/bin/env -S gjs -m` to the GJS bundle. */
17
- shebang?: boolean;
16
+ /**
17
+ * Shebang to prepend to the GJS bundle.
18
+ * `true` → default `#!/usr/bin/env -S gjs -m`
19
+ * `false` → no shebang
20
+ * `"…"` → custom line, supports `${env:NAME[:-default]}` placeholders
21
+ */
22
+ shebang?: boolean | string;
18
23
  }
19
24
  /**
20
25
  * Build the Rolldown configuration template + plugin array for the given
@@ -1,2 +1,19 @@
1
1
  import type { Plugin } from 'rolldown';
2
- export declare function cssAsStringPlugin(): Plugin;
2
+ export interface CssAsStringOptions {
3
+ /**
4
+ * lightningcss browser targets passed to `bundleAsync`. When set,
5
+ * nesting + modern syntax are lowered for the given engines. The
6
+ * GJS orchestrator defaults this to `{ firefox: 60 << 16 }` to
7
+ * match the GTK4 CSS parser. Omit or leave undefined to skip
8
+ * lowering (output stays as-authored except for `@import` inlining).
9
+ */
10
+ targets?: import('lightningcss').Targets;
11
+ /**
12
+ * When true (default), `@import` statements are resolved by
13
+ * lightningcss `bundleAsync`. Set false to fall back to a plain
14
+ * `readFile` — useful only when you want to keep `@import` strings
15
+ * verbatim in the bundled JS (rare).
16
+ */
17
+ bundle?: boolean;
18
+ }
19
+ export declare function cssAsStringPlugin(options?: CssAsStringOptions): Plugin;
@@ -12,18 +12,27 @@
12
12
  //
13
13
  // — the canonical pattern for `Gtk.CssProvider` under GJS.
14
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.
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.
19
25
  import { readFile } from 'node:fs/promises';
20
- export function cssAsStringPlugin() {
26
+ export function cssAsStringPlugin(options = {}) {
27
+ const { targets, bundle = true } = options;
21
28
  return {
22
29
  name: 'gjsify-css-as-string',
23
30
  load: {
24
31
  filter: { id: /\.css$/ },
25
32
  async handler(id) {
26
- const code = await readFile(id, 'utf8');
33
+ const code = bundle
34
+ ? new TextDecoder('utf-8').decode(await loadAndBundleCss(id, targets))
35
+ : await readFile(id, 'utf8');
27
36
  return {
28
37
  code: `export default ${JSON.stringify(code)};`,
29
38
  moduleType: 'js',
@@ -32,3 +41,13 @@ export function cssAsStringPlugin() {
32
41
  },
33
42
  };
34
43
  }
44
+ async function loadAndBundleCss(filename, targets) {
45
+ const { bundleAsync } = await import('lightningcss');
46
+ const result = await bundleAsync({
47
+ filename,
48
+ targets,
49
+ minify: false,
50
+ errorRecovery: true,
51
+ });
52
+ return result.code;
53
+ }
@@ -6,3 +6,25 @@ export interface ShebangPluginOptions {
6
6
  line?: string;
7
7
  }
8
8
  export declare function shebangPlugin(options?: ShebangPluginOptions): Plugin | null;
9
+ /**
10
+ * Expand `${env:NAME}` and `${env:NAME:-default}` placeholders against
11
+ * `process.env`. Missing without default → `''`. Used to let the shebang
12
+ * config field reference build-time env vars (e.g. `GJS_CONSOLE` set by
13
+ * meson-driven Flatpak builds where the GJS interpreter lives at
14
+ * `/usr/bin/gjs-console`).
15
+ */
16
+ export declare function expandEnvTemplate(input: string, env?: Record<string, string | undefined>): string;
17
+ /**
18
+ * Normalize the user-facing `shebang` config value into the literal line
19
+ * that should be prepended to the bundle (without trailing newline), or
20
+ * `null` when shebang injection is disabled.
21
+ *
22
+ * `true` → default GJS shebang
23
+ * `false|undefined` → null (disabled)
24
+ * `"…"` → string with `${env:NAME[:-default]}` expanded
25
+ *
26
+ * If the resolved string does not start with `#!`, it is prefixed
27
+ * automatically so users can write `"shebang": "/usr/bin/gjs -m"` instead
28
+ * of `"#!/usr/bin/gjs -m"`.
29
+ */
30
+ export declare function resolveShebangLine(value: boolean | string | undefined): string | null;
@@ -24,3 +24,41 @@ export function shebangPlugin(options = {}) {
24
24
  },
25
25
  };
26
26
  }
27
+ /**
28
+ * Expand `${env:NAME}` and `${env:NAME:-default}` placeholders against
29
+ * `process.env`. Missing without default → `''`. Used to let the shebang
30
+ * config field reference build-time env vars (e.g. `GJS_CONSOLE` set by
31
+ * meson-driven Flatpak builds where the GJS interpreter lives at
32
+ * `/usr/bin/gjs-console`).
33
+ */
34
+ export function expandEnvTemplate(input, env = process.env) {
35
+ return input.replace(/\$\{env:([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (_match, name, fallback) => {
36
+ const value = env[name];
37
+ if (value !== undefined && value !== '')
38
+ return value;
39
+ return fallback ?? '';
40
+ });
41
+ }
42
+ /**
43
+ * Normalize the user-facing `shebang` config value into the literal line
44
+ * that should be prepended to the bundle (without trailing newline), or
45
+ * `null` when shebang injection is disabled.
46
+ *
47
+ * `true` → default GJS shebang
48
+ * `false|undefined` → null (disabled)
49
+ * `"…"` → string with `${env:NAME[:-default]}` expanded
50
+ *
51
+ * If the resolved string does not start with `#!`, it is prefixed
52
+ * automatically so users can write `"shebang": "/usr/bin/gjs -m"` instead
53
+ * of `"#!/usr/bin/gjs -m"`.
54
+ */
55
+ export function resolveShebangLine(value) {
56
+ if (value === undefined || value === false)
57
+ return null;
58
+ if (value === true)
59
+ return GJS_SHEBANG;
60
+ const expanded = expandEnvTemplate(value);
61
+ if (!expanded.trim())
62
+ return null;
63
+ return expanded.startsWith('#!') ? expanded : '#!' + expanded;
64
+ }
@@ -0,0 +1,10 @@
1
+ import type { Plugin } from 'rolldown';
2
+ export interface TextLoaderPluginOptions {
3
+ /**
4
+ * Map of file extension (with leading `.`) → loader kind. Currently only
5
+ * `'text'` is implemented; the field is shaped this way to leave room
6
+ * for `'json'` / `'binary'` later without a config break.
7
+ */
8
+ loaders?: Record<string, 'text'>;
9
+ }
10
+ export declare function textLoaderPlugin(options?: TextLoaderPluginOptions): Plugin | null;
@@ -0,0 +1,41 @@
1
+ // Generic "load file as JS string default export" plugin.
2
+ //
3
+ // Mirrors `css-as-string` but lets the user opt-in arbitrary extensions
4
+ // through `bundler.loaders` config, e.g.:
5
+ //
6
+ // "bundler": { "loaders": { ".ui": "text", ".asm": "text" } }
7
+ //
8
+ // — replaces the esbuild `loader: { '.ui': 'text' }` shorthand from the
9
+ // pre-Rolldown era. Rolldown does not classify unknown extensions as text
10
+ // by default; without a hook it tries to parse them as JS and fails.
11
+ import { readFile } from 'node:fs/promises';
12
+ export function textLoaderPlugin(options = {}) {
13
+ const exts = Object.entries(options.loaders ?? {})
14
+ .filter(([, kind]) => kind === 'text')
15
+ .map(([ext]) => ext);
16
+ if (exts.length === 0)
17
+ return null;
18
+ // Build a single regex matching any of the configured extensions:
19
+ // ['.ui', '.asm'] → /\.(ui|asm)$/
20
+ const escaped = exts.map((e) => e.replace(/^\./, '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
21
+ const filter = new RegExp(`\\.(?:${escaped.join('|')})$`);
22
+ // Use the function-form `load(id)` (Rollup-compatible) rather than the
23
+ // newer `load: { filter, handler }` shape. The newer shape was observed
24
+ // not to claim unknown-extension files reliably under Rolldown rc.18 —
25
+ // Rolldown's parser ran BEFORE the filtered handler fired and rejected
26
+ // `.ui`/`.asm` content as invalid JS/JSX. Using the function form (same
27
+ // as `@gjsify/vite-plugin-blueprint`'s `.blp` hook) intercepts during
28
+ // module-load lookup and works under both Vite and Rolldown.
29
+ return {
30
+ name: 'gjsify-text-loader',
31
+ async load(id) {
32
+ if (!filter.test(id))
33
+ return null;
34
+ const code = await readFile(id, 'utf8');
35
+ return {
36
+ code: `export default ${JSON.stringify(code)};`,
37
+ moduleType: 'js',
38
+ };
39
+ },
40
+ };
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/rolldown-plugin-gjsify",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "description": "Rolldown / Rollup / Vite plugin orchestrator for GJS, Node, and Browser targets",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -42,10 +42,10 @@
42
42
  ],
43
43
  "license": "MIT",
44
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",
45
+ "@gjsify/resolve-npm": "^0.3.16",
46
+ "@gjsify/rolldown-plugin-deepkit": "^0.3.16",
47
+ "@gjsify/rolldown-plugin-pnp": "^0.3.16",
48
+ "@gjsify/vite-plugin-blueprint": "^0.3.16",
49
49
  "@rollup/pluginutils": "^5.1.4",
50
50
  "acorn": "^8.14.0",
51
51
  "acorn-walk": "^8.3.4",
package/src/app/gjs.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Mirrors the esbuild predecessor's `setupForGjs` exactly in terms of the
4
4
  // effective build behaviour: same externals, same alias map, same target
5
- // (firefox128 for JS, firefox60 for CSS), same console-shim injection,
5
+ // (firefox140 for JS, firefox60 for CSS), same console-shim injection,
6
6
  // same process-stub banner, same `random-access-file` fs-backed-fallback.
7
7
  //
8
8
  // Returns a partial `RolldownOptions` template plus the plugin array the
@@ -26,7 +26,7 @@ import {
26
26
  } from '../plugins/rewrite-node-modules-paths.js';
27
27
  import { processStubPlugin } from '../plugins/process-stub.js';
28
28
  import { cssAsStringPlugin } from '../plugins/css-as-string.js';
29
- import { shebangPlugin, GJS_SHEBANG } from '../plugins/shebang.js';
29
+ import { shebangPlugin, resolveShebangLine } from '../plugins/shebang.js';
30
30
 
31
31
  const _shimDir = dirname(fileURLToPath(import.meta.url));
32
32
 
@@ -47,8 +47,13 @@ export interface GjsFactoryInput {
47
47
  userBanner?: string;
48
48
  /** User-supplied resolve.alias overrides. */
49
49
  userAliases?: Record<string, string>;
50
- /** Whether to prepend the `#!/usr/bin/env -S gjs -m` shebang. */
51
- shebang?: boolean;
50
+ /**
51
+ * Shebang to prepend to the output bundle.
52
+ * `true` → default `#!/usr/bin/env -S gjs -m`
53
+ * `false` → no shebang
54
+ * `"…"` → custom line, supports `${env:NAME[:-default]}` placeholders
55
+ */
56
+ shebang?: boolean | string;
52
57
  /** Plugin options forwarded to sub-plugins (deepkit, css, …). */
53
58
  pluginOptions: PluginOptions;
54
59
  }
@@ -111,8 +116,8 @@ export const setupForGjs = async (input: GjsFactoryInput): Promise<GjsBuildConfi
111
116
  conditionNames: format === 'esm' ? ['browser', 'import'] : ['browser', 'require', 'import'],
112
117
  },
113
118
  transform: {
114
- // Compile target: GJS 1.86 / SpiderMonkey 128firefox128.
115
- target: 'firefox128',
119
+ // Compile target: GJS 1.86 / SpiderMonkey 140firefox140.
120
+ target: 'firefox140',
116
121
  define: {
117
122
  global: 'globalThis',
118
123
  window: 'globalThis',
@@ -160,10 +165,19 @@ export const setupForGjs = async (input: GjsFactoryInput): Promise<GjsBuildConfi
160
165
  }),
161
166
  blueprintPlugin() as RolldownPluginOption,
162
167
  deepkitPlugin({ reflection: input.pluginOptions.reflection }),
163
- cssAsStringPlugin(),
168
+ // GTK4's CSS engine is much older than browser engines — its
169
+ // parser predates nesting + many modern selectors. Targeting
170
+ // `firefox: 60 << 16` makes lightningcss flatten the source
171
+ // into the subset GTK4 understands.
172
+ cssAsStringPlugin({ targets: { firefox: 60 << 16 } }),
164
173
  nodeModulesPathRewritePlugin({ bundleDir }),
165
174
  processStubPlugin({ userBanner: input.userBanner }),
166
- shebangPlugin({ enabled: input.shebang === true, line: GJS_SHEBANG }),
175
+ // resolveShebangLine returns null when disabled (false/undefined) and
176
+ // the resolved line otherwise — also handles `${env:…}` expansion.
177
+ (() => {
178
+ const line = resolveShebangLine(input.shebang);
179
+ return shebangPlugin({ enabled: line !== null, line: line ?? undefined });
180
+ })(),
167
181
  ];
168
182
 
169
183
  return { options, plugins };
package/src/index.ts CHANGED
@@ -20,7 +20,9 @@ export type {
20
20
  export { processStubPlugin, GJS_PROCESS_STUB, composeBanner } from './plugins/process-stub.js';
21
21
  export type { ProcessStubPluginOptions } from './plugins/process-stub.js';
22
22
  export { cssAsStringPlugin } from './plugins/css-as-string.js';
23
- export { shebangPlugin, GJS_SHEBANG } from './plugins/shebang.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';
24
26
  export type { ShebangPluginOptions } from './plugins/shebang.js';
25
27
  export { gjsImportsEmptyPlugin } from './plugins/gjs-imports-empty.js';
26
28
 
package/src/plugin.ts CHANGED
@@ -27,8 +27,13 @@ export interface GjsifyPluginInput {
27
27
  userExternal?: string[];
28
28
  userBanner?: string;
29
29
  userAliases?: Record<string, string>;
30
- /** Whether to prepend `#!/usr/bin/env -S gjs -m` to the GJS bundle. */
31
- shebang?: boolean;
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;
32
37
  }
33
38
 
34
39
  /**
@@ -12,21 +12,48 @@
12
12
  //
13
13
  // — the canonical pattern for `Gtk.CssProvider` under GJS.
14
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.
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.
19
25
 
20
26
  import { readFile } from 'node:fs/promises';
21
27
  import type { Plugin } from 'rolldown';
22
28
 
23
- export function cssAsStringPlugin(): Plugin {
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;
24
49
  return {
25
50
  name: 'gjsify-css-as-string',
26
51
  load: {
27
52
  filter: { id: /\.css$/ },
28
53
  async handler(id: string) {
29
- const code = await readFile(id, 'utf8');
54
+ const code = bundle
55
+ ? new TextDecoder('utf-8').decode(await loadAndBundleCss(id, targets))
56
+ : await readFile(id, 'utf8');
30
57
  return {
31
58
  code: `export default ${JSON.stringify(code)};`,
32
59
  moduleType: 'js' as const,
@@ -35,3 +62,17 @@ export function cssAsStringPlugin(): Plugin {
35
62
  },
36
63
  };
37
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
+ }
@@ -31,3 +31,39 @@ export function shebangPlugin(options: ShebangPluginOptions = {}): Plugin | null
31
31
  },
32
32
  };
33
33
  }
34
+
35
+ /**
36
+ * Expand `${env:NAME}` and `${env:NAME:-default}` placeholders against
37
+ * `process.env`. Missing without default → `''`. Used to let the shebang
38
+ * config field reference build-time env vars (e.g. `GJS_CONSOLE` set by
39
+ * meson-driven Flatpak builds where the GJS interpreter lives at
40
+ * `/usr/bin/gjs-console`).
41
+ */
42
+ export function expandEnvTemplate(input: string, env: Record<string, string | undefined> = process.env): string {
43
+ return input.replace(/\$\{env:([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (_match, name: string, fallback?: string) => {
44
+ const value = env[name];
45
+ if (value !== undefined && value !== '') return value;
46
+ return fallback ?? '';
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Normalize the user-facing `shebang` config value into the literal line
52
+ * that should be prepended to the bundle (without trailing newline), or
53
+ * `null` when shebang injection is disabled.
54
+ *
55
+ * `true` → default GJS shebang
56
+ * `false|undefined` → null (disabled)
57
+ * `"…"` → string with `${env:NAME[:-default]}` expanded
58
+ *
59
+ * If the resolved string does not start with `#!`, it is prefixed
60
+ * automatically so users can write `"shebang": "/usr/bin/gjs -m"` instead
61
+ * of `"#!/usr/bin/gjs -m"`.
62
+ */
63
+ export function resolveShebangLine(value: boolean | string | undefined): string | null {
64
+ if (value === undefined || value === false) return null;
65
+ if (value === true) return GJS_SHEBANG;
66
+ const expanded = expandEnvTemplate(value);
67
+ if (!expanded.trim()) return null;
68
+ return expanded.startsWith('#!') ? expanded : '#!' + expanded;
69
+ }
@@ -0,0 +1,54 @@
1
+ // Generic "load file as JS string default export" plugin.
2
+ //
3
+ // Mirrors `css-as-string` but lets the user opt-in arbitrary extensions
4
+ // through `bundler.loaders` config, e.g.:
5
+ //
6
+ // "bundler": { "loaders": { ".ui": "text", ".asm": "text" } }
7
+ //
8
+ // — replaces the esbuild `loader: { '.ui': 'text' }` shorthand from the
9
+ // pre-Rolldown era. Rolldown does not classify unknown extensions as text
10
+ // by default; without a hook it tries to parse them as JS and fails.
11
+
12
+ import { readFile } from 'node:fs/promises';
13
+ import type { Plugin } from 'rolldown';
14
+
15
+ export interface TextLoaderPluginOptions {
16
+ /**
17
+ * Map of file extension (with leading `.`) → loader kind. Currently only
18
+ * `'text'` is implemented; the field is shaped this way to leave room
19
+ * for `'json'` / `'binary'` later without a config break.
20
+ */
21
+ loaders?: Record<string, 'text'>;
22
+ }
23
+
24
+ export function textLoaderPlugin(options: TextLoaderPluginOptions = {}): Plugin | null {
25
+ const exts = Object.entries(options.loaders ?? {})
26
+ .filter(([, kind]) => kind === 'text')
27
+ .map(([ext]) => ext);
28
+
29
+ if (exts.length === 0) return null;
30
+
31
+ // Build a single regex matching any of the configured extensions:
32
+ // ['.ui', '.asm'] → /\.(ui|asm)$/
33
+ const escaped = exts.map((e) => e.replace(/^\./, '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
34
+ const filter = new RegExp(`\\.(?:${escaped.join('|')})$`);
35
+
36
+ // Use the function-form `load(id)` (Rollup-compatible) rather than the
37
+ // newer `load: { filter, handler }` shape. The newer shape was observed
38
+ // not to claim unknown-extension files reliably under Rolldown rc.18 —
39
+ // Rolldown's parser ran BEFORE the filtered handler fired and rejected
40
+ // `.ui`/`.asm` content as invalid JS/JSX. Using the function form (same
41
+ // as `@gjsify/vite-plugin-blueprint`'s `.blp` hook) intercepts during
42
+ // module-load lookup and works under both Vite and Rolldown.
43
+ return {
44
+ name: 'gjsify-text-loader',
45
+ async load(id: string) {
46
+ if (!filter.test(id)) return null;
47
+ const code = await readFile(id, 'utf8');
48
+ return {
49
+ code: `export default ${JSON.stringify(code)};`,
50
+ moduleType: 'js' as const,
51
+ };
52
+ },
53
+ };
54
+ }