@gjsify/cli 0.51.1 → 0.53.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 +539 -493
- package/lib/actions/build.d.ts +22 -0
- package/lib/actions/build.js +37 -9
- package/lib/bundler-pick.d.ts +37 -1
- package/lib/bundler-pick.js +108 -17
- package/lib/cli-app.js +7 -2
- package/lib/commands/affected-classify.d.ts +7 -1
- package/lib/commands/affected-classify.js +66 -9
- package/lib/commands/affected-script-refs.d.ts +20 -0
- package/lib/commands/affected-script-refs.js +218 -0
- package/lib/commands/affected.js +24 -2
- package/lib/commands/env.d.ts +31 -0
- package/lib/commands/env.js +114 -0
- package/lib/commands/exec.d.ts +10 -0
- package/lib/commands/exec.js +208 -0
- package/lib/commands/flatpak/diff.js +8 -2
- package/lib/commands/flatpak/init.js +4 -1
- package/lib/commands/flatpak/sources.js +4 -3
- package/lib/commands/flatpak/sync-flathub.d.ts +2 -0
- package/lib/commands/flatpak/sync-flathub.js +149 -7
- package/lib/commands/flatpak/utils.d.ts +10 -0
- package/lib/commands/flatpak/utils.js +10 -0
- package/lib/commands/foreach.js +5 -97
- package/lib/commands/format.js +26 -6
- package/lib/commands/index.d.ts +4 -0
- package/lib/commands/index.js +4 -0
- package/lib/commands/install.d.ts +2 -2
- package/lib/commands/install.js +81 -5
- package/lib/commands/link.d.ts +13 -0
- package/lib/commands/link.js +243 -0
- package/lib/commands/onboard.js +18 -2
- package/lib/commands/publish.d.ts +34 -0
- package/lib/commands/publish.js +153 -8
- package/lib/commands/self-update.js +2 -1
- package/lib/commands/ship.js +19 -1
- package/lib/commands/test.d.ts +18 -9
- package/lib/commands/test.js +56 -11
- package/lib/commands/tsc.js +4 -4
- package/lib/commands/upgrade.d.ts +15 -0
- package/lib/commands/upgrade.js +76 -2
- package/lib/commands/webext/index.d.ts +2 -0
- package/lib/commands/webext/index.js +15 -0
- package/lib/commands/webext/subcommands.d.ts +16 -0
- package/lib/commands/webext/subcommands.js +204 -0
- package/lib/types/config-data.d.ts +2 -2
- package/lib/utils/bin-shim.d.ts +8 -2
- package/lib/utils/bin-shim.js +6 -5
- package/lib/utils/bundle-inputs.d.ts +84 -0
- package/lib/utils/bundle-inputs.js +268 -0
- package/lib/utils/check-system-deps.d.ts +5 -19
- package/lib/utils/check-system-deps.js +31 -94
- package/lib/utils/dev-link.d.ts +225 -0
- package/lib/utils/dev-link.js +684 -0
- package/lib/utils/exec-bin.d.ts +137 -0
- package/lib/utils/exec-bin.js +321 -0
- package/lib/utils/gi-runtime-paths.js +3 -2
- package/lib/utils/gitignore.d.ts +15 -0
- package/lib/utils/gitignore.js +178 -0
- package/lib/utils/gjsify-shim.d.ts +47 -0
- package/lib/utils/gjsify-shim.js +97 -8
- package/lib/utils/hang-watchdog.d.ts +95 -0
- package/lib/utils/hang-watchdog.js +194 -0
- package/lib/utils/install-backend-native.d.ts +33 -1
- package/lib/utils/install-backend-native.js +184 -20
- package/lib/utils/install-extraneous.d.ts +90 -0
- package/lib/utils/install-extraneous.js +120 -0
- package/lib/utils/install-lock.js +4 -25
- package/lib/utils/install-tarball-cache.d.ts +37 -9
- package/lib/utils/install-tarball-cache.js +59 -14
- package/lib/utils/native-skip-scan.d.ts +20 -0
- package/lib/utils/native-skip-scan.js +343 -0
- package/lib/utils/node-script.js +1 -1
- package/lib/utils/normalize-bundler-options.js +11 -0
- package/lib/utils/oxc-resolve.js +42 -3
- package/lib/utils/process-table.d.ts +22 -0
- package/lib/utils/process-table.js +188 -0
- package/lib/utils/prune-prefix.js +1 -1
- package/lib/utils/publish-pins.d.ts +38 -0
- package/lib/utils/publish-pins.js +47 -0
- package/lib/utils/publish-readback.d.ts +103 -17
- package/lib/utils/publish-readback.js +326 -47
- package/lib/utils/run-gjs.d.ts +15 -1
- package/lib/utils/run-gjs.js +98 -13
- package/lib/utils/run-node.d.ts +2 -0
- package/lib/utils/run-node.js +2 -1
- package/lib/utils/runtimes.js +14 -13
- package/lib/utils/ship/gzip.js +1 -1
- package/lib/utils/ship/icns.d.ts +14 -0
- package/lib/utils/ship/icns.js +83 -0
- package/lib/utils/ship/ico.d.ts +25 -0
- package/lib/utils/ship/ico.js +120 -0
- package/lib/utils/ship/icons.d.ts +197 -0
- package/lib/utils/ship/icons.js +399 -0
- package/lib/utils/ship/launcher.js +16 -1
- package/lib/utils/ship/layout.d.ts +32 -0
- package/lib/utils/ship/layout.js +112 -38
- package/lib/utils/ship/msi.d.ts +1 -1
- package/lib/utils/ship/msi.js +30 -5
- package/lib/utils/ship/pe-launcher.d.ts +18 -0
- package/lib/utils/ship/pe-launcher.js +125 -4
- package/lib/utils/ship/plan.d.ts +1 -34
- package/lib/utils/ship/plan.js +7 -56
- package/lib/utils/ship/plist.d.ts +20 -10
- package/lib/utils/ship/plist.js +29 -12
- package/lib/utils/ship/settings.js +3 -2
- package/lib/utils/spawn.d.ts +13 -0
- package/lib/utils/spawn.js +10 -5
- package/lib/utils/watch-loop.d.ts +15 -1
- package/lib/utils/watch-loop.js +9 -5
- package/lib/utils/webext/build.d.ts +42 -0
- package/lib/utils/webext/build.js +120 -0
- package/lib/utils/webext/config.d.ts +72 -0
- package/lib/utils/webext/config.js +234 -0
- package/lib/utils/webext/icons.d.ts +24 -0
- package/lib/utils/webext/icons.js +83 -0
- package/lib/utils/webext/launch.d.ts +32 -0
- package/lib/utils/webext/launch.js +77 -0
- package/lib/utils/webext/manifest.d.ts +53 -0
- package/lib/utils/webext/manifest.js +219 -0
- package/lib/utils/webext/pages.d.ts +26 -0
- package/lib/utils/webext/pages.js +61 -0
- package/lib/utils/webext/targets.d.ts +13 -0
- package/lib/utils/webext/targets.js +36 -0
- package/lib/utils/win32-command.d.ts +17 -0
- package/lib/utils/win32-command.js +21 -6
- package/package.json +20 -20
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { ExampleRuntime } from './runtimes.js';
|
|
2
|
+
/** `gjsify exec`'s own options and the bin invocation that follows them. */
|
|
3
|
+
export interface ExecInvocation {
|
|
4
|
+
runtime: ExampleRuntime | null;
|
|
5
|
+
rebuild: boolean;
|
|
6
|
+
verbose: boolean;
|
|
7
|
+
bin: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Split `gjsify exec [options] <bin> [args…]`. Options are gjsify's only BEFORE the
|
|
12
|
+
* bin name; everything after it belongs to the bin, flags included, as with `npx`
|
|
13
|
+
* — `gjsify exec wxt --runtime x` hands `--runtime x` to wxt. yargs cannot draw
|
|
14
|
+
* that line (a declared option is taken wherever it appears), so the command
|
|
15
|
+
* declares none and this function reads the leading ones.
|
|
16
|
+
*
|
|
17
|
+
* `tail` is what yargs split off at the first `--`: one AFTER the bin belongs to
|
|
18
|
+
* the bin, one BEFORE it only separates (`gjsify exec -- --weird-bin-name`).
|
|
19
|
+
*/
|
|
20
|
+
export declare function splitExecArgv(entries: readonly string[], tail?: readonly string[]): ExecInvocation;
|
|
21
|
+
/** A bin name resolved to the files that implement it. */
|
|
22
|
+
export interface ResolvedBin {
|
|
23
|
+
binName: string;
|
|
24
|
+
pkgName: string;
|
|
25
|
+
pkgVersion: string;
|
|
26
|
+
/** Real path of the owning package's directory. */
|
|
27
|
+
pkgDir: string;
|
|
28
|
+
/** npm `bin` target (real path) — the file node/bun/deno run. Null when only `gjsify.bin` names it. */
|
|
29
|
+
entry: string | null;
|
|
30
|
+
/** `gjsify.bin` target (real path) — a bundle the package already ships for GJS. */
|
|
31
|
+
gjsEntry: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* The `node_modules` directory the package was found through, or null for the
|
|
34
|
+
* project's OWN bin. The rebuilt bundle is written below it — see
|
|
35
|
+
* {@link execCacheRoot} for why it cannot live in the XDG cache.
|
|
36
|
+
*/
|
|
37
|
+
nodeModules: string | null;
|
|
38
|
+
/**
|
|
39
|
+
* True when the package's real location is inside a `node_modules` tree, i.e. an
|
|
40
|
+
* INSTALLED copy whose version string identifies its content. A workspace link or
|
|
41
|
+
* the project's own bin is edited in place without a version bump, so its rebuild
|
|
42
|
+
* is never reused.
|
|
43
|
+
*/
|
|
44
|
+
installed: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare class ExecBinNotFoundError extends Error {
|
|
47
|
+
readonly binName: string;
|
|
48
|
+
readonly searched: readonly string[];
|
|
49
|
+
constructor(binName: string, searched: readonly string[]);
|
|
50
|
+
}
|
|
51
|
+
export declare class ExecPlanError extends Error {
|
|
52
|
+
constructor(message: string);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve `binName` the way `npx` does for an installed bin: the project's OWN
|
|
56
|
+
* `package.json#bin` first, then every `node_modules` from `cwd` up to the root.
|
|
57
|
+
*
|
|
58
|
+
* `.bin/<name>` is only a HINT. Its shape depends on who wrote it — a symlink (npm,
|
|
59
|
+
* pnpm), an `sh` launcher (`gjsify install` for a `gjsify.bin`), a `.cmd`/`.ps1` pair
|
|
60
|
+
* (Windows) — and only the symlink names its target, so the other shapes fall back to
|
|
61
|
+
* reading the package manifests of that `node_modules`. A package whose NAME is the
|
|
62
|
+
* bin name wins that scan, as `npx <pkg>` would pick it.
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveExecBin(binName: string, cwd: string): ResolvedBin;
|
|
65
|
+
/** What `gjsify exec` does with a resolved bin on a given runtime. */
|
|
66
|
+
export type ExecPlan =
|
|
67
|
+
/** node/bun/deno: run the npm entry unchanged — nothing is built. */
|
|
68
|
+
{
|
|
69
|
+
kind: 'direct';
|
|
70
|
+
runtime: Exclude<ExampleRuntime, 'gjs'>;
|
|
71
|
+
file: string;
|
|
72
|
+
}
|
|
73
|
+
/** gjs, and the package ships its own GJS bundle (`gjsify.bin`): run that. */
|
|
74
|
+
| {
|
|
75
|
+
kind: 'gjs-bundle';
|
|
76
|
+
file: string;
|
|
77
|
+
}
|
|
78
|
+
/** gjs, Node-only package: rebuild the npm entry `--app gjs`, then run the bundle. */
|
|
79
|
+
| {
|
|
80
|
+
kind: 'rebuild';
|
|
81
|
+
entry: string;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Pick the plan. `runtime` is the HOST runtime unless the caller passed an explicit
|
|
85
|
+
* `--runtime`; there is no implicit fallback from gjs to node — a failed rebuild is
|
|
86
|
+
* reported, never papered over by a runtime the user did not ask for.
|
|
87
|
+
*/
|
|
88
|
+
export declare function planExec(bin: ResolvedBin, runtime: ExampleRuntime): ExecPlan;
|
|
89
|
+
/** Inputs that decide whether a cached rebuild still describes the bin. */
|
|
90
|
+
export interface ExecCacheKeyInput {
|
|
91
|
+
pkgName: string;
|
|
92
|
+
pkgVersion: string;
|
|
93
|
+
/** The entry, relative to the package directory. */
|
|
94
|
+
entry: string;
|
|
95
|
+
/** The running CLI's version: a new bundler or polyfill set is a new artifact. */
|
|
96
|
+
cliVersion: string;
|
|
97
|
+
/** sha256 of the project's lockfile, or null when there is none. */
|
|
98
|
+
lockfileHash: string | null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Content key of a rebuilt bin. The package version identifies the entry's own code;
|
|
102
|
+
* the LOCKFILE identifies its dependencies, which a reinstall can move without the
|
|
103
|
+
* version changing; the CLI version identifies the bundler and every polyfill that
|
|
104
|
+
* went into the artifact. With no lockfile, dependency drift is invisible to the key,
|
|
105
|
+
* which is why `--rebuild` exists.
|
|
106
|
+
*/
|
|
107
|
+
export declare function execCacheKey(input: ExecCacheKeyInput): string;
|
|
108
|
+
/** The nearest lockfile walking up from `start`, or null. */
|
|
109
|
+
export declare function findProjectLockfile(start: string): string | null;
|
|
110
|
+
export declare function hashFile(path: string): string;
|
|
111
|
+
/**
|
|
112
|
+
* Where rebuilt bins are written: `<node_modules>/.cache/gjsify/exec/`.
|
|
113
|
+
*
|
|
114
|
+
* NOT `$XDG_CACHE_HOME`, although the key is content-addressed and would allow it:
|
|
115
|
+
* a bundled dependency that reads its own files (`package.json` for `--version`, a
|
|
116
|
+
* template, a locale) has its `import.meta.url`/`__dirname` rewritten to resolve the
|
|
117
|
+
* package AT RUNTIME from the bundle's location (`shims/module-resolve.ts`). From a
|
|
118
|
+
* directory outside the project's `node_modules` that walk finds nothing and falls
|
|
119
|
+
* back to the bundle's own directory, so the read misses.
|
|
120
|
+
*/
|
|
121
|
+
export declare function execCacheRoot(nodeModules: string): string;
|
|
122
|
+
/**
|
|
123
|
+
* Directory + file name of a rebuilt bin. The hash goes in the DIRECTORY and the
|
|
124
|
+
* file keeps the entry's basename: a bin that tests `process.argv[1]` against its
|
|
125
|
+
* own name (the standard is-main guard) must still see it — the same lesson as
|
|
126
|
+
* `bundleFileForGjsCached`'s layout.
|
|
127
|
+
*/
|
|
128
|
+
export declare function execArtifactPath(cacheRoot: string, bin: Pick<ResolvedBin, 'pkgName' | 'pkgVersion'>, entry: string, key: string): string;
|
|
129
|
+
/**
|
|
130
|
+
* The entry's basename, except that `.cjs` becomes `.mjs`: the artifact is ESM, and
|
|
131
|
+
* the bundler reads the output FORMAT off a `.cjs` file name — measured on prettier's
|
|
132
|
+
* `bin/prettier.cjs`, whose rebuild came out as CommonJS with `require("gi://GLib")`
|
|
133
|
+
* and died under gjs with `ReferenceError: require is not defined`.
|
|
134
|
+
*/
|
|
135
|
+
export declare function artifactFileName(entry: string): string;
|
|
136
|
+
/** The entry path relative to its package, for the key. */
|
|
137
|
+
export declare function entryWithinPackage(bin: Pick<ResolvedBin, 'pkgDir'>, entry: string): string;
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// `gjsify exec <bin>` — the pure half: which file a bin name means, which runtime
|
|
2
|
+
// runs it, and the key its rebuilt artifact is cached under (ADR 0076).
|
|
3
|
+
//
|
|
4
|
+
// Kept free of spawning and bundling so every decision is unit-testable on any host;
|
|
5
|
+
// `commands/exec.ts` is the thin shell that acts on the answers.
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
8
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
9
|
+
import { normalizeBinMap } from './bin-shim.js';
|
|
10
|
+
import { DEP_CHANGE_LOCKFILES } from './package-inputs.js';
|
|
11
|
+
import { readPackageJson } from './pkg-json.js';
|
|
12
|
+
const EXEC_RUNTIMES = ['gjs', 'node', 'bun', 'deno'];
|
|
13
|
+
/**
|
|
14
|
+
* Split `gjsify exec [options] <bin> [args…]`. Options are gjsify's only BEFORE the
|
|
15
|
+
* bin name; everything after it belongs to the bin, flags included, as with `npx`
|
|
16
|
+
* — `gjsify exec wxt --runtime x` hands `--runtime x` to wxt. yargs cannot draw
|
|
17
|
+
* that line (a declared option is taken wherever it appears), so the command
|
|
18
|
+
* declares none and this function reads the leading ones.
|
|
19
|
+
*
|
|
20
|
+
* `tail` is what yargs split off at the first `--`: one AFTER the bin belongs to
|
|
21
|
+
* the bin, one BEFORE it only separates (`gjsify exec -- --weird-bin-name`).
|
|
22
|
+
*/
|
|
23
|
+
export function splitExecArgv(entries, tail = []) {
|
|
24
|
+
const out = { runtime: null, rebuild: false, verbose: false };
|
|
25
|
+
const setRuntime = (value) => {
|
|
26
|
+
if (value === undefined || !EXEC_RUNTIMES.includes(value)) {
|
|
27
|
+
throw new ExecPlanError(`gjsify exec: --runtime expects one of ${EXEC_RUNTIMES.join(', ')}, got ${value === undefined ? 'nothing' : `"${value}"`}.`);
|
|
28
|
+
}
|
|
29
|
+
out.runtime = value;
|
|
30
|
+
};
|
|
31
|
+
let i = 0;
|
|
32
|
+
for (; i < entries.length; i++) {
|
|
33
|
+
const word = entries[i];
|
|
34
|
+
if (!word.startsWith('-'))
|
|
35
|
+
break;
|
|
36
|
+
if (word === '--rebuild')
|
|
37
|
+
out.rebuild = true;
|
|
38
|
+
else if (word === '--verbose')
|
|
39
|
+
out.verbose = true;
|
|
40
|
+
else if (word === '--runtime')
|
|
41
|
+
setRuntime(entries[++i]);
|
|
42
|
+
else if (word.startsWith('--runtime='))
|
|
43
|
+
setRuntime(word.slice('--runtime='.length));
|
|
44
|
+
else {
|
|
45
|
+
throw new ExecPlanError(`gjsify exec: unknown option "${word}" before the bin name. Options for the bin go AFTER it ` +
|
|
46
|
+
'(`gjsify exec <bin> --flag`); gjsify exec itself takes --runtime <gjs|node|bun|deno>, --rebuild, --verbose.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (i < entries.length) {
|
|
50
|
+
return { ...out, bin: entries[i], args: [...entries.slice(i + 1), ...(tail.length ? ['--', ...tail] : [])] };
|
|
51
|
+
}
|
|
52
|
+
if (tail.length > 0)
|
|
53
|
+
return { ...out, bin: tail[0], args: tail.slice(1) };
|
|
54
|
+
throw new ExecPlanError('gjsify exec: no bin given. Usage: gjsify exec [--runtime <r>] [--rebuild] <bin> [args…]');
|
|
55
|
+
}
|
|
56
|
+
export class ExecBinNotFoundError extends Error {
|
|
57
|
+
binName;
|
|
58
|
+
searched;
|
|
59
|
+
constructor(binName, searched) {
|
|
60
|
+
super(`gjsify exec: no bin named "${binName}" in this project.\n` +
|
|
61
|
+
`Looked in the nearest package.json and in:\n${searched.map((d) => ` ${d}`).join('\n') || ' (no node_modules found)'}\n` +
|
|
62
|
+
`Install the package that provides it (\`gjsify install <pkg>\`), or use \`gjsify dlx\` for a one-off run.`);
|
|
63
|
+
this.binName = binName;
|
|
64
|
+
this.searched = searched;
|
|
65
|
+
this.name = 'ExecBinNotFoundError';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export class ExecPlanError extends Error {
|
|
69
|
+
constructor(message) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = 'ExecPlanError';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function readManifest(pkgDir) {
|
|
75
|
+
return readPackageJson(join(pkgDir, 'package.json'));
|
|
76
|
+
}
|
|
77
|
+
function realOrNull(path) {
|
|
78
|
+
try {
|
|
79
|
+
return realpathSync(path);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function isInsideNodeModules(path) {
|
|
86
|
+
return path.split(sep).includes('node_modules');
|
|
87
|
+
}
|
|
88
|
+
/** The bin's targets in `pkgDir`, or null when the package does not declare `binName`. */
|
|
89
|
+
function binFromPackage(pkgDir, binName, nodeModules) {
|
|
90
|
+
const manifest = readManifest(pkgDir);
|
|
91
|
+
if (!manifest?.name)
|
|
92
|
+
return null;
|
|
93
|
+
const npmTarget = manifest.bin === undefined ? undefined : normalizeBinMap(manifest.name, manifest.bin).get(binName);
|
|
94
|
+
const gjsTarget = manifest.gjsify?.bin === undefined
|
|
95
|
+
? undefined
|
|
96
|
+
: normalizeBinMap(manifest.name, manifest.gjsify.bin).get(binName);
|
|
97
|
+
if (npmTarget === undefined && gjsTarget === undefined)
|
|
98
|
+
return null;
|
|
99
|
+
const realDir = realOrNull(pkgDir) ?? resolve(pkgDir);
|
|
100
|
+
// A declared target that is not on disk is a broken install, not a different bin:
|
|
101
|
+
// report it as unresolvable rather than silently picking another package.
|
|
102
|
+
const entry = npmTarget === undefined ? null : realOrNull(join(pkgDir, npmTarget));
|
|
103
|
+
const gjsEntry = gjsTarget === undefined ? null : realOrNull(join(pkgDir, gjsTarget));
|
|
104
|
+
if (entry === null && gjsEntry === null)
|
|
105
|
+
return null;
|
|
106
|
+
return {
|
|
107
|
+
binName,
|
|
108
|
+
pkgName: manifest.name,
|
|
109
|
+
pkgVersion: manifest.version ?? '0.0.0',
|
|
110
|
+
pkgDir: realDir,
|
|
111
|
+
entry,
|
|
112
|
+
gjsEntry,
|
|
113
|
+
nodeModules,
|
|
114
|
+
installed: isInsideNodeModules(realDir),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Top-level package directories of a `node_modules` (scoped ones expanded). */
|
|
118
|
+
function listPackages(nodeModules) {
|
|
119
|
+
const out = [];
|
|
120
|
+
let names;
|
|
121
|
+
try {
|
|
122
|
+
names = readdirSync(nodeModules);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
for (const name of names) {
|
|
128
|
+
if (name.startsWith('.'))
|
|
129
|
+
continue;
|
|
130
|
+
if (name.startsWith('@')) {
|
|
131
|
+
let scoped;
|
|
132
|
+
try {
|
|
133
|
+
scoped = readdirSync(join(nodeModules, name));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
for (const s of scoped)
|
|
139
|
+
out.push(join(nodeModules, name, s));
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
out.push(join(nodeModules, name));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The package that owns the file `.bin/<name>` links to: the nearest ancestor with a
|
|
149
|
+
* `package.json` that declares the bin. npm, pnpm and yarn all write a SYMLINK there
|
|
150
|
+
* on POSIX, which makes this the cheap path.
|
|
151
|
+
*/
|
|
152
|
+
function ownerOfLinkedBin(linkTarget, binName, nodeModules) {
|
|
153
|
+
let dir = dirname(linkTarget);
|
|
154
|
+
for (;;) {
|
|
155
|
+
if (existsSync(join(dir, 'package.json'))) {
|
|
156
|
+
const hit = binFromPackage(dir, binName, nodeModules);
|
|
157
|
+
if (hit && (hit.entry === linkTarget || hit.gjsEntry === linkTarget))
|
|
158
|
+
return hit;
|
|
159
|
+
}
|
|
160
|
+
const parent = dirname(dir);
|
|
161
|
+
if (parent === dir)
|
|
162
|
+
return null;
|
|
163
|
+
dir = parent;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Resolve `binName` the way `npx` does for an installed bin: the project's OWN
|
|
168
|
+
* `package.json#bin` first, then every `node_modules` from `cwd` up to the root.
|
|
169
|
+
*
|
|
170
|
+
* `.bin/<name>` is only a HINT. Its shape depends on who wrote it — a symlink (npm,
|
|
171
|
+
* pnpm), an `sh` launcher (`gjsify install` for a `gjsify.bin`), a `.cmd`/`.ps1` pair
|
|
172
|
+
* (Windows) — and only the symlink names its target, so the other shapes fall back to
|
|
173
|
+
* reading the package manifests of that `node_modules`. A package whose NAME is the
|
|
174
|
+
* bin name wins that scan, as `npx <pkg>` would pick it.
|
|
175
|
+
*/
|
|
176
|
+
export function resolveExecBin(binName, cwd) {
|
|
177
|
+
const searched = [];
|
|
178
|
+
let dir = resolve(cwd);
|
|
179
|
+
for (;;) {
|
|
180
|
+
if (existsSync(join(dir, 'package.json'))) {
|
|
181
|
+
const own = binFromPackage(dir, binName, null);
|
|
182
|
+
if (own)
|
|
183
|
+
return own;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
const parent = dirname(dir);
|
|
187
|
+
if (parent === dir)
|
|
188
|
+
break;
|
|
189
|
+
dir = parent;
|
|
190
|
+
}
|
|
191
|
+
dir = resolve(cwd);
|
|
192
|
+
for (;;) {
|
|
193
|
+
const nodeModules = join(dir, 'node_modules');
|
|
194
|
+
if (existsSync(nodeModules)) {
|
|
195
|
+
searched.push(nodeModules);
|
|
196
|
+
const binDir = join(nodeModules, '.bin');
|
|
197
|
+
const hint = [binName, `${binName}.cmd`].some((n) => existsSync(join(binDir, n)));
|
|
198
|
+
if (hint) {
|
|
199
|
+
const link = join(binDir, binName);
|
|
200
|
+
const isLink = (() => {
|
|
201
|
+
try {
|
|
202
|
+
return lstatSync(link).isSymbolicLink();
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
})();
|
|
208
|
+
const linkTarget = isLink ? realOrNull(link) : null;
|
|
209
|
+
const linked = linkTarget ? ownerOfLinkedBin(linkTarget, binName, nodeModules) : null;
|
|
210
|
+
if (linked)
|
|
211
|
+
return linked;
|
|
212
|
+
const candidates = listPackages(nodeModules).sort((a, b) => Number(basename(b) === binName) - Number(basename(a) === binName));
|
|
213
|
+
for (const pkgDir of candidates) {
|
|
214
|
+
const hit = binFromPackage(pkgDir, binName, nodeModules);
|
|
215
|
+
if (hit)
|
|
216
|
+
return hit;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const parent = dirname(dir);
|
|
221
|
+
if (parent === dir)
|
|
222
|
+
break;
|
|
223
|
+
dir = parent;
|
|
224
|
+
}
|
|
225
|
+
throw new ExecBinNotFoundError(binName, searched);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Pick the plan. `runtime` is the HOST runtime unless the caller passed an explicit
|
|
229
|
+
* `--runtime`; there is no implicit fallback from gjs to node — a failed rebuild is
|
|
230
|
+
* reported, never papered over by a runtime the user did not ask for.
|
|
231
|
+
*/
|
|
232
|
+
export function planExec(bin, runtime) {
|
|
233
|
+
if (runtime === 'gjs') {
|
|
234
|
+
if (bin.gjsEntry)
|
|
235
|
+
return { kind: 'gjs-bundle', file: bin.gjsEntry };
|
|
236
|
+
if (bin.entry)
|
|
237
|
+
return { kind: 'rebuild', entry: bin.entry };
|
|
238
|
+
}
|
|
239
|
+
else if (bin.entry) {
|
|
240
|
+
return { kind: 'direct', runtime, file: bin.entry };
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
throw new ExecPlanError(`gjsify exec: "${bin.binName}" from ${bin.pkgName} is declared only as a GJS bundle (\`gjsify.bin\`), ` +
|
|
244
|
+
`which ${runtime} cannot run. Run it on GJS: \`gjsify exec --runtime gjs ${bin.binName}\`.`);
|
|
245
|
+
}
|
|
246
|
+
throw new ExecPlanError(`gjsify exec: "${bin.binName}" from ${bin.pkgName} names no file that exists.`);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Content key of a rebuilt bin. The package version identifies the entry's own code;
|
|
250
|
+
* the LOCKFILE identifies its dependencies, which a reinstall can move without the
|
|
251
|
+
* version changing; the CLI version identifies the bundler and every polyfill that
|
|
252
|
+
* went into the artifact. With no lockfile, dependency drift is invisible to the key,
|
|
253
|
+
* which is why `--rebuild` exists.
|
|
254
|
+
*/
|
|
255
|
+
export function execCacheKey(input) {
|
|
256
|
+
const payload = JSON.stringify([
|
|
257
|
+
'gjsify-exec/v1',
|
|
258
|
+
input.pkgName,
|
|
259
|
+
input.pkgVersion,
|
|
260
|
+
input.entry.split(sep).join('/'),
|
|
261
|
+
input.cliVersion,
|
|
262
|
+
input.lockfileHash,
|
|
263
|
+
]);
|
|
264
|
+
return createHash('sha256').update(payload).digest('hex');
|
|
265
|
+
}
|
|
266
|
+
/** Lockfiles that pin a dependency tree, `DEP_CHANGE_LOCKFILES` plus bun's. */
|
|
267
|
+
const EXEC_LOCKFILES = [...DEP_CHANGE_LOCKFILES, 'bun.lock', 'bun.lockb'];
|
|
268
|
+
/** The nearest lockfile walking up from `start`, or null. */
|
|
269
|
+
export function findProjectLockfile(start) {
|
|
270
|
+
let dir = resolve(start);
|
|
271
|
+
for (;;) {
|
|
272
|
+
for (const name of EXEC_LOCKFILES) {
|
|
273
|
+
const candidate = join(dir, name);
|
|
274
|
+
if (existsSync(candidate))
|
|
275
|
+
return candidate;
|
|
276
|
+
}
|
|
277
|
+
const parent = dirname(dir);
|
|
278
|
+
if (parent === dir)
|
|
279
|
+
return null;
|
|
280
|
+
dir = parent;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
export function hashFile(path) {
|
|
284
|
+
return createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Where rebuilt bins are written: `<node_modules>/.cache/gjsify/exec/`.
|
|
288
|
+
*
|
|
289
|
+
* NOT `$XDG_CACHE_HOME`, although the key is content-addressed and would allow it:
|
|
290
|
+
* a bundled dependency that reads its own files (`package.json` for `--version`, a
|
|
291
|
+
* template, a locale) has its `import.meta.url`/`__dirname` rewritten to resolve the
|
|
292
|
+
* package AT RUNTIME from the bundle's location (`shims/module-resolve.ts`). From a
|
|
293
|
+
* directory outside the project's `node_modules` that walk finds nothing and falls
|
|
294
|
+
* back to the bundle's own directory, so the read misses.
|
|
295
|
+
*/
|
|
296
|
+
export function execCacheRoot(nodeModules) {
|
|
297
|
+
return join(nodeModules, '.cache', 'gjsify', 'exec');
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Directory + file name of a rebuilt bin. The hash goes in the DIRECTORY and the
|
|
301
|
+
* file keeps the entry's basename: a bin that tests `process.argv[1]` against its
|
|
302
|
+
* own name (the standard is-main guard) must still see it — the same lesson as
|
|
303
|
+
* `bundleFileForGjsCached`'s layout.
|
|
304
|
+
*/
|
|
305
|
+
export function execArtifactPath(cacheRoot, bin, entry, key) {
|
|
306
|
+
const safe = `${bin.pkgName}@${bin.pkgVersion}`.replace(/[^a-zA-Z0-9._@-]/g, '_');
|
|
307
|
+
return join(cacheRoot, `${safe}-${key.slice(0, 16)}`, artifactFileName(entry));
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* The entry's basename, except that `.cjs` becomes `.mjs`: the artifact is ESM, and
|
|
311
|
+
* the bundler reads the output FORMAT off a `.cjs` file name — measured on prettier's
|
|
312
|
+
* `bin/prettier.cjs`, whose rebuild came out as CommonJS with `require("gi://GLib")`
|
|
313
|
+
* and died under gjs with `ReferenceError: require is not defined`.
|
|
314
|
+
*/
|
|
315
|
+
export function artifactFileName(entry) {
|
|
316
|
+
return basename(entry).replace(/\.cjs$/i, '.mjs');
|
|
317
|
+
}
|
|
318
|
+
/** The entry path relative to its package, for the key. */
|
|
319
|
+
export function entryWithinPackage(bin, entry) {
|
|
320
|
+
return relative(bin.pkgDir, entry);
|
|
321
|
+
}
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
// launcher goes through, so the only repair that survives is one the process makes to
|
|
10
10
|
// girepository's own search paths from the inside. Measurements, and what this reaches
|
|
11
11
|
// (loads AFTER module evaluation starts, not a static `gi://` import):
|
|
12
|
-
// `status/open-todos.md` § "A globally installed GJS launcher still cannot load a
|
|
13
|
-
// system GTK on macOS".
|
|
12
|
+
// `status/open-todos/macos.md` § "A globally installed GJS launcher still cannot load a
|
|
13
|
+
// system GTK on macOS", and the darwin-arm64 end-to-end run under it. The static half is
|
|
14
|
+
// ADR 0085 (`docs/poc/gi-prologue-import-order.md` is its placement study).
|
|
14
15
|
//
|
|
15
16
|
// A SHIPPED BUNDLE RUNS ON A MACHINE THE BUILD NEVER SAW, which rules out both
|
|
16
17
|
// tempting sources. `systemGiLibraryDirs()` MEASURES the build host and answers `[]`
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type IgnoreMatch = 'ignore' | 'whitelist' | null;
|
|
2
|
+
export interface GitignoreMatcher {
|
|
3
|
+
/** Directory the patterns are relative to. */
|
|
4
|
+
readonly base: string;
|
|
5
|
+
/** Decide `path` alone; `null` when no line matches or `path` is outside `base`. */
|
|
6
|
+
matched(path: string, isDir: boolean): IgnoreMatch;
|
|
7
|
+
/** Decide `path`, falling back to its parent directories up to `base`. */
|
|
8
|
+
matchedPathOrAnyParents(path: string, isDir: boolean): IgnoreMatch;
|
|
9
|
+
}
|
|
10
|
+
/** Translate one glob to a regex source; `*`, `?` and classes stay inside a segment. */
|
|
11
|
+
export declare function globToRegexSource(glob: string): string;
|
|
12
|
+
/** Build a matcher from pattern lines, relative to `base`. */
|
|
13
|
+
export declare function compileGitignore(lines: readonly string[], base: string): GitignoreMatcher;
|
|
14
|
+
/** Read an ignore file; a missing or unreadable file yields `null`. */
|
|
15
|
+
export declare function readGitignoreFile(file: string, base?: string): GitignoreMatcher | null;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Gitignore-syntax matcher, the semantics of the Rust `ignore` crate's
|
|
2
|
+
// `Gitignore` that oxfmt (and oxlint) build every ignore layer from:
|
|
3
|
+
// `.gitignore`, `.git/info/exclude`, `.prettierignore` and a config's
|
|
4
|
+
// `ignorePatterns`. One reader for all of them, so a layer cannot be read
|
|
5
|
+
// with different rules than its neighbour.
|
|
6
|
+
//
|
|
7
|
+
// Rules held (gitignore(5)): `#` comments, `\#`/`\!` escapes, unescaped
|
|
8
|
+
// trailing spaces dropped, `!` re-includes, a trailing `/` matches
|
|
9
|
+
// directories only, a pattern with a `/` before its end is anchored to the
|
|
10
|
+
// file's directory while one without matches a basename at any depth,
|
|
11
|
+
// `**/`, `/**/` and `/**`, `*`/`?` never cross `/`, `[...]` classes. The
|
|
12
|
+
// LAST matching line of one matcher decides.
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { dirname, isAbsolute, relative, sep } from 'node:path';
|
|
15
|
+
/** Translate one glob to a regex source; `*`, `?` and classes stay inside a segment. */
|
|
16
|
+
export function globToRegexSource(glob) {
|
|
17
|
+
let out = '';
|
|
18
|
+
let i = 0;
|
|
19
|
+
while (i < glob.length) {
|
|
20
|
+
const c = glob[i];
|
|
21
|
+
if (c === '*' && glob[i + 1] === '*' && (i === 0 || glob[i - 1] === '/')) {
|
|
22
|
+
if (i + 2 === glob.length) {
|
|
23
|
+
out += '.*';
|
|
24
|
+
i += 2;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (glob[i + 2] === '/') {
|
|
28
|
+
out += '(?:.*/)?';
|
|
29
|
+
i += 3;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (c === '*') {
|
|
34
|
+
out += '[^/]*';
|
|
35
|
+
while (glob[i + 1] === '*')
|
|
36
|
+
i++;
|
|
37
|
+
}
|
|
38
|
+
else if (c === '?') {
|
|
39
|
+
out += '[^/]';
|
|
40
|
+
}
|
|
41
|
+
else if (c === '[') {
|
|
42
|
+
const close = findClassEnd(glob, i);
|
|
43
|
+
if (close === -1) {
|
|
44
|
+
out += '\\[';
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
let body = glob.slice(i + 1, close);
|
|
48
|
+
let negate = false;
|
|
49
|
+
if (body.startsWith('!') || body.startsWith('^')) {
|
|
50
|
+
negate = true;
|
|
51
|
+
body = body.slice(1);
|
|
52
|
+
}
|
|
53
|
+
// Gitignore class escapes (`\-`, `\\`, `\]`) are already valid
|
|
54
|
+
// regex class escapes — doubling the backslashes would turn
|
|
55
|
+
// `[a\-z]` into a range.
|
|
56
|
+
body = body.replace(/\]/g, '\\]');
|
|
57
|
+
out += `[${negate ? '^/' : ''}${body}]`;
|
|
58
|
+
i = close;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if (c === '\\' && i + 1 < glob.length) {
|
|
62
|
+
i++;
|
|
63
|
+
out += escapeRegex(glob[i]);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
out += escapeRegex(c);
|
|
67
|
+
}
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function findClassEnd(glob, open) {
|
|
73
|
+
let j = open + 1;
|
|
74
|
+
if (glob[j] === '!' || glob[j] === '^')
|
|
75
|
+
j++;
|
|
76
|
+
if (glob[j] === ']')
|
|
77
|
+
j++;
|
|
78
|
+
for (; j < glob.length; j++)
|
|
79
|
+
if (glob[j] === ']')
|
|
80
|
+
return j;
|
|
81
|
+
return -1;
|
|
82
|
+
}
|
|
83
|
+
function escapeRegex(c) {
|
|
84
|
+
return /[.+^${}()|[\]\\*?]/.test(c) ? `\\${c}` : c;
|
|
85
|
+
}
|
|
86
|
+
function parseLine(raw) {
|
|
87
|
+
let line = raw.replace(/\r$/, '');
|
|
88
|
+
// Trailing spaces are dropped unless the last one is backslash-escaped.
|
|
89
|
+
line = line.replace(/(?<!\\)\s+$/, '');
|
|
90
|
+
if (line === '' || line.startsWith('#'))
|
|
91
|
+
return null;
|
|
92
|
+
let negate = false;
|
|
93
|
+
if (line.startsWith('!')) {
|
|
94
|
+
negate = true;
|
|
95
|
+
line = line.slice(1);
|
|
96
|
+
}
|
|
97
|
+
else if (line.startsWith('\\!') || line.startsWith('\\#')) {
|
|
98
|
+
line = line.slice(1);
|
|
99
|
+
}
|
|
100
|
+
let dirOnly = false;
|
|
101
|
+
if (line.endsWith('/') && !line.endsWith('\\/')) {
|
|
102
|
+
dirOnly = true;
|
|
103
|
+
line = line.slice(0, -1);
|
|
104
|
+
}
|
|
105
|
+
if (line === '')
|
|
106
|
+
return null;
|
|
107
|
+
const anchored = line.includes('/');
|
|
108
|
+
if (line.startsWith('/'))
|
|
109
|
+
line = line.slice(1);
|
|
110
|
+
const src = globToRegexSource(line);
|
|
111
|
+
const prefix = anchored ? '' : '(?:.*/)?';
|
|
112
|
+
return { re: new RegExp(`^${prefix}${src}$`), negate, dirOnly };
|
|
113
|
+
}
|
|
114
|
+
/** Build a matcher from pattern lines, relative to `base`. */
|
|
115
|
+
export function compileGitignore(lines, base) {
|
|
116
|
+
const rules = [];
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
const rule = parseLine(line);
|
|
119
|
+
if (rule)
|
|
120
|
+
rules.push(rule);
|
|
121
|
+
}
|
|
122
|
+
const relOf = (path) => {
|
|
123
|
+
const rel = relative(base, path);
|
|
124
|
+
if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
125
|
+
return null;
|
|
126
|
+
return sep === '/' ? rel : rel.split(sep).join('/');
|
|
127
|
+
};
|
|
128
|
+
const decide = (rel, isDir) => {
|
|
129
|
+
for (let k = rules.length - 1; k >= 0; k--) {
|
|
130
|
+
const rule = rules[k];
|
|
131
|
+
if (rule.dirOnly && !isDir)
|
|
132
|
+
continue;
|
|
133
|
+
if (rule.re.test(rel))
|
|
134
|
+
return rule.negate ? 'whitelist' : 'ignore';
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
base,
|
|
140
|
+
matched(path, isDir) {
|
|
141
|
+
if (rules.length === 0)
|
|
142
|
+
return null;
|
|
143
|
+
const rel = relOf(path);
|
|
144
|
+
return rel === null ? null : decide(rel, isDir);
|
|
145
|
+
},
|
|
146
|
+
matchedPathOrAnyParents(path, isDir) {
|
|
147
|
+
if (rules.length === 0)
|
|
148
|
+
return null;
|
|
149
|
+
let rel = relOf(path);
|
|
150
|
+
if (rel === null)
|
|
151
|
+
return null;
|
|
152
|
+
// The nearest path component any line decides wins — an ignored
|
|
153
|
+
// ancestor directory hides everything under it.
|
|
154
|
+
let dir = isDir;
|
|
155
|
+
for (;;) {
|
|
156
|
+
const m = decide(rel, dir);
|
|
157
|
+
if (m !== null)
|
|
158
|
+
return m;
|
|
159
|
+
const slash = rel.lastIndexOf('/');
|
|
160
|
+
if (slash === -1)
|
|
161
|
+
return null;
|
|
162
|
+
rel = rel.slice(0, slash);
|
|
163
|
+
dir = true;
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** Read an ignore file; a missing or unreadable file yields `null`. */
|
|
169
|
+
export function readGitignoreFile(file, base = dirname(file)) {
|
|
170
|
+
let text;
|
|
171
|
+
try {
|
|
172
|
+
text = readFileSync(file, 'utf-8');
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return compileGitignore(text.split('\n'), base);
|
|
178
|
+
}
|