@gjsify/cli 0.44.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/cli.gjs.mjs +192 -165
  2. package/lib/commands/gettext.d.ts +2 -1
  3. package/lib/commands/gettext.js +105 -47
  4. package/lib/commands/onboard.d.ts +7 -0
  5. package/lib/commands/onboard.js +349 -96
  6. package/lib/commands/ship.d.ts +3 -0
  7. package/lib/commands/ship.js +833 -54
  8. package/lib/commands/trust.d.ts +6 -0
  9. package/lib/commands/trust.js +1 -1
  10. package/lib/types/config-data.d.ts +63 -1
  11. package/lib/utils/msgfmt-merge.d.ts +35 -0
  12. package/lib/utils/msgfmt-merge.js +102 -0
  13. package/lib/utils/npm-otp.d.ts +27 -2
  14. package/lib/utils/npm-otp.js +82 -7
  15. package/lib/utils/onboard-discovery.d.ts +68 -0
  16. package/lib/utils/onboard-discovery.js +178 -0
  17. package/lib/utils/onboard-probe.d.ts +118 -1
  18. package/lib/utils/onboard-probe.js +153 -4
  19. package/lib/utils/prompt-output.d.ts +12 -0
  20. package/lib/utils/prompt-output.js +54 -0
  21. package/lib/utils/prompt.js +22 -2
  22. package/lib/utils/publishable-packages.d.ts +1 -1
  23. package/lib/utils/publishable-packages.js +1 -0
  24. package/lib/utils/resolve-npm-package.d.ts +56 -0
  25. package/lib/utils/resolve-npm-package.js +69 -0
  26. package/lib/utils/ship/app-runtime.d.ts +214 -0
  27. package/lib/utils/ship/app-runtime.js +429 -0
  28. package/lib/utils/ship/depends.d.ts +50 -0
  29. package/lib/utils/ship/depends.js +117 -3
  30. package/lib/utils/ship/discover.js +18 -1
  31. package/lib/utils/ship/dmg.d.ts +71 -0
  32. package/lib/utils/ship/dmg.js +181 -0
  33. package/lib/utils/ship/flatpak.js +1 -1
  34. package/lib/utils/ship/formats.d.ts +85 -7
  35. package/lib/utils/ship/formats.js +734 -15
  36. package/lib/utils/ship/launcher.d.ts +13 -3
  37. package/lib/utils/ship/launcher.js +362 -17
  38. package/lib/utils/ship/layout.d.ts +268 -0
  39. package/lib/utils/ship/layout.js +351 -0
  40. package/lib/utils/ship/localize-metadata.d.ts +22 -0
  41. package/lib/utils/ship/localize-metadata.js +126 -0
  42. package/lib/utils/ship/mime.js +1 -9
  43. package/lib/utils/ship/msi.d.ts +126 -0
  44. package/lib/utils/ship/msi.js +417 -0
  45. package/lib/utils/ship/node-runtime.d.ts +71 -0
  46. package/lib/utils/ship/node-runtime.js +122 -0
  47. package/lib/utils/ship/payload.d.ts +156 -5
  48. package/lib/utils/ship/payload.js +580 -14
  49. package/lib/utils/ship/plan.d.ts +21 -0
  50. package/lib/utils/ship/plan.js +20 -7
  51. package/lib/utils/ship/plist.d.ts +58 -0
  52. package/lib/utils/ship/plist.js +198 -0
  53. package/lib/utils/ship/rpm.js +9 -3
  54. package/lib/utils/ship/schemas.d.ts +61 -0
  55. package/lib/utils/ship/schemas.js +118 -0
  56. package/lib/utils/ship/scripts.js +7 -3
  57. package/lib/utils/ship/settings.d.ts +19 -1
  58. package/lib/utils/ship/settings.js +12 -1
  59. package/lib/utils/ship/share-dirs.d.ts +23 -0
  60. package/lib/utils/ship/share-dirs.js +46 -0
  61. package/lib/utils/ship/signing.d.ts +195 -0
  62. package/lib/utils/ship/signing.js +317 -0
  63. package/lib/utils/ship/stage-manifest.d.ts +40 -12
  64. package/lib/utils/ship/stage-manifest.js +99 -13
  65. package/lib/utils/ship/stage-writer.d.ts +48 -0
  66. package/lib/utils/ship/stage-writer.js +71 -1
  67. package/lib/utils/ship/types.d.ts +158 -13
  68. package/lib/utils/ship/xml.d.ts +7 -0
  69. package/lib/utils/ship/xml.js +25 -0
  70. package/lib/utils/ship/zip.d.ts +69 -0
  71. package/lib/utils/ship/zip.js +245 -0
  72. package/lib/utils/trust-registry.js +5 -0
  73. package/package.json +19 -19
@@ -0,0 +1,126 @@
1
+ // Folding the staged gettext catalogues into the generated freedesktop metadata.
2
+ //
3
+ // `gjsify ship` already discovered, validated and staged the compiled `.mo`
4
+ // catalogues, and `app-metadata.ts` already rendered a `.desktop` entry and an
5
+ // AppStream component. The two never met: no `Name[xx]=` and no `xml:lang=` was
6
+ // emitted anywhere, so a fully translated app still showed an English name in
7
+ // the GNOME app menu and in Software. Nobody reported it because both files
8
+ // stay VALID without the translations — `desktop-file-validate` and
9
+ // `appstreamcli validate` pass an untranslated file at exit 0, which is why the
10
+ // tests for this assert the localised keys are PRESENT rather than trusting a
11
+ // validator to notice they are gone.
12
+ //
13
+ // The msgfmt constraints this rests on live in `../msgfmt-merge.ts`, which runs
14
+ // the chain for `gjsify gettext` too. Two more are local to here, both from what
15
+ // a STAGED locale tree may hold that a `po/` directory cannot:
16
+ //
17
+ // * The catalogues reach us as `.mo` and msgfmt reads `.po` — handing it a
18
+ // `.mo` gives `de.mo:1:2: syntax error`, exit 1. `msgunfmt` converts back,
19
+ // and the round trip is exact for the msgid/msgstr pairs these two formats
20
+ // consume.
21
+ // * One language may arrive as SEVERAL catalogues, because the tree is keyed by
22
+ // `<lang>/LC_MESSAGES/<domain>.mo` and a package may ship more than one text
23
+ // domain. `msgcat --use-first` folds those into one before the chain sees
24
+ // them — the alternative is `msgfmt-merge.ts` constraint 5, an invalid file
25
+ // out of nothing but exit-0 calls.
26
+ //
27
+ // Both tools are in the same `gettext` package as `msgfmt`, so neither adds a
28
+ // dependency the fold did not already have.
29
+ import { execFileSync } from 'node:child_process';
30
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
31
+ import { tmpdir } from 'node:os';
32
+ import { join } from 'node:path';
33
+ import { mergeCatalogues } from '../msgfmt-merge.js';
34
+ /**
35
+ * The locale a staged catalogue path declares.
36
+ *
37
+ * Read rather than re-validated: `discoverLocales` has already refused anything
38
+ * that is not `<lang>/LC_MESSAGES/<domain>.mo`, and a second validator here would
39
+ * be a second definition of the layout.
40
+ */
41
+ function localeOf(rel) {
42
+ return rel.split('/')[0] ?? '';
43
+ }
44
+ /** Chain one metadata file through every catalogue, and read the result back. */
45
+ function fold(mode, source, extension, catalogues, workDir) {
46
+ // The source is a STRING, so it has to become a file before msgfmt can be its
47
+ // template — and that file needs `extension` for the same reason the
48
+ // intermediates do (`msgfmt-merge.ts`, constraint 4).
49
+ const template = join(workDir, `source${extension}`);
50
+ writeFileSync(template, source);
51
+ return readFileSync(mergeCatalogues({ mode, template, extension, catalogues, workDir }), 'utf-8');
52
+ }
53
+ /**
54
+ * Translate the generated metadata with the catalogues the package ships.
55
+ *
56
+ * Returns the input unchanged when there is nothing to fold in, so a project
57
+ * without a `localeDir` never needs the gettext tools. A project WITH catalogues
58
+ * that lacks them is REFUSED rather than shipped in English: an untranslated
59
+ * `.desktop` is indistinguishable from a correct one at install time, and the
60
+ * package would have promised translations it does not deliver.
61
+ */
62
+ export function localizeMetadata(metadata, localeFiles) {
63
+ if (localeFiles.length === 0)
64
+ return metadata;
65
+ const workDir = mkdtempSync(join(tmpdir(), 'gjsify-ship-l10n-'));
66
+ try {
67
+ // Sorted so the emitted `Name[xx]=` order — and therefore the artifact's
68
+ // bytes — does not depend on directory-read order. `listFilesRecursive`
69
+ // already sorts; relying on that from here would put the determinism of this
70
+ // file's output in another module's keeping. The sort ALSO fixes which
71
+ // domain wins below, so `--use-first` is a choice and not a coin toss.
72
+ const sorted = [...localeFiles].sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
73
+ // Grouped by LOCALE, not one entry per file. `discoverLocales` accepts every
74
+ // `<lang>/LC_MESSAGES/<domain>.mo`, and a package that ships two text domains
75
+ // (its own plus a bundled library's) therefore hands us two catalogues for one
76
+ // language. Chained as-is that is `msgfmt-merge.ts` constraint 5: two exit-0
77
+ // calls and a file both validators reject — `multiple keys named "Name[de]"`
78
+ // (exit 1) and `tag-duplicated name (lang=de)` (exit 3). Measured.
79
+ const byLocale = new Map();
80
+ for (const [index, file] of sorted.entries()) {
81
+ const po = join(workDir, `${index}.po`);
82
+ execFileSync('msgunfmt', [file.abs, '--output-file', po], { stdio: 'pipe' });
83
+ const locale = localeOf(file.rel);
84
+ byLocale.set(locale, [...(byLocale.get(locale) ?? []), po]);
85
+ }
86
+ const catalogues = [...byLocale].map(([locale, files]) => ({
87
+ locale,
88
+ // `--use-first` rather than a plain concatenation: two domains translating
89
+ // the same source string is not an error a packager can act on, and the
90
+ // alternatives are both worse. msgfmt given both files at once refuses
91
+ // outright (`duplicate message definition`, 2 fatal errors, exit 1), and a
92
+ // plain `msgcat` writes `#-#-#-#-#` conflict markers straight into the
93
+ // `Name=` a user reads. First in the sort above wins, deterministically.
94
+ po: files.length === 1 ? files[0] : msgcat(files, join(workDir, `${locale}.merged.po`)),
95
+ }));
96
+ return {
97
+ metainfo: fold('--xml', metadata.metainfo, '.metainfo.xml', catalogues, workDir),
98
+ desktopEntry: metadata.desktopEntry === undefined
99
+ ? undefined
100
+ : fold('--desktop', metadata.desktopEntry, '.desktop', catalogues, workDir),
101
+ };
102
+ }
103
+ catch (error) {
104
+ throw new Error(describeFailure(error, localeFiles.length));
105
+ }
106
+ finally {
107
+ rmSync(workDir, { recursive: true, force: true });
108
+ }
109
+ }
110
+ /** Fold several `.po` for ONE language into one, first translation wins. Returns its path. */
111
+ function msgcat(files, out) {
112
+ execFileSync('msgcat', ['--use-first', '--output-file', out, ...files], { stdio: 'pipe' });
113
+ return out;
114
+ }
115
+ /** One actionable line for a failed merge, naming the tool the host is missing. */
116
+ function describeFailure(error, count) {
117
+ const e = error;
118
+ if (e?.code === 'ENOENT') {
119
+ return ('gjsify ship: `msgfmt`/`msgunfmt`/`msgcat` are not on PATH, but the package stages ' +
120
+ `${count} gettext catalogue(s). Install them (package: gettext) — shipping the metadata ` +
121
+ 'untranslated would install an English app menu entry for a translated app, which nothing ' +
122
+ 'downstream can detect.');
123
+ }
124
+ const stderr = e?.stderr === undefined ? '' : `\n${String(e.stderr).trim()}`;
125
+ return `gjsify ship: could not fold the gettext catalogues into the freedesktop metadata.${stderr}`;
126
+ }
@@ -9,15 +9,7 @@
9
9
  //
10
10
  // So a project that invents a type also ships a shared-mime-info document, and the package refreshes
11
11
  // the MIME cache on install (see `scripts.ts`) — an unrefreshed cache is the same silence again.
12
- /** XML text escaping. Only the five that matter in element content and attributes. */
13
- function esc(value) {
14
- return value
15
- .replace(/&/g, '&amp;')
16
- .replace(/</g, '&lt;')
17
- .replace(/>/g, '&gt;')
18
- .replace(/"/g, '&quot;')
19
- .replace(/'/g, '&apos;');
20
- }
12
+ import { xmlEscape as esc } from './xml.js';
21
13
  const TYPE_RE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/;
22
14
  /**
23
15
  * Validate the declared types, throwing on anything that would install and then not work.
@@ -0,0 +1,126 @@
1
+ import type { HostOs, PackSettings } from './types.js';
2
+ /**
3
+ * The UUID namespace every GUID in a generated `.msi` is derived under.
4
+ *
5
+ * A CONSTANT, and changing it is a breaking change rather than a cosmetic one:
6
+ * the `UpgradeCode` derived from it is how Windows Installer recognises an
7
+ * already-installed copy of the same product, so a new namespace makes every
8
+ * future release a SECOND product that installs beside the old one instead of
9
+ * over it. Written down here rather than passed in for that reason — a knob would
10
+ * be a knob that silently forks a product line.
11
+ */
12
+ export declare const MSI_UUID_NAMESPACE = "7b3a1f6e-0c4d-4a2b-9e51-3d8c6f2a0b14";
13
+ /** RFC 4122 § 4.3 name-based UUID, SHA-1 flavour. */
14
+ export declare function uuid5(namespace: string, name: string): string;
15
+ /**
16
+ * The code that identifies the PRODUCT LINE, stable across every version.
17
+ *
18
+ * Derived from the app id alone. That is what `MajorUpgrade` compares, so it must
19
+ * NOT move when the version does — an `UpgradeCode` that changed per release would
20
+ * leave every old version installed beside the new one, silently, with two entries
21
+ * in Add/Remove Programs and two copies on disk.
22
+ */
23
+ export declare function msiUpgradeCode(settings: PackSettings): string;
24
+ /**
25
+ * The code that identifies THIS build, and it has to move when the version does.
26
+ *
27
+ * The exact inverse of {@link msiUpgradeCode}: Windows Installer refuses to
28
+ * install a second package carrying a `ProductCode` it already has, so a code that
29
+ * did not change per version would make an upgrade a no-op. The arch is in the
30
+ * material because an x64 and a (future) arm64 build of one version are two
31
+ * products, not one.
32
+ */
33
+ export declare function msiProductCode(settings: PackSettings, archLabel: string): string;
34
+ /** A component's GUID, stable for as long as its path is. */
35
+ export declare function msiComponentGuid(settings: PackSettings, path: string): string;
36
+ /**
37
+ * MSI's `ProductVersion`, which is a much smaller vocabulary than npm's.
38
+ *
39
+ * `major.minor.build`, with `major`/`minor` at most 255 and `build` at most 65535 —
40
+ * anything outside that is not a version Windows Installer will compare, and the
41
+ * fourth field it accepts is IGNORED by upgrade detection, which is why nothing is
42
+ * ever put there.
43
+ *
44
+ * A PRERELEASE IS REFUSED rather than truncated, and that is the interesting case.
45
+ * `normaliseVersion` turns `1.2.0-rc.1` into `1.2.0~rc.1` because dpkg and rpm sort
46
+ * a `~` before the release; MSI has no such spelling, so the only two options were
47
+ * to drop the suffix or to say no. Dropping it makes `1.2.0~rc.1` and `1.2.0` the
48
+ * same `ProductVersion` — two products `MajorUpgrade` cannot tell apart, so
49
+ * installing the release over the candidate leaves BOTH on the machine. That is
50
+ * the "installs cleanly, does the wrong thing" class, produced by the field meant
51
+ * to prevent it.
52
+ */
53
+ export declare function msiProductVersion(version: string): string;
54
+ /**
55
+ * An MSI `Identifier`: `[A-Za-z_][A-Za-z0-9_.]{0,71}`.
56
+ *
57
+ * The readable tail is not decoration — `msiinfo export <msi> File` is the oracle,
58
+ * and an id of pure hash makes a mismatch unreadable. The digest is what makes it
59
+ * UNIQUE: it is taken over the full path, so two files whose last 57 characters
60
+ * agree still differ. `renderWxs` asserts uniqueness anyway rather than trusting
61
+ * 48 bits, because the failure of a collision is two files fighting over one row.
62
+ */
63
+ export declare function msiIdentifier(prefix: string, path: string): string;
64
+ /** One payload file, as the installer will address it. */
65
+ export interface MsiFile {
66
+ /** Program-directory-relative path, POSIX-separated. */
67
+ path: string;
68
+ /**
69
+ * Where the bytes are, RELATIVE to the directory holding the `.wxs` and
70
+ * POSIX-separated — see measurement 2 in this file's header. An absolute path
71
+ * would make the document unusable on the second machine that compiles it.
72
+ */
73
+ source: string;
74
+ }
75
+ /** Where {@link buildMsi} writes the payload, and therefore what `Source=` is relative to. */
76
+ export declare const MSI_PAYLOAD_DIR = "root";
77
+ export interface WxsInput {
78
+ settings: PackSettings;
79
+ /** Everything the program directory carries, in any order. */
80
+ files: readonly MsiFile[];
81
+ /** The directory the installer lays down — `windowsProgramDirName(settings)`. */
82
+ programDirName: string;
83
+ /** The format's spelling of the architecture, e.g. `x64`. */
84
+ archLabel: string;
85
+ }
86
+ /**
87
+ * Render the installer's source document.
88
+ *
89
+ * PURE — a string in, a string out, no filesystem and no compiler — which is what
90
+ * makes every decision above unit-testable on a machine with neither backend
91
+ * installed. The one thing it cannot answer is whether a compiler accepts what it
92
+ * wrote, and that is what the two legs are for.
93
+ */
94
+ export declare function renderWxs(input: WxsInput): string;
95
+ /** Which compiler a host has. */
96
+ export type MsiBackend = 'wixl' | 'wix3';
97
+ /**
98
+ * The backend for one host.
99
+ *
100
+ * A FUNCTION of the host and nothing else, so the choice is testable from either
101
+ * OS. `assertHostCanFinish` has already refused anything but these two by the time
102
+ * this runs — the throw is for the caller that forgets to.
103
+ */
104
+ export declare function msiBackendFor(host: string): MsiBackend;
105
+ export interface MsiPackInput {
106
+ settings: PackSettings;
107
+ files: readonly MsiFile[];
108
+ programDirName: string;
109
+ archLabel: string;
110
+ /** Working root for the rendered document and the compiler's intermediates. */
111
+ workDir: string;
112
+ /** Absolute path the finished `.msi` is written to. */
113
+ target: string;
114
+ verbose: boolean;
115
+ /** Injected so the dispatch is testable off the host it selects for. */
116
+ host?: HostOs;
117
+ }
118
+ /**
119
+ * Render the document and compile it into `input.target`.
120
+ *
121
+ * The `.wxs` is KEPT in `workDir` rather than written to a temporary file, and
122
+ * that is deliberate: it is the one artifact of this milestone a human can read,
123
+ * it is what a bug report should carry, and it is what the Windows leg compiles a
124
+ * SECOND time with WiX to produce the file the Linux oracle reads back.
125
+ */
126
+ export declare function buildMsi(input: MsiPackInput): Promise<string>;