@gjsify/cli 0.41.0 → 0.43.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/README.md +79 -33
- package/dist/cli.gjs.mjs +1068 -405
- package/lib/actions/build.d.ts +20 -0
- package/lib/actions/build.js +118 -15
- package/lib/bundler-pick.js +10 -2
- package/lib/cli-app.js +2 -1
- package/lib/commands/browse.js +1 -1
- package/lib/commands/debug.js +1 -1
- package/lib/commands/dev.d.ts +13 -0
- package/lib/commands/dev.js +132 -0
- package/lib/commands/dlx.js +1 -1
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/run.js +38 -85
- package/lib/commands/ship.d.ts +2 -0
- package/lib/commands/ship.js +285 -88
- package/lib/commands/showcase.js +1 -1
- package/lib/commands/storybook.js +17 -74
- package/lib/commands/test.js +12 -16
- package/lib/commands/tsc.js +96 -45
- package/lib/config.js +4 -0
- package/lib/types/config-data.d.ts +59 -0
- package/lib/utils/app-metadata.js +27 -6
- package/lib/utils/check-system-deps.js +49 -7
- package/lib/utils/daemon-command.d.ts +7 -0
- package/lib/utils/daemon-command.js +30 -0
- package/lib/utils/dev-plan.d.ts +52 -0
- package/lib/utils/dev-plan.js +117 -0
- package/lib/utils/gi-runtime-paths.d.ts +27 -0
- package/lib/utils/gi-runtime-paths.js +84 -0
- package/lib/utils/gi-typelib.d.ts +49 -0
- package/lib/utils/gi-typelib.js +160 -0
- package/lib/utils/gjs-bundle-guard.d.ts +57 -3
- package/lib/utils/gjs-bundle-guard.js +122 -13
- package/lib/utils/jsx-config.d.ts +45 -0
- package/lib/utils/jsx-config.js +98 -0
- package/lib/utils/node-script.d.ts +4 -1
- package/lib/utils/node-script.js +27 -3
- package/lib/utils/npm-oidc.js +8 -5
- package/lib/utils/onboard-probe.d.ts +23 -0
- package/lib/utils/onboard-probe.js +65 -0
- package/lib/utils/run-gjs.d.ts +12 -1
- package/lib/utils/run-gjs.js +24 -31
- package/lib/utils/run-node.d.ts +9 -4
- package/lib/utils/run-node.js +34 -41
- package/lib/utils/ship/deb.d.ts +3 -3
- package/lib/utils/ship/deb.js +6 -2
- package/lib/utils/ship/depends.d.ts +14 -6
- package/lib/utils/ship/depends.js +45 -4
- package/lib/utils/ship/discover.js +178 -7
- package/lib/utils/ship/formats.js +9 -1
- package/lib/utils/ship/launcher.js +27 -4
- package/lib/utils/ship/mime.d.ts +19 -0
- package/lib/utils/ship/mime.js +89 -0
- package/lib/utils/ship/payload.d.ts +111 -0
- package/lib/utils/ship/payload.js +174 -0
- package/lib/utils/ship/plan.d.ts +30 -1
- package/lib/utils/ship/plan.js +74 -2
- package/lib/utils/ship/rpm.d.ts +3 -3
- package/lib/utils/ship/rpm.js +20 -3
- package/lib/utils/ship/scripts.d.ts +14 -4
- package/lib/utils/ship/scripts.js +26 -8
- package/lib/utils/ship/settings.d.ts +8 -1
- package/lib/utils/ship/settings.js +26 -1
- package/lib/utils/ship/stage-manifest.d.ts +154 -0
- package/lib/utils/ship/stage-manifest.js +408 -0
- package/lib/utils/ship/stage-writer.d.ts +30 -3
- package/lib/utils/ship/stage-writer.js +76 -9
- package/lib/utils/ship/types.d.ts +110 -18
- package/lib/utils/ship/types.js +5 -0
- package/lib/utils/simple-command.d.ts +16 -0
- package/lib/utils/simple-command.js +77 -0
- package/lib/utils/spawn.d.ts +7 -1
- package/lib/utils/spawn.js +34 -7
- package/lib/utils/system-gi.d.ts +24 -0
- package/lib/utils/system-gi.js +14 -3
- package/lib/utils/watch-loop.d.ts +91 -0
- package/lib/utils/watch-loop.js +207 -0
- package/package.json +19 -18
- package/lib/templates/app/desktop.tmpl +0 -10
package/lib/utils/run-node.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ExampleRuntime } from './runtimes.js';
|
|
2
|
+
import { type SpawnCompletionContract } from './spawn.js';
|
|
2
3
|
/**
|
|
3
4
|
* Walk up from `startDir` for `<node_modules>/@gjsify/node-gi`, returning its package directory
|
|
4
5
|
* or null. A plain fs walk because Node's own resolver is not available under the GJS-bundled CLI.
|
|
@@ -19,6 +20,8 @@ export declare function resolveNodeGiForBundle(bundlePath: string, cwd: string):
|
|
|
19
20
|
/** Pick the node executable: the current one when on Node, else PATH `node`. */
|
|
20
21
|
export declare function nodeBinary(): string;
|
|
21
22
|
export interface RunNodeBundleOptions {
|
|
23
|
+
/** The caller's teardown contract — see `RunGjsBundleOptions.completion`. */
|
|
24
|
+
completion: SpawnCompletionContract;
|
|
22
25
|
/** Exit this process with code 0 once the child succeeds (terminal callers). */
|
|
23
26
|
exitOnSuccess?: boolean;
|
|
24
27
|
/** Suppress the `$ <env> node …` echo — see `RunGjsBundleOptions.quiet`. */
|
|
@@ -28,12 +31,14 @@ export interface RunNodeBundleOptions {
|
|
|
28
31
|
* Run a `--app node` bundle on Node.js with the native typelib env set (same detection as
|
|
29
32
|
* {@link runGjsBundle}). Throws when `@gjsify/node-gi` is not resolvable.
|
|
30
33
|
*/
|
|
31
|
-
export declare function runNodeBundle(bundlePath: string, extraArgs
|
|
34
|
+
export declare function runNodeBundle(bundlePath: string, extraArgs: string[] | undefined, options: RunNodeBundleOptions): Promise<void>;
|
|
32
35
|
/**
|
|
33
36
|
* Whether a `--app node` bundle actually consumes `@gjsify/node-gi` — kept EXTERNAL, so a bundle
|
|
34
37
|
* using gi:// imports the shim by name. A plain Node app does NOT, and must run on node/bun/deno
|
|
35
|
-
* WITHOUT node-gi installed, which is why the check is conditional
|
|
36
|
-
*
|
|
38
|
+
* WITHOUT node-gi installed, which is why the check is conditional rather than a flat "this is a
|
|
39
|
+
* node bundle, demand node-gi". The supervised launcher (`utils/watch-loop.ts`) asks the same
|
|
40
|
+
* question for the same reason: `gjsify dev` on a web-server template builds a node bundle that
|
|
41
|
+
* never reaches a typelib.
|
|
37
42
|
*/
|
|
38
43
|
export declare function bundleRequiresNodeGi(bundlePath: string): boolean;
|
|
39
44
|
/**
|
|
@@ -44,4 +49,4 @@ export declare function bundleRequiresNodeGi(bundlePath: string): boolean;
|
|
|
44
49
|
*
|
|
45
50
|
* `gjs` is intentionally NOT handled here — the caller dispatches it to {@link runGjsBundle}.
|
|
46
51
|
*/
|
|
47
|
-
export declare function runRuntimeBundle(runtime: Exclude<ExampleRuntime, 'gjs'>, bundlePath: string, extraArgs
|
|
52
|
+
export declare function runRuntimeBundle(runtime: Exclude<ExampleRuntime, 'gjs'>, bundlePath: string, extraArgs: string[] | undefined, options: RunNodeBundleOptions): Promise<void>;
|
package/lib/utils/run-node.js
CHANGED
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
//
|
|
16
16
|
// The node binary is `process.execPath` when the CLI runs under Node, else PATH `node` — under
|
|
17
17
|
// the GJS bundle `process.execPath` is `gjs`, which cannot run a `--app node` bundle.
|
|
18
|
-
import { spawn } from 'node:child_process';
|
|
19
18
|
import { existsSync, readFileSync } from 'node:fs';
|
|
20
19
|
import { dirname, join, resolve } from 'node:path';
|
|
21
20
|
import { isNode } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
22
21
|
import { computeNativeEnvForBundle } from './run-gjs.js';
|
|
23
22
|
import { RUNTIMES } from './runtimes.js';
|
|
23
|
+
import { describeExit, spawnToCompletion } from './spawn.js';
|
|
24
24
|
/**
|
|
25
25
|
* Walk up from `startDir` for `<node_modules>/@gjsify/node-gi`, returning its package directory
|
|
26
26
|
* or null. A plain fs walk because Node's own resolver is not available under the GJS-bundled CLI.
|
|
@@ -60,7 +60,7 @@ export function nodeBinary() {
|
|
|
60
60
|
* Run a `--app node` bundle on Node.js with the native typelib env set (same detection as
|
|
61
61
|
* {@link runGjsBundle}). Throws when `@gjsify/node-gi` is not resolvable.
|
|
62
62
|
*/
|
|
63
|
-
export async function runNodeBundle(bundlePath, extraArgs = [], options
|
|
63
|
+
export async function runNodeBundle(bundlePath, extraArgs = [], options) {
|
|
64
64
|
const cwd = process.cwd();
|
|
65
65
|
if (!resolveNodeGiForBundle(bundlePath, cwd)) {
|
|
66
66
|
throw new Error('Cannot run the storybook on Node: `@gjsify/node-gi` is not installed.\n' +
|
|
@@ -74,27 +74,28 @@ export async function runNodeBundle(bundlePath, extraArgs = [], options = {}) {
|
|
|
74
74
|
// Echo on stderr, so a child speaking a protocol on stdout stays uncontaminated.
|
|
75
75
|
const cmd = [nodeBin, ...nodeArgs.map((a) => (a.includes(' ') ? `"${a}"` : a))].join(' ');
|
|
76
76
|
console.error(`$ ${envPrefix ? `${envPrefix} ` : ''}${cmd}`);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
else
|
|
83
|
-
resolvePromise();
|
|
84
|
-
});
|
|
85
|
-
child.on('error', reject);
|
|
86
|
-
}).catch((err) => {
|
|
77
|
+
let result;
|
|
78
|
+
try {
|
|
79
|
+
result = await spawnToCompletion(nodeBin, nodeArgs, { completion: options.completion, env });
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
87
82
|
console.error(err.message);
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
83
|
+
return process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
if (result.code !== 0) {
|
|
86
|
+
console.error(`node exited with ${describeExit(result)}`);
|
|
87
|
+
return process.exit(result.code ?? 1);
|
|
88
|
+
}
|
|
90
89
|
if (options.exitOnSuccess)
|
|
91
|
-
process.exit(0);
|
|
90
|
+
return process.exit(0);
|
|
92
91
|
}
|
|
93
92
|
/**
|
|
94
93
|
* Whether a `--app node` bundle actually consumes `@gjsify/node-gi` — kept EXTERNAL, so a bundle
|
|
95
94
|
* using gi:// imports the shim by name. A plain Node app does NOT, and must run on node/bun/deno
|
|
96
|
-
* WITHOUT node-gi installed, which is why the check is conditional
|
|
97
|
-
*
|
|
95
|
+
* WITHOUT node-gi installed, which is why the check is conditional rather than a flat "this is a
|
|
96
|
+
* node bundle, demand node-gi". The supervised launcher (`utils/watch-loop.ts`) asks the same
|
|
97
|
+
* question for the same reason: `gjsify dev` on a web-server template builds a node bundle that
|
|
98
|
+
* never reaches a typelib.
|
|
98
99
|
*/
|
|
99
100
|
export function bundleRequiresNodeGi(bundlePath) {
|
|
100
101
|
try {
|
|
@@ -112,7 +113,7 @@ export function bundleRequiresNodeGi(bundlePath) {
|
|
|
112
113
|
*
|
|
113
114
|
* `gjs` is intentionally NOT handled here — the caller dispatches it to {@link runGjsBundle}.
|
|
114
115
|
*/
|
|
115
|
-
export async function runRuntimeBundle(runtime, bundlePath, extraArgs = [], options
|
|
116
|
+
export async function runRuntimeBundle(runtime, bundlePath, extraArgs = [], options) {
|
|
116
117
|
const cwd = process.cwd();
|
|
117
118
|
const resolvedBundle = resolve(bundlePath);
|
|
118
119
|
if (bundleRequiresNodeGi(resolvedBundle) && !resolveNodeGiForBundle(resolvedBundle, cwd)) {
|
|
@@ -131,32 +132,24 @@ export async function runRuntimeBundle(runtime, bundlePath, extraArgs = [], opti
|
|
|
131
132
|
const shown = [cmd, ...launchArgs.map((a) => (a.includes(' ') ? `"${a}"` : a))].join(' ');
|
|
132
133
|
if (!options.quiet)
|
|
133
134
|
console.error(`$ ${envPrefix ? `${envPrefix} ` : ''}${shown}`);
|
|
134
|
-
const child = spawn(cmd, launchArgs, { env, stdio: 'inherit' });
|
|
135
135
|
// Re-raise the CHILD's own code rather than a blanket 1 — a script run through
|
|
136
136
|
// `--node-script` reports its status through this path, and a caller reading `$?` needs
|
|
137
|
-
// the real one.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
resolvePromise();
|
|
148
|
-
});
|
|
149
|
-
child.on('error', reject);
|
|
150
|
-
}).catch((err) => {
|
|
137
|
+
// the real one. A spawn failure has no code of its own and falls back to 1.
|
|
138
|
+
const failed = (code) => {
|
|
139
|
+
process.exitCode = code;
|
|
140
|
+
return process.exit(code);
|
|
141
|
+
};
|
|
142
|
+
let result;
|
|
143
|
+
try {
|
|
144
|
+
result = await spawnToCompletion(cmd, launchArgs, { completion: options.completion, env });
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
151
147
|
console.error(err.message);
|
|
152
|
-
failed
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (failed) {
|
|
158
|
-
process.exitCode = childCode;
|
|
159
|
-
return process.exit(childCode);
|
|
148
|
+
return failed(1);
|
|
149
|
+
}
|
|
150
|
+
if (result.code !== 0) {
|
|
151
|
+
console.error(`${runtime} exited with ${describeExit(result)}`);
|
|
152
|
+
return failed(result.code ?? 1);
|
|
160
153
|
}
|
|
161
154
|
if (options.exitOnSuccess)
|
|
162
155
|
return process.exit(0);
|
package/lib/utils/ship/deb.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type {
|
|
1
|
+
import { type PayloadEntry } from './payload.js';
|
|
2
|
+
import type { PackSettings } from './types.js';
|
|
3
3
|
export interface DebInputs {
|
|
4
|
-
settings:
|
|
4
|
+
settings: PackSettings;
|
|
5
5
|
/** The staged payload, prefix-relative. */
|
|
6
6
|
payload: readonly PayloadEntry[];
|
|
7
7
|
/** Install prefix, e.g. `/usr`. */
|
package/lib/utils/ship/deb.js
CHANGED
|
@@ -20,8 +20,9 @@ import { createArArchive } from './ar.js';
|
|
|
20
20
|
import { formatDebDepend } from './depends.js';
|
|
21
21
|
import { gzipDeterministic } from './gzip.js';
|
|
22
22
|
import { renderDebScripts } from './scripts.js';
|
|
23
|
+
import { readPayloadFacts } from './payload.js';
|
|
23
24
|
export async function buildDeb(inputs) {
|
|
24
|
-
const {
|
|
25
|
+
const { payload, mtime } = inputs;
|
|
25
26
|
const prefix = inputs.prefix.replace(/^\/+/, '');
|
|
26
27
|
const named = payload.map((entry) => ({ ...entry, tarPath: `./${posix.join(prefix, entry.path)}` }));
|
|
27
28
|
const directories = ancestorDirectories(named.map((entry) => entry.tarPath));
|
|
@@ -44,7 +45,10 @@ export async function buildDeb(inputs) {
|
|
|
44
45
|
{ name: './control', body: control, mode: 0o644, mtime },
|
|
45
46
|
{ name: './md5sums', body: md5sums, mode: 0o644, mtime },
|
|
46
47
|
];
|
|
47
|
-
|
|
48
|
+
// From the PAYLOAD, not the settings: the maintainer scripts refresh exactly the
|
|
49
|
+
// directories this package wrote into, and since ADR 0024 § A2 this packer also runs on a
|
|
50
|
+
// host that has the staged tree and no project to ask.
|
|
51
|
+
const maintainerScripts = renderDebScripts(readPayloadFacts(payload), inputs.prefix);
|
|
48
52
|
for (const [name, body] of Object.entries(maintainerScripts)) {
|
|
49
53
|
// dpkg refuses a maintainer script outside 0555–0775 with the mode in
|
|
50
54
|
// the error, so this is 0755 rather than "whatever the source had".
|
|
@@ -14,16 +14,19 @@ import type { FormatId } from './types.js';
|
|
|
14
14
|
* is how a project whose bundle really does run on older GJS lowers it.
|
|
15
15
|
*/
|
|
16
16
|
export declare const DEFAULT_GJS_FLOOR = "1.86";
|
|
17
|
-
/**
|
|
18
|
-
* Warn when the derived floor cannot be satisfied by a released Debian.
|
|
19
|
-
* Returns the warning lines; empty when the floor is fine.
|
|
20
|
-
*/
|
|
21
17
|
export declare function warnAboutGjsFloor(format: FormatId, floor: string): string[];
|
|
22
18
|
export interface DependsInputs {
|
|
23
19
|
/** GI namespaces the bundle imports, in `Ns-Version` spelling. */
|
|
24
20
|
namespaces: readonly string[];
|
|
25
|
-
/**
|
|
26
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Whether the payload installs into `share/icons/hicolor/`.
|
|
23
|
+
*
|
|
24
|
+
* Was `kind: 'app' | 'cli'`, which is a proxy for the question and not the question:
|
|
25
|
+
* `hicolor-icon-theme` is depended on because it OWNS that directory, so an app that
|
|
26
|
+
* installs no icon used to declare a dependency it never touched. Derived from the payload
|
|
27
|
+
* (`readPayloadFacts`), which is also the only thing the packing host has (ADR 0024 § A2).
|
|
28
|
+
*/
|
|
29
|
+
hasIcons: boolean;
|
|
27
30
|
/** Whether the payload installs GSettings schemas. */
|
|
28
31
|
hasSchemas: boolean;
|
|
29
32
|
/** User-supplied additions for this format. */
|
|
@@ -33,6 +36,11 @@ export interface DependsInputs {
|
|
|
33
36
|
deb: string;
|
|
34
37
|
rpm: string;
|
|
35
38
|
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Paths of typelib files the payload carries itself. The namespaces they cover need no distro
|
|
41
|
+
* dependency — and are derived from these filenames, never declared separately.
|
|
42
|
+
*/
|
|
43
|
+
bundledTypelibs?: readonly string[];
|
|
36
44
|
/** Minimum GJS. Default {@link DEFAULT_GJS_FLOOR}. */
|
|
37
45
|
minGjsVersion?: string;
|
|
38
46
|
}
|
|
@@ -35,14 +35,34 @@
|
|
|
35
35
|
* is how a project whose bundle really does run on older GJS lowers it.
|
|
36
36
|
*/
|
|
37
37
|
export const DEFAULT_GJS_FLOOR = '1.86';
|
|
38
|
+
/**
|
|
39
|
+
* The package providing `glib-compile-schemas`, per format.
|
|
40
|
+
*
|
|
41
|
+
* A record, not `format === 'deb' ? … : …`. The ternary reads as a two-way
|
|
42
|
+
* choice and is really "deb, or ELSE rpm's package name" — measured on a third
|
|
43
|
+
* format, `dmg` got `glib2`, at exit 0, in the `Depends:` of a package that has
|
|
44
|
+
* no such thing. Adding a `FormatId` now fails to build here instead.
|
|
45
|
+
*/
|
|
46
|
+
const SCHEMA_COMPILER_PACKAGE = { deb: 'libglib2.0-bin', rpm: 'glib2' };
|
|
38
47
|
/** Debian's `gjs` versions per suite, as measured 2026-08-15 — see {@link warnAboutGjsFloor}. */
|
|
39
48
|
const DEBIAN_GJS = 'trixie ships 1.82.3 and forky 1.88.1; 1.84 and 1.86 were skipped';
|
|
40
49
|
/**
|
|
41
50
|
* Warn when the derived floor cannot be satisfied by a released Debian.
|
|
42
51
|
* Returns the warning lines; empty when the floor is fine.
|
|
43
52
|
*/
|
|
53
|
+
/**
|
|
54
|
+
* Which formats this Debian-suite warning is ABOUT.
|
|
55
|
+
*
|
|
56
|
+
* A `Record<FormatId, …>` and not `format !== 'deb'`: the negative form answers
|
|
57
|
+
* for every format that will ever exist, and answers "stay quiet" — so a third
|
|
58
|
+
* format would inherit silence about a floor nobody has checked for its distro,
|
|
59
|
+
* with no compile error. As a record, adding a `FormatId` fails to build until
|
|
60
|
+
* someone decides. `rpm` is false because Fedora ships a current GJS, which is a
|
|
61
|
+
* measured fact about Fedora, not a default.
|
|
62
|
+
*/
|
|
63
|
+
const GJS_FLOOR_IS_DEBIAN_NEWS = { deb: true, rpm: false };
|
|
44
64
|
export function warnAboutGjsFloor(format, floor) {
|
|
45
|
-
if (format
|
|
65
|
+
if (!GJS_FLOOR_IS_DEBIAN_NEWS[format])
|
|
46
66
|
return [];
|
|
47
67
|
// Only a floor forky ACTUALLY satisfies is quiet. The first cut tested
|
|
48
68
|
// `>= 1.88.1` and therefore went silent for 1.90 and 2.0 as well — the
|
|
@@ -105,6 +125,12 @@ const TYPELIB_PACKAGES = {
|
|
|
105
125
|
'Notify-0.7': { deb: 'gir1.2-notify-0.7', rpm: 'libnotify' },
|
|
106
126
|
'Vte-3.91': { deb: 'gir1.2-vte-3.91', rpm: 'vte291-gtk4' },
|
|
107
127
|
'Manette-0.2': { deb: 'gir1.2-manette-0.2', rpm: 'libmanette' },
|
|
128
|
+
// libgda — what `@gjsify/sqlite` binds, so every project with a local database lands here.
|
|
129
|
+
// The rpm name is measured on this machine (`rpm -qf …/Gda-6.0.typelib` → libgda); the deb name
|
|
130
|
+
// follows the `gir1.2-<name>-<version>` convention every row above uses and is NOT verified
|
|
131
|
+
// against a Debian system. Said plainly because a wrong name fails at `apt install` with a
|
|
132
|
+
// clear error, while no row at all fails the BUILD of every such project.
|
|
133
|
+
'Gda-6.0': { deb: 'gir1.2-gda-6.0', rpm: 'libgda' },
|
|
108
134
|
};
|
|
109
135
|
/**
|
|
110
136
|
* A typelib the payload CARRIES, rather than one the system provides.
|
|
@@ -131,8 +157,21 @@ const BUNDLED_TYPELIB = /^Gjsify[A-Z]/;
|
|
|
131
157
|
export function deriveDepends(format, inputs) {
|
|
132
158
|
const out = [`gjs >= ${inputs.minGjsVersion ?? DEFAULT_GJS_FLOOR}`];
|
|
133
159
|
const unmapped = [];
|
|
160
|
+
// Namespaces the package SHIPS ITSELF, read off the staged filenames rather than from a
|
|
161
|
+
// separate declaration: `Gwebgl-0.1.typelib` → `Gwebgl-0.1` and `Gwebgl`. Deriving it is the
|
|
162
|
+
// point — a project that declared a namespace bundled without the file being there would get
|
|
163
|
+
// a package that installs and dies at the first import, which is exactly what this check
|
|
164
|
+
// exists to prevent.
|
|
165
|
+
const shipped = new Set();
|
|
166
|
+
for (const file of inputs.bundledTypelibs ?? []) {
|
|
167
|
+
const match = /([A-Za-z0-9]+)-([\d.]+)\.typelib$/.exec(file);
|
|
168
|
+
if (!match)
|
|
169
|
+
continue;
|
|
170
|
+
shipped.add(`${match[1]}-${match[2]}`);
|
|
171
|
+
shipped.add(match[1]);
|
|
172
|
+
}
|
|
134
173
|
for (const namespace of [...new Set(inputs.namespaces)].sort()) {
|
|
135
|
-
if (BUNDLED_TYPELIB.test(namespace))
|
|
174
|
+
if (BUNDLED_TYPELIB.test(namespace) || shipped.has(namespace))
|
|
136
175
|
continue;
|
|
137
176
|
const entry = lookupTypelib(namespace, inputs.typelibPackages);
|
|
138
177
|
if (entry === undefined) {
|
|
@@ -152,7 +191,9 @@ export function deriveDepends(format, inputs) {
|
|
|
152
191
|
'have to. (`gjsify.ship.depends.<format>` is for dependencies that are not typelibs at all — it ' +
|
|
153
192
|
'appends, and deliberately does not silence this.)');
|
|
154
193
|
}
|
|
155
|
-
|
|
194
|
+
// The package that owns `/usr/share/icons/hicolor` — so this follows the icons, not the
|
|
195
|
+
// app/cli distinction that used to stand in for them.
|
|
196
|
+
if (inputs.hasIcons)
|
|
156
197
|
out.push('hicolor-icon-theme');
|
|
157
198
|
// The package that ships `glib-compile-schemas`, NOT `gsettings-desktop-schemas`
|
|
158
199
|
// (which ships GNOME's own `org.gnome.desktop.*` schemas and has nothing to
|
|
@@ -163,7 +204,7 @@ export function deriveDepends(format, inputs) {
|
|
|
163
204
|
// `Gio.Settings.new()` aborts the app — an install that succeeds and an app
|
|
164
205
|
// that does not start.
|
|
165
206
|
if (inputs.hasSchemas)
|
|
166
|
-
out.push(format
|
|
207
|
+
out.push(SCHEMA_COMPILER_PACKAGE[format]);
|
|
167
208
|
out.push(...inputs.extra);
|
|
168
209
|
// Set-dedupe keeps first-insertion order, so `gjs` stays first and the
|
|
169
210
|
// list is stable across runs.
|
|
@@ -4,23 +4,128 @@
|
|
|
4
4
|
// so `settings.ts` and `plan.ts` can stay pure. Every probe has a configured
|
|
5
5
|
// override; the probes exist so a project that follows the GNOME convention
|
|
6
6
|
// (`data/icons/hicolor/…`, `data/*.gschema.xml`) needs no configuration at all.
|
|
7
|
-
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
8
|
-
import { isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
7
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
8
|
+
import { dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
9
9
|
import { LICENSE_FILE_NAMES } from './settings.js';
|
|
10
10
|
const ICON_EXTENSIONS = ['.svg', '.png'];
|
|
11
11
|
export function discoverPayload(input) {
|
|
12
12
|
const { projectDir, ship } = input;
|
|
13
13
|
const bundlePath = resolveBundle(input);
|
|
14
14
|
const bundleDir = resolve(bundlePath, '..');
|
|
15
|
+
const localeFiles = discoverLocales(projectDir, ship.localeDir);
|
|
15
16
|
return {
|
|
16
17
|
bundlePath,
|
|
17
18
|
bundleDir,
|
|
18
|
-
|
|
19
|
+
// Everything beside the bundle, MINUS the locale tree when it lives there.
|
|
20
|
+
//
|
|
21
|
+
// `dist/locale/` sits next to `dist/app.gjs.mjs` in the layout
|
|
22
|
+
// `@gjsify/vite-plugin-gettext` writes by default, and the wholesale bundle staging would
|
|
23
|
+
// then put every catalogue in `lib/<binary>/locale/` as well as in `share/locale/`.
|
|
24
|
+
// Measured on a probe package: the same `.mo` appeared at both paths. The `lib/` copy is
|
|
25
|
+
// dead weight — nothing looks there — and it is the same failure that once shipped the test
|
|
26
|
+
// suite: whatever is beside the bundle gets carried whether or not it belongs in a package.
|
|
27
|
+
bundleFiles: withoutLocaleTree(listFilesRecursive(bundleDir), bundleDir, projectDir, ship.localeDir),
|
|
19
28
|
iconFiles: discoverIcons(projectDir, ship.icon ?? input.flatpakIcon),
|
|
20
29
|
schemaFiles: discoverSchemas(projectDir, ship.schemas),
|
|
30
|
+
typelibFiles: discoverTypelibs(projectDir, ship.bundledTypelibs),
|
|
31
|
+
localeFiles,
|
|
21
32
|
licenseFile: discoverLicense(projectDir, ship.licenseFile),
|
|
22
33
|
};
|
|
23
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Typelibs (and their shared libraries) the PROJECT ships itself.
|
|
37
|
+
*
|
|
38
|
+
* Necessary because gjsify's own GI libraries — `Gwebgl` for the WebGL bridge, the GTK runtime
|
|
39
|
+
* bundles, the napi host — arrive as npm prebuilds, not as distro packages. An app using one has
|
|
40
|
+
* no `gir1.2-…` to depend on: it must carry the files, and something must point GI at them.
|
|
41
|
+
*
|
|
42
|
+
* A directory rather than a list of files, because a typelib is never alone: `Gwebgl-0.1.typelib`
|
|
43
|
+
* is useless without `libgwebgl.so`, and staging one without the other produces a package that
|
|
44
|
+
* installs and then fails at the first import.
|
|
45
|
+
*/
|
|
46
|
+
function discoverTypelibs(projectDir, dirs) {
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const dir of dirs ?? []) {
|
|
49
|
+
const root = resolve(projectDir, dir);
|
|
50
|
+
if (!existsSync(root)) {
|
|
51
|
+
throw new Error(`gjsify ship: \`gjsify.ship.bundledTypelibs\` names ${dir}, which does not exist. ` +
|
|
52
|
+
'A missing directory here means the package would install without the typelib it promises.');
|
|
53
|
+
}
|
|
54
|
+
for (const file of listFilesRecursive(root)) {
|
|
55
|
+
if (/\.typelib$|\.so(\.\d+)*$/.test(file))
|
|
56
|
+
out.push(join(root, file));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Drop the declared locale tree from the wholesale bundle staging.
|
|
63
|
+
*
|
|
64
|
+
* Only when it actually lies inside the bundle directory — a `localeDir` elsewhere in the project
|
|
65
|
+
* was never part of `bundleFiles` and there is nothing to subtract.
|
|
66
|
+
*/
|
|
67
|
+
function withoutLocaleTree(bundleFiles, bundleDir, projectDir, localeDir) {
|
|
68
|
+
if (localeDir === undefined)
|
|
69
|
+
return bundleFiles;
|
|
70
|
+
const localeRoot = resolve(projectDir, localeDir);
|
|
71
|
+
const rel = relative(bundleDir, localeRoot);
|
|
72
|
+
// Outside the bundle dir: `relative` climbs out (`..`) or returns an absolute path.
|
|
73
|
+
if (rel === '' || rel.startsWith('..') || isAbsolute(rel))
|
|
74
|
+
return bundleFiles;
|
|
75
|
+
// Only `rel` needs normalising: it comes from `relative`, so it is host-shaped, while
|
|
76
|
+
// `bundleFiles` is POSIX by `listFilesRecursive`'s contract. Comparing a host-shaped prefix
|
|
77
|
+
// against POSIX entries matches nothing on Windows — and matching nothing here means the
|
|
78
|
+
// catalogues quietly ship twice again, on that platform only.
|
|
79
|
+
const prefix = `${rel.split(sep).join(posix.sep)}${posix.sep}`;
|
|
80
|
+
return bundleFiles.filter((file) => !`${file}${posix.sep}`.startsWith(prefix));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Compiled gettext catalogues the project ships (`gjsify.ship.localeDir`).
|
|
84
|
+
*
|
|
85
|
+
* Only `.mo` files, and only in the layout `bindtextdomain` actually reads —
|
|
86
|
+
* `<lang>/LC_MESSAGES/<domain>.mo`. Both refusals cover the same failure, which is the quiet kind:
|
|
87
|
+
* a catalogue in the wrong place, or left as `.po`, installs perfectly and the app then shows the
|
|
88
|
+
* untranslated msgid forever. That is indistinguishable from "no translation exists", so it is not
|
|
89
|
+
* something a user or a maintainer would ever report as a packaging bug.
|
|
90
|
+
*/
|
|
91
|
+
function discoverLocales(projectDir, dir) {
|
|
92
|
+
if (dir === undefined)
|
|
93
|
+
return [];
|
|
94
|
+
const root = resolve(projectDir, dir);
|
|
95
|
+
if (!existsSync(root)) {
|
|
96
|
+
throw new Error(`gjsify ship: \`gjsify.ship.localeDir\` names ${dir}, which does not exist. ` +
|
|
97
|
+
'Compile the catalogues first (msgfmt, or @gjsify/vite-plugin-gettext) — a missing ' +
|
|
98
|
+
'directory here means the package would install without the translations it promises.');
|
|
99
|
+
}
|
|
100
|
+
const out = [];
|
|
101
|
+
const strays = [];
|
|
102
|
+
for (const rel of listFilesRecursive(root)) {
|
|
103
|
+
if (rel.endsWith('.po') || rel.endsWith('.pot')) {
|
|
104
|
+
strays.push(`${rel} (a .po source; bindtextdomain reads .mo only)`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!rel.endsWith('.mo'))
|
|
108
|
+
continue;
|
|
109
|
+
// `listFilesRecursive` normalises to POSIX, so one separator is all there is to split on.
|
|
110
|
+
const parts = rel.split(posix.sep);
|
|
111
|
+
if (parts.length !== 3 || parts[1] !== 'LC_MESSAGES') {
|
|
112
|
+
strays.push(`${rel} (expected <lang>/LC_MESSAGES/<domain>.mo)`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
out.push({ rel: parts.join('/'), abs: join(root, rel) });
|
|
116
|
+
}
|
|
117
|
+
if (strays.length > 0) {
|
|
118
|
+
throw new Error(`gjsify ship: \`gjsify.ship.localeDir\` (${dir}) holds files no catalogue lookup will find:\n` +
|
|
119
|
+
strays.map((s) => ` ${s}`).join('\n') +
|
|
120
|
+
'\nA misplaced catalogue installs and then shows nothing, which reads exactly like having ' +
|
|
121
|
+
'no translation at all. Lay them out as <lang>/LC_MESSAGES/<domain>.mo.');
|
|
122
|
+
}
|
|
123
|
+
if (out.length === 0) {
|
|
124
|
+
throw new Error(`gjsify ship: \`gjsify.ship.localeDir\` (${dir}) contains no \`.mo\` catalogue. ` +
|
|
125
|
+
'Declaring a locale directory that ships nothing is a promise the package does not keep.');
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
24
129
|
function resolveBundle(input) {
|
|
25
130
|
const declared = input.declaredBundle;
|
|
26
131
|
if (declared === undefined) {
|
|
@@ -84,6 +189,34 @@ function discoverSchemas(projectDir, configured) {
|
|
|
84
189
|
}
|
|
85
190
|
return [];
|
|
86
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* The licence text this package ships — searched UP the tree, not just beside
|
|
194
|
+
* `package.json`.
|
|
195
|
+
*
|
|
196
|
+
* A monorepo package does not carry its own copy. `packages/infra/cli`
|
|
197
|
+
* declares `"license": "MIT"` and the text lives one `LICENSE` up at the
|
|
198
|
+
* repository root, shared by sixty-odd packages; searching `projectDir` alone
|
|
199
|
+
* found nothing, `planOverlay` turned "nothing" into an empty overlay, and the
|
|
200
|
+
* `.deb` shipped without the `/usr/share/doc/<pkg>/copyright` that Debian
|
|
201
|
+
* Policy § 12.5 requires. Nothing downstream noticed: `dpkg-deb --info` prints
|
|
202
|
+
* a clean control file and `dpkg -i` installs it. It took the first leg that
|
|
203
|
+
* ran a real `lintian` to say so.
|
|
204
|
+
*
|
|
205
|
+
* WHERE THE SEARCH STOPS is the whole design. It climbs to the first ancestor
|
|
206
|
+
* that is a project root — one holding `.git`, or a `package.json` declaring
|
|
207
|
+
* `workspaces` — and searches that directory too, then stops. Above that line
|
|
208
|
+
* a `LICENSE` belongs to some unrelated tree that happens to be a parent
|
|
209
|
+
* directory (`~/Projekte/LICENSE` is not yours), and packaging its text would
|
|
210
|
+
* make a specific false legal claim. When NO marker is found the search is the
|
|
211
|
+
* project directory alone: without a marker there is no way to tell where the
|
|
212
|
+
* project ends, and guessing wrong is the same false claim.
|
|
213
|
+
*
|
|
214
|
+
* What this cannot check is whether the inherited text is the RIGHT licence
|
|
215
|
+
* for this package — an Apache-2.0 package inside an MIT monorepo would
|
|
216
|
+
* inherit the wrong one. Reading an SPDX identifier out of licence prose is
|
|
217
|
+
* not something to guess at, so the caller PRINTS the inherited path instead
|
|
218
|
+
* (see `commands/ship.ts`), and `gjsify.ship.licenseFile` overrides it.
|
|
219
|
+
*/
|
|
87
220
|
function discoverLicense(projectDir, configured) {
|
|
88
221
|
if (configured !== undefined) {
|
|
89
222
|
const target = resolve(projectDir, configured);
|
|
@@ -92,13 +225,51 @@ function discoverLicense(projectDir, configured) {
|
|
|
92
225
|
}
|
|
93
226
|
return target;
|
|
94
227
|
}
|
|
95
|
-
for (const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
228
|
+
for (const dir of licenseSearchDirs(projectDir)) {
|
|
229
|
+
for (const name of LICENSE_FILE_NAMES) {
|
|
230
|
+
const target = join(dir, name);
|
|
231
|
+
if (existsSync(target))
|
|
232
|
+
return target;
|
|
233
|
+
}
|
|
99
234
|
}
|
|
100
235
|
return undefined;
|
|
101
236
|
}
|
|
237
|
+
/** `projectDir` first, then each ancestor up to and including the project root. */
|
|
238
|
+
function licenseSearchDirs(projectDir) {
|
|
239
|
+
const dirs = [];
|
|
240
|
+
for (let current = projectDir;;) {
|
|
241
|
+
dirs.push(current);
|
|
242
|
+
if (isProjectRoot(current))
|
|
243
|
+
return dirs;
|
|
244
|
+
const parent = dirname(current);
|
|
245
|
+
if (parent === current)
|
|
246
|
+
return [projectDir];
|
|
247
|
+
current = parent;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Is this the top of the tree the project belongs to?
|
|
252
|
+
*
|
|
253
|
+
* `.git` is tested with `existsSync` rather than `statSync().isDirectory()`
|
|
254
|
+
* because in a git WORKTREE it is a file, and this repository is developed in
|
|
255
|
+
* worktrees.
|
|
256
|
+
*/
|
|
257
|
+
function isProjectRoot(dir) {
|
|
258
|
+
if (existsSync(join(dir, '.git')))
|
|
259
|
+
return true;
|
|
260
|
+
const manifest = join(dir, 'package.json');
|
|
261
|
+
if (!existsSync(manifest))
|
|
262
|
+
return false;
|
|
263
|
+
try {
|
|
264
|
+
return JSON.parse(readFileSync(manifest, 'utf-8')).workspaces !== undefined;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// A malformed `package.json` in some ancestor is not this command's
|
|
268
|
+
// business, and it must not turn a licence search into a ship failure.
|
|
269
|
+
// Not a root, then: the climb continues and `.git` still stops it.
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
102
273
|
/**
|
|
103
274
|
* Every regular file under `dir`, as POSIX-separated relative paths.
|
|
104
275
|
*
|
|
@@ -58,7 +58,15 @@ export const FORMATS = {
|
|
|
58
58
|
fileName: (s, archLabel) => `${s.binaryName}-${s.version}-${s.release}.${archLabel}.rpm`,
|
|
59
59
|
},
|
|
60
60
|
};
|
|
61
|
-
|
|
61
|
+
// DERIVED, never a second list. `FORMATS` is `Record<FormatId, …>`, so the
|
|
62
|
+
// compiler already refuses a `FormatId` with no descriptor — reading the keys
|
|
63
|
+
// back inherits that guarantee for free. Written out by hand this was the one
|
|
64
|
+
// unbound copy of the vocabulary: adding a format to `FormatId` and `FORMATS`
|
|
65
|
+
// compiled fine and left this list short, which would have made the new format
|
|
66
|
+
// absent from the default targets (`ship.ts` uses it as the `??` fallback),
|
|
67
|
+
// missing from `--help`, and REFUSED by `readStage` as an unknown id.
|
|
68
|
+
// Insertion order is stable for string keys, and `resolveFormats` sorts anyway.
|
|
69
|
+
export const FORMAT_IDS = Object.keys(FORMATS);
|
|
62
70
|
/** Parse `--target deb,rpm` into a sorted, deduplicated descriptor list. */
|
|
63
71
|
export function resolveFormats(raw) {
|
|
64
72
|
const names = raw
|
|
@@ -23,7 +23,7 @@ function shellQuote(value) {
|
|
|
23
23
|
export function renderLauncher(settings, bundleRelPath) {
|
|
24
24
|
const lib = `"$prefix"/lib/${settings.binaryName}`;
|
|
25
25
|
const args = settings.execArgs.map(shellQuote).join(' ');
|
|
26
|
-
|
|
26
|
+
const lines = [
|
|
27
27
|
'#!/bin/sh',
|
|
28
28
|
'# Generated by `gjsify ship` — do not edit.',
|
|
29
29
|
'set -e',
|
|
@@ -31,7 +31,30 @@ export function renderLauncher(settings, bundleRelPath) {
|
|
|
31
31
|
'prefix=$(dirname "$(dirname "$self")")',
|
|
32
32
|
'XDG_DATA_DIRS="$prefix/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"',
|
|
33
33
|
'export XDG_DATA_DIRS',
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
];
|
|
35
|
+
// Typelibs the package carries itself (see `gjsify.ship.bundledTypelibs`). PREPENDED, not
|
|
36
|
+
// replacing: an app that ships `Gwebgl` still needs the system `Gtk`, and clobbering the path
|
|
37
|
+
// would trade one missing namespace for all the others. Only emitted when something was
|
|
38
|
+
// actually staged — an empty `GI_TYPELIB_PATH` entry makes GI search a directory that is not
|
|
39
|
+
// there, on every import, forever.
|
|
40
|
+
if (settings.typelibFiles.length > 0) {
|
|
41
|
+
const giDir = '"$prefix"/lib/' + settings.binaryName + '/gi';
|
|
42
|
+
for (const variable of ['GI_TYPELIB_PATH', 'LD_LIBRARY_PATH']) {
|
|
43
|
+
// `${VAR:+:$VAR}` appends the existing value with a separator ONLY when it is set —
|
|
44
|
+
// a plain `:$VAR` leaves a trailing colon, which both loaders read as "and the current
|
|
45
|
+
// directory", and a package that silently adds `.` to a library path is a bad package.
|
|
46
|
+
lines.push(`${variable}=${giDir}\${${variable}:+:$${variable}}`, `export ${variable}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Where the app's gettext catalogues landed. `bindtextdomain` takes a DIRECTORY and there is no
|
|
50
|
+
// standard environment variable it reads on its own (`TEXTDOMAINDIR` is honoured by the gettext
|
|
51
|
+
// command-line tools, not by the library), so the prefix has to reach the app somehow. Passing
|
|
52
|
+
// it from the launcher keeps the app free of install-layout knowledge, exactly as XDG_DATA_DIRS
|
|
53
|
+
// above does for icons and schemas — a baked `/usr/share/locale` would be wrong in a Flatpak
|
|
54
|
+
// and in any `--prefix` tree.
|
|
55
|
+
if (settings.localeFiles.length > 0) {
|
|
56
|
+
lines.push('GJSIFY_LOCALE_DIR="$prefix"/share/locale', 'export GJSIFY_LOCALE_DIR');
|
|
57
|
+
}
|
|
58
|
+
lines.push(`exec gjs -m ${lib}/${bundleRelPath}${args ? ` ${args}` : ''} "$@"`, '');
|
|
59
|
+
return lines.join('\n');
|
|
37
60
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ShipMimeType } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validate the declared types, throwing on anything that would install and then not work.
|
|
4
|
+
*
|
|
5
|
+
* Each refusal is a silent failure made loud:
|
|
6
|
+
* - a malformed type name is written into the XML and ignored by `update-mime-database`;
|
|
7
|
+
* - a glob without `*.` never matches (`bauplan` matches only a file called exactly that);
|
|
8
|
+
* - a type with no glob and no parent type can never be DETECTED, so it is registered and unused;
|
|
9
|
+
* - a duplicate definition makes which comment wins depend on document order.
|
|
10
|
+
*/
|
|
11
|
+
export declare function validateMimeTypes(types: readonly ShipMimeType[]): void;
|
|
12
|
+
/**
|
|
13
|
+
* The shared-mime-info document for `types`.
|
|
14
|
+
*
|
|
15
|
+
* One file per package, named after the app id, because `share/mime/packages/` is shared: a generic
|
|
16
|
+
* name is a collision with whatever another package put there — the same reason the schema staging
|
|
17
|
+
* uses the app id.
|
|
18
|
+
*/
|
|
19
|
+
export declare function renderMimePackage(types: readonly ShipMimeType[]): string;
|