@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.
- package/dist/cli.gjs.mjs +798 -0
- package/lib/actions/build.js +4 -17
- package/lib/bundler-pick.d.ts +79 -0
- package/lib/bundler-pick.js +428 -0
- package/lib/commands/foreach.d.ts +16 -0
- package/lib/commands/foreach.js +268 -0
- package/lib/commands/index.d.ts +2 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/install.d.ts +1 -0
- package/lib/commands/install.js +222 -26
- package/lib/commands/run.d.ts +1 -1
- package/lib/commands/run.js +133 -20
- package/lib/commands/workspace.d.ts +8 -0
- package/lib/commands/workspace.js +69 -0
- package/lib/config.js +26 -0
- package/lib/index.js +11 -3
- package/lib/types/config-data.d.ts +10 -1
- package/lib/utils/install-backend-native.d.ts +5 -1
- package/lib/utils/install-backend-native.js +88 -11
- package/lib/utils/install-backend.d.ts +11 -1
- package/lib/utils/install-backend.js +4 -2
- package/lib/utils/pkg-json-edit.d.ts +47 -0
- package/lib/utils/pkg-json-edit.js +108 -0
- package/package.json +36 -12
- package/src/actions/build.ts +0 -431
- package/src/actions/index.ts +0 -1
- package/src/commands/build.ts +0 -146
- package/src/commands/check.ts +0 -87
- package/src/commands/create.ts +0 -63
- package/src/commands/dlx.ts +0 -195
- package/src/commands/flatpak/build.ts +0 -225
- package/src/commands/flatpak/ci.ts +0 -173
- package/src/commands/flatpak/deps.ts +0 -120
- package/src/commands/flatpak/index.ts +0 -53
- package/src/commands/flatpak/init.ts +0 -191
- package/src/commands/flatpak/utils.ts +0 -76
- package/src/commands/gettext.ts +0 -258
- package/src/commands/gresource.ts +0 -97
- package/src/commands/gsettings.ts +0 -87
- package/src/commands/index.ts +0 -12
- package/src/commands/info.ts +0 -70
- package/src/commands/install.ts +0 -195
- package/src/commands/run.ts +0 -33
- package/src/commands/showcase.ts +0 -149
- package/src/config.ts +0 -289
- package/src/constants.ts +0 -1
- package/src/index.ts +0 -37
- package/src/types/cli-build-options.ts +0 -100
- package/src/types/command.ts +0 -10
- package/src/types/config-data-library.ts +0 -5
- package/src/types/config-data-typescript.ts +0 -6
- package/src/types/config-data.ts +0 -225
- package/src/types/cosmiconfig-result.ts +0 -5
- package/src/types/index.ts +0 -6
- package/src/utils/check-system-deps.ts +0 -480
- package/src/utils/detect-native-packages.ts +0 -153
- package/src/utils/discover-showcases.ts +0 -75
- package/src/utils/dlx-cache.ts +0 -135
- package/src/utils/install-backend-native.ts +0 -363
- package/src/utils/install-backend.ts +0 -88
- package/src/utils/install-global.ts +0 -182
- package/src/utils/normalize-bundler-options.ts +0 -129
- package/src/utils/parse-spec.ts +0 -48
- package/src/utils/resolve-gjs-entry.ts +0 -96
- package/src/utils/resolve-plugin-by-name.ts +0 -106
- package/src/utils/run-gjs.ts +0 -90
- 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
|
-
}
|
package/src/utils/parse-spec.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/run-gjs.ts
DELETED
|
@@ -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
|
-
}
|