@gjsify/cli 0.39.0 → 0.41.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 +202 -181
- package/lib/cli-app.js +12 -25
- package/lib/commands/create.d.ts +3 -0
- package/lib/commands/create.js +49 -6
- package/lib/commands/flatpak/check.js +1 -1
- package/lib/commands/flatpak/ci.js +1 -1
- package/lib/commands/flatpak/diff.js +1 -1
- package/lib/commands/flatpak/init.js +1 -1
- package/lib/commands/flatpak/scaffold.d.ts +2 -4
- package/lib/commands/flatpak/scaffold.js +25 -298
- package/lib/commands/flatpak/sync-flathub.js +1 -1
- package/lib/commands/foreach.js +34 -20
- 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 +32 -1
- package/lib/commands/prune.d.ts +11 -0
- package/lib/commands/prune.js +84 -0
- package/lib/commands/self-update.d.ts +1 -0
- package/lib/commands/self-update.js +55 -0
- package/lib/commands/ship.d.ts +11 -0
- package/lib/commands/ship.js +217 -0
- package/lib/config.d.ts +12 -0
- package/lib/config.js +15 -0
- package/lib/templates/install.mjs.tmpl +8 -3
- package/lib/types/config-data.d.ts +136 -46
- package/lib/utils/app-metadata.d.ts +51 -0
- package/lib/utils/app-metadata.js +338 -0
- package/lib/utils/check-system-deps.d.ts +2 -2
- package/lib/utils/check-system-deps.js +43 -6
- package/lib/utils/cli-runtime-closure.d.ts +23 -0
- package/lib/utils/cli-runtime-closure.js +274 -0
- package/lib/utils/install-provenance.d.ts +44 -0
- package/lib/utils/install-provenance.js +106 -0
- package/lib/utils/prune-prefix.d.ts +83 -0
- package/lib/utils/prune-prefix.js +333 -0
- package/lib/utils/publish-headers.d.ts +13 -0
- package/lib/utils/publish-headers.js +30 -4
- package/lib/utils/ship/ar.d.ts +21 -0
- package/lib/utils/ship/ar.js +57 -0
- package/lib/utils/ship/bytes.d.ts +4 -0
- package/lib/utils/ship/bytes.js +23 -0
- package/lib/utils/ship/cpio.d.ts +22 -0
- package/lib/utils/ship/cpio.js +93 -0
- package/lib/utils/ship/deb.d.ts +24 -0
- package/lib/utils/ship/deb.js +118 -0
- package/lib/utils/ship/depends.d.ts +62 -0
- package/lib/utils/ship/depends.js +221 -0
- package/lib/utils/ship/discover.d.ts +21 -0
- package/lib/utils/ship/discover.js +129 -0
- package/lib/utils/ship/formats.d.ts +5 -0
- package/lib/utils/ship/formats.js +83 -0
- package/lib/utils/ship/gi-namespaces.d.ts +7 -0
- package/lib/utils/ship/gi-namespaces.js +49 -0
- package/lib/utils/ship/gzip.d.ts +2 -0
- package/lib/utils/ship/gzip.js +21 -0
- package/lib/utils/ship/launcher.d.ts +7 -0
- package/lib/utils/ship/launcher.js +37 -0
- package/lib/utils/ship/payload.d.ts +30 -0
- package/lib/utils/ship/payload.js +59 -0
- package/lib/utils/ship/plan.d.ts +34 -0
- package/lib/utils/ship/plan.js +167 -0
- package/lib/utils/ship/rpm-header.d.ts +29 -0
- package/lib/utils/ship/rpm-header.js +170 -0
- package/lib/utils/ship/rpm.d.ts +12 -0
- package/lib/utils/ship/rpm.js +377 -0
- package/lib/utils/ship/scripts.d.ts +31 -0
- package/lib/utils/ship/scripts.js +76 -0
- package/lib/utils/ship/settings.d.ts +60 -0
- package/lib/utils/ship/settings.js +184 -0
- package/lib/utils/ship/stage-writer.d.ts +17 -0
- package/lib/utils/ship/stage-writer.js +49 -0
- package/lib/utils/ship/types.d.ts +114 -0
- package/lib/utils/ship/types.js +10 -0
- package/lib/utils/ship/version.d.ts +17 -0
- package/lib/utils/ship/version.js +55 -0
- package/package.json +27 -18
- /package/lib/templates/{flatpak → app}/desktop.tmpl +0 -0
package/lib/config.js
CHANGED
|
@@ -233,6 +233,21 @@ export class Config {
|
|
|
233
233
|
excludeGlobals: config.nodeScript?.excludeGlobals ?? config.excludeGlobals,
|
|
234
234
|
};
|
|
235
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* The config for a command that is NOT a build — `gjsify flatpak …`,
|
|
238
|
+
* `gjsify ship …`.
|
|
239
|
+
*
|
|
240
|
+
* They all want exactly what the loader already produces: the merged
|
|
241
|
+
* `package.json#gjsify` + config-file object, with none of `forBuild`'s
|
|
242
|
+
* build-specific defaulting. Reaching it through `forBuild({} as never)`
|
|
243
|
+
* — five copies of that line before this method existed — lies to the type
|
|
244
|
+
* system and drags a tsconfig read and bundler defaults into a command
|
|
245
|
+
* that has no bundle.
|
|
246
|
+
*/
|
|
247
|
+
async forCommand(searchFrom = process.cwd()) {
|
|
248
|
+
const { config } = await this.load(searchFrom);
|
|
249
|
+
return config;
|
|
250
|
+
}
|
|
236
251
|
async forBuild(cliArgs) {
|
|
237
252
|
const configFile = await this.load(process.cwd());
|
|
238
253
|
const configData = { ...configFile.config };
|
|
@@ -188,9 +188,14 @@ function checkGjsVersion() {
|
|
|
188
188
|
if (major < 1 || (major === 1 && minor < 86)) {
|
|
189
189
|
error(`gjs ${major}.${minor} is too old — gjsify requires gjs 1.86 or newer.`);
|
|
190
190
|
error('Install hints:');
|
|
191
|
-
error(' Fedora 43+:
|
|
192
|
-
error('
|
|
193
|
-
|
|
191
|
+
error(' Fedora 43+: sudo dnf install gjs');
|
|
192
|
+
error(' Arch: sudo pacman -S gjs');
|
|
193
|
+
// NOT "Debian 13+". Measured 2026-08-16 against tracker.debian.org: trixie
|
|
194
|
+
// (13, stable) ships 1.82.3, forky (testing) 1.88.1, sid 1.89.2 — 1.84 and
|
|
195
|
+
// 1.86 were skipped entirely. Sending a trixie user to `apt install gjs`
|
|
196
|
+
// hands them the very version this branch just rejected, from the branch
|
|
197
|
+
// that rejected it. Same fact, same wording as `utils/ship/depends.ts`.
|
|
198
|
+
error(' Debian forky/sid: sudo apt install gjs (trixie ships 1.82.3 — too old)');
|
|
194
199
|
exit(1);
|
|
195
200
|
}
|
|
196
201
|
}
|
|
@@ -171,6 +171,13 @@ export interface ConfigData {
|
|
|
171
171
|
* defaults declaratively. CLI flags override these.
|
|
172
172
|
*/
|
|
173
173
|
flatpak?: ConfigDataFlatpak;
|
|
174
|
+
/**
|
|
175
|
+
* Config for `gjsify ship` — the installable artifacts (`.deb`, `.rpm`)
|
|
176
|
+
* built from one staged payload. Metadata left unset here falls back to
|
|
177
|
+
* `gjsify.flatpak`, because both blocks describe the SAME application
|
|
178
|
+
* (ADR 0024 § 8) and no project should have to write it twice.
|
|
179
|
+
*/
|
|
180
|
+
ship?: ConfigDataShip;
|
|
174
181
|
/**
|
|
175
182
|
* Config for `gjsify format` / `lint` / `fix`. A thin shell: oxc's own
|
|
176
183
|
* `.oxfmtrc.json` / `.oxlintrc.json` are the real configuration files.
|
|
@@ -197,52 +204,14 @@ export interface ConfigDataTest {
|
|
|
197
204
|
/** Default runtimes when `--runtime` not specified. Default: `['gjs', 'node']`. */
|
|
198
205
|
runtimes?: Array<'gjs' | 'node'>;
|
|
199
206
|
}
|
|
200
|
-
/**
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
*/
|
|
209
|
-
runtime?: 'gnome' | 'freedesktop';
|
|
210
|
-
/** Runtime/SDK version, e.g. `'50'` for GNOME or `'24.08'` for Freedesktop. */
|
|
211
|
-
runtimeVersion?: string;
|
|
212
|
-
/**
|
|
213
|
-
* Extra SDK extensions for the manifest, e.g.
|
|
214
|
-
* `['org.freedesktop.Sdk.Extension.llvm17']` for native code needing a specific
|
|
215
|
-
* toolchain. Leave empty for pure gjsify projects: the GNOME runtime already
|
|
216
|
-
* ships GJS + GLib + libsoup and `gjsify build` produces a self-contained bundle
|
|
217
|
-
* that needs no build-time Node.
|
|
218
|
-
*/
|
|
219
|
-
sdkExtensions?: string[];
|
|
220
|
-
/** Path components prepended to PATH inside the build sandbox. */
|
|
221
|
-
appendPath?: string[];
|
|
222
|
-
/** The binary name to run (`/app/bin/<command>`). Defaults to `appId`. */
|
|
223
|
-
command?: string;
|
|
224
|
-
/** Finish-args (capabilities). Default depends on `runtime` + `--cli-only`. */
|
|
225
|
-
finishArgs?: string[];
|
|
226
|
-
/** Extra Flatpak modules prepended before the app's own meson/simple module (e.g. `blueprint-compiler` build). */
|
|
227
|
-
extraModules?: unknown[];
|
|
228
|
-
/**
|
|
229
|
-
* Full replacement for the manifest's `modules` array: used verbatim, with
|
|
230
|
-
* neither `extraModules` nor the meson default added. For CLI tools that ship a
|
|
231
|
-
* pre-built bundle and install via shell commands (`buildsystem: simple`).
|
|
232
|
-
*/
|
|
233
|
-
modules?: unknown[];
|
|
234
|
-
/** Cleanup glob patterns applied to the final manifest, e.g. `['/include', '/lib/pkgconfig']`. */
|
|
235
|
-
cleanup?: string[];
|
|
236
|
-
/** Source-of-truth lockfile for `gjsify flatpak deps` — `yarn.lock` or `package-lock.json`. */
|
|
237
|
-
lockfile?: string;
|
|
238
|
-
/**
|
|
239
|
-
* GitHub-Actions container image override for `gjsify flatpak ci`. Default is
|
|
240
|
-
* derived from runtime + runtimeVersion, e.g. gnome+50 →
|
|
241
|
-
* `ghcr.io/flathub-infra/flatpak-github-actions:gnome-50`.
|
|
242
|
-
*/
|
|
243
|
-
ciContainer?: string;
|
|
244
|
-
/** Branches the generated workflow triggers on. Default `['main']`. */
|
|
245
|
-
ciBranches?: string[];
|
|
207
|
+
/**
|
|
208
|
+
* The app metadata every install format needs — AppStream MetaInfo fields plus
|
|
209
|
+
* what a freedesktop `.desktop` entry is built from. Shared on purpose: a
|
|
210
|
+
* `.deb`, an `.rpm`, a Flatpak and a macOS bundle all describe the SAME
|
|
211
|
+
* application, so this block belongs to the app rather than to whichever packer
|
|
212
|
+
* happens to read it (ADR 0024 § 8).
|
|
213
|
+
*/
|
|
214
|
+
export interface AppMetadata {
|
|
246
215
|
/**
|
|
247
216
|
* `'app'` (default) → desktop-application MetaInfo, GUI finish-args, .desktop +
|
|
248
217
|
* icon required. `'cli'` → console-application MetaInfo with
|
|
@@ -394,6 +363,127 @@ export interface ConfigDataFlatpak {
|
|
|
394
363
|
displayLengthMin?: number;
|
|
395
364
|
controls?: Array<'keyboard' | 'pointing' | 'touch' | 'gamepad' | 'tablet' | 'console'>;
|
|
396
365
|
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* `gjsify ship` configuration — the packaging half of {@link AppMetadata}.
|
|
369
|
+
*
|
|
370
|
+
* Everything here has a derived default; a project that already has a
|
|
371
|
+
* `gjsify.flatpak` block usually needs no `gjsify.ship` block at all.
|
|
372
|
+
*/
|
|
373
|
+
export interface ConfigDataShip extends AppMetadata {
|
|
374
|
+
/** Reverse-DNS app id. Falls back to `gjsify.flatpak.appId`, then `package.json#name`. */
|
|
375
|
+
appId?: string;
|
|
376
|
+
/**
|
|
377
|
+
* Package name and `bin/` entry. Default: `package.json#name` with the npm
|
|
378
|
+
* scope stripped, lowercased, non-alphanumerics folded to `-`.
|
|
379
|
+
*/
|
|
380
|
+
binaryName?: string;
|
|
381
|
+
/** Override the upstream version. Default: `package.json#version`, normalised. */
|
|
382
|
+
version?: string;
|
|
383
|
+
/** Package revision within one upstream version (deb revision / rpm release). Default `'1'`. */
|
|
384
|
+
release?: string;
|
|
385
|
+
/** `Maintainer:` / `Packager:` as `Name <email>`. Default: `package.json#author`. */
|
|
386
|
+
maintainer?: string;
|
|
387
|
+
/** Formats to build when `--target` is not given. Default `['deb', 'rpm']`. */
|
|
388
|
+
targets?: string[];
|
|
389
|
+
/** Output root, relative to the project. Default `'ship'`. */
|
|
390
|
+
outDir?: string;
|
|
391
|
+
/**
|
|
392
|
+
* The built bundle `bin/<name>` executes. Default: `gjsify.main`, then
|
|
393
|
+
* `package.json#main`. Its whole directory is staged into `lib/<name>/`.
|
|
394
|
+
*/
|
|
395
|
+
bundle?: string;
|
|
396
|
+
/** Icon file, or a directory of them. Sizes are read from the path or the filename. */
|
|
397
|
+
icon?: string;
|
|
398
|
+
/** A `*.gschema.xml` file, or a directory of them. */
|
|
399
|
+
schemas?: string;
|
|
400
|
+
/** Licence file. Default: the first of LICENSE, LICENSE.md, LICENSE.txt, COPYING. */
|
|
401
|
+
licenseFile?: string;
|
|
402
|
+
/** deb `Section:`. Default: derived from `categories`. */
|
|
403
|
+
section?: string;
|
|
404
|
+
/** rpm `Group:`. Default: derived from `categories`. */
|
|
405
|
+
group?: string;
|
|
406
|
+
/**
|
|
407
|
+
* Minimum GJS the emitted dependency asks for. Default `'1.86'` — what the
|
|
408
|
+
* bundler targets. Lower it only if the bundle genuinely runs on an older
|
|
409
|
+
* GJS: no released Debian ships 1.86, so the default makes a `.deb` that
|
|
410
|
+
* apt refuses on trixie rather than one that installs and then fails to
|
|
411
|
+
* start.
|
|
412
|
+
*/
|
|
413
|
+
minGjsVersion?: string;
|
|
414
|
+
/**
|
|
415
|
+
* Runtime dependencies appended to the set derived from the bundle's
|
|
416
|
+
* `gi://` imports — the escape hatch for a namespace the table does not
|
|
417
|
+
* know, and for anything that is not a typelib at all.
|
|
418
|
+
*/
|
|
419
|
+
depends?: {
|
|
420
|
+
deb?: string[];
|
|
421
|
+
rpm?: string[];
|
|
422
|
+
};
|
|
423
|
+
/**
|
|
424
|
+
* GI namespace → the package that ships its typelib, filling gaps in the
|
|
425
|
+
* built-in table (`Nautilus-3.0`, a namespace from a private library, …).
|
|
426
|
+
* A row here wins over the built-in one.
|
|
427
|
+
*
|
|
428
|
+
* This is what unblocks a namespace `gjsify ship` does not know; `depends`
|
|
429
|
+
* is for dependencies that are not typelibs at all and deliberately does
|
|
430
|
+
* not silence the unmapped-namespace failure.
|
|
431
|
+
*/
|
|
432
|
+
typelibPackages?: Record<string, {
|
|
433
|
+
deb: string;
|
|
434
|
+
rpm: string;
|
|
435
|
+
}>;
|
|
436
|
+
/** Extra payload entries: prefix-relative destination → project-relative source. */
|
|
437
|
+
extraFiles?: Record<string, string>;
|
|
438
|
+
/** Arguments the launcher appends before the user's own. */
|
|
439
|
+
execArgs?: string[];
|
|
440
|
+
}
|
|
441
|
+
/** Flatpak-toolchain config for the `gjsify flatpak` subcommand group. */
|
|
442
|
+
export interface ConfigDataFlatpak extends AppMetadata {
|
|
443
|
+
/** Reverse-DNS app id, e.g. `eu.jumplink.Learn6502`. Defaults to `package.json#name` if it looks like a reverse-DNS id. */
|
|
444
|
+
appId?: string;
|
|
445
|
+
/**
|
|
446
|
+
* Runtime family. Default `'gnome'` — needed at runtime by GJS bundles
|
|
447
|
+
* for GLib/GObject/GIO. `'freedesktop'` is only suitable for non-gjsify
|
|
448
|
+
* CLI tools (no GJS interpreter ships in the Freedesktop runtime).
|
|
449
|
+
*/
|
|
450
|
+
runtime?: 'gnome' | 'freedesktop';
|
|
451
|
+
/** Runtime/SDK version, e.g. `'50'` for GNOME or `'24.08'` for Freedesktop. */
|
|
452
|
+
runtimeVersion?: string;
|
|
453
|
+
/**
|
|
454
|
+
* Extra SDK extensions for the manifest, e.g.
|
|
455
|
+
* `['org.freedesktop.Sdk.Extension.llvm17']` for native code needing a specific
|
|
456
|
+
* toolchain. Leave empty for pure gjsify projects: the GNOME runtime already
|
|
457
|
+
* ships GJS + GLib + libsoup and `gjsify build` produces a self-contained bundle
|
|
458
|
+
* that needs no build-time Node.
|
|
459
|
+
*/
|
|
460
|
+
sdkExtensions?: string[];
|
|
461
|
+
/** Path components prepended to PATH inside the build sandbox. */
|
|
462
|
+
appendPath?: string[];
|
|
463
|
+
/** The binary name to run (`/app/bin/<command>`). Defaults to `appId`. */
|
|
464
|
+
command?: string;
|
|
465
|
+
/** Finish-args (capabilities). Default depends on `runtime` + `--cli-only`. */
|
|
466
|
+
finishArgs?: string[];
|
|
467
|
+
/** Extra Flatpak modules prepended before the app's own meson/simple module (e.g. `blueprint-compiler` build). */
|
|
468
|
+
extraModules?: unknown[];
|
|
469
|
+
/**
|
|
470
|
+
* Full replacement for the manifest's `modules` array: used verbatim, with
|
|
471
|
+
* neither `extraModules` nor the meson default added. For CLI tools that ship a
|
|
472
|
+
* pre-built bundle and install via shell commands (`buildsystem: simple`).
|
|
473
|
+
*/
|
|
474
|
+
modules?: unknown[];
|
|
475
|
+
/** Cleanup glob patterns applied to the final manifest, e.g. `['/include', '/lib/pkgconfig']`. */
|
|
476
|
+
cleanup?: string[];
|
|
477
|
+
/** Source-of-truth lockfile for `gjsify flatpak deps` — `yarn.lock` or `package-lock.json`. */
|
|
478
|
+
lockfile?: string;
|
|
479
|
+
/**
|
|
480
|
+
* GitHub-Actions container image override for `gjsify flatpak ci`. Default is
|
|
481
|
+
* derived from runtime + runtimeVersion, e.g. gnome+50 →
|
|
482
|
+
* `ghcr.io/flathub-infra/flatpak-github-actions:gnome-50`.
|
|
483
|
+
*/
|
|
484
|
+
ciContainer?: string;
|
|
485
|
+
/** Branches the generated workflow triggers on. Default `['main']`. */
|
|
486
|
+
ciBranches?: string[];
|
|
397
487
|
/**
|
|
398
488
|
* Flathub tracking-repo override for `gjsify flatpak sync-flathub` / `diff`.
|
|
399
489
|
* Default `flathub/<appId>`; set when the upstream repo deviates.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AppMetadata } from '../types/config-data.js';
|
|
2
|
+
/** Everything a renderer needs that is not part of {@link AppMetadata} itself. */
|
|
3
|
+
export interface AppMetadataInputs {
|
|
4
|
+
/** Reverse-DNS application id — the MetaInfo `<id>` and the icon/desktop basename. */
|
|
5
|
+
appId: string;
|
|
6
|
+
/** Human-readable display name. */
|
|
7
|
+
name: string;
|
|
8
|
+
/** The binary to launch (`Exec=`, and the default `<provides><binary>`). */
|
|
9
|
+
command: string;
|
|
10
|
+
/** `'app'` renders a desktop-application component, `'cli'` a console one. */
|
|
11
|
+
kind: 'app' | 'cli';
|
|
12
|
+
metadata: AppMetadata;
|
|
13
|
+
/**
|
|
14
|
+
* Config path the validator names in its hints, e.g. `gjsify.flatpak` or
|
|
15
|
+
* `gjsify.ship`. Both read the same fields, so the hint has to say which
|
|
16
|
+
* block the caller was actually reading.
|
|
17
|
+
*/
|
|
18
|
+
configKey: string;
|
|
19
|
+
/**
|
|
20
|
+
* Year for the copyright comment. Passed in rather than read from the
|
|
21
|
+
* clock: this XML is PAYLOAD, so `new Date()` here makes the artifact's
|
|
22
|
+
* bytes depend on when it was packed — a reproducibility hole no test
|
|
23
|
+
* running inside one second can see. Defaults to the current year for
|
|
24
|
+
* callers that write the file to a source tree rather than into a package.
|
|
25
|
+
*/
|
|
26
|
+
copyrightYear?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface MissingFieldError {
|
|
29
|
+
field: string;
|
|
30
|
+
hint: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validate that the metadata has the minimum set of fields AppStream needs.
|
|
34
|
+
* Returns the missing fields with actionable hints; an empty list means OK.
|
|
35
|
+
*/
|
|
36
|
+
export declare function validateAppMetadata(inputs: AppMetadataInputs): MissingFieldError[];
|
|
37
|
+
/** Render the MetaInfo XML for a desktop application. */
|
|
38
|
+
export declare function renderMetainfoApp(inputs: AppMetadataInputs): string;
|
|
39
|
+
/** Render the MetaInfo XML for a console application. */
|
|
40
|
+
export declare function renderMetainfoCli(inputs: AppMetadataInputs): string;
|
|
41
|
+
/**
|
|
42
|
+
* Render the `.desktop` entry (app kind only).
|
|
43
|
+
*
|
|
44
|
+
* `Exec=` carries a field code when the app declares MIME types, because a
|
|
45
|
+
* desktop entry with `MimeType=` and no `%f`/`%u` is registered as a handler
|
|
46
|
+
* that is then launched with no argument — the file the user double-clicked is
|
|
47
|
+
* dropped on the floor, and `desktop-file-validate` says so. `x-scheme-handler/*`
|
|
48
|
+
* wins over file types: a URL handler must take `%u`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function renderDesktopEntry(inputs: AppMetadataInputs): string;
|
|
51
|
+
export declare function escapeXml(value: string): string;
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// AppStream MetaInfo XML + freedesktop `.desktop` rendering, shared by every
|
|
2
|
+
// install format.
|
|
3
|
+
//
|
|
4
|
+
// These files describe the APPLICATION, not the container it ships in: a
|
|
5
|
+
// `.deb`, an `.rpm`, a Flatpak and (later) a macOS bundle all want the same
|
|
6
|
+
// `<name>`, `<summary>`, categories, keywords and icon name. They lived under
|
|
7
|
+
// `commands/flatpak/` while Flatpak was the only packer; ADR 0024 § 8 moves
|
|
8
|
+
// them out so `gjsify ship` reads them without importing another command.
|
|
9
|
+
//
|
|
10
|
+
// The MetaInfo XML is built line by line in TypeScript rather than substituted
|
|
11
|
+
// into a template: the AppStream surface (description blocks, per-release rich
|
|
12
|
+
// notes, translator hints, kudos, supports/requires/recommends, content_rating
|
|
13
|
+
// attributes, provides) has too many optional nested sections for a
|
|
14
|
+
// template+placeholder approach to stay legible. The `.desktop` entry stays
|
|
15
|
+
// template-based — it is flat `key=value`.
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
/**
|
|
18
|
+
* Lazy template loader. `static-read-inliner` matches this exact shape
|
|
19
|
+
* (`readFileSync(new URL(<literal>, import.meta.url), 'utf-8')`) and inlines
|
|
20
|
+
* the template into the GJS bundle at build time — a `join(__dirname, …)`
|
|
21
|
+
* spelling silently loses that and ENOENTs under GJS.
|
|
22
|
+
*/
|
|
23
|
+
function loadDesktopTemplate() {
|
|
24
|
+
return readFileSync(new URL('../templates/app/desktop.tmpl', import.meta.url), 'utf-8');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate that the metadata has the minimum set of fields AppStream needs.
|
|
28
|
+
* Returns the missing fields with actionable hints; an empty list means OK.
|
|
29
|
+
*/
|
|
30
|
+
export function validateAppMetadata(inputs) {
|
|
31
|
+
const m = inputs.metadata;
|
|
32
|
+
const key = inputs.configKey;
|
|
33
|
+
const missing = [];
|
|
34
|
+
if (!m.developer?.id || !m.developer?.name) {
|
|
35
|
+
missing.push({
|
|
36
|
+
field: `${key}.developer`,
|
|
37
|
+
hint: `Set \`${key}.developer = { "id": "io.github.you", "name": "Your Name" }\` in package.json. The id is reverse-DNS.`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (!m.summary) {
|
|
41
|
+
missing.push({
|
|
42
|
+
field: `${key}.summary`,
|
|
43
|
+
hint: 'One-line app summary, ≤80 chars, no trailing period. Example: "Learn 6502 assembly language".',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (!m.description) {
|
|
47
|
+
missing.push({
|
|
48
|
+
field: `${key}.description`,
|
|
49
|
+
hint: 'Plain text (split on blank lines) or DescriptionBlock[] for rich content with bullet lists + translator hints.',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (!m.license?.project) {
|
|
53
|
+
missing.push({
|
|
54
|
+
field: `${key}.license.project`,
|
|
55
|
+
hint: 'SPDX identifier of the project license, e.g. "MIT", "GPL-3.0-or-later".',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (!m.homepageUrl) {
|
|
59
|
+
missing.push({
|
|
60
|
+
field: `${key}.homepageUrl`,
|
|
61
|
+
hint: 'Required by Flathub. Example: "https://github.com/you/your-repo".',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return missing;
|
|
65
|
+
}
|
|
66
|
+
/** Render the MetaInfo XML for a desktop application. */
|
|
67
|
+
export function renderMetainfoApp(inputs) {
|
|
68
|
+
return renderMetainfo(inputs, 'desktop-application');
|
|
69
|
+
}
|
|
70
|
+
/** Render the MetaInfo XML for a console application. */
|
|
71
|
+
export function renderMetainfoCli(inputs) {
|
|
72
|
+
return renderMetainfo(inputs, 'console-application');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Render the `.desktop` entry (app kind only).
|
|
76
|
+
*
|
|
77
|
+
* `Exec=` carries a field code when the app declares MIME types, because a
|
|
78
|
+
* desktop entry with `MimeType=` and no `%f`/`%u` is registered as a handler
|
|
79
|
+
* that is then launched with no argument — the file the user double-clicked is
|
|
80
|
+
* dropped on the floor, and `desktop-file-validate` says so. `x-scheme-handler/*`
|
|
81
|
+
* wins over file types: a URL handler must take `%u`.
|
|
82
|
+
*/
|
|
83
|
+
export function renderDesktopEntry(inputs) {
|
|
84
|
+
const m = inputs.metadata;
|
|
85
|
+
const categoriesLine = (m.categories ?? ['Utility']).join(';') + ';';
|
|
86
|
+
const keywordsLine = m.keywords?.length ? `Keywords=${m.keywords.join(';')};\n` : '';
|
|
87
|
+
// `MimeType=` is sourced from `<config>.provides.mimetypes` — the same field
|
|
88
|
+
// already populates `<mediatype>` entries in the MetaInfo XML, so callers
|
|
89
|
+
// configure both with one knob. Typical entries: `x-scheme-handler/<scheme>`
|
|
90
|
+
// for URL-scheme handlers, or `application/<mime>` for file-type handlers.
|
|
91
|
+
const mimetypes = m.provides?.mimetypes ?? [];
|
|
92
|
+
const mimetypesLine = mimetypes.length ? `MimeType=${mimetypes.join(';')};\n` : '';
|
|
93
|
+
return substitute(loadDesktopTemplate(), {
|
|
94
|
+
NAME: inputs.name,
|
|
95
|
+
SUMMARY: m.summary ?? inputs.name,
|
|
96
|
+
COMMAND: `${inputs.command}${execFieldCode(mimetypes)}`,
|
|
97
|
+
APP_ID: inputs.appId,
|
|
98
|
+
CATEGORIES_LINE: categoriesLine,
|
|
99
|
+
KEYWORDS_LINE: keywordsLine,
|
|
100
|
+
MIMETYPES_LINE: mimetypesLine,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** The `Exec=` field code implied by the declared MIME types. */
|
|
104
|
+
function execFieldCode(mimetypes) {
|
|
105
|
+
if (mimetypes.some((t) => t.startsWith('x-scheme-handler/')))
|
|
106
|
+
return ' %u';
|
|
107
|
+
if (mimetypes.length > 0)
|
|
108
|
+
return ' %f';
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
// ─── MetaInfo XML builder ────────────────────────────────────────────────
|
|
112
|
+
function renderMetainfo(inputs, kind) {
|
|
113
|
+
const m = inputs.metadata;
|
|
114
|
+
const year = inputs.copyrightYear ?? new Date().getFullYear();
|
|
115
|
+
const developerName = m.developer?.name ?? '';
|
|
116
|
+
const lines = [];
|
|
117
|
+
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
118
|
+
lines.push(`<!-- Copyright ${year} ${escapeXml(developerName)} -->`);
|
|
119
|
+
lines.push(`<component type="${kind}">`);
|
|
120
|
+
lines.push(` <id>${escapeXml(inputs.appId)}</id>`);
|
|
121
|
+
lines.push(` <metadata_license>${escapeXml(m.license?.metadata ?? 'CC0-1.0')}</metadata_license>`);
|
|
122
|
+
lines.push(` <project_license>${escapeXml(m.license?.project ?? '')}</project_license>`);
|
|
123
|
+
lines.push(` <name>${escapeXml(inputs.name)}</name>`);
|
|
124
|
+
pushTranslatorHint(lines, m.summaryTranslatorHint, ' ');
|
|
125
|
+
lines.push(` <summary>${escapeXml(m.summary ?? inputs.name)}</summary>`);
|
|
126
|
+
if (m.iconRemote) {
|
|
127
|
+
lines.push(` <icon type="remote">${escapeXml(m.iconRemote)}</icon>`);
|
|
128
|
+
}
|
|
129
|
+
// <description>
|
|
130
|
+
lines.push(' <description>');
|
|
131
|
+
for (const blockLine of renderDescriptionBlocks(m.description ?? '', ' ')) {
|
|
132
|
+
lines.push(blockLine);
|
|
133
|
+
}
|
|
134
|
+
lines.push(' </description>');
|
|
135
|
+
// <developer>
|
|
136
|
+
if (m.developer?.id && m.developer?.name) {
|
|
137
|
+
lines.push(` <developer id="${escapeXml(m.developer.id)}">`);
|
|
138
|
+
const translateAttr = m.developer.nameTranslatable === true ? '' : ' translate="no"';
|
|
139
|
+
lines.push(` <name${translateAttr}>${escapeXml(m.developer.name)}</name>`);
|
|
140
|
+
if (m.developer.email) {
|
|
141
|
+
lines.push(` <email>${escapeXml(m.developer.email)}</email>`);
|
|
142
|
+
}
|
|
143
|
+
lines.push(' </developer>');
|
|
144
|
+
}
|
|
145
|
+
if (kind === 'desktop-application') {
|
|
146
|
+
lines.push(` <launchable type="desktop-id">${escapeXml(inputs.appId)}.desktop</launchable>`);
|
|
147
|
+
}
|
|
148
|
+
// <screenshots>
|
|
149
|
+
if (m.screenshots?.length) {
|
|
150
|
+
lines.push(' <screenshots>');
|
|
151
|
+
m.screenshots.forEach((s, i) => {
|
|
152
|
+
const type = s.type ?? (i === 0 ? 'default' : undefined);
|
|
153
|
+
const typeAttr = type ? ` type="${escapeXml(type)}"` : '';
|
|
154
|
+
const envAttr = s.environment ? ` environment="${escapeXml(s.environment)}"` : '';
|
|
155
|
+
lines.push(` <screenshot${typeAttr}${envAttr}>`);
|
|
156
|
+
lines.push(` <image>${escapeXml(s.url)}</image>`);
|
|
157
|
+
if (s.caption) {
|
|
158
|
+
pushTranslatorHint(lines, s.captionTranslatorHint, ' ');
|
|
159
|
+
lines.push(` <caption>${escapeXml(s.caption)}</caption>`);
|
|
160
|
+
}
|
|
161
|
+
lines.push(' </screenshot>');
|
|
162
|
+
});
|
|
163
|
+
lines.push(' </screenshots>');
|
|
164
|
+
}
|
|
165
|
+
// <url> entries
|
|
166
|
+
if (m.homepageUrl)
|
|
167
|
+
lines.push(` <url type="homepage">${escapeXml(m.homepageUrl)}</url>`);
|
|
168
|
+
if (m.bugtrackerUrl)
|
|
169
|
+
lines.push(` <url type="bugtracker">${escapeXml(m.bugtrackerUrl)}</url>`);
|
|
170
|
+
if (m.vcsBrowserUrl)
|
|
171
|
+
lines.push(` <url type="vcs-browser">${escapeXml(m.vcsBrowserUrl)}</url>`);
|
|
172
|
+
if (m.donationUrl)
|
|
173
|
+
lines.push(` <url type="donation">${escapeXml(m.donationUrl)}</url>`);
|
|
174
|
+
if (m.translateUrl)
|
|
175
|
+
lines.push(` <url type="translate">${escapeXml(m.translateUrl)}</url>`);
|
|
176
|
+
// <content_rating>
|
|
177
|
+
const cr = normaliseContentRating(m.contentRating);
|
|
178
|
+
if (cr.attributes && Object.keys(cr.attributes).length > 0) {
|
|
179
|
+
lines.push(` <content_rating type="${escapeXml(cr.type)}">`);
|
|
180
|
+
for (const [key, value] of Object.entries(cr.attributes)) {
|
|
181
|
+
lines.push(` <content_attribute id="${escapeXml(key)}">${escapeXml(value)}</content_attribute>`);
|
|
182
|
+
}
|
|
183
|
+
lines.push(' </content_rating>');
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
lines.push(` <content_rating type="${escapeXml(cr.type)}" />`);
|
|
187
|
+
}
|
|
188
|
+
// <releases>
|
|
189
|
+
if (m.releases?.length) {
|
|
190
|
+
lines.push(' <releases>');
|
|
191
|
+
for (const r of m.releases) {
|
|
192
|
+
if (r.description === undefined) {
|
|
193
|
+
lines.push(` <release version="${escapeXml(r.version)}" date="${escapeXml(r.date)}" />`);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
lines.push(` <release version="${escapeXml(r.version)}" date="${escapeXml(r.date)}">`);
|
|
197
|
+
lines.push(' <description>');
|
|
198
|
+
for (const blockLine of renderDescriptionBlocks(r.description, ' ')) {
|
|
199
|
+
lines.push(blockLine);
|
|
200
|
+
}
|
|
201
|
+
lines.push(' </description>');
|
|
202
|
+
lines.push(' </release>');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
lines.push(' </releases>');
|
|
206
|
+
}
|
|
207
|
+
// <categories>
|
|
208
|
+
if (m.categories?.length) {
|
|
209
|
+
lines.push(' <categories>');
|
|
210
|
+
for (const c of m.categories)
|
|
211
|
+
lines.push(` <category>${escapeXml(c)}</category>`);
|
|
212
|
+
lines.push(' </categories>');
|
|
213
|
+
}
|
|
214
|
+
// <keywords>
|
|
215
|
+
if (m.keywords?.length) {
|
|
216
|
+
lines.push(' <keywords>');
|
|
217
|
+
for (const k of m.keywords)
|
|
218
|
+
lines.push(` <keyword>${escapeXml(k)}</keyword>`);
|
|
219
|
+
lines.push(' </keywords>');
|
|
220
|
+
}
|
|
221
|
+
// <branding> (apps only — Flathub ignores it on CLI)
|
|
222
|
+
if (kind === 'desktop-application' && m.branding) {
|
|
223
|
+
lines.push(' <branding>');
|
|
224
|
+
lines.push(` <color type="primary" scheme_preference="light">${escapeXml(m.branding.accentLight)}</color>`);
|
|
225
|
+
lines.push(` <color type="primary" scheme_preference="dark">${escapeXml(m.branding.accentDark)}</color>`);
|
|
226
|
+
lines.push(' </branding>');
|
|
227
|
+
}
|
|
228
|
+
// <kudos>
|
|
229
|
+
if (m.kudos?.length) {
|
|
230
|
+
lines.push(' <kudos>');
|
|
231
|
+
for (const k of m.kudos)
|
|
232
|
+
lines.push(` <kudo>${escapeXml(k)}</kudo>`);
|
|
233
|
+
lines.push(' </kudos>');
|
|
234
|
+
}
|
|
235
|
+
// <provides> — always emit <binary> for both kinds; <mediatype>/<dbus> only when configured
|
|
236
|
+
const binaries = m.provides?.binaries ?? [inputs.command];
|
|
237
|
+
const mimetypes = m.provides?.mimetypes ?? [];
|
|
238
|
+
const dbus = m.provides?.dbus ?? [];
|
|
239
|
+
if (binaries.length || mimetypes.length || dbus.length) {
|
|
240
|
+
lines.push(' <provides>');
|
|
241
|
+
for (const b of binaries)
|
|
242
|
+
lines.push(` <binary>${escapeXml(b)}</binary>`);
|
|
243
|
+
for (const t of mimetypes)
|
|
244
|
+
lines.push(` <mediatype>${escapeXml(t)}</mediatype>`);
|
|
245
|
+
for (const d of dbus)
|
|
246
|
+
lines.push(` <dbus type="${escapeXml(d.type)}">${escapeXml(d.id)}</dbus>`);
|
|
247
|
+
lines.push(' </provides>');
|
|
248
|
+
}
|
|
249
|
+
// <supports>
|
|
250
|
+
if (m.supports?.controls?.length || m.supports?.internet) {
|
|
251
|
+
lines.push(' <supports>');
|
|
252
|
+
for (const c of m.supports.controls ?? [])
|
|
253
|
+
lines.push(` <control>${escapeXml(c)}</control>`);
|
|
254
|
+
if (m.supports.internet)
|
|
255
|
+
lines.push(` <internet>${escapeXml(m.supports.internet)}</internet>`);
|
|
256
|
+
lines.push(' </supports>');
|
|
257
|
+
}
|
|
258
|
+
// <requires>
|
|
259
|
+
if (m.requires?.displayLengthMin || m.requires?.controls?.length || m.requires?.internet) {
|
|
260
|
+
lines.push(' <requires>');
|
|
261
|
+
if (m.requires.displayLengthMin) {
|
|
262
|
+
lines.push(` <display_length compare="ge">${m.requires.displayLengthMin}</display_length>`);
|
|
263
|
+
}
|
|
264
|
+
for (const c of m.requires.controls ?? [])
|
|
265
|
+
lines.push(` <control>${escapeXml(c)}</control>`);
|
|
266
|
+
if (m.requires.internet)
|
|
267
|
+
lines.push(` <internet>${escapeXml(m.requires.internet)}</internet>`);
|
|
268
|
+
lines.push(' </requires>');
|
|
269
|
+
}
|
|
270
|
+
// <recommends>
|
|
271
|
+
if (m.recommends?.displayLengthMin || m.recommends?.controls?.length) {
|
|
272
|
+
lines.push(' <recommends>');
|
|
273
|
+
if (m.recommends.displayLengthMin) {
|
|
274
|
+
lines.push(` <display_length compare="ge">${m.recommends.displayLengthMin}</display_length>`);
|
|
275
|
+
}
|
|
276
|
+
for (const c of m.recommends.controls ?? [])
|
|
277
|
+
lines.push(` <control>${escapeXml(c)}</control>`);
|
|
278
|
+
lines.push(' </recommends>');
|
|
279
|
+
}
|
|
280
|
+
lines.push('</component>');
|
|
281
|
+
return lines.join('\n') + '\n';
|
|
282
|
+
}
|
|
283
|
+
// ─── Description block renderer ──────────────────────────────────────────
|
|
284
|
+
function renderDescriptionBlocks(description, indent) {
|
|
285
|
+
const blocks = typeof description === 'string' ? stringToBlocks(description) : description;
|
|
286
|
+
const out = [];
|
|
287
|
+
for (const block of blocks) {
|
|
288
|
+
if ('p' in block) {
|
|
289
|
+
pushTranslatorHint(out, block.translatorHint, indent);
|
|
290
|
+
out.push(`${indent}<p>${escapeXml(block.p.trim().replace(/\s+/g, ' '))}</p>`);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
pushTranslatorHint(out, block.translatorHint, indent);
|
|
294
|
+
out.push(`${indent}<ul>`);
|
|
295
|
+
for (const item of block.ul) {
|
|
296
|
+
if (typeof item === 'string') {
|
|
297
|
+
out.push(`${indent} <li>${escapeXml(item)}</li>`);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
pushTranslatorHint(out, item.translatorHint, `${indent} `);
|
|
301
|
+
out.push(`${indent} <li>${escapeXml(item.item)}</li>`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
out.push(`${indent}</ul>`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
/** Auto-convert blank-line-split string into paragraph blocks. */
|
|
310
|
+
function stringToBlocks(s) {
|
|
311
|
+
return s
|
|
312
|
+
.trim()
|
|
313
|
+
.split(/\n\n+/)
|
|
314
|
+
.map((para) => ({ p: para.trim().replace(/\s+/g, ' ') }));
|
|
315
|
+
}
|
|
316
|
+
function pushTranslatorHint(out, hint, indent) {
|
|
317
|
+
if (!hint)
|
|
318
|
+
return;
|
|
319
|
+
out.push(`${indent}<!-- TRANSLATORS: ${hint} -->`);
|
|
320
|
+
}
|
|
321
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
322
|
+
function normaliseContentRating(cr) {
|
|
323
|
+
if (cr === undefined)
|
|
324
|
+
return { type: 'oars-1.1' };
|
|
325
|
+
if (typeof cr === 'string')
|
|
326
|
+
return { type: cr };
|
|
327
|
+
return { type: cr.type ?? 'oars-1.1', attributes: cr.attributes };
|
|
328
|
+
}
|
|
329
|
+
function substitute(template, tokens) {
|
|
330
|
+
let out = template;
|
|
331
|
+
for (const [key, value] of Object.entries(tokens)) {
|
|
332
|
+
out = out.split(`{{${key}}}`).join(value);
|
|
333
|
+
}
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
export function escapeXml(value) {
|
|
337
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
338
|
+
}
|
|
@@ -126,8 +126,8 @@ export declare const PACKAGE_DEPS: Record<string, string[]>;
|
|
|
126
126
|
*/
|
|
127
127
|
export declare function runAllChecks(cwd: string): DepCheck[];
|
|
128
128
|
/**
|
|
129
|
-
* Minimal checks needed to run any GJS example (Node
|
|
130
|
-
* Used by `gjsify showcase` for examples that have no native deps.
|
|
129
|
+
* Minimal checks needed to run any GJS example (the GJS binary; Node is reported but
|
|
130
|
+
* not required). Used by `gjsify showcase` for examples that have no native deps.
|
|
131
131
|
*/
|
|
132
132
|
export declare function runMinimalChecks(): DepCheck[];
|
|
133
133
|
/**
|