@gjsify/cli 0.3.20 → 0.4.0

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 (67) hide show
  1. package/dist/cli.gjs.mjs +798 -0
  2. package/lib/actions/build.js +4 -17
  3. package/lib/bundler-pick.d.ts +79 -0
  4. package/lib/bundler-pick.js +428 -0
  5. package/lib/commands/foreach.d.ts +16 -0
  6. package/lib/commands/foreach.js +268 -0
  7. package/lib/commands/index.d.ts +2 -0
  8. package/lib/commands/index.js +2 -0
  9. package/lib/commands/install.d.ts +1 -0
  10. package/lib/commands/install.js +222 -26
  11. package/lib/commands/run.d.ts +1 -1
  12. package/lib/commands/run.js +133 -20
  13. package/lib/commands/workspace.d.ts +8 -0
  14. package/lib/commands/workspace.js +69 -0
  15. package/lib/config.js +26 -0
  16. package/lib/index.js +11 -3
  17. package/lib/types/config-data.d.ts +10 -1
  18. package/lib/utils/install-backend-native.d.ts +5 -1
  19. package/lib/utils/install-backend-native.js +88 -11
  20. package/lib/utils/install-backend.d.ts +11 -1
  21. package/lib/utils/install-backend.js +4 -2
  22. package/lib/utils/pkg-json-edit.d.ts +47 -0
  23. package/lib/utils/pkg-json-edit.js +108 -0
  24. package/package.json +36 -12
  25. package/src/actions/build.ts +0 -431
  26. package/src/actions/index.ts +0 -1
  27. package/src/commands/build.ts +0 -146
  28. package/src/commands/check.ts +0 -87
  29. package/src/commands/create.ts +0 -63
  30. package/src/commands/dlx.ts +0 -195
  31. package/src/commands/flatpak/build.ts +0 -225
  32. package/src/commands/flatpak/ci.ts +0 -173
  33. package/src/commands/flatpak/deps.ts +0 -120
  34. package/src/commands/flatpak/index.ts +0 -53
  35. package/src/commands/flatpak/init.ts +0 -191
  36. package/src/commands/flatpak/utils.ts +0 -76
  37. package/src/commands/gettext.ts +0 -258
  38. package/src/commands/gresource.ts +0 -97
  39. package/src/commands/gsettings.ts +0 -87
  40. package/src/commands/index.ts +0 -12
  41. package/src/commands/info.ts +0 -70
  42. package/src/commands/install.ts +0 -195
  43. package/src/commands/run.ts +0 -33
  44. package/src/commands/showcase.ts +0 -149
  45. package/src/config.ts +0 -289
  46. package/src/constants.ts +0 -1
  47. package/src/index.ts +0 -37
  48. package/src/types/cli-build-options.ts +0 -100
  49. package/src/types/command.ts +0 -10
  50. package/src/types/config-data-library.ts +0 -5
  51. package/src/types/config-data-typescript.ts +0 -6
  52. package/src/types/config-data.ts +0 -225
  53. package/src/types/cosmiconfig-result.ts +0 -5
  54. package/src/types/index.ts +0 -6
  55. package/src/utils/check-system-deps.ts +0 -480
  56. package/src/utils/detect-native-packages.ts +0 -153
  57. package/src/utils/discover-showcases.ts +0 -75
  58. package/src/utils/dlx-cache.ts +0 -135
  59. package/src/utils/install-backend-native.ts +0 -363
  60. package/src/utils/install-backend.ts +0 -88
  61. package/src/utils/install-global.ts +0 -182
  62. package/src/utils/normalize-bundler-options.ts +0 -129
  63. package/src/utils/parse-spec.ts +0 -48
  64. package/src/utils/resolve-gjs-entry.ts +0 -96
  65. package/src/utils/resolve-plugin-by-name.ts +0 -106
  66. package/src/utils/run-gjs.ts +0 -90
  67. package/tsconfig.json +0 -16
@@ -1,182 +0,0 @@
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
-
24
- import * as fs from 'node:fs';
25
- import * as os from 'node:os';
26
- import * as path from 'node:path';
27
-
28
- export interface GlobalLayout {
29
- /** Where extracted package trees live: `<prefix>/node_modules/<pkg>/`. */
30
- prefix: string;
31
- /** Where bin-name symlinks land. Typically `~/.local/bin`. */
32
- binDir: string;
33
- }
34
-
35
- /**
36
- * Compute the canonical global install layout for the current user. Honours
37
- * `XDG_DATA_HOME` (per the XDG Base Directory Spec) plus
38
- * `GJSIFY_GLOBAL_PREFIX` and `GJSIFY_GLOBAL_BIN_DIR` escape hatches for tests.
39
- */
40
- export function defaultGlobalLayout(): GlobalLayout {
41
- const prefixOverride = process.env.GJSIFY_GLOBAL_PREFIX;
42
- const binOverride = process.env.GJSIFY_GLOBAL_BIN_DIR;
43
- const home = os.homedir();
44
- const xdgData = process.env.XDG_DATA_HOME ?? path.join(home, '.local', 'share');
45
- return {
46
- prefix: prefixOverride ?? path.join(xdgData, 'gjsify', 'global'),
47
- binDir: binOverride ?? path.join(home, '.local', 'bin'),
48
- };
49
- }
50
-
51
- export interface LinkedBin {
52
- /** The bin-name (key from `bin` / `gjsify.bin`). */
53
- name: string;
54
- /** Absolute path to the file the symlink points at. */
55
- target: string;
56
- /** Absolute path to the symlink (under `binDir`). */
57
- link: string;
58
- }
59
-
60
- /**
61
- * Install each top-level package's bin entries into `binDir` as small POSIX
62
- * `sh` launchers that exec the real bin. Reads each package's installed
63
- * `package.json` to discover bins; prefers `gjsify.bin` (GJS-bundled bins)
64
- * over the npm `bin` field, falling back to npm `bin` when no GJS map is
65
- * declared. Stale launchers are replaced (latest install wins).
66
- *
67
- * Why launchers instead of symlinks:
68
- *
69
- * When a GJS bundle is invoked via a symlink in $PATH, the kernel follows
70
- * the symlink to find the executable but passes the original (symlink)
71
- * path to it. The bundle's shebang then runs as `gjs -m <symlink-path>`,
72
- * which makes `import.meta.url` resolve to the symlink directory — so any
73
- * path computation relative to `import.meta.url` (e.g. ts-for-gir's
74
- * `findTemplatesRoot()`, version-discovery `readFileSync`s, gjsify's own
75
- * `import.meta.url` rewrites) looks for assets in the wrong place.
76
- *
77
- * A `sh` launcher invokes the real path explicitly, so the bundle sees its
78
- * real install location in `import.meta.url` and every relative read works.
79
- * This costs ~50 bytes per bin and one extra `exec` per launch — both
80
- * negligible compared to `gjs -m` cold-start time.
81
- *
82
- * Plain Node bins are unaffected by either approach (Node defaults to
83
- * resolving symlinks in ESM module URLs); launchers are uniform for
84
- * simplicity and so we don't need to discriminate by runtime here.
85
- */
86
- export function linkGlobalBins(packageNames: string[], layout: GlobalLayout): LinkedBin[] {
87
- fs.mkdirSync(layout.binDir, { recursive: true });
88
- const created: LinkedBin[] = [];
89
-
90
- for (const pkgName of packageNames) {
91
- const pkgDir = path.join(layout.prefix, 'node_modules', pkgName);
92
- const pkgJsonPath = path.join(pkgDir, 'package.json');
93
- if (!fs.existsSync(pkgJsonPath)) continue;
94
-
95
- const pkgJson = readJson(pkgJsonPath);
96
- const binMap = pickBinMap(pkgName, pkgJson);
97
- if (!binMap || binMap.size === 0) continue;
98
-
99
- for (const [binName, binTarget] of binMap) {
100
- const targetAbs = path.join(pkgDir, binTarget);
101
- if (!fs.existsSync(targetAbs)) continue;
102
- try {
103
- fs.chmodSync(targetAbs, 0o755);
104
- } catch {
105
- /* best effort */
106
- }
107
- const linkPath = path.join(layout.binDir, binName);
108
- fs.rmSync(linkPath, { force: true });
109
- // Inline `${target}` directly — this file is rewritten on every
110
- // install, paths are user-owned, and POSIX `sh` quoting via
111
- // single-quotes plus `'\''` for embedded quotes is well-defined.
112
- const launcher = `#!/bin/sh\nexec ${shQuote(targetAbs)} "$@"\n`;
113
- fs.writeFileSync(linkPath, launcher);
114
- fs.chmodSync(linkPath, 0o755);
115
- created.push({ name: binName, target: targetAbs, link: linkPath });
116
- }
117
- }
118
-
119
- return created;
120
- }
121
-
122
- function shQuote(s: string): string {
123
- return `'${s.replace(/'/g, `'\\''`)}'`;
124
- }
125
-
126
- function pickBinMap(
127
- pkgName: string,
128
- pkgJson: Record<string, unknown>,
129
- ): Map<string, string> | null {
130
- const gjsifyEntry = pkgJson.gjsify as { bin?: string | Record<string, string> } | undefined;
131
- if (gjsifyEntry?.bin !== undefined) {
132
- return normalizeBin(pkgName, gjsifyEntry.bin);
133
- }
134
- const npmBin = pkgJson.bin as string | Record<string, string> | undefined;
135
- if (npmBin !== undefined) {
136
- return normalizeBin(pkgName, npmBin);
137
- }
138
- return null;
139
- }
140
-
141
- function normalizeBin(
142
- pkgName: string,
143
- bin: string | Record<string, string>,
144
- ): Map<string, string> {
145
- const out = new Map<string, string>();
146
- if (typeof bin === 'string') {
147
- const baseName = pkgName.startsWith('@')
148
- ? pkgName.slice(pkgName.indexOf('/') + 1)
149
- : pkgName;
150
- out.set(baseName, bin);
151
- return out;
152
- }
153
- for (const [k, v] of Object.entries(bin)) out.set(k, v);
154
- return out;
155
- }
156
-
157
- function readJson(file: string): Record<string, unknown> {
158
- return JSON.parse(fs.readFileSync(file, 'utf-8')) as Record<string, unknown>;
159
- }
160
-
161
- /** Returns `true` if `binDir` is on the user's PATH. */
162
- export function binDirOnPath(binDir: string): boolean {
163
- const PATH = process.env.PATH ?? '';
164
- const sep = process.platform === 'win32' ? ';' : ':';
165
- const want = path.resolve(binDir);
166
- return PATH.split(sep).some((entry) => entry && path.resolve(entry) === want);
167
- }
168
-
169
- /**
170
- * Extracts the package name from a spec like `name@1.2`, `@scope/name`,
171
- * or `@scope/name@latest`. Returns the unchanged string for plain names.
172
- */
173
- export function specToPackageName(spec: string): string {
174
- if (spec.startsWith('@')) {
175
- const slash = spec.indexOf('/');
176
- if (slash < 0) return spec;
177
- const at = spec.indexOf('@', slash);
178
- return at < 0 ? spec : spec.slice(0, at);
179
- }
180
- const at = spec.indexOf('@');
181
- return at < 0 ? spec : spec.slice(0, at);
182
- }
@@ -1,129 +0,0 @@
1
- // One-release deprecation shim: maps the legacy `esbuild?: BuildOptions`
2
- // field on `.gjsifyrc.js` / `package.json#gjsify` into the equivalent
3
- // `bundler?: RolldownOptions` shape. Logs a single warning per build.
4
- //
5
- // Drop in 0.5.0.
6
-
7
- import type { OutputOptions } from 'rolldown';
8
- import type { ConfigData, BundlerOptions, LegacyEsbuildOptions } from '../types/config-data.js';
9
-
10
- let warnedOnce = false;
11
-
12
- export function normalizeBundlerOptions(configData: ConfigData): BundlerOptions {
13
- const fromBundler: BundlerOptions = (configData.bundler ?? {}) as BundlerOptions;
14
- if (!configData.esbuild) return fromBundler;
15
-
16
- if (!warnedOnce) {
17
- warnedOnce = true;
18
- // eslint-disable-next-line no-console
19
- console.warn(
20
- "[gjsify] DEPRECATION: the 'esbuild' config key is deprecated and will be removed in 0.5.0. " +
21
- "Rename it to 'bundler' (typed as RolldownOptions). See the migration notes in the gjsify CHANGELOG.",
22
- );
23
- }
24
-
25
- const fromEsbuild = legacyEsbuildToRolldown(configData.esbuild);
26
- // Plain user-config merge — we deliberately do NOT call
27
- // `mergeBundlerOptions` here, because that function strips `input` and
28
- // `external` from its overrides arg (it assumes the *orchestrator* is
29
- // the override source and the user the base). Here both inputs are
30
- // user-provided config and `input` must survive the merge.
31
- const out: BundlerOptions = { ...fromEsbuild, ...fromBundler };
32
- if (fromEsbuild.output || fromBundler.output) {
33
- out.output = { ...(fromEsbuild.output ?? {}), ...(fromBundler.output ?? {}) };
34
- }
35
- if (fromEsbuild.transform || fromBundler.transform) {
36
- out.transform = { ...(fromEsbuild.transform ?? {}), ...(fromBundler.transform ?? {}) };
37
- if (fromEsbuild.transform?.define || fromBundler.transform?.define) {
38
- out.transform.define = {
39
- ...(fromEsbuild.transform?.define ?? {}),
40
- ...(fromBundler.transform?.define ?? {}),
41
- };
42
- }
43
- }
44
- if (fromEsbuild.resolve || fromBundler.resolve) {
45
- out.resolve = { ...(fromEsbuild.resolve ?? {}), ...(fromBundler.resolve ?? {}) };
46
- }
47
- return out;
48
- }
49
-
50
- /** Map the supported subset of esbuild BuildOptions into RolldownOptions. */
51
- function legacyEsbuildToRolldown(esb: LegacyEsbuildOptions): BundlerOptions {
52
- const out: BundlerOptions = {};
53
- const output: OutputOptions = {};
54
- const transform: NonNullable<BundlerOptions['transform']> = {};
55
- const resolve: NonNullable<BundlerOptions['resolve']> = {};
56
-
57
- if (esb.outfile !== undefined) output.file = esb.outfile;
58
- if (esb.outdir !== undefined) output.dir = esb.outdir;
59
- if (esb.format !== undefined) output.format = esb.format;
60
- if (esb.minify !== undefined) output.minify = esb.minify;
61
- if (esb.sourcemap !== undefined) {
62
- // esbuild has 'external' / 'both' which Rolldown doesn't — coerce to boolean.
63
- output.sourcemap =
64
- esb.sourcemap === 'inline'
65
- ? 'inline'
66
- : Boolean(esb.sourcemap);
67
- }
68
- if (esb.banner?.js !== undefined) output.banner = esb.banner.js;
69
-
70
- if (esb.target !== undefined) {
71
- transform.target = Array.isArray(esb.target) ? esb.target.join(',') : esb.target;
72
- }
73
- if (esb.define !== undefined) transform.define = esb.define;
74
-
75
- if (esb.mainFields !== undefined) resolve.mainFields = esb.mainFields;
76
- if (esb.conditions !== undefined) resolve.conditionNames = esb.conditions;
77
-
78
- if (esb.external !== undefined) out.external = esb.external;
79
- if (esb.platform !== undefined) out.platform = esb.platform;
80
-
81
- if (Object.keys(output).length > 0) out.output = output;
82
- if (Object.keys(transform).length > 0) out.transform = transform;
83
- if (Object.keys(resolve).length > 0) out.resolve = resolve;
84
-
85
- // Discarded silently:
86
- // esb.inject — esbuild's array-of-side-effect-files; surfaced at the
87
- // CLI layer instead, via input expansion.
88
- // esb.loader — Rolldown infers module types from extensions natively.
89
- return out;
90
- }
91
-
92
- /**
93
- * Shallow merge with deep-merge of `output`, `transform`, and `resolve`. The
94
- * second argument wins on conflicts, matching `merge(target, ...sources)`
95
- * semantics from `@gjsify/rolldown-plugin-gjsify/utils/merge`.
96
- *
97
- * `base` is typically the Rolldown-generic shape returned by the orchestrator;
98
- * `overrides` is the user's `BundlerOptions` from `.gjsifyrc.js` plus CLI
99
- * flag merges. Single-output assumption matches `BundlerOptions['output']`.
100
- *
101
- * The orchestrator-side `input` is authoritative — it's the post-glob-expansion
102
- * value. Overriding it with the user's raw glob string would re-introduce
103
- * unresolved glob patterns into the final Rolldown call. Same for `external`,
104
- * which the orchestrator concatenates with platform defaults already.
105
- */
106
- export function mergeBundlerOptions(
107
- base: BundlerOptions,
108
- overrides: BundlerOptions,
109
- ): BundlerOptions {
110
- // Strip fields the orchestrator owns authoritatively — the user has
111
- // already had their say via the orchestrator's `userExternal` / input
112
- // expansion; merging the raw values back on top would clobber the
113
- // post-processing.
114
- const { input: _ignoredInput, external: _ignoredExternal, ...overridesRest } = overrides;
115
- const out: BundlerOptions = { ...base, ...overridesRest };
116
- if (base.output || overrides.output) {
117
- out.output = { ...(base.output ?? {}), ...(overrides.output ?? {}) };
118
- }
119
- if (base.transform || overrides.transform) {
120
- out.transform = { ...(base.transform ?? {}), ...(overrides.transform ?? {}) };
121
- if (base.transform?.define || overrides.transform?.define) {
122
- out.transform.define = { ...(base.transform?.define ?? {}), ...(overrides.transform?.define ?? {}) };
123
- }
124
- }
125
- if (base.resolve || overrides.resolve) {
126
- out.resolve = { ...(base.resolve ?? {}), ...(overrides.resolve ?? {}) };
127
- }
128
- return out;
129
- }
@@ -1,48 +0,0 @@
1
- // Parse a `gjsify dlx` package spec — distinguishes local paths from npm specs.
2
-
3
- import { existsSync } from 'node:fs';
4
- import { isAbsolute, resolve } from 'node:path';
5
-
6
- export type ParsedSpec =
7
- | { kind: 'local'; path: string }
8
- | { kind: 'registry'; name: string; version?: string; spec: string };
9
-
10
- const NPM_NAME_RE = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
11
-
12
- /**
13
- * Parse a CLI input into either a local-path or an npm-registry spec.
14
- *
15
- * Local: starts with `./`, `../`, `/`, or is an existing directory path.
16
- * Registry: `<name>` | `<name>@<version>` | `@scope/<name>` | `@scope/<name>@<version>`
17
- *
18
- * The full spec string is preserved on the registry case so it can be passed
19
- * verbatim to `npm install` (which already understands dist-tags, ranges,
20
- * git URIs, tarball URLs, etc. — we don't re-parse those).
21
- */
22
- export function parseSpec(input: string): ParsedSpec {
23
- if (!input) throw new Error('dlx: empty package spec');
24
-
25
- if (input.startsWith('./') || input.startsWith('../') || isAbsolute(input)) {
26
- return { kind: 'local', path: resolve(input) };
27
- }
28
-
29
- if (existsSync(input)) {
30
- return { kind: 'local', path: resolve(input) };
31
- }
32
-
33
- // Registry spec: split off the version after the LAST `@` that isn't the
34
- // leading scope separator.
35
- let name = input;
36
- let version: string | undefined;
37
- const lastAt = input.lastIndexOf('@');
38
- if (lastAt > 0) {
39
- name = input.slice(0, lastAt);
40
- version = input.slice(lastAt + 1);
41
- }
42
-
43
- if (!NPM_NAME_RE.test(name)) {
44
- throw new Error(`dlx: invalid package name "${name}"`);
45
- }
46
-
47
- return { kind: 'registry', name, version, spec: input };
48
- }
@@ -1,96 +0,0 @@
1
- // Resolve the GJS entry point of an installed package.
2
- //
3
- // Per the `gjsify` field convention (see CLI reference):
4
- //
5
- // {
6
- // "gjsify": {
7
- // "main": "dist/gjs.js",
8
- // "bin": { "name-a": "dist/a.js", "name-b": "dist/b.js" }
9
- // }
10
- // }
11
- //
12
- // Resolution order:
13
- // 1. user-supplied bin name + `gjsify.bin[name]` → that path
14
- // 2. single-entry `gjsify.bin` → the only path
15
- // 3. `gjsify.main` → that path
16
- // 4. fallback: `package.json#main` → that path (advisory warning)
17
- // 5. otherwise: hard-fail with a fix hint
18
-
19
- import { readFileSync } from 'node:fs';
20
- import { existsSync } from 'node:fs';
21
- import { join, resolve } from 'node:path';
22
-
23
- interface ResolvedEntry {
24
- bundlePath: string;
25
- binName: string | null;
26
- fromFallback: boolean;
27
- }
28
-
29
- interface PackageJson {
30
- name?: string;
31
- main?: string;
32
- gjsify?: {
33
- main?: string;
34
- bin?: Record<string, string>;
35
- prebuilds?: string;
36
- };
37
- }
38
-
39
- export function resolveGjsEntry(
40
- pkgDir: string,
41
- binName: string | null,
42
- ): ResolvedEntry {
43
- const pkgJsonPath = join(pkgDir, 'package.json');
44
- if (!existsSync(pkgJsonPath)) {
45
- throw new Error(`dlx: no package.json found at ${pkgDir}`);
46
- }
47
- const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as PackageJson;
48
-
49
- const gjsifyBin = pkg.gjsify?.bin;
50
- const gjsifyMain = pkg.gjsify?.main;
51
- const fallbackMain = pkg.main;
52
-
53
- let entry: string | undefined;
54
- let resolvedBin: string | null = null;
55
- let fromFallback = false;
56
-
57
- if (binName !== null) {
58
- if (!gjsifyBin || !gjsifyBin[binName]) {
59
- const known = gjsifyBin ? Object.keys(gjsifyBin).join(', ') : '(none)';
60
- throw new Error(
61
- `dlx: package "${pkg.name ?? pkgDir}" has no GJS bin named "${binName}" — known: ${known}`,
62
- );
63
- }
64
- entry = gjsifyBin[binName];
65
- resolvedBin = binName;
66
- } else if (gjsifyBin && Object.keys(gjsifyBin).length === 1) {
67
- const onlyBin = Object.keys(gjsifyBin)[0];
68
- entry = gjsifyBin[onlyBin];
69
- resolvedBin = onlyBin;
70
- } else if (gjsifyMain) {
71
- entry = gjsifyMain;
72
- } else if (fallbackMain) {
73
- entry = fallbackMain;
74
- fromFallback = true;
75
- }
76
-
77
- if (gjsifyBin && Object.keys(gjsifyBin).length > 1 && binName === null) {
78
- const names = Object.keys(gjsifyBin).join(', ');
79
- throw new Error(
80
- `dlx: package "${pkg.name ?? pkgDir}" defines multiple GJS bins — pass one of: ${names}`,
81
- );
82
- }
83
-
84
- if (!entry) {
85
- throw new Error(
86
- `dlx: package "${pkg.name ?? pkgDir}" has no GJS entry — set \`gjsify.main\` (or \`gjsify.bin\`) in its package.json`,
87
- );
88
- }
89
-
90
- const bundlePath = resolve(pkgDir, entry);
91
- if (!existsSync(bundlePath)) {
92
- throw new Error(`dlx: GJS entry not found: ${bundlePath}`);
93
- }
94
-
95
- return { bundlePath, binName: resolvedBin, fromFallback };
96
- }
@@ -1,106 +0,0 @@
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
-
16
- import { createRequire } from 'node:module';
17
- import { join } from 'node:path';
18
- import { pathToFileURL } from 'node:url';
19
- import type { RolldownPluginOption } from 'rolldown';
20
-
21
- /** User-supplied entry: a package name + optional named export and options. */
22
- export interface PluginByName {
23
- name: string;
24
- /** Named export to invoke. Defaults to the module's default export. */
25
- export?: string;
26
- /** Options forwarded to the plugin factory. */
27
- options?: unknown;
28
- }
29
-
30
- /** Type-guard: a `PluginByName` shape rather than a Rolldown plugin object. */
31
- export function isPluginByName(value: unknown): value is PluginByName {
32
- return (
33
- typeof value === 'object' &&
34
- value !== null &&
35
- typeof (value as { name?: unknown }).name === 'string' &&
36
- // RolldownPluginOption can be `false | null | undefined | Plugin | Promise<Plugin>`.
37
- // A real plugin always has a function-shape behavior; `name` alone is shared
38
- // with our shape, so we additionally require absence of plugin-shape fields.
39
- !('apply' in value) &&
40
- !('resolveId' in value) &&
41
- !('load' in value) &&
42
- !('transform' in value) &&
43
- !('renderChunk' in value) &&
44
- !('generateBundle' in value)
45
- );
46
- }
47
-
48
- /**
49
- * Resolve a list of mixed user plugins. Entries that are already plugin
50
- * objects pass through unchanged; entries shaped like `PluginByName` get
51
- * dynamically imported, instantiated with their `options`, and returned in
52
- * the same position. Resolution is anchored at `projectDir`.
53
- *
54
- * Throws when a name fails to resolve, when the chosen export is not a
55
- * function, or when the factory returns nothing.
56
- */
57
- export async function resolveUserPlugins(
58
- plugins: ReadonlyArray<RolldownPluginOption | PluginByName>,
59
- projectDir: string,
60
- ): Promise<RolldownPluginOption[]> {
61
- const requireFromProject = createRequire(join(projectDir, 'package.json'));
62
- const out: RolldownPluginOption[] = [];
63
-
64
- for (const entry of plugins) {
65
- if (!isPluginByName(entry)) {
66
- out.push(entry as RolldownPluginOption);
67
- continue;
68
- }
69
-
70
- let resolvedPath: string;
71
- try {
72
- resolvedPath = requireFromProject.resolve(entry.name);
73
- } catch (err) {
74
- throw new Error(
75
- `gjsify config: failed to resolve plugin "${entry.name}" from ${projectDir}. ` +
76
- `Add it to your project's dependencies, or pass a Plugin object directly. ` +
77
- `(${(err as Error).message})`,
78
- );
79
- }
80
-
81
- const mod = await import(pathToFileURL(resolvedPath).href);
82
- const exportName = entry.export ?? 'default';
83
- const factory = (mod as Record<string, unknown>)[exportName];
84
-
85
- if (typeof factory !== 'function') {
86
- const available = Object.keys(mod).filter(
87
- (k) => typeof (mod as Record<string, unknown>)[k] === 'function',
88
- );
89
- throw new Error(
90
- `gjsify config: plugin "${entry.name}" has no function export "${exportName}". ` +
91
- `Available function exports: ${available.length ? available.join(', ') : '(none)'}.`,
92
- );
93
- }
94
-
95
- const plugin = await (factory as (opts?: unknown) => unknown)(entry.options);
96
- if (plugin === undefined || plugin === null) {
97
- throw new Error(
98
- `gjsify config: plugin "${entry.name}" factory returned ${plugin}. ` +
99
- `Check the plugin's signature — it should return a Rolldown/Vite plugin object.`,
100
- );
101
- }
102
- out.push(plugin as RolldownPluginOption);
103
- }
104
-
105
- return out;
106
- }
@@ -1,90 +0,0 @@
1
- // Shared utility for running a GJS bundle with native package env vars.
2
- // Used by `gjsify run`, `gjsify dlx`, and the showcase command (via dlx).
3
- //
4
- // Detection runs the same exhaustive node_modules walker (`detectNativePackages`)
5
- // from two starting points and merges by package name (CWD shadows bundle):
6
- //
7
- // 1. process.cwd() — picks up native deps in the user's project
8
- // (yarn / pnpm / npm node_modules walking up).
9
- // 2. dirname(bundlePath) — picks up native deps in whatever node_modules
10
- // the bundle lives in. Critical for `gjsify dlx`
11
- // where the bundle resides in
12
- // `~/.cache/gjsify/dlx/<sha>/.../node_modules/<pkg>/dist/`
13
- // and the user's CWD is unrelated. The bundle-side
14
- // walk also catches transitive deps' typelibs.
15
- //
16
- // Env composition is split out as `computeNativeEnvForBundle()` — a pure
17
- // function that takes a bundle path + cwd and returns the env it would inject.
18
- // This lets the e2e tests assert the env without spawning gjs.
19
-
20
- import { spawn } from 'node:child_process';
21
- import { dirname, resolve } from 'node:path';
22
- import { detectNativePackages, buildNativeEnv } from './detect-native-packages.js';
23
-
24
- /**
25
- * Pure env computation for a given bundle. Returns the LD_LIBRARY_PATH /
26
- * GI_TYPELIB_PATH values that {@link runGjsBundle} would inject into the
27
- * spawned `gjs` process, plus the formatted env-prefix string used for the
28
- * `$ …` echo.
29
- */
30
- export function computeNativeEnvForBundle(
31
- bundlePath: string,
32
- cwd: string = process.cwd(),
33
- ): { env: { LD_LIBRARY_PATH: string; GI_TYPELIB_PATH: string }; envPrefix: string } {
34
- const resolvedBundle = resolve(bundlePath);
35
-
36
- const cwdPackages = detectNativePackages(cwd);
37
- const bundlePackages = detectNativePackages(dirname(resolvedBundle));
38
-
39
- const seen = new Set(cwdPackages.map((p) => p.name));
40
- const nativePackages = [
41
- ...cwdPackages,
42
- ...bundlePackages.filter((p) => !seen.has(p.name)),
43
- ];
44
-
45
- const env = buildNativeEnv(nativePackages);
46
- const envPrefix = Object.entries(env)
47
- .filter(([, value]) => value !== undefined && value !== '')
48
- .map(([key, value]) => `${key}=${value}`)
49
- .join(' ');
50
-
51
- return { env, envPrefix };
52
- }
53
-
54
- /**
55
- * Run a GJS bundle, automatically setting LD_LIBRARY_PATH and GI_TYPELIB_PATH
56
- * for any installed native gjsify packages discoverable from either the CWD
57
- * or the bundle's own node_modules tree.
58
- */
59
- export async function runGjsBundle(bundlePath: string, extraArgs: string[] = []): Promise<void> {
60
- const { env: nativeEnv, envPrefix } = computeNativeEnvForBundle(bundlePath);
61
-
62
- const env = {
63
- ...process.env,
64
- ...nativeEnv,
65
- };
66
-
67
- const gjsArgs = ['-m', bundlePath, ...extraArgs];
68
-
69
- // Print the exact command being executed so users can copy-paste it to
70
- // run gjs directly without the wrapper. Env vars are only shown if we
71
- // actually set any (i.e. native gjsify packages were detected).
72
- const gjsCommand = ['gjs', ...gjsArgs.map((a) => (a.includes(' ') ? `"${a}"` : a))].join(' ');
73
- console.log(`$ ${envPrefix ? `${envPrefix} ` : ''}${gjsCommand}`);
74
-
75
- const child = spawn('gjs', gjsArgs, { env, stdio: 'inherit' });
76
-
77
- await new Promise<void>((resolvePromise, reject) => {
78
- child.on('close', (code) => {
79
- if (code !== 0) {
80
- reject(new Error(`gjs exited with code ${code}`));
81
- } else {
82
- resolvePromise();
83
- }
84
- });
85
- child.on('error', reject);
86
- }).catch((err) => {
87
- console.error(err.message);
88
- process.exit(1);
89
- });
90
- }
package/tsconfig.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "rootDir": "src",
4
- "outDir": "lib",
5
- "declarationDir": "lib",
6
- "declaration": true,
7
- "target": "ESNext",
8
- "module": "NodeNext",
9
- "moduleResolution": "NodeNext",
10
- "types": ["node"],
11
- "esModuleInterop": true,
12
- "strict": true,
13
- "skipLibCheck": true
14
- },
15
- "files": ["src/index.ts"]
16
- }