@gjsify/cli 0.43.0 → 0.44.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.
@@ -630,7 +630,7 @@ export class BuildAction {
630
630
  throw new Error('gjsify build: an .html entry needs an explicit --outfile ' +
631
631
  '(e.g. --outfile dist-app/app.js); the emitted index.html is written beside it.');
632
632
  }
633
- const { consoleShim, globals } = this.configData;
633
+ const { consoleShim, globals, dialect } = this.configData;
634
634
  const userExternal = Array.isArray(userBundler.external) ? userBundler.external : undefined;
635
635
  const userBanner = typeof userBundler.output?.banner === 'string' ? userBundler.output.banner : undefined;
636
636
  const pluginOpts = {
@@ -640,6 +640,7 @@ export class BuildAction {
640
640
  exclude,
641
641
  reflection: typescript?.reflection,
642
642
  consoleShim,
643
+ ...(dialect !== undefined ? { dialect } : {}),
643
644
  ...(aliases ? { aliases } : {}),
644
645
  ...(opts.preserveDefaultExport ? { preserveDefaultExport: true } : {}),
645
646
  ...(opts.toolchainAnchor !== undefined ? { toolchainAnchor: opts.toolchainAnchor } : {}),
@@ -85,11 +85,13 @@ const IGNORE = [
85
85
  /^docs\//,
86
86
  ...OTHER_WORKFLOW_INPUTS,
87
87
  // Neither `@gjsify/node-gi` (the Node-native GI engine) nor `@gjsify/napi` (the
88
- // N-API host over SpiderMonkey) is a gjsify workspace member: the GJS-first
89
- // install/foreach tooling cannot build their node-gyp addons or Vala+C++/meson
90
- // shim, so `main.yml` neither builds nor tests them and their own workflows are
91
- // the source of truth. Without the carve-outs their files map to no workspace,
92
- // land in `unmatched`, and force a full run on every node-gi/napi PR.
88
+ // N-API host over SpiderMonkey) is a gjsify workspace member — WHY is ADR 0031,
89
+ // which this comment used to carry alone. The short of it: node-gi's `build` is
90
+ // `node-gyp rebuild`, so a member would enter `foreach build` in a container with
91
+ // no GTK/GI toolchain. `main.yml` therefore neither builds nor tests them and
92
+ // their own workflows are the source of truth. Without the carve-outs their files
93
+ // map to no workspace, land in `unmatched`, and force a full run on every
94
+ // node-gi/napi PR.
93
95
  /^packages\/node-gi\//,
94
96
  /^packages\/napi\//,
95
97
  // Flatpak build/distribution tooling (SDK-extension manifest + metainfo). No
@@ -1,5 +1,6 @@
1
1
  import { Config } from '../config.js';
2
2
  import { BuildAction } from '../actions/build.js';
3
+ import { SOURCE_DIALECTS } from '@gjsify/rolldown-plugin-gjsify';
3
4
  // NO `normalize: true` ON THE GLOB-BEARING OPTIONS — `entry-points` and
4
5
  // `exclude` are PATTERNS, not paths.
5
6
  //
@@ -106,6 +107,11 @@ export const buildCommand = {
106
107
  type: 'string',
107
108
  choices: ['silent', 'error', 'warning', 'info', 'debug', 'verbose'],
108
109
  default: 'warning',
110
+ })
111
+ .option('dialect', {
112
+ description: "The dialect the SOURCE is written in. 'react-native' aliases 'react-native' onto '@gjsify/react-native' and fails the build on an import whose ADR 0032 support-table status is not supported/partial. GJS and Node app builds. This is NOT gjsify.runtimes['react-native'], which declares the runtime a package RUNS on — the two answer opposite questions.",
113
+ type: 'string',
114
+ choices: [...SOURCE_DIALECTS],
109
115
  })
110
116
  .option('console-shim', {
111
117
  description: 'Inject a console shim into GJS builds for clean output without the GLib prefix and with working ANSI colors. Use --no-console-shim to disable. Only applies to GJS app builds.',
@@ -9,7 +9,9 @@
9
9
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
10
  import { dirname, resolve } from 'node:path';
11
11
  import { Config } from '../../config.js';
12
- import { defaultCiContainer, looksLikeAppId, readPackageJson, resolveRuntime } from './utils.js';
12
+ import { defaultCiContainer, looksLikeAppId, readPackageJson } from './utils.js';
13
+ import { resolveRuntime } from '../../utils/flatpak-runtime.js';
14
+ import { pickFlatpakBuildKeys } from '../../utils/ship/flatpak-config.js';
13
15
  export const flatpakCiCommand = {
14
16
  command: 'ci',
15
17
  description: 'Scaffold .github/workflows/flatpak.yml using the flathub-infra container + flatpak-builder@v6 action.',
@@ -68,7 +70,13 @@ export const flatpakCiCommand = {
68
70
  }
69
71
  const manifest = args.manifest ?? `${appId}.json`;
70
72
  const bundle = args.bundle ?? `${appId}.flatpak`;
71
- const { runtime, runtimeVersion } = resolveRuntime(flatpak, {});
73
+ // Through the deprecation window, not straight off `gjsify.flatpak`: a
74
+ // project told to move these keys to `gjsify.ship.flatpak` must not lose
75
+ // them here — the CI container tag is derived from the runtime, so
76
+ // reading the old block alone scaffolds a workflow against the default
77
+ // GNOME release rather than the pinned one.
78
+ const buildKeys = pickFlatpakBuildKeys(configData.ship?.flatpak, flatpak).values;
79
+ const { runtime, runtimeVersion } = resolveRuntime(buildKeys, {});
72
80
  const runtimeImage = args.runtimeImage ??
73
81
  flatpak.ciContainer ??
74
82
  defaultCiContainer(runtime, runtimeVersion);
@@ -13,7 +13,9 @@
13
13
  // with `skip-icons-check`.
14
14
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
15
15
  import { dirname, resolve } from 'node:path';
16
- import { DEFAULT_CLI_FINISH_ARGS, DEFAULT_GUI_FINISH_ARGS, looksLikeAppId, readPackageJson, resolveRuntime, } from './utils.js';
16
+ import { looksLikeAppId, readPackageJson } from './utils.js';
17
+ import { DEFAULT_CLI_FINISH_ARGS, DEFAULT_GUI_FINISH_ARGS, deriveAppendPath, resolveRuntime, } from '../../utils/flatpak-runtime.js';
18
+ import { pickFlatpakBuildKeys } from '../../utils/ship/flatpak-config.js';
17
19
  import { renderDesktop, renderFlathubJson, renderMetainfoApp, renderMetainfoCli, validateScaffoldInputs, } from './scaffold.js';
18
20
  import { Config } from '../../config.js';
19
21
  import { OxcNotFoundError, hasOxcDevDep, runOxfmt } from '../../utils/oxc-resolve.js';
@@ -109,17 +111,24 @@ export const flatpakInitCommand = {
109
111
  'or rename the package to a reverse-DNS id like org.example.MyApp.');
110
112
  }
111
113
  const kind = args.kind ?? flatpak.kind ?? (args.cliOnly ? 'cli' : 'app');
112
- const { runtime, runtimeId, sdk, runtimeVersion } = resolveRuntime(flatpak, {
114
+ // The six BUILD keys come through the deprecation window
115
+ // (`utils/ship/flatpak-config.ts`), not straight off `gjsify.flatpak`.
116
+ // These commands did NOT move, but the keys did: a project that followed
117
+ // `ship`'s own advice and moved them would otherwise lose them here and
118
+ // get a committed manifest against a different runtime version with
119
+ // different finish-args, silently, at exit 0.
120
+ const buildKeys = pickFlatpakBuildKeys(configData.ship?.flatpak, flatpak).values;
121
+ const { runtime, runtimeId, sdk, runtimeVersion } = resolveRuntime(buildKeys, {
113
122
  runtime: args.runtime,
114
123
  runtimeVersion: args.runtimeVersion,
115
124
  });
116
- const sdkExtensions = mergeArrays(flatpak.sdkExtensions, args.sdkExtension);
117
- const appendPath = flatpak.appendPath ?? (sdkExtensions?.length ? deriveAppendPath(sdkExtensions) : undefined);
125
+ const sdkExtensions = mergeArrays(buildKeys.sdkExtensions, args.sdkExtension);
126
+ const appendPath = buildKeys.appendPath ?? (sdkExtensions?.length ? deriveAppendPath(sdkExtensions) : undefined);
118
127
  const command = args.command ?? flatpak.command ?? appId;
119
128
  const explicitFinishArgs = args.finishArg;
120
129
  const finishArgs = explicitFinishArgs !== undefined
121
130
  ? explicitFinishArgs
122
- : (flatpak.finishArgs ?? (kind === 'cli' ? DEFAULT_CLI_FINISH_ARGS : DEFAULT_GUI_FINISH_ARGS));
131
+ : (buildKeys.finishArgs ?? (kind === 'cli' ? DEFAULT_CLI_FINISH_ARGS : DEFAULT_GUI_FINISH_ARGS));
123
132
  const manifest = {
124
133
  id: appId,
125
134
  runtime: runtimeId,
@@ -133,7 +142,7 @@ export const flatpakInitCommand = {
133
142
  }
134
143
  manifest.command = command;
135
144
  manifest['finish-args'] = finishArgs;
136
- const cleanup = flatpak.cleanup;
145
+ const cleanup = buildKeys.cleanup;
137
146
  if (cleanup?.length)
138
147
  manifest.cleanup = cleanup;
139
148
  // Module assembly. Two precedence rules:
@@ -253,16 +262,6 @@ function mergeArrays(a, b) {
253
262
  return undefined;
254
263
  return [...(a ?? []), ...(b ?? [])];
255
264
  }
256
- function deriveAppendPath(sdkExtensions) {
257
- const out = [];
258
- for (const ext of sdkExtensions) {
259
- const m = /^org\.freedesktop\.Sdk\.Extension\.([A-Za-z0-9-]+)$/.exec(ext);
260
- if (m)
261
- out.push(`/usr/lib/sdk/${m[1]}/bin`);
262
- }
263
- out.push('/app/bin');
264
- return out;
265
- }
266
265
  function deriveModuleName(appId) {
267
266
  const parts = appId.split('.');
268
267
  return parts[parts.length - 1] || appId;
@@ -1,32 +1,6 @@
1
- import type { ConfigDataFlatpak } from '../../types/config-data.js';
2
- /**
3
- * Default GNOME-Platform runtime version. Bumped per release window.
4
- * GNOME 50 = April 2026 stable; tracked in
5
- * https://docs.flathub.org/docs/for-app-authors/requirements.
6
- */
7
- export declare const DEFAULT_GNOME_RUNTIME_VERSION = "50";
8
- /** Default Freedesktop-Platform runtime version (LTS-ish). */
9
- export declare const DEFAULT_FREEDESKTOP_RUNTIME_VERSION = "24.08";
10
- /** Permissive GUI defaults for GTK4 + Adwaita apps. */
11
- export declare const DEFAULT_GUI_FINISH_ARGS: string[];
12
- /** Lean defaults for headless CLI tools — no display, no GPU. */
13
- export declare const DEFAULT_CLI_FINISH_ARGS: string[];
14
1
  /** Read package.json from a directory. Throws a helpful error if missing/invalid. */
15
2
  export declare function readPackageJson(dir: string): Record<string, unknown>;
16
3
  /** True if a name string looks like a reverse-DNS Flatpak app id. */
17
4
  export declare function looksLikeAppId(value: unknown): value is string;
18
- /**
19
- * Pick the runtime + sdk + version triple from config + CLI overrides.
20
- * `--runtime` and `--runtime-version` flags win over config values.
21
- */
22
- export declare function resolveRuntime(flatpak: ConfigDataFlatpak | undefined, overrides: {
23
- runtime?: string;
24
- runtimeVersion?: string;
25
- }): {
26
- runtime: 'gnome' | 'freedesktop';
27
- runtimeId: string;
28
- sdk: string;
29
- runtimeVersion: string;
30
- };
31
5
  /** Default container image for the GitHub Actions workflow. */
32
6
  export declare function defaultCiContainer(runtime: 'gnome' | 'freedesktop', runtimeVersion: string): string;
@@ -1,18 +1,10 @@
1
1
  // Shared helpers for the `gjsify flatpak <sub>` subcommand group.
2
+ //
3
+ // The runtime/SDK/finish-args half moved to `utils/flatpak-runtime.ts` when
4
+ // `gjsify ship --target flatpak` became its second caller — see that file's
5
+ // header. What is left here is what only these subcommands ask.
2
6
  import { readFileSync } from 'node:fs';
3
7
  import { resolve } from 'node:path';
4
- /**
5
- * Default GNOME-Platform runtime version. Bumped per release window.
6
- * GNOME 50 = April 2026 stable; tracked in
7
- * https://docs.flathub.org/docs/for-app-authors/requirements.
8
- */
9
- export const DEFAULT_GNOME_RUNTIME_VERSION = '50';
10
- /** Default Freedesktop-Platform runtime version (LTS-ish). */
11
- export const DEFAULT_FREEDESKTOP_RUNTIME_VERSION = '24.08';
12
- /** Permissive GUI defaults for GTK4 + Adwaita apps. */
13
- export const DEFAULT_GUI_FINISH_ARGS = ['--device=dri', '--share=ipc', '--socket=fallback-x11', '--socket=wayland'];
14
- /** Lean defaults for headless CLI tools — no display, no GPU. */
15
- export const DEFAULT_CLI_FINISH_ARGS = [];
16
8
  /** Read package.json from a directory. Throws a helpful error if missing/invalid. */
17
9
  export function readPackageJson(dir) {
18
10
  const path = resolve(dir, 'package.json');
@@ -34,23 +26,6 @@ export function readPackageJson(dir) {
34
26
  export function looksLikeAppId(value) {
35
27
  return typeof value === 'string' && /^[A-Za-z][A-Za-z0-9_-]*(\.[A-Za-z][A-Za-z0-9_-]*){2,}$/.test(value);
36
28
  }
37
- /**
38
- * Pick the runtime + sdk + version triple from config + CLI overrides.
39
- * `--runtime` and `--runtime-version` flags win over config values.
40
- */
41
- export function resolveRuntime(flatpak, overrides) {
42
- const runtime = (overrides.runtime ?? flatpak?.runtime ?? 'gnome');
43
- if (runtime !== 'gnome' && runtime !== 'freedesktop') {
44
- throw new Error(`gjsify flatpak: unknown runtime "${runtime}" (expected "gnome" or "freedesktop")`);
45
- }
46
- const runtimeVersion = overrides.runtimeVersion ??
47
- flatpak?.runtimeVersion ??
48
- (runtime === 'gnome' ? DEFAULT_GNOME_RUNTIME_VERSION : DEFAULT_FREEDESKTOP_RUNTIME_VERSION);
49
- if (runtime === 'gnome') {
50
- return { runtime, runtimeId: 'org.gnome.Platform', sdk: 'org.gnome.Sdk', runtimeVersion };
51
- }
52
- return { runtime, runtimeId: 'org.freedesktop.Platform', sdk: 'org.freedesktop.Sdk', runtimeVersion };
53
- }
54
29
  /** Default container image for the GitHub Actions workflow. */
55
30
  export function defaultCiContainer(runtime, runtimeVersion) {
56
31
  const tag = `${runtime}-${runtimeVersion}`;
@@ -20,7 +20,7 @@
20
20
  // this command, are the later stages of the same ADR — and they are the reason
21
21
  // the split exists at all: a `.dmg` is a UDIF image over a real HFS+/APFS
22
22
  // volume and no Linux host in this tree can write one (§ A1).
23
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
23
+ import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
24
24
  import { basename, dirname, join, relative, resolve } from 'node:path';
25
25
  import { Config } from '../config.js';
26
26
  import { renderDesktopEntry, renderMetainfoApp, renderMetainfoCli, validateAppMetadata, } from '../utils/app-metadata.js';
@@ -29,24 +29,31 @@ import { describeExit, spawnToCompletion } from '../utils/spawn.js';
29
29
  import { buildDeb } from '../utils/ship/deb.js';
30
30
  import { deriveDepends, warnAboutGjsFloor } from '../utils/ship/depends.js';
31
31
  import { discoverPayload } from '../utils/ship/discover.js';
32
- import { resolveFormats, FORMAT_IDS } from '../utils/ship/formats.js';
32
+ import { buildFlatpakBundle } from '../utils/ship/flatpak.js';
33
+ import { assertHostCanFinish, assertToolsInstalled, resolveFormats, DEFAULT_FORMAT_IDS, FORMAT_IDS, } from '../utils/ship/formats.js';
33
34
  import { scanGiNamespaces } from '../utils/ship/gi-namespaces.js';
34
35
  import { assertPayloadMatchesArch, buildTimestamp, isArchIndependent, readPayloadFacts, } from '../utils/ship/payload.js';
35
36
  import { assertOverlayIsLicensed, planOverlay, planStage } from '../utils/ship/plan.js';
36
37
  import { renderLauncher } from '../utils/ship/launcher.js';
37
38
  import { buildRpm } from '../utils/ship/rpm.js';
38
39
  import { declaredBundlePath, resolveShipSettings } from '../utils/ship/settings.js';
39
- import { assertExpectedTarget, assertFormatsStaged, formatTarget, overlayFiles, readStageManifest, resolveStageMtime, warnOnToolSkew, writeStageManifest, } from '../utils/ship/stage-manifest.js';
40
+ import { assertExpectedTarget, assertFormatsStaged, formatTarget, overlayFiles, readStageManifest, resolveStageMtime, warnOnToolSkew, writeStageManifest, STAGE_MANIFEST_FILE, } from '../utils/ship/stage-manifest.js';
40
41
  import { readStage, writeStage } from '../utils/ship/stage-writer.js';
41
42
  const LOG = '[gjsify ship]';
42
43
  export const shipCommand = {
43
44
  command: 'ship',
44
45
  description: `Build installable artifacts (${FORMAT_IDS.join(', ')}) from the current project.`,
46
+ // `DEFAULT_FORMAT_IDS` in the flag text, `FORMAT_IDS` in the description:
47
+ // one of them answers "what can this build", the other "what will it build
48
+ // if I say nothing", and they stopped being the same list when a host-bound
49
+ // format arrived.
45
50
  builder: (yargs) => yargs
46
51
  .option('target', {
47
52
  type: 'string',
48
53
  array: true,
49
- description: `Formats to build, comma-separated or repeated. Default: ${FORMAT_IDS.join(',')}.`,
54
+ description: `Formats to build, comma-separated or repeated. Default: ${DEFAULT_FORMAT_IDS.join(',')}. ` +
55
+ `Also available, and opt-in because it needs tooling this CLI does not carry: ` +
56
+ `${FORMAT_IDS.filter((id) => !DEFAULT_FORMAT_IDS.includes(id)).join(', ')}.`,
50
57
  })
51
58
  .option('out', {
52
59
  type: 'string',
@@ -81,7 +88,8 @@ export const shipCommand = {
81
88
  .option('verbose', {
82
89
  type: 'boolean',
83
90
  default: false,
84
- description: 'Print each staged file and the GI namespaces the bundle imports.',
91
+ description: 'Print each staged file, the GI namespaces the bundle imports, and every tool a ' +
92
+ 'host-bound packer invokes.',
85
93
  }),
86
94
  handler: async (args) => {
87
95
  // The two phases share nothing but `packOne`, and the branch is first
@@ -111,7 +119,15 @@ async function assemble(args) {
111
119
  const pkg = (readPackageJson(join(projectDir, 'package.json')) ?? {});
112
120
  // Resolved before anything is built or written: a typo'd `--target`
113
121
  // should not cost a build and leave a half-written `ship/stage/`.
114
- const formats = resolveFormats(args.target ?? ship.targets ?? FORMAT_IDS);
122
+ const formats = resolveFormats(args.target ?? ship.targets ?? DEFAULT_FORMAT_IDS);
123
+ // BEFORE the build, not before the pack. `gjsify ship` runs the project's
124
+ // own `build` script first, so discovering a missing `flatpak-builder`
125
+ // afterwards costs the whole build for a refusal that was knowable up
126
+ // front. Skipped under `--stage`, which is precisely the phase that does
127
+ // NOT need the format's tooling — that asymmetry is the point of the split.
128
+ if (!args.stage)
129
+ for (const format of formats)
130
+ assertCanPack(format);
115
131
  if (!args['skip-build'])
116
132
  await runProjectBuild(projectDir);
117
133
  const discovered = discoverPayload({
@@ -189,17 +205,26 @@ async function assemble(args) {
189
205
  // paths the payload will have.
190
206
  const facts = readPayloadFacts(staged);
191
207
  for (const format of formats) {
192
- deriveDepends(format.id, {
208
+ // `format.depends`, not `format.id`: a Flatpak has no package
209
+ // dependency list at all, and asking for one would either invent a
210
+ // third column in the typelib table or silently reuse rpm's — which is
211
+ // the exact defect `check-ship-format-vocabulary.mjs`'s header records
212
+ // from the other direction (a third format taking rpm's package name
213
+ // into a Debian `Depends:`, at exit 0).
214
+ if (format.depends === null)
215
+ continue;
216
+ deriveDepends(format.depends, {
193
217
  namespaces,
194
218
  hasIcons: facts.hasIcons,
195
219
  hasSchemas: facts.hasSchemas,
196
- extra: settings.extraDepends[format.id],
220
+ extra: settings.extraDepends[format.depends],
197
221
  typelibPackages: settings.typelibPackages,
198
222
  bundledTypelibs: facts.bundledTypelibs,
199
223
  minGjsVersion: settings.minGjsVersion,
200
224
  });
201
- for (const warning of warnAboutGjsFloor(format.id, settings.minGjsVersion))
225
+ for (const warning of warnAboutGjsFloor(format.depends, settings.minGjsVersion)) {
202
226
  console.warn(`${LOG} ${warning}`);
227
+ }
203
228
  }
204
229
  // Written unconditionally, not only under `--stage`: a `ship/stage/` that
205
230
  // is sometimes packable elsewhere and sometimes not is a worse contract
@@ -222,6 +247,7 @@ async function assemble(args) {
222
247
  outRoot,
223
248
  namespaces,
224
249
  mtime,
250
+ verbose: args.verbose,
225
251
  }));
226
252
  }
227
253
  printArtifacts(projectDir, artifacts);
@@ -256,6 +282,8 @@ async function finishFromStage(args, fromStage) {
256
282
  assertExpectedTarget(manifest, args['expect-target']);
257
283
  const formats = resolveFormats(args.target ?? manifest.formats);
258
284
  assertFormatsStaged(manifest, formats);
285
+ for (const format of formats)
286
+ assertCanPack(format);
259
287
  for (const warning of warnOnToolSkew(manifest))
260
288
  console.warn(`${LOG} ${warning}`);
261
289
  const { mtime, warnings } = resolveStageMtime(manifest);
@@ -275,8 +303,10 @@ async function finishFromStage(args, fromStage) {
275
303
  const outRoot = args.out === undefined ? dirname(stageDir) : resolve(cwd, args.out);
276
304
  const artifacts = [];
277
305
  for (const format of formats) {
278
- for (const warning of warnAboutGjsFloor(format.id, manifest.settings.minGjsVersion)) {
279
- console.warn(`${LOG} ${warning}`);
306
+ if (format.depends !== null) {
307
+ for (const warning of warnAboutGjsFloor(format.depends, manifest.settings.minGjsVersion)) {
308
+ console.warn(`${LOG} ${warning}`);
309
+ }
280
310
  }
281
311
  artifacts.push(await packOne({
282
312
  format,
@@ -288,6 +318,7 @@ async function finishFromStage(args, fromStage) {
288
318
  outRoot,
289
319
  namespaces: manifest.namespaces,
290
320
  mtime,
321
+ verbose: args.verbose,
291
322
  }));
292
323
  }
293
324
  printArtifacts(cwd, artifacts);
@@ -302,15 +333,17 @@ async function packOne(input) {
302
333
  // this answers used to be answered from the project's file lists, which are
303
334
  // absolute paths on the build host and therefore unavailable here.
304
335
  const facts = readPayloadFacts(payload);
305
- const depends = deriveDepends(format.id, {
306
- namespaces: input.namespaces,
307
- hasIcons: facts.hasIcons,
308
- hasSchemas: facts.hasSchemas,
309
- extra: settings.extraDepends[format.id],
310
- typelibPackages: settings.typelibPackages,
311
- bundledTypelibs: facts.bundledTypelibs,
312
- minGjsVersion: settings.minGjsVersion,
313
- });
336
+ const depends = format.depends === null
337
+ ? []
338
+ : deriveDepends(format.depends, {
339
+ namespaces: input.namespaces,
340
+ hasIcons: facts.hasIcons,
341
+ hasSchemas: facts.hasSchemas,
342
+ extra: settings.extraDepends[format.depends],
343
+ typelibPackages: settings.typelibPackages,
344
+ bundledTypelibs: facts.bundledTypelibs,
345
+ minGjsVersion: settings.minGjsVersion,
346
+ });
314
347
  // `--arch` is a CLAIM about the payload and nothing else compared it to one:
315
348
  // an x86-64 `.so` staged with `--arch arm64` packed at exit 0 and `rpm -qp
316
349
  // --qf %{ARCH}` confirmed `aarch64`, because rpm reads the header and the
@@ -327,36 +360,65 @@ async function packOne(input) {
327
360
  assertPayloadMatchesArch(payload, input.arch);
328
361
  const archLabel = format.archName(input.arch, isArchIndependent(payload));
329
362
  const common = { settings, payload, prefix: format.prefix, depends, archLabel, mtime };
363
+ const outDir = join(outRoot, 'out');
364
+ mkdirSync(outDir, { recursive: true });
365
+ const target = join(outDir, format.fileName(settings, archLabel));
330
366
  // A SWITCH with a `never` guard, not a ternary. `format.id === 'deb' ? deb : rpm`
331
367
  // read as a dispatch and behaved as one only while there were exactly two
332
368
  // formats: a third would have taken the else-branch and written an RPM under a
333
369
  // `.dmg` name, at exit 0. This is the same closed-vocabulary hazard the format
334
370
  // list has, one level worse — a wrong ARTIFACT rather than a rejected
335
371
  // declaration — so the compiler owns it now.
336
- let bytes;
372
+ //
373
+ // Each branch WRITES `target` rather than returning bytes. It used to return
374
+ // them, which is the right shape only while every packer is a byte writer: a
375
+ // Flatpak is produced by `flatpak build-bundle`, so its bytes exist as a file
376
+ // before this process could hold them, and reading a bundle back into memory
377
+ // to hand it to one `writeFileSync` would be a copy for the sake of a
378
+ // signature. The size is stat'ed off what landed, for every format.
337
379
  switch (format.id) {
338
380
  case 'deb':
339
- bytes = await buildDeb(common);
381
+ writeFileSync(target, await buildDeb(common));
340
382
  break;
341
383
  case 'rpm':
342
- bytes = await buildRpm(common);
384
+ writeFileSync(target, await buildRpm(common));
385
+ break;
386
+ case 'flatpak':
387
+ await buildFlatpakBundle({
388
+ settings,
389
+ stageDir,
390
+ overlayDir,
391
+ stageManifestFile: STAGE_MANIFEST_FILE,
392
+ workDir: join(outRoot, 'flatpak'),
393
+ target,
394
+ archLabel,
395
+ verbose: input.verbose,
396
+ });
343
397
  break;
344
398
  default: {
345
399
  const unhandled = format.id;
346
400
  throw new Error(`gjsify ship: no packer is wired for format "${String(unhandled)}".`);
347
401
  }
348
402
  }
349
- const outDir = join(outRoot, 'out');
350
- mkdirSync(outDir, { recursive: true });
351
- const target = join(outDir, format.fileName(settings, archLabel));
352
- writeFileSync(target, bytes);
353
- return { format: format.id, path: target, size: bytes.byteLength };
403
+ return { format: format.id, path: target, size: statSync(target).size };
354
404
  }
355
405
  function printArtifacts(base, artifacts) {
356
406
  for (const artifact of artifacts) {
357
407
  console.log(`${LOG} ${artifact.format}: ${relative(base, artifact.path)} (${artifact.size} bytes)`);
358
408
  }
359
409
  }
410
+ /**
411
+ * Refuse a format this host cannot finish, and say which half is missing.
412
+ *
413
+ * Two checks and not one, because the fixes differ: the wrong OS needs another
414
+ * machine, an absent tool needs a package. Both run on the pack path only —
415
+ * `--stage` deliberately needs neither, which is what makes a Flatpak
416
+ * assemblable on a host that cannot build one (ADR 0024 § A1).
417
+ */
418
+ function assertCanPack(format) {
419
+ assertHostCanFinish(format);
420
+ assertToolsInstalled(format);
421
+ }
360
422
  /**
361
423
  * Refuse a build target this command cannot package correctly.
362
424
  *
package/lib/config.js CHANGED
@@ -4,6 +4,7 @@ import { basename, dirname } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
5
  import { isGjs, hostRuntime } from '@gjsify/rolldown-plugin-gjsify/runtime';
6
6
  import { buildAppForRuntime } from './utils/runtimes.js';
7
+ import { DIALECT_APPS, isSourceDialect, SOURCE_DIALECTS } from '@gjsify/rolldown-plugin-gjsify';
7
8
  /**
8
9
  * Does the failed `import()` of a config look like a MODULE-RESOLUTION failure
9
10
  * (the config imports something GJS's ESM loader can't resolve for an external
@@ -268,6 +269,18 @@ export class Config {
268
269
  configData.exclude = cliArgs.exclude || [];
269
270
  if (cliArgs.consoleShim !== undefined)
270
271
  configData.consoleShim = cliArgs.consoleShim;
272
+ // `--dialect` narrows here rather than at the call site. yargs `choices`
273
+ // already rejects an unknown value typed on the command line, but a
274
+ // `gjsify.dialect` in package.json or `.gjsifyrc.*` never passes through
275
+ // yargs — so the one place both spellings meet is the one place that
276
+ // checks, and it names the alternatives instead of silently ignoring.
277
+ if (cliArgs.dialect !== undefined) {
278
+ if (!isSourceDialect(cliArgs.dialect)) {
279
+ throw new Error(`gjsify build: "${cliArgs.dialect}" is not a source dialect. Known: ${SOURCE_DIALECTS.join(', ')}. ` +
280
+ 'This is the dialect the SOURCE is written in — not gjsify.runtimes, which declares the runtime a package RUNS on.');
281
+ }
282
+ configData.dialect = cliArgs.dialect;
283
+ }
271
284
  // Default `--app` FOLLOWS the host runtime the CLI executes in: gjs when
272
285
  // run under gjs, node when run under node/bun/deno (both consume the
273
286
  // `--app node` bundle). Applied post-merge (not as a yargs `default:`)
@@ -278,6 +291,18 @@ export class Config {
278
291
  if (cliArgs.app !== undefined)
279
292
  configData.app = cliArgs.app;
280
293
  configData.app ??= buildAppForRuntime(hostRuntime());
294
+ // A dialect only composes on the targets that have a plugin for it, and
295
+ // saying so is the whole point: `--dialect react-native --app browser`
296
+ // parses, resolves and then composes NOTHING, so the alias never happens,
297
+ // the support gate never runs, and the build succeeds while doing none of
298
+ // what was asked. That is the shape this repo keeps paying for — a flag
299
+ // accepted and ignored. Checked here rather than in the plugin because the
300
+ // plugin only ever sees the targets it was composed on.
301
+ if (configData.dialect !== undefined && !DIALECT_APPS.has(configData.app)) {
302
+ throw new Error(`gjsify build: --dialect ${configData.dialect} has no effect on --app ${configData.app}. ` +
303
+ `It composes on ${[...DIALECT_APPS].join(' and ')} only. ` +
304
+ 'Drop the dialect, or build one of those targets.');
305
+ }
281
306
  if (cliArgs.globals !== undefined)
282
307
  configData.globals = cliArgs.globals;
283
308
  // Fallback applied post-merge (not as a yargs `default:`) so a
@@ -25,6 +25,17 @@ export interface CliBuildOptions {
25
25
  * working ANSI colors). Disable with `--no-console-shim`. Default: true.
26
26
  */
27
27
  consoleShim?: boolean;
28
+ /**
29
+ * The dialect the SOURCE is written in — `--dialect react-native` (ADR 0032):
30
+ * alias `react-native` onto `@gjsify/react-native` and fail the build on an
31
+ * import whose support-table status is not `supported` or `partial`. `gjs` and
32
+ * `node` targets. Default: unset — a tree with a phone leg has the real
33
+ * `react-native` on purpose.
34
+ *
35
+ * A `string` here and a `SourceDialect` in `ConfigData`: yargs hands back
36
+ * whatever was typed, and the narrowing happens once, where the value is read.
37
+ */
38
+ dialect?: string;
28
39
  /**
29
40
  * Comma-separated global identifiers the code needs, e.g.
30
41
  * `"fetch,Buffer,process,URL,crypto"`. Each maps to the corresponding
@@ -1,5 +1,5 @@
1
1
  import type { RolldownOptions, OutputOptions, RolldownPluginOption } from 'rolldown';
2
- import type { App } from '@gjsify/rolldown-plugin-gjsify';
2
+ import type { App, SourceDialect } from '@gjsify/rolldown-plugin-gjsify';
3
3
  import type { ConfigDataLibrary, ConfigDataTypescript } from './index.js';
4
4
  /**
5
5
  * Plugin entry resolvable by package name from the project's `node_modules`, so
@@ -82,6 +82,20 @@ export interface ConfigData {
82
82
  * working ANSI colors). Default: true.
83
83
  */
84
84
  consoleShim?: boolean;
85
+ /**
86
+ * The dialect the SOURCE is written in (ADR 0032 § 2 + § 8). `'react-native'`
87
+ * aliases `react-native` onto `@gjsify/react-native` and gates every named
88
+ * import against the support table at BUILD time. `gjs` and `node` app builds.
89
+ * Default: unset.
90
+ *
91
+ * Opt-in and never inferred from the dependency list: a monorepo with a phone
92
+ * leg installs the real `react-native` on purpose, and redirecting the
93
+ * specifier for every build in that tree would change what that leg resolves.
94
+ *
95
+ * NOT `gjsify.runtimes['react-native']`, which answers the opposite question —
96
+ * see the header of `SourceDialect`.
97
+ */
98
+ dialect?: SourceDialect;
85
99
  /** Comma-separated global identifiers to register. Format: see CliBuildOptions. */
86
100
  globals?: string;
87
101
  /**
@@ -496,8 +510,66 @@ export interface ConfigDataShip extends AppMetadata {
496
510
  extraFiles?: Record<string, string>;
497
511
  /** Arguments the launcher appends before the user's own. */
498
512
  execArgs?: string[];
513
+ /**
514
+ * How `--target flatpak` builds the app: the runtime it links against and
515
+ * what the finished app is allowed to do.
516
+ *
517
+ * The new home of the `gjsify.flatpak` BUILD keys (ADR 0024 § 8). The old
518
+ * spelling still resolves and warns — the window, and what deliberately did
519
+ * NOT move, is `utils/ship/flatpak-config.ts`.
520
+ */
521
+ flatpak?: ShipFlatpakOptions;
499
522
  }
500
- /** Flatpak-toolchain config for the `gjsify flatpak` subcommand group. */
523
+ /**
524
+ * The Flatpak knobs `gjsify ship` reads. Every one has a derived default.
525
+ *
526
+ * There is no `modules` / `extraModules` here, and that is a decision rather
527
+ * than an omission: under `ship` the module list IS the staged payload
528
+ * (`buildsystem: simple` + `cp -a stage/.`), and an escape hatch injecting
529
+ * arbitrary build modules would put the second staging model back in the tree —
530
+ * the one thing ADR 0024 § 8 gates the whole migration on. A project that has
531
+ * to BUILD something inside the sandbox still has `gjsify flatpak init` +
532
+ * `gjsify flatpak build`, unchanged.
533
+ */
534
+ export interface ShipFlatpakOptions {
535
+ /**
536
+ * Runtime family. Default `'gnome'` — a GJS bundle needs GLib/GObject/GIO at
537
+ * runtime, and no GJS interpreter ships in the Freedesktop runtime.
538
+ */
539
+ runtime?: 'gnome' | 'freedesktop';
540
+ /** Runtime/SDK version, e.g. `'50'` for GNOME or `'24.08'` for Freedesktop. */
541
+ runtimeVersion?: string;
542
+ /**
543
+ * The branch the app is exported under — the last segment of
544
+ * `app/<id>/<arch>/<branch>`. Default `'stable'`, which is what Flathub
545
+ * publishes and what `flatpak install` resolves without being told.
546
+ */
547
+ branch?: string;
548
+ /** Extra SDK extensions, e.g. `['org.freedesktop.Sdk.Extension.llvm17']`. */
549
+ sdkExtensions?: string[];
550
+ /**
551
+ * Path components prepended to `PATH` inside the build sandbox. Derived from
552
+ * `sdkExtensions` when unset, because an extension whose `bin` is not on
553
+ * PATH is one the build cannot use.
554
+ */
555
+ appendPath?: string[];
556
+ /** Finish-args (capabilities). Default depends on `kind`: GUI args for an app, none for a CLI. */
557
+ finishArgs?: string[];
558
+ /** Cleanup globs applied to `/app`, e.g. `['/include', '/lib/pkgconfig']`. */
559
+ cleanup?: string[];
560
+ }
561
+ /**
562
+ * Flatpak-toolchain config for the `gjsify flatpak` subcommand group.
563
+ *
564
+ * Six of these keys — `runtime`, `runtimeVersion`, `sdkExtensions`,
565
+ * `appendPath`, `finishArgs`, `cleanup` — have a second home at
566
+ * {@link ShipFlatpakOptions}, and `gjsify ship` warns when it reads them from
567
+ * here (ADR 0024 § 8; the window is `utils/ship/flatpak-config.ts`). They are
568
+ * NOT marked `@deprecated`: for the subcommands in this group they are still
569
+ * the only spelling, and those commands have not moved under `ship` yet
570
+ * (`status/open-todos.md`). Nothing else in this block is deprecated at all —
571
+ * the {@link AppMetadata} half is a designed alias, not a legacy one.
572
+ */
501
573
  export interface ConfigDataFlatpak extends AppMetadata {
502
574
  /** Reverse-DNS app id, e.g. `eu.jumplink.Learn6502`. Defaults to `package.json#name` if it looks like a reverse-DNS id. */
503
575
  appId?: string;