@gjsify/cli 0.64.1 → 0.64.2-next.255

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.
@@ -94,6 +94,22 @@ export declare class BuildAction {
94
94
  * on the prediction, not a second opinion.
95
95
  */
96
96
  private assertNodeOutputGlobals;
97
+ /**
98
+ * Post-bundle gate for `--app nativescript`: every GI namespace the emitted
99
+ * bundle loads has to be one the APK was told to carry (ADR 0117 § 2).
100
+ *
101
+ * THE ORACLE IS THE EMITTED FILE, like the two gates above and for the same
102
+ * reason — that is the artifact that gets installed. What it reads there is a
103
+ * LOWER BOUND, which is why only one direction is fatal: a namespace the scan
104
+ * found and the config omits WILL be missing from the APK, while a namespace the
105
+ * config names and the scan missed may simply be reached in a way no static read
106
+ * can see (`planGiSelection` carries the full argument).
107
+ *
108
+ * Android is the one target where this is not advice. A `.deb` that under-declares
109
+ * its typelibs can still be fixed by installing a package; an APK carries its
110
+ * whole GI stack or does without it.
111
+ */
112
+ private assertNativescriptGiSelected;
97
113
  /**
98
114
  * Post-bundle step for a `--app browser` HTML entry: write the processed
99
115
  * `index.html` beside the JS bundle, with the entry `<script>`'s `src`
@@ -15,6 +15,8 @@ import { collectEntryPaths, describeJsxConfig, findJsxEntryPoint, jsxConfigMissi
15
15
  import { giSystemProbes } from '../utils/gi-runtime-paths.js';
16
16
  import { escapeRawNulForGjs } from '../utils/gjs-source-escape.js';
17
17
  import { assertNodeBundleGlobalsShimmed } from '../utils/node-bundle-guard.js';
18
+ import { scanGiNamespaces } from '../utils/ship/gi-namespaces.js';
19
+ import { planGiSelection } from '../utils/ship/gi-selection.js';
18
20
  import { DEP_CHANGE_LOCKFILES } from '../utils/package-inputs.js';
19
21
  const DEFAULT_GJS_SHEBANG = '#!/usr/bin/env -S gjs -m';
20
22
  /**
@@ -439,6 +441,51 @@ export class BuildAction {
439
441
  assertNodeBundleGlobalsShimmed(code, item.fileName ? `${label} (${item.fileName})` : label);
440
442
  }
441
443
  }
444
+ /**
445
+ * Post-bundle gate for `--app nativescript`: every GI namespace the emitted
446
+ * bundle loads has to be one the APK was told to carry (ADR 0117 § 2).
447
+ *
448
+ * THE ORACLE IS THE EMITTED FILE, like the two gates above and for the same
449
+ * reason — that is the artifact that gets installed. What it reads there is a
450
+ * LOWER BOUND, which is why only one direction is fatal: a namespace the scan
451
+ * found and the config omits WILL be missing from the APK, while a namespace the
452
+ * config names and the scan missed may simply be reached in a way no static read
453
+ * can see (`planGiSelection` carries the full argument).
454
+ *
455
+ * Android is the one target where this is not advice. A `.deb` that under-declares
456
+ * its typelibs can still be fixed by installing a package; an APK carries its
457
+ * whole GI stack or does without it.
458
+ */
459
+ assertNativescriptGiSelected(result, outfile, outdir) {
460
+ const label = outfile ?? outdir ?? 'the nativescript bundle';
461
+ const scanned = new Set();
462
+ for (const item of result.output ?? []) {
463
+ if (item.type !== 'chunk')
464
+ continue;
465
+ const code = item.code;
466
+ if (typeof code !== 'string' || code.length === 0)
467
+ continue;
468
+ for (const namespace of scanGiNamespaces(code))
469
+ scanned.add(namespace);
470
+ }
471
+ let selection;
472
+ try {
473
+ selection = planGiSelection({
474
+ declared: this.configData.nativescript?.giNamespaces ?? [],
475
+ scanned: [...scanned],
476
+ });
477
+ }
478
+ catch (error) {
479
+ // Re-thrown with the artifact in it: `planGiSelection` is pure and knows
480
+ // the namespaces but not which file they were read from.
481
+ throw new Error(`gjsify build: ${label}: ${error.message}`);
482
+ }
483
+ for (const namespace of selection.unscanned) {
484
+ console.warn(`[gjsify] --app nativescript: nativescript.giNamespaces declares ${namespace}, which ` +
485
+ `${label} does not appear to import. The APK carries it anyway — drop it if nothing ` +
486
+ 'needs it, since every selected namespace costs APK size.');
487
+ }
488
+ }
442
489
  /**
443
490
  * Post-bundle step for a `--app browser` HTML entry: write the processed
444
491
  * `index.html` beside the JS bundle, with the entry `<script>`'s `src`
@@ -849,6 +896,13 @@ export class BuildAction {
849
896
  if (app === 'node' && autoMode && !pluginOpts.nodeGiGlobalsInject) {
850
897
  this.assertNodeOutputGlobals(writeResult, outfile, outdir);
851
898
  }
899
+ // Refuse to hand back a `--app nativescript` bundle that imports a GI namespace
900
+ // the APK will not carry. The payload is selected from a pinned sysroot at
901
+ // package-build time (ADR 0117), so there is no host to fall back on: the
902
+ // alternative to this gate is an APK that installs and throws at the import.
903
+ if (app === 'nativescript') {
904
+ this.assertNativescriptGiSelected(writeResult, outfile, outdir);
905
+ }
852
906
  // Browser HTML entry: emit the processed `index.html` beside the bundle
853
907
  // (engine-portable post-step — the native rolldown engine has no
854
908
  // `emitFile`, so this can't live in a plugin). `outfile` is guaranteed
package/lib/config.js CHANGED
@@ -317,6 +317,25 @@ export class Config {
317
317
  'introspection to answer `gi://` with. `--app gjs` resolves it natively and `--app node` ' +
318
318
  'through @gjsify/node-gi, so neither needs a widget renderer standing in for it.');
319
319
  }
320
+ // And the same rule once more for the namespace selection, which only an
321
+ // Android payload can act on: on every other target `gi://` is answered by a
322
+ // system typelib or by `requireGi`, so a list here would be read, validated
323
+ // and then change nothing about the artifact.
324
+ if (configData.nativescript?.giNamespaces !== undefined && configData.app !== 'nativescript') {
325
+ throw new Error(`gjsify build: nativescript.giNamespaces has no effect on --app ${configData.app}. ` +
326
+ 'It selects what an Android APK carries (ADR 0117); every other target resolves ' +
327
+ '`gi://` against a GI stack the host already has.');
328
+ }
329
+ for (const entry of configData.nativescript?.giNamespaces ?? []) {
330
+ // `Ns` or `Ns-Version`, because that is what the AAR builder resolves
331
+ // against the typelib names in the sysroot. A free-form string here
332
+ // surfaces as "the prefix has no typelib for …" hours later, naming the
333
+ // typo rather than the shape.
334
+ if (!/^[A-Za-z][A-Za-z\d_]*(-\d+(\.\d+)*)?$/.test(entry)) {
335
+ throw new Error(`gjsify build: nativescript.giNamespaces entry "${entry}" is not a GI namespace. ` +
336
+ 'Write it as `Gtk` or `Gtk-4.0`.');
337
+ }
338
+ }
320
339
  if (cliArgs.globals !== undefined)
321
340
  configData.globals = cliArgs.globals;
322
341
  // Fallback applied post-merge (not as a yargs `default:`) so a
@@ -213,6 +213,37 @@ export interface ConfigData {
213
213
  format?: ConfigDataFormat;
214
214
  /** Config for `gjsify test`. `--entry`, `--outdir`, `--runtime` override these. */
215
215
  test?: ConfigDataTest;
216
+ /** Config for the `nativescript` target — what the APK carries (ADR 0117). */
217
+ nativescript?: ConfigDataNativescript;
218
+ }
219
+ /**
220
+ * What a NativeScript build decides that no other target has to.
221
+ *
222
+ * Its own namespace rather than top-level keys, for the reason `flatpak` and `ship`
223
+ * have one: these settings mean nothing on `gjs`, `node` or `browser`, and a
224
+ * top-level `giNamespaces` would read like a bundler option on every target.
225
+ */
226
+ export interface ConfigDataNativescript {
227
+ /**
228
+ * The GI namespaces this APK carries, as `Ns` or `Ns-Version` (ADR 0117 § 2).
229
+ *
230
+ * ANDROID HAS NO SYSTEM GI STACK, so unlike a `.deb` there is no `Depends:` that
231
+ * a host can satisfy: every typelib and every backing `.so` is in the APK or is
232
+ * not reachable at all. The payload is cross-compiled against a sha256-pinned
233
+ * sysroot long before an app build, so this list cannot conjure a namespace the
234
+ * sysroot lacks — it SELECTS from what the sysroot has.
235
+ *
236
+ * Declared rather than inferred, and the asymmetry is the point: the scan of the
237
+ * emitted bundle is a LOWER BOUND (a namespace reached through a computed
238
+ * specifier is invisible to it), so an imported namespace missing from this list
239
+ * fails the build by name, while a listed one the scan did not see is only a
240
+ * warning about a larger-than-necessary APK.
241
+ *
242
+ * Unset is not the same as `[]` in intent but is in effect: both mean the
243
+ * measured core set (GLib, GObject, Gio and friends), which backs the runtime
244
+ * itself and ships whether or not anything declares it.
245
+ */
246
+ giNamespaces?: string[];
216
247
  }
217
248
  /** Optional pointer to a non-default oxc config file. */
218
249
  export interface ConfigDataFormat {
@@ -323,10 +354,14 @@ export interface AppMetadata {
323
354
  /**
324
355
  * App-store screenshots. `url` must be an absolute HTTPS URL to a PNG;
325
356
  * `environment` lets Flathub group by desktop; the first entry defaults to
326
- * `type="default"`.
357
+ * `type="default"`. `localizedUrls` maps a language code (`de`, `pt_BR`)
358
+ * to a screenshot of the app in that language; each becomes an extra
359
+ * `<image xml:lang="…">`, so software centres show the user's language
360
+ * and fall back to `url`.
327
361
  */
328
362
  screenshots?: Array<{
329
363
  url: string;
364
+ localizedUrls?: Record<string, string>;
330
365
  caption?: string;
331
366
  captionTranslatorHint?: string;
332
367
  environment?: 'plasma' | 'gnome' | 'cli';
@@ -176,6 +176,9 @@ function renderMetainfo(inputs, kind) {
176
176
  const envAttr = s.environment ? ` environment="${escapeXml(s.environment)}"` : '';
177
177
  lines.push(` <screenshot${typeAttr}${envAttr}>`);
178
178
  lines.push(` <image>${escapeXml(s.url)}</image>`);
179
+ for (const [lang, url] of Object.entries(s.localizedUrls ?? {})) {
180
+ lines.push(` <image xml:lang="${escapeXml(lang)}">${escapeXml(url)}</image>`);
181
+ }
179
182
  if (s.caption) {
180
183
  pushTranslatorHint(lines, s.captionTranslatorHint, ' ');
181
184
  lines.push(` <caption>${escapeXml(s.caption)}</caption>`);
@@ -306,9 +306,12 @@ export async function lintMetainfo(xml, options = {}) {
306
306
  add('screenshot-missing', 'warning', 'at-least-one-screenshot', 'screenshots', 'at least one screenshot is expected');
307
307
  }
308
308
  else {
309
+ // A screenshot with an untagged image plus `<image xml:lang="de">` twins is English: the
310
+ // twins are alternatives, and the untagged one is what a non-German user sees.
311
+ const englishTag = (t) => t === '' || langBase(t) === 'en';
309
312
  const isEnglish = (s) => {
310
- const tags = [lang(s), ...kids(s, 'image').map(lang)];
311
- return tags.every((t) => t === '' || langBase(t) === 'en');
313
+ const images = kids(s, 'image').map(lang);
314
+ return englishTag(lang(s)) && (images.length === 0 || images.some(englishTag));
312
315
  };
313
316
  if (!shots.some(isEnglish)) {
314
317
  add('screenshot-no-english', 'warning', 'at-least-one-screenshot', 'screenshots', 'no screenshot without xml:lang (or in English)');
@@ -0,0 +1,18 @@
1
+ /** What {@link planGiSelection} decided, and what it wants the build to say out loud. */
2
+ export interface GiSelection {
3
+ /** The namespaces to hand the AAR builder, as the config spells them, deduped and sorted. */
4
+ selected: string[];
5
+ /** Declared but not found in the bundle — worth a warning, never a refusal. */
6
+ unscanned: string[];
7
+ }
8
+ /**
9
+ * Reconcile the configured namespace set with the one the emitted bundle imports.
10
+ *
11
+ * @param spec.declared `nativescript.giNamespaces`, as `Ns` or `Ns-Version`
12
+ * @param spec.scanned what {@link scanGiNamespaces} read off the emitted bundle
13
+ * @throws when the bundle imports a namespace the config does not cover
14
+ */
15
+ export declare function planGiSelection({ declared, scanned }: {
16
+ declared: string[];
17
+ scanned: string[];
18
+ }): GiSelection;
@@ -0,0 +1,73 @@
1
+ // Which GI namespaces does this Android APK carry? — ADR 0117 § 2, the CLI half.
2
+ //
3
+ // THE CONFIG DECIDES AND THE BUNDLE IS CHECKED AGAINST IT, which is the asymmetry
4
+ // the ADR turns on. The payload is cross-compiled hours earlier by
5
+ // `build-gi-runtime-android.mjs` against a sha256-pinned sysroot; nothing at app-build
6
+ // time can conjure a typelib that is not in it. So the config is the input, and the
7
+ // scan of the emitted bundle is the EVIDENCE that the input is complete.
8
+ //
9
+ // The scan is a LOWER BOUND, and the two directions of disagreement are therefore not
10
+ // symmetric:
11
+ //
12
+ // * IMPORTED, NOT CONFIGURED → build error naming the namespace. The scanner found a
13
+ // real import; the APK will not have the typelib; the device throws at that import,
14
+ // after the download, reading like an application bug. This is the same failure
15
+ // `deriveDepends` refuses for distro packages (ADR 0024 § 6), reached from Android.
16
+ // * CONFIGURED, NOT IMPORTED → warning. `scanGiNamespaces` reads the emitted file and
17
+ // cannot see a namespace reached through a computed specifier, nor one loaded by a
18
+ // native dependency. Refusing here would make a correct project unbuildable on the
19
+ // scanner's blind spot — so it reports a probably-larger-than-necessary APK and
20
+ // builds it.
21
+ //
22
+ // COMPARED BY NAMESPACE, NOT BY STRING. A config may pin an ABI (`Gtk-4.0`) where the
23
+ // bundle's specifier carries no version (`import 'gi://Gtk'`), and vice versa. Both
24
+ // sides mean the same namespace, and string equality would fail a correct project on
25
+ // the spelling.
26
+ /** `Gtk-4.0` → `Gtk`; `Gtk` → `Gtk`. */
27
+ function namespaceOf(entry) {
28
+ const dash = entry.indexOf('-');
29
+ return dash === -1 ? entry : entry.slice(0, dash);
30
+ }
31
+ /**
32
+ * The namespaces every AAR carries whether or not anything declares them.
33
+ *
34
+ * These back `libnode_gi.so` itself, so they are in the payload's DT_NEEDED closure
35
+ * before any selection happens (ADR 0110's measured core set). Demanding them in the
36
+ * config would make the key boilerplate and fail every project that exists today, for
37
+ * a selection that cannot change the bytes.
38
+ */
39
+ const ALWAYS_PRESENT = new Set(['GLib', 'GObject', 'Gio', 'GioUnix', 'GLibUnix', 'GModule', 'GIRepository']);
40
+ /**
41
+ * Reconcile the configured namespace set with the one the emitted bundle imports.
42
+ *
43
+ * @param spec.declared `nativescript.giNamespaces`, as `Ns` or `Ns-Version`
44
+ * @param spec.scanned what {@link scanGiNamespaces} read off the emitted bundle
45
+ * @throws when the bundle imports a namespace the config does not cover
46
+ */
47
+ export function planGiSelection({ declared, scanned }) {
48
+ // A bare and a pinned declaration of one namespace are one selection, and the
49
+ // pinned spelling wins: it says something the bare one does not.
50
+ const byNamespace = new Map();
51
+ for (const entry of declared) {
52
+ const ns = namespaceOf(entry);
53
+ const held = byNamespace.get(ns);
54
+ if (held === undefined || (held === ns && entry !== ns))
55
+ byNamespace.set(ns, entry);
56
+ }
57
+ const missing = [
58
+ ...new Set(scanned.map(namespaceOf).filter((ns) => !byNamespace.has(ns) && !ALWAYS_PRESENT.has(ns))),
59
+ ].sort();
60
+ if (missing.length > 0) {
61
+ throw new Error(`The bundle imports ${missing.join(', ')}, which nativescript.giNamespaces does not declare. ` +
62
+ 'An Android APK carries only the typelibs the config selects (ADR 0117), so this would ' +
63
+ `install and then throw at the import. Add ${missing.length === 1 ? 'it' : 'them'} to ` +
64
+ 'nativescript.giNamespaces, or stop importing ' +
65
+ `${missing.length === 1 ? 'it' : 'them'}.`);
66
+ }
67
+ const seen = new Set(scanned.map(namespaceOf));
68
+ const unscanned = [...byNamespace.entries()]
69
+ .filter(([ns]) => !seen.has(ns))
70
+ .map(([, entry]) => entry)
71
+ .sort();
72
+ return { selected: [...byNamespace.values()].sort(), unscanned };
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.64.1",
3
+ "version": "0.64.2-next.255",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -142,22 +142,22 @@
142
142
  "cli"
143
143
  ],
144
144
  "dependencies": {
145
- "@gjsify/blueprint": "^0.64.1",
146
- "@gjsify/buffer": "^0.64.1",
147
- "@gjsify/create-app": "^0.64.1",
148
- "@gjsify/node-globals": "^0.64.1",
149
- "@gjsify/node-polyfills": "^0.64.1",
150
- "@gjsify/npm-registry": "^0.64.1",
151
- "@gjsify/resolve-npm": "^0.64.1",
152
- "@gjsify/rolldown-plugin-gjsify": "^0.64.1",
153
- "@gjsify/vite-plugin-blueprint": "^0.64.1",
154
- "@gjsify/rolldown-plugin-pnp": "^0.64.1",
155
- "@gjsify/semver": "^0.64.1",
156
- "@gjsify/tar": "^0.64.1",
157
- "@gjsify/tsc": "^0.64.1",
158
- "@gjsify/utils": "^0.64.1",
159
- "@gjsify/web-polyfills": "^0.64.1",
160
- "@gjsify/workspace": "^0.64.1",
145
+ "@gjsify/blueprint": "0.64.2-next.255",
146
+ "@gjsify/buffer": "0.64.2-next.255",
147
+ "@gjsify/create-app": "0.64.2-next.255",
148
+ "@gjsify/node-globals": "0.64.2-next.255",
149
+ "@gjsify/node-polyfills": "0.64.2-next.255",
150
+ "@gjsify/npm-registry": "0.64.2-next.255",
151
+ "@gjsify/resolve-npm": "0.64.2-next.255",
152
+ "@gjsify/rolldown-plugin-gjsify": "0.64.2-next.255",
153
+ "@gjsify/vite-plugin-blueprint": "0.64.2-next.255",
154
+ "@gjsify/rolldown-plugin-pnp": "0.64.2-next.255",
155
+ "@gjsify/semver": "0.64.2-next.255",
156
+ "@gjsify/tar": "0.64.2-next.255",
157
+ "@gjsify/tsc": "0.64.2-next.255",
158
+ "@gjsify/utils": "0.64.2-next.255",
159
+ "@gjsify/web-polyfills": "0.64.2-next.255",
160
+ "@gjsify/workspace": "0.64.2-next.255",
161
161
  "acorn": "^8.17.0",
162
162
  "cosmiconfig": "^9.0.2",
163
163
  "get-tsconfig": "^4.14.0",
@@ -166,13 +166,13 @@
166
166
  "yargs": "^18.0.0"
167
167
  },
168
168
  "devDependencies": {
169
- "@gjsify/unit": "^0.64.1",
169
+ "@gjsify/unit": "0.64.2-next.255",
170
170
  "@types/yargs": "^17.0.35",
171
171
  "typescript": "^6.0.3"
172
172
  },
173
173
  "peerDependencies": {
174
- "@gjsify/oxfmt-native": "^0.64.1",
175
- "@gjsify/rolldown-native": "^0.64.1"
174
+ "@gjsify/oxfmt-native": "0.64.2-next.255",
175
+ "@gjsify/rolldown-native": "0.64.2-next.255"
176
176
  },
177
177
  "peerDependenciesMeta": {
178
178
  "@gjsify/oxfmt-native": {
@@ -191,5 +191,6 @@
191
191
  "bugs": {
192
192
  "url": "https://github.com/gjsify/gjsify/issues"
193
193
  },
194
- "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/infra/cli#readme"
194
+ "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/infra/cli#readme",
195
+ "gitHead": "6c88ac780b9742fce781e9b35aa2f45c125d8064"
195
196
  }