@gjsify/cli 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.
Files changed (43) hide show
  1. package/lib/actions/build.d.ts +6 -2
  2. package/lib/actions/build.js +37 -8
  3. package/lib/commands/flatpak/build.d.ts +16 -0
  4. package/lib/commands/flatpak/build.js +187 -0
  5. package/lib/commands/flatpak/ci.d.ts +13 -0
  6. package/lib/commands/flatpak/ci.js +133 -0
  7. package/lib/commands/flatpak/deps.d.ts +12 -0
  8. package/lib/commands/flatpak/deps.js +96 -0
  9. package/lib/commands/flatpak/index.d.ts +7 -0
  10. package/lib/commands/flatpak/index.js +23 -0
  11. package/lib/commands/flatpak/init.d.ts +15 -0
  12. package/lib/commands/flatpak/init.js +154 -0
  13. package/lib/commands/flatpak/utils.d.ts +32 -0
  14. package/lib/commands/flatpak/utils.js +63 -0
  15. package/lib/commands/gsettings.d.ts +9 -0
  16. package/lib/commands/gsettings.js +72 -0
  17. package/lib/commands/index.d.ts +2 -0
  18. package/lib/commands/index.js +2 -0
  19. package/lib/commands/install.d.ts +1 -0
  20. package/lib/commands/install.js +66 -11
  21. package/lib/config.js +54 -2
  22. package/lib/index.js +3 -1
  23. package/lib/types/config-data.d.ts +145 -4
  24. package/lib/utils/install-global.d.ts +54 -0
  25. package/lib/utils/install-global.js +153 -0
  26. package/lib/utils/resolve-plugin-by-name.d.ts +21 -0
  27. package/lib/utils/resolve-plugin-by-name.js +75 -0
  28. package/package.json +10 -10
  29. package/src/actions/build.ts +41 -8
  30. package/src/commands/flatpak/build.ts +225 -0
  31. package/src/commands/flatpak/ci.ts +173 -0
  32. package/src/commands/flatpak/deps.ts +120 -0
  33. package/src/commands/flatpak/index.ts +53 -0
  34. package/src/commands/flatpak/init.ts +191 -0
  35. package/src/commands/flatpak/utils.ts +76 -0
  36. package/src/commands/gsettings.ts +87 -0
  37. package/src/commands/index.ts +2 -0
  38. package/src/commands/install.ts +90 -11
  39. package/src/config.ts +58 -2
  40. package/src/index.ts +4 -0
  41. package/src/types/config-data.ts +142 -4
  42. package/src/utils/install-global.ts +182 -0
  43. package/src/utils/resolve-plugin-by-name.ts +106 -0
@@ -1,5 +1,23 @@
1
- import type { RolldownOptions, OutputOptions } from 'rolldown';
1
+ import type { RolldownOptions, OutputOptions, RolldownPluginOption } from 'rolldown';
2
2
  import type { ConfigDataLibrary, ConfigDataTypescript } from './index.js';
3
+ /**
4
+ * Plugin entry resolvable by package name from the project's `node_modules`.
5
+ * Lets users describe the plugin chain in `package.json#gjsify` without
6
+ * dropping to a JS-form config file. The CLI imports the named module,
7
+ * picks the chosen export (defaults to `default`), and calls it with
8
+ * `options`.
9
+ *
10
+ * Example:
11
+ * ```jsonc
12
+ * { "name": "@gjsify/vite-plugin-blueprint", "options": { "minify": true } }
13
+ * { "name": "@gjsify/vite-plugin-gettext", "export": "msgfmtPlugin", "options": { ... } }
14
+ * ```
15
+ */
16
+ export interface BundlerPluginByName {
17
+ name: string;
18
+ export?: string;
19
+ options?: unknown;
20
+ }
3
21
  /**
4
22
  * Subset of `RolldownOptions` accepted in `.gjsifyrc.js`. Mirrors the legacy
5
23
  * `esbuild?: BuildOptions` field — a thin pass-through. The orchestrator
@@ -9,9 +27,14 @@ import type { ConfigDataLibrary, ConfigDataTypescript } from './index.js';
9
27
  * `output` is constrained to a single `OutputOptions` object (Rolldown also
10
28
  * accepts an array for multi-output builds, but the CLI surface targets the
11
29
  * single-output use case).
30
+ *
31
+ * `plugins` is widened to also accept `BundlerPluginByName` entries — these
32
+ * are resolved by the CLI from the project's `node_modules` before the
33
+ * Rolldown call.
12
34
  */
13
- export type BundlerOptions = Omit<RolldownOptions, 'output'> & {
35
+ export type BundlerOptions = Omit<RolldownOptions, 'output' | 'plugins'> & {
14
36
  output?: OutputOptions;
37
+ plugins?: Array<RolldownPluginOption | BundlerPluginByName>;
15
38
  };
16
39
  /**
17
40
  * Legacy `esbuild?: BuildOptions` shape — kept as a compatibility shim for
@@ -68,9 +91,21 @@ export interface ConfigData {
68
91
  */
69
92
  globals?: string;
70
93
  /**
71
- * Prepend GJS shebang to output and mark executable. See CliBuildOptions.
94
+ * Prepend a shebang to the output bundle and mark it executable.
95
+ *
96
+ * `true` → use the default `#!/usr/bin/env -S gjs -m` line
97
+ * `false` → no shebang (default)
98
+ * `"…"` → custom line. Supports `${env:NAME}` and `${env:NAME:-default}`
99
+ * placeholders against `process.env`. The leading `#!` is
100
+ * added automatically if omitted. Useful when an outer
101
+ * build tool (Meson, Flatpak) exports the GJS interpreter
102
+ * path as `GJS_CONSOLE` (e.g. `/usr/bin/gjs-console`).
103
+ *
104
+ * Example: `"shebang": "${env:GJS_CONSOLE:-/usr/bin/env -S gjs} -m"`
105
+ *
106
+ * See also `CliBuildOptions.shebang`.
72
107
  */
73
- shebang?: boolean;
108
+ shebang?: boolean | string;
74
109
  /**
75
110
  * Extra module aliases layered on top of the built-in alias map.
76
111
  * Comes from `gjsify build --alias FROM=TO`.
@@ -83,4 +118,110 @@ export interface ConfigData {
83
118
  * Example: `["fetch", "XMLHttpRequest"]` excludes the HTTP polyfill stack.
84
119
  */
85
120
  excludeGlobals?: string[];
121
+ /**
122
+ * Compile-time defines populated from `package.json` fields. Each entry
123
+ * maps a JS identifier (the define key) to a dotted package.json path.
124
+ * Values are JSON-stringified before merging into `bundler.transform.define`.
125
+ *
126
+ * Example:
127
+ * ```jsonc
128
+ * "defineFromPackageJson": {
129
+ * "__PACKAGE_VERSION__": { "field": "version" },
130
+ * "__PACKAGE_NAME__": { "field": "name" }
131
+ * }
132
+ * ```
133
+ *
134
+ * Replaces the wrapper-script pattern (`spawnSync('gjsify', ['build',
135
+ * '--define', '__VERSION__=' + JSON.stringify(pkg.version)])`) used by
136
+ * `@ts-for-gir/cli` before this option existed.
137
+ */
138
+ defineFromPackageJson?: Record<string, {
139
+ field: string;
140
+ }>;
141
+ /**
142
+ * Compile-time defines populated from `process.env` at config-load time.
143
+ * Each entry maps a JS identifier to an environment variable name with an
144
+ * optional default. Values are JSON-stringified before merging into
145
+ * `bundler.transform.define`. When the variable is unset and no default
146
+ * is provided, the identifier is replaced with the literal `undefined`
147
+ * so consumer code can safely guard with `typeof X === 'undefined'` or
148
+ * `X ?? fallback`.
149
+ *
150
+ * Example:
151
+ * ```jsonc
152
+ * "defineFromEnv": {
153
+ * "__APPLICATION_ID__": { "env": "APPLICATION_ID", "default": "org.example.App" },
154
+ * "__PREFIX__": { "env": "PREFIX" }
155
+ * }
156
+ * ```
157
+ *
158
+ * Designed for projects whose build is driven by an outer tool (Meson,
159
+ * Make, CI) that exports environment variables — avoids a wrapper script
160
+ * just to thread them through to the bundler.
161
+ */
162
+ defineFromEnv?: Record<string, {
163
+ env: string;
164
+ default?: string;
165
+ }>;
166
+ /**
167
+ * Extension → loader-kind map for files Rolldown does not classify
168
+ * natively. Currently only `'text'` is implemented — the file's content
169
+ * becomes the JS string default export (`export default "<content>"`).
170
+ * Replaces the legacy esbuild `loader: { '.ui': 'text' }` pattern.
171
+ *
172
+ * Example:
173
+ * ```jsonc
174
+ * "loaders": { ".ui": "text", ".asm": "text" }
175
+ * ```
176
+ *
177
+ * Lives at the top level (not under `bundler`) so it doesn't leak into
178
+ * Rolldown's options on pass-through; the CLI converts it into a
179
+ * `text-loader` plugin prepended to the bundler's plugin chain.
180
+ */
181
+ loaders?: Record<string, 'text'>;
182
+ /**
183
+ * Flatpak-related configuration consumed by `gjsify flatpak <sub>`.
184
+ * Lives in its own top-level namespace so the bundler config doesn't
185
+ * accumulate concerns and `flatpak init` / `flatpak ci` can read defaults
186
+ * declaratively. CLI flags override these values.
187
+ */
188
+ flatpak?: ConfigDataFlatpak;
189
+ }
190
+ /**
191
+ * Flatpak-toolchain config consumed by the `gjsify flatpak` subcommand
192
+ * group. All fields optional — sensible defaults apply when missing.
193
+ */
194
+ export interface ConfigDataFlatpak {
195
+ /** Reverse-DNS app id, e.g. `eu.jumplink.Learn6502`. Defaults to `package.json#name` if it looks like a reverse-DNS id. */
196
+ appId?: string;
197
+ /**
198
+ * Runtime family. Default `'gnome'` — needed at runtime by GJS bundles
199
+ * for GLib/GObject/GIO. `'freedesktop'` is only suitable for non-gjsify
200
+ * CLI tools (no GJS interpreter ships in the Freedesktop runtime).
201
+ */
202
+ runtime?: 'gnome' | 'freedesktop';
203
+ /** Runtime/SDK version, e.g. `'50'` for GNOME or `'24.08'` for Freedesktop. */
204
+ runtimeVersion?: string;
205
+ /** Extra SDK extensions, e.g. `['org.freedesktop.Sdk.Extension.node24']` for build-time `yarn install`. */
206
+ sdkExtensions?: string[];
207
+ /** Path components prepended to PATH inside the build sandbox. */
208
+ appendPath?: string[];
209
+ /** The binary name to run (`/app/bin/<command>`). Defaults to `appId`. */
210
+ command?: string;
211
+ /** Finish-args (capabilities). Default depends on `runtime` + `--cli-only`. */
212
+ finishArgs?: string[];
213
+ /** Extra Flatpak modules prepended before the app's own meson/simple module (e.g. `blueprint-compiler` build). */
214
+ extraModules?: unknown[];
215
+ /** Cleanup glob patterns applied to the final manifest, e.g. `['/include', '/lib/pkgconfig']`. */
216
+ cleanup?: string[];
217
+ /** Source-of-truth lockfile for `gjsify flatpak deps` — `yarn.lock` or `package-lock.json`. */
218
+ lockfile?: string;
219
+ /**
220
+ * GitHub-Actions container image override for `gjsify flatpak ci`.
221
+ * Default derived from runtime + runtimeVersion:
222
+ * gnome+50 → `ghcr.io/flathub-infra/flatpak-github-actions:gnome-50`
223
+ */
224
+ ciContainer?: string;
225
+ /** Branches the generated workflow triggers on. Default `['main']`. */
226
+ ciBranches?: string[];
86
227
  }
@@ -0,0 +1,54 @@
1
+ export interface GlobalLayout {
2
+ /** Where extracted package trees live: `<prefix>/node_modules/<pkg>/`. */
3
+ prefix: string;
4
+ /** Where bin-name symlinks land. Typically `~/.local/bin`. */
5
+ binDir: string;
6
+ }
7
+ /**
8
+ * Compute the canonical global install layout for the current user. Honours
9
+ * `XDG_DATA_HOME` (per the XDG Base Directory Spec) plus
10
+ * `GJSIFY_GLOBAL_PREFIX` and `GJSIFY_GLOBAL_BIN_DIR` escape hatches for tests.
11
+ */
12
+ export declare function defaultGlobalLayout(): GlobalLayout;
13
+ export interface LinkedBin {
14
+ /** The bin-name (key from `bin` / `gjsify.bin`). */
15
+ name: string;
16
+ /** Absolute path to the file the symlink points at. */
17
+ target: string;
18
+ /** Absolute path to the symlink (under `binDir`). */
19
+ link: string;
20
+ }
21
+ /**
22
+ * Install each top-level package's bin entries into `binDir` as small POSIX
23
+ * `sh` launchers that exec the real bin. Reads each package's installed
24
+ * `package.json` to discover bins; prefers `gjsify.bin` (GJS-bundled bins)
25
+ * over the npm `bin` field, falling back to npm `bin` when no GJS map is
26
+ * declared. Stale launchers are replaced (latest install wins).
27
+ *
28
+ * Why launchers instead of symlinks:
29
+ *
30
+ * When a GJS bundle is invoked via a symlink in $PATH, the kernel follows
31
+ * the symlink to find the executable but passes the original (symlink)
32
+ * path to it. The bundle's shebang then runs as `gjs -m <symlink-path>`,
33
+ * which makes `import.meta.url` resolve to the symlink directory — so any
34
+ * path computation relative to `import.meta.url` (e.g. ts-for-gir's
35
+ * `findTemplatesRoot()`, version-discovery `readFileSync`s, gjsify's own
36
+ * `import.meta.url` rewrites) looks for assets in the wrong place.
37
+ *
38
+ * A `sh` launcher invokes the real path explicitly, so the bundle sees its
39
+ * real install location in `import.meta.url` and every relative read works.
40
+ * This costs ~50 bytes per bin and one extra `exec` per launch — both
41
+ * negligible compared to `gjs -m` cold-start time.
42
+ *
43
+ * Plain Node bins are unaffected by either approach (Node defaults to
44
+ * resolving symlinks in ESM module URLs); launchers are uniform for
45
+ * simplicity and so we don't need to discriminate by runtime here.
46
+ */
47
+ export declare function linkGlobalBins(packageNames: string[], layout: GlobalLayout): LinkedBin[];
48
+ /** Returns `true` if `binDir` is on the user's PATH. */
49
+ export declare function binDirOnPath(binDir: string): boolean;
50
+ /**
51
+ * Extracts the package name from a spec like `name@1.2`, `@scope/name`,
52
+ * or `@scope/name@latest`. Returns the unchanged string for plain names.
53
+ */
54
+ export declare function specToPackageName(spec: string): string;
@@ -0,0 +1,153 @@
1
+ // Global-install helpers for `gjsify install -g <pkg>`.
2
+ //
3
+ // Layout (XDG-compliant, ~ = $HOME):
4
+ //
5
+ // ~/.local/share/gjsify/global/
6
+ // node_modules/<pkg>/ ← extracted package contents
7
+ // package.json
8
+ // bin/<pkg> ← original npm bin
9
+ // bin/<pkg>-gjs ← GJS bundle (declared via `gjsify.bin`)
10
+ // <pkg-data-files> ← e.g. `dist-templates/` for ts-for-gir
11
+ // ~/.local/bin/<bin-name> ← symlink to the matching bin under node_modules
12
+ //
13
+ // Unlike the project install path (`gjsify install <pkg>` without -g), this
14
+ // mode installs the requested top-level packages plus their runtime deps into
15
+ // a user-owned XDG location, never mutates a project package.json, and links
16
+ // bins into the user's PATH so the package is invocable as `<bin-name>` from
17
+ // anywhere — the closest GJS equivalent of `npm i -g`.
18
+ //
19
+ // `gjsify.bin` (when declared) wins over the standard npm `bin` field on this
20
+ // path because the user explicitly asked for the GJS-runnable artifact: e.g.
21
+ // `ts-for-gir` (npm bin = bin/ts-for-gir Node script) becomes a symlink to
22
+ // `bin/ts-for-gir-gjs` (the self-contained GJS bundle) instead.
23
+ import * as fs from 'node:fs';
24
+ import * as os from 'node:os';
25
+ import * as path from 'node:path';
26
+ /**
27
+ * Compute the canonical global install layout for the current user. Honours
28
+ * `XDG_DATA_HOME` (per the XDG Base Directory Spec) plus
29
+ * `GJSIFY_GLOBAL_PREFIX` and `GJSIFY_GLOBAL_BIN_DIR` escape hatches for tests.
30
+ */
31
+ export function defaultGlobalLayout() {
32
+ const prefixOverride = process.env.GJSIFY_GLOBAL_PREFIX;
33
+ const binOverride = process.env.GJSIFY_GLOBAL_BIN_DIR;
34
+ const home = os.homedir();
35
+ const xdgData = process.env.XDG_DATA_HOME ?? path.join(home, '.local', 'share');
36
+ return {
37
+ prefix: prefixOverride ?? path.join(xdgData, 'gjsify', 'global'),
38
+ binDir: binOverride ?? path.join(home, '.local', 'bin'),
39
+ };
40
+ }
41
+ /**
42
+ * Install each top-level package's bin entries into `binDir` as small POSIX
43
+ * `sh` launchers that exec the real bin. Reads each package's installed
44
+ * `package.json` to discover bins; prefers `gjsify.bin` (GJS-bundled bins)
45
+ * over the npm `bin` field, falling back to npm `bin` when no GJS map is
46
+ * declared. Stale launchers are replaced (latest install wins).
47
+ *
48
+ * Why launchers instead of symlinks:
49
+ *
50
+ * When a GJS bundle is invoked via a symlink in $PATH, the kernel follows
51
+ * the symlink to find the executable but passes the original (symlink)
52
+ * path to it. The bundle's shebang then runs as `gjs -m <symlink-path>`,
53
+ * which makes `import.meta.url` resolve to the symlink directory — so any
54
+ * path computation relative to `import.meta.url` (e.g. ts-for-gir's
55
+ * `findTemplatesRoot()`, version-discovery `readFileSync`s, gjsify's own
56
+ * `import.meta.url` rewrites) looks for assets in the wrong place.
57
+ *
58
+ * A `sh` launcher invokes the real path explicitly, so the bundle sees its
59
+ * real install location in `import.meta.url` and every relative read works.
60
+ * This costs ~50 bytes per bin and one extra `exec` per launch — both
61
+ * negligible compared to `gjs -m` cold-start time.
62
+ *
63
+ * Plain Node bins are unaffected by either approach (Node defaults to
64
+ * resolving symlinks in ESM module URLs); launchers are uniform for
65
+ * simplicity and so we don't need to discriminate by runtime here.
66
+ */
67
+ export function linkGlobalBins(packageNames, layout) {
68
+ fs.mkdirSync(layout.binDir, { recursive: true });
69
+ const created = [];
70
+ for (const pkgName of packageNames) {
71
+ const pkgDir = path.join(layout.prefix, 'node_modules', pkgName);
72
+ const pkgJsonPath = path.join(pkgDir, 'package.json');
73
+ if (!fs.existsSync(pkgJsonPath))
74
+ continue;
75
+ const pkgJson = readJson(pkgJsonPath);
76
+ const binMap = pickBinMap(pkgName, pkgJson);
77
+ if (!binMap || binMap.size === 0)
78
+ continue;
79
+ for (const [binName, binTarget] of binMap) {
80
+ const targetAbs = path.join(pkgDir, binTarget);
81
+ if (!fs.existsSync(targetAbs))
82
+ continue;
83
+ try {
84
+ fs.chmodSync(targetAbs, 0o755);
85
+ }
86
+ catch {
87
+ /* best effort */
88
+ }
89
+ const linkPath = path.join(layout.binDir, binName);
90
+ fs.rmSync(linkPath, { force: true });
91
+ // Inline `${target}` directly — this file is rewritten on every
92
+ // install, paths are user-owned, and POSIX `sh` quoting via
93
+ // single-quotes plus `'\''` for embedded quotes is well-defined.
94
+ const launcher = `#!/bin/sh\nexec ${shQuote(targetAbs)} "$@"\n`;
95
+ fs.writeFileSync(linkPath, launcher);
96
+ fs.chmodSync(linkPath, 0o755);
97
+ created.push({ name: binName, target: targetAbs, link: linkPath });
98
+ }
99
+ }
100
+ return created;
101
+ }
102
+ function shQuote(s) {
103
+ return `'${s.replace(/'/g, `'\\''`)}'`;
104
+ }
105
+ function pickBinMap(pkgName, pkgJson) {
106
+ const gjsifyEntry = pkgJson.gjsify;
107
+ if (gjsifyEntry?.bin !== undefined) {
108
+ return normalizeBin(pkgName, gjsifyEntry.bin);
109
+ }
110
+ const npmBin = pkgJson.bin;
111
+ if (npmBin !== undefined) {
112
+ return normalizeBin(pkgName, npmBin);
113
+ }
114
+ return null;
115
+ }
116
+ function normalizeBin(pkgName, bin) {
117
+ const out = new Map();
118
+ if (typeof bin === 'string') {
119
+ const baseName = pkgName.startsWith('@')
120
+ ? pkgName.slice(pkgName.indexOf('/') + 1)
121
+ : pkgName;
122
+ out.set(baseName, bin);
123
+ return out;
124
+ }
125
+ for (const [k, v] of Object.entries(bin))
126
+ out.set(k, v);
127
+ return out;
128
+ }
129
+ function readJson(file) {
130
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
131
+ }
132
+ /** Returns `true` if `binDir` is on the user's PATH. */
133
+ export function binDirOnPath(binDir) {
134
+ const PATH = process.env.PATH ?? '';
135
+ const sep = process.platform === 'win32' ? ';' : ':';
136
+ const want = path.resolve(binDir);
137
+ return PATH.split(sep).some((entry) => entry && path.resolve(entry) === want);
138
+ }
139
+ /**
140
+ * Extracts the package name from a spec like `name@1.2`, `@scope/name`,
141
+ * or `@scope/name@latest`. Returns the unchanged string for plain names.
142
+ */
143
+ export function specToPackageName(spec) {
144
+ if (spec.startsWith('@')) {
145
+ const slash = spec.indexOf('/');
146
+ if (slash < 0)
147
+ return spec;
148
+ const at = spec.indexOf('@', slash);
149
+ return at < 0 ? spec : spec.slice(0, at);
150
+ }
151
+ const at = spec.indexOf('@');
152
+ return at < 0 ? spec : spec.slice(0, at);
153
+ }
@@ -0,0 +1,21 @@
1
+ import type { RolldownPluginOption } from 'rolldown';
2
+ /** User-supplied entry: a package name + optional named export and options. */
3
+ export interface PluginByName {
4
+ name: string;
5
+ /** Named export to invoke. Defaults to the module's default export. */
6
+ export?: string;
7
+ /** Options forwarded to the plugin factory. */
8
+ options?: unknown;
9
+ }
10
+ /** Type-guard: a `PluginByName` shape rather than a Rolldown plugin object. */
11
+ export declare function isPluginByName(value: unknown): value is PluginByName;
12
+ /**
13
+ * Resolve a list of mixed user plugins. Entries that are already plugin
14
+ * objects pass through unchanged; entries shaped like `PluginByName` get
15
+ * dynamically imported, instantiated with their `options`, and returned in
16
+ * the same position. Resolution is anchored at `projectDir`.
17
+ *
18
+ * Throws when a name fails to resolve, when the chosen export is not a
19
+ * function, or when the factory returns nothing.
20
+ */
21
+ export declare function resolveUserPlugins(plugins: ReadonlyArray<RolldownPluginOption | PluginByName>, projectDir: string): Promise<RolldownPluginOption[]>;
@@ -0,0 +1,75 @@
1
+ // Resolve `bundler.plugins` entries that are specified by package name in
2
+ // the user's gjsify config, e.g.:
3
+ //
4
+ // "bundler": {
5
+ // "plugins": [
6
+ // { "name": "@gjsify/vite-plugin-blueprint", "options": { "minify": true } },
7
+ // { "name": "@gjsify/vite-plugin-gettext", "export": "msgfmtPlugin", "options": { ... } }
8
+ // ]
9
+ // }
10
+ //
11
+ // Lets `package.json#gjsify` describe the full plugin chain without dropping
12
+ // to a JS-form config file (`gjsify.config.mjs`). Resolution is anchored at
13
+ // the project root (where the config lives) so the project's own
14
+ // `node_modules` wins over the CLI's own dependencies.
15
+ import { createRequire } from 'node:module';
16
+ import { join } from 'node:path';
17
+ import { pathToFileURL } from 'node:url';
18
+ /** Type-guard: a `PluginByName` shape rather than a Rolldown plugin object. */
19
+ export function isPluginByName(value) {
20
+ return (typeof value === 'object' &&
21
+ value !== null &&
22
+ typeof value.name === 'string' &&
23
+ // RolldownPluginOption can be `false | null | undefined | Plugin | Promise<Plugin>`.
24
+ // A real plugin always has a function-shape behavior; `name` alone is shared
25
+ // with our shape, so we additionally require absence of plugin-shape fields.
26
+ !('apply' in value) &&
27
+ !('resolveId' in value) &&
28
+ !('load' in value) &&
29
+ !('transform' in value) &&
30
+ !('renderChunk' in value) &&
31
+ !('generateBundle' in value));
32
+ }
33
+ /**
34
+ * Resolve a list of mixed user plugins. Entries that are already plugin
35
+ * objects pass through unchanged; entries shaped like `PluginByName` get
36
+ * dynamically imported, instantiated with their `options`, and returned in
37
+ * the same position. Resolution is anchored at `projectDir`.
38
+ *
39
+ * Throws when a name fails to resolve, when the chosen export is not a
40
+ * function, or when the factory returns nothing.
41
+ */
42
+ export async function resolveUserPlugins(plugins, projectDir) {
43
+ const requireFromProject = createRequire(join(projectDir, 'package.json'));
44
+ const out = [];
45
+ for (const entry of plugins) {
46
+ if (!isPluginByName(entry)) {
47
+ out.push(entry);
48
+ continue;
49
+ }
50
+ let resolvedPath;
51
+ try {
52
+ resolvedPath = requireFromProject.resolve(entry.name);
53
+ }
54
+ catch (err) {
55
+ throw new Error(`gjsify config: failed to resolve plugin "${entry.name}" from ${projectDir}. ` +
56
+ `Add it to your project's dependencies, or pass a Plugin object directly. ` +
57
+ `(${err.message})`);
58
+ }
59
+ const mod = await import(pathToFileURL(resolvedPath).href);
60
+ const exportName = entry.export ?? 'default';
61
+ const factory = mod[exportName];
62
+ if (typeof factory !== 'function') {
63
+ const available = Object.keys(mod).filter((k) => typeof mod[k] === 'function');
64
+ throw new Error(`gjsify config: plugin "${entry.name}" has no function export "${exportName}". ` +
65
+ `Available function exports: ${available.length ? available.join(', ') : '(none)'}.`);
66
+ }
67
+ const plugin = await factory(entry.options);
68
+ if (plugin === undefined || plugin === null) {
69
+ throw new Error(`gjsify config: plugin "${entry.name}" factory returned ${plugin}. ` +
70
+ `Check the plugin's signature — it should return a Rolldown/Vite plugin object.`);
71
+ }
72
+ out.push(plugin);
73
+ }
74
+ return out;
75
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -23,15 +23,15 @@
23
23
  "cli"
24
24
  ],
25
25
  "dependencies": {
26
- "@gjsify/create-app": "^0.3.14",
27
- "@gjsify/node-polyfills": "^0.3.14",
28
- "@gjsify/npm-registry": "^0.3.14",
29
- "@gjsify/resolve-npm": "^0.3.14",
30
- "@gjsify/rolldown-plugin-gjsify": "^0.3.14",
31
- "@gjsify/rolldown-plugin-pnp": "^0.3.14",
32
- "@gjsify/semver": "^0.3.14",
33
- "@gjsify/tar": "^0.3.14",
34
- "@gjsify/web-polyfills": "^0.3.14",
26
+ "@gjsify/create-app": "^0.3.16",
27
+ "@gjsify/node-polyfills": "^0.3.16",
28
+ "@gjsify/npm-registry": "^0.3.16",
29
+ "@gjsify/resolve-npm": "^0.3.16",
30
+ "@gjsify/rolldown-plugin-gjsify": "^0.3.16",
31
+ "@gjsify/rolldown-plugin-pnp": "^0.3.16",
32
+ "@gjsify/semver": "^0.3.16",
33
+ "@gjsify/tar": "^0.3.16",
34
+ "@gjsify/web-polyfills": "^0.3.16",
35
35
  "cosmiconfig": "^9.0.1",
36
36
  "get-tsconfig": "^4.14.0",
37
37
  "pkg-types": "^2.3.1",
@@ -2,7 +2,8 @@ import type { ConfigData, BundlerOptions } from "../types/index.js";
2
2
  import type { App, PluginOptions } from "@gjsify/rolldown-plugin-gjsify";
3
3
  import type { RolldownOutput, RolldownPluginOption } from "rolldown";
4
4
  import { rolldown } from "rolldown";
5
- import { gjsifyPlugin } from "@gjsify/rolldown-plugin-gjsify";
5
+ import { gjsifyPlugin, textLoaderPlugin, resolveShebangLine } from "@gjsify/rolldown-plugin-gjsify";
6
+ import { resolveUserPlugins } from "../utils/resolve-plugin-by-name.js";
6
7
  import {
7
8
  resolveGlobalsList,
8
9
  writeRegisterInjectFile,
@@ -13,7 +14,7 @@ import { dirname, extname } from "node:path";
13
14
  import { chmod, readFile, writeFile } from "node:fs/promises";
14
15
  import { normalizeBundlerOptions, mergeBundlerOptions } from "../utils/normalize-bundler-options.js";
15
16
 
16
- const GJS_SHEBANG = "#!/usr/bin/env -S gjs -m\n";
17
+ const DEFAULT_GJS_SHEBANG = "#!/usr/bin/env -S gjs -m";
17
18
 
18
19
  /**
19
20
  * `true` when `path` points at a location that's unsafe to use as a build
@@ -190,8 +191,12 @@ export class BuildAction {
190
191
  }
191
192
 
192
193
  /**
193
- * Post-processing: prepend GJS shebang and mark the output file executable.
194
- * Only runs for GJS app builds with a resolvable single outfile.
194
+ * Post-processing: prepend the resolved shebang line and mark the
195
+ * output executable. Only runs for GJS app builds with a single outfile.
196
+ * The shebang plugin in `@gjsify/rolldown-plugin-gjsify` already injects
197
+ * during bundling — this hook is the safety net for anything that
198
+ * bypassed the plugin (e.g. user-supplied banners that out-ordered it),
199
+ * plus the chmod.
195
200
  */
196
201
  private async applyShebang(
197
202
  outfile: string | undefined,
@@ -205,6 +210,8 @@ export class BuildAction {
205
210
  return;
206
211
  }
207
212
 
213
+ const line = resolveShebangLine(this.configData.shebang) ?? DEFAULT_GJS_SHEBANG;
214
+
208
215
  const content = await readFile(outfile, "utf-8");
209
216
  if (content.startsWith("#!")) {
210
217
  if (verbose)
@@ -212,12 +219,12 @@ export class BuildAction {
212
219
  `[gjsify] --shebang skipped: ${outfile} already starts with a shebang`,
213
220
  );
214
221
  } else {
215
- await writeFile(outfile, GJS_SHEBANG + content);
222
+ await writeFile(outfile, line + "\n" + content);
216
223
  }
217
224
  await chmod(outfile, 0o755);
218
225
  if (verbose)
219
226
  console.debug(
220
- `[gjsify] --shebang: wrote shebang + chmod 0o755 to ${outfile}`,
227
+ `[gjsify] --shebang: wrote ${line} + chmod 0o755 to ${outfile}`,
221
228
  );
222
229
  }
223
230
 
@@ -283,6 +290,26 @@ export class BuildAction {
283
290
  const pnp = await buildPnpPlugin();
284
291
  const pnpPlugins: RolldownPluginOption[] = pnp ? [pnp] : [];
285
292
 
293
+ // User-supplied text loaders need to be available during BOTH the
294
+ // auto-globals pre-build (`detectAutoGlobals`) and the final build —
295
+ // otherwise Rolldown's parser hits unknown extensions like `.ui` /
296
+ // `.asm` during the pre-build, fails to parse them as JS/JSX, and
297
+ // the auto-globals iteration aborts before the final plugin chain is
298
+ // ever assembled. Build the user-plugin chain once, up front, and
299
+ // pass it into both passes.
300
+ const userTextLoader = textLoaderPlugin({ loaders: this.configData.loaders });
301
+ const userPlugins: RolldownPluginOption[] = userTextLoader ? [userTextLoader] : [];
302
+
303
+ // User-supplied bundler.plugins (mix of plugin objects + by-name
304
+ // entries) — resolved from the project's node_modules. Same
305
+ // ordering rationale as the text loader: must be present during
306
+ // auto-globals pre-build to avoid claiming the same files via
307
+ // Rolldown's default classifier.
308
+ if (userBundler.plugins?.length) {
309
+ const resolved = await resolveUserPlugins(userBundler.plugins, process.cwd());
310
+ userPlugins.push(...resolved);
311
+ }
312
+
286
313
  // --- Auto mode (with optional extras): iterative multi-pass build ---
287
314
  if (app === "gjs" && autoMode) {
288
315
  const gjsifyPluginFactory = async (opts: PluginOptions) => {
@@ -303,7 +330,7 @@ export class BuildAction {
303
330
  const { injectPath } = await detectAutoGlobals(
304
331
  {
305
332
  input: userBundler.input,
306
- plugins: pnpPlugins,
333
+ plugins: [...pnpPlugins, ...userPlugins],
307
334
  external: userBundler.external,
308
335
  transform: userBundler.transform,
309
336
  format,
@@ -337,9 +364,15 @@ export class BuildAction {
337
364
  );
338
365
 
339
366
  const merged = mergeBundlerOptions(cfg.options as BundlerOptions, userBundler);
367
+
340
368
  const finalOpts: BundlerOptions = {
341
369
  ...merged,
342
- plugins: [...pnpPlugins, ...cfg.plugins],
370
+ // Drop user-config plugins from `merged` — they survived
371
+ // mergeBundlerOptions via spread but have already been resolved
372
+ // and appended into `userPlugins` above. Re-emitting the raw
373
+ // entries (which may include `BundlerPluginByName` shapes
374
+ // Rolldown doesn't understand) would crash the build.
375
+ plugins: [...pnpPlugins, ...userPlugins, ...cfg.plugins],
343
376
  };
344
377
 
345
378
  const build = await rolldown(finalOpts);