@gjsify/cli 0.64.13 → 0.64.14

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/lib/cli-app.js CHANGED
@@ -18,7 +18,7 @@
18
18
  import yargs from 'yargs';
19
19
  import { classifyCliFailure } from './cli-fail.js';
20
20
  import { cliVersion } from './utils/publish-headers.js';
21
- import { buildCommand as build, clearCommand as clear, copyCommand as copy, envCommand as env, execCommand as exec, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, shipCommand as ship, dlxCommand as dlx, installCommand as install, linkCommand as link, unlinkCommand as unlink, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, pruneCommand as prune, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, siteCommand as site, devCommand as dev, debugCommand as debug, devtoolsCommand as devtools, browseCommand as browse, webextCommand as webext, blueprintCommand as blueprint, } from './commands/index.js';
21
+ import { buildCommand as build, clearCommand as clear, copyCommand as copy, envCommand as env, execCommand as exec, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, shipCommand as ship, dlxCommand as dlx, installCommand as install, linkCommand as link, unlinkCommand as unlink, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, releaseNotesCommand as releaseNotes, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, pruneCommand as prune, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, siteCommand as site, devCommand as dev, debugCommand as debug, devtoolsCommand as devtools, browseCommand as browse, webextCommand as webext, blueprintCommand as blueprint, } from './commands/index.js';
22
22
  import { APP_NAME } from './constants.js';
23
23
  import { isBun, isDeno, isNode, gjsSystemVersion } from '@gjsify/rolldown-plugin-gjsify/runtime';
24
24
  import { ensureGjsifyShimOnPath } from './utils/gjsify-shim.js';
@@ -117,6 +117,7 @@ export async function runCli(argv) {
117
117
  .command(pack.command, pack.description, pack.builder, pack.handler)
118
118
  .command(publish.command, publish.description, publish.builder, publish.handler)
119
119
  .command(whoami.command, whoami.description, whoami.builder, whoami.handler)
120
+ .command(releaseNotes.command, releaseNotes.description, releaseNotes.builder, releaseNotes.handler)
120
121
  .command(login.command, login.description, login.builder, login.handler)
121
122
  .command(logout.command, logout.description, logout.builder, logout.handler)
122
123
  .command(trust.command, trust.description, trust.builder, trust.handler)
@@ -22,6 +22,7 @@ export * from './foreach.js';
22
22
  export * from './workspace.js';
23
23
  export * from './pack.js';
24
24
  export * from './publish.js';
25
+ export * from './release-notes.js';
25
26
  export * from './whoami.js';
26
27
  export * from './login.js';
27
28
  export * from './logout.js';
@@ -22,6 +22,7 @@ export * from './foreach.js';
22
22
  export * from './workspace.js';
23
23
  export * from './pack.js';
24
24
  export * from './publish.js';
25
+ export * from './release-notes.js';
25
26
  export * from './whoami.js';
26
27
  export * from './login.js';
27
28
  export * from './logout.js';
@@ -0,0 +1,7 @@
1
+ import type { Command } from '../types/index.js';
2
+ interface ReleaseNotesOptions {
3
+ release?: string;
4
+ compare?: boolean;
5
+ }
6
+ export declare const releaseNotesCommand: Command<unknown, ReleaseNotesOptions>;
7
+ export {};
@@ -0,0 +1,42 @@
1
+ // `gjsify release-notes [release] [--compare]`
2
+ //
3
+ // Prints one `releases` entry as GitHub Markdown, so a release workflow can pass
4
+ // it to `gh release create --notes-file -` instead of an empty body. Exits 1 when
5
+ // the version has no entry or no description: a release job must fail loudly
6
+ // rather than publish nothing.
7
+ import { Config } from '../config.js';
8
+ import { readPackageJson } from '../utils/pkg-json.js';
9
+ import { renderReleaseNotes, stripVersionPrefix } from '../utils/release-notes.js';
10
+ import { join } from 'node:path';
11
+ export const releaseNotesCommand = {
12
+ command: 'release-notes [release]',
13
+ description: 'Print the `releases` entry for a version as GitHub Markdown. Fails when there is no entry or no description.',
14
+ builder: (yargs) => yargs
15
+ .positional('release', {
16
+ description: 'Version or tag (a leading `v` is accepted). Default: the version in package.json.',
17
+ type: 'string',
18
+ })
19
+ .option('compare', {
20
+ description: 'Append a `**Full changelog:**` compare link to the previous entry (needs `vcsBrowserUrl`).',
21
+ type: 'boolean',
22
+ default: false,
23
+ }),
24
+ handler: async (args) => {
25
+ const projectDir = process.cwd();
26
+ try {
27
+ const config = await new Config().forCommand(projectDir);
28
+ const ship = config.ship ?? {};
29
+ const flatpak = config.flatpak ?? {};
30
+ const version = args.release ?? readPackageJson(join(projectDir, 'package.json'))?.version;
31
+ if (!version)
32
+ throw new Error('no version given and package.json has none');
33
+ const releases = ship.releases ?? flatpak.releases;
34
+ const repoUrl = args.compare === true ? (ship.vcsBrowserUrl ?? flatpak.vcsBrowserUrl) : undefined;
35
+ process.stdout.write(renderReleaseNotes({ releases, version: stripVersionPrefix(version), repoUrl }));
36
+ }
37
+ catch (err) {
38
+ process.stderr.write(`gjsify release-notes: ${err instanceof Error ? err.message : String(err)}\n`);
39
+ return process.exit(1);
40
+ }
41
+ },
42
+ };
@@ -1,4 +1,4 @@
1
- import type { AppMetadata } from '../types/config-data.js';
1
+ import type { AppMetadata, DescriptionBlock } from '../types/config-data.js';
2
2
  /** Everything a renderer needs that is not part of {@link AppMetadata} itself. */
3
3
  export interface AppMetadataInputs {
4
4
  /** Reverse-DNS application id — the MetaInfo `<id>` and the icon/desktop basename. */
@@ -50,4 +50,6 @@ export declare function renderMetainfoCli(inputs: AppMetadataInputs): string;
50
50
  * wins over file types: a URL handler must take `%u`.
51
51
  */
52
52
  export declare function renderDesktopEntry(inputs: AppMetadataInputs): string;
53
+ /** Auto-convert blank-line-split string into paragraph blocks. */
54
+ export declare function stringToBlocks(s: string): DescriptionBlock[];
53
55
  export declare function escapeXml(value: string): string;
@@ -332,7 +332,7 @@ function renderDescriptionBlocks(description, indent) {
332
332
  return out;
333
333
  }
334
334
  /** Auto-convert blank-line-split string into paragraph blocks. */
335
- function stringToBlocks(s) {
335
+ export function stringToBlocks(s) {
336
336
  return s
337
337
  .trim()
338
338
  .split(/\n\n+/)
@@ -281,6 +281,7 @@ export const PACKAGE_DEPS = {
281
281
  '@gjsify/gamepad': ['manette'],
282
282
  '@gjsify/speech-synthesis': ['spiel'],
283
283
  '@gjsify/webaudio': ['gstreamer', 'gst-app'],
284
+ '@gjsify/media-capture': ['gstreamer', 'gst-app'],
284
285
  '@gjsify/iframe': ['webkitgtk'],
285
286
  '@gjsify/canvas2d': ['gdk-pixbuf', 'pango', 'pangocairo', 'cairo'],
286
287
  '@gjsify/canvas2d-core': ['gdk-pixbuf', 'pango', 'pangocairo', 'cairo'],
@@ -0,0 +1,19 @@
1
+ import type { DescriptionBlock } from '../types/config-data.js';
2
+ export interface ReleaseEntry {
3
+ version: string;
4
+ date: string;
5
+ description?: string | DescriptionBlock[];
6
+ }
7
+ /** Paragraphs → paragraphs, lists → `- ` items. Translator hints are dropped. */
8
+ export declare function descriptionToMarkdown(description: string | DescriptionBlock[]): string;
9
+ /** `v0.1.7` and `0.1.7` name the same release. */
10
+ export declare function stripVersionPrefix(version: string): string;
11
+ export interface ReleaseNotesInput {
12
+ releases: readonly ReleaseEntry[] | undefined;
13
+ /** Requested version, with or without a leading `v`. */
14
+ version: string;
15
+ /** Append the compare link when set and a previous release exists. */
16
+ repoUrl?: string;
17
+ }
18
+ /** Throws an `Error` whose message is meant to be printed as-is. */
19
+ export declare function renderReleaseNotes({ releases, version, repoUrl }: ReleaseNotesInput): string;
@@ -0,0 +1,49 @@
1
+ // GitHub-Markdown rendering of one `releases` entry, for `gjsify release-notes`.
2
+ //
3
+ // The AppStream `<release>` blocks and a GitHub release body describe the same
4
+ // release, so both read the one `releases` list in the config. The string →
5
+ // block normalisation is `stringToBlocks` from app-metadata.ts, not a second copy.
6
+ import { stringToBlocks } from './app-metadata.js';
7
+ /** Paragraphs → paragraphs, lists → `- ` items. Translator hints are dropped. */
8
+ export function descriptionToMarkdown(description) {
9
+ const blocks = typeof description === 'string' ? stringToBlocks(description) : description;
10
+ const out = [];
11
+ for (const block of blocks) {
12
+ if ('p' in block) {
13
+ const text = block.p.trim().replace(/\s+/g, ' ');
14
+ if (text)
15
+ out.push(text);
16
+ }
17
+ else {
18
+ const items = block.ul.map((item) => (typeof item === 'string' ? item : item.item).trim()).filter(Boolean);
19
+ if (items.length)
20
+ out.push(items.map((item) => `- ${item}`).join('\n'));
21
+ }
22
+ }
23
+ return out.join('\n\n');
24
+ }
25
+ /** `v0.1.7` and `0.1.7` name the same release. */
26
+ export function stripVersionPrefix(version) {
27
+ return version.trim().replace(/^v/i, '');
28
+ }
29
+ /** Throws an `Error` whose message is meant to be printed as-is. */
30
+ export function renderReleaseNotes({ releases, version, repoUrl }) {
31
+ const wanted = stripVersionPrefix(version);
32
+ const list = releases ?? [];
33
+ const index = list.findIndex((r) => stripVersionPrefix(r.version) === wanted);
34
+ if (index < 0) {
35
+ throw new Error(`no \`releases\` entry for version ${wanted} in the gjsify config` +
36
+ (list.length ? ` (found: ${list.map((r) => r.version).join(', ')})` : ' (no `releases` configured)'));
37
+ }
38
+ const entry = list[index];
39
+ const body = entry.description === undefined ? '' : descriptionToMarkdown(entry.description);
40
+ if (!body)
41
+ throw new Error(`the \`releases\` entry for version ${wanted} has no description`);
42
+ // Most recent first, so the previous release is the next entry.
43
+ const previous = list[index + 1];
44
+ if (!repoUrl || !previous)
45
+ return `${body}\n`;
46
+ const base = repoUrl.replace(/\/+$/, '');
47
+ const compare = `${base}/compare/v${stripVersionPrefix(previous.version)}...v${wanted}`;
48
+ return `${body}\n\n**Full changelog:** ${compare}\n`;
49
+ }
@@ -170,6 +170,13 @@ export interface Layout {
170
170
  * first line is `import Gtk from 'gi://Gtk?version=4.0'`.
171
171
  */
172
172
  shippedRuntime: 'gjs' | 'node';
173
+ /**
174
+ * Set when the artifact's container stores names in a DECOMPOSED form and a
175
+ * seal over those names is byte-exact: every path BELOW the layout root is
176
+ * then staged in that form, so the bytes the seal was made over are the
177
+ * bytes the container hands back. See {@link nameFormBelowRoot}.
178
+ */
179
+ nameForm?: 'NFD';
173
180
  /**
174
181
  * Why the staged launcher cannot name {@link shippedRuntime} yet, or absent
175
182
  * when it already does.
@@ -402,6 +409,17 @@ export declare function assertLayoutSupportsArch(layout: Layout, arch: string):
402
409
  * `lib/<name>/`, because on macOS those two go to different directories.
403
410
  */
404
411
  export declare function place(layout: Layout, identity: LayoutIdentity, rel: string): string;
412
+ /**
413
+ * A staged path with every segment BELOW the layout root put in the layout's
414
+ * {@link Layout.nameForm}; the first segment (`<App>.app`) is left as given.
415
+ *
416
+ * The root is exempt for two reasons: no seal covers its own name, and the
417
+ * packers look the directory up by `${name}.app` on a Linux host whose
418
+ * filesystem compares bytes. Only ever changes non-ASCII names — an ASCII
419
+ * path is returned unchanged, so every bundle that already verified keeps
420
+ * its exact bytes.
421
+ */
422
+ export declare function nameFormBelowRoot(layout: Layout, path: string): string;
405
423
  /**
406
424
  * Apply a layout to a whole planned payload, and add what the layout itself owns.
407
425
  *
@@ -268,6 +268,14 @@ export const LAYOUTS = {
268
268
  'with `PangoCairo.FontMap.get_default().list_families()`, and `PANGOCAIRO_BACKEND=bogus` to ' +
269
269
  'print which backends it was built with.',
270
270
  launcherExt: '',
271
+ // `codesign` seals the bytes of every name inside the bundle, and the `.dmg` is an
272
+ // HFS+ volume (`dmg.ts`), which stores names decomposed. Measured on macOS 27
273
+ // with an app whose display name holds "ä": the bundle sealed with the precomposed
274
+ // `Contents/Helpers/<name>` verifies out of the zip and fails inside the mounted
275
+ // image ("file added / file missing" on that one path), because HFS+ rewrote the
276
+ // name to a + U+0308. APFS keeps the bytes it is given, so staging NFD makes
277
+ // zip and image agree.
278
+ nameForm: 'NFD',
271
279
  root: appBundleDir,
272
280
  // Apple's, all four. `Contents/MacOS` holds executables, `Contents/Resources`
273
281
  // everything that is not, and `Contents/Frameworks` the dylibs — which is
@@ -680,6 +688,26 @@ export function place(layout, identity, rel) {
680
688
  return posix.join(dirs.data, rel.slice('share/'.length));
681
689
  return posix.join(dirs.other, rel);
682
690
  }
691
+ /**
692
+ * A staged path with every segment BELOW the layout root put in the layout's
693
+ * {@link Layout.nameForm}; the first segment (`<App>.app`) is left as given.
694
+ *
695
+ * The root is exempt for two reasons: no seal covers its own name, and the
696
+ * packers look the directory up by `${name}.app` on a Linux host whose
697
+ * filesystem compares bytes. Only ever changes non-ASCII names — an ASCII
698
+ * path is returned unchanged, so every bundle that already verified keeps
699
+ * its exact bytes.
700
+ */
701
+ export function nameFormBelowRoot(layout, path) {
702
+ if (layout.nameForm === undefined)
703
+ return path;
704
+ const form = layout.nameForm;
705
+ // Staged paths are stage-relative and built with `posix.join` on every host.
706
+ const [root, ...below] = path.split(posix.sep);
707
+ if (below.length === 0)
708
+ return path;
709
+ return [root, ...below.map((segment) => segment.normalize(form))].join(posix.sep);
710
+ }
683
711
  /**
684
712
  * Apply a layout to a whole planned payload, and add what the layout itself owns.
685
713
  *
@@ -712,7 +740,8 @@ export function place(layout, identity, rel) {
712
740
  export function placeStage(layout, identity, files, carried = []) {
713
741
  const byPlaced = new Map();
714
742
  const out = [];
715
- const claim = (path, from, file) => {
743
+ const claim = (rawPath, from, file) => {
744
+ const path = nameFormBelowRoot(layout, rawPath);
716
745
  const previous = byPlaced.get(path);
717
746
  if (previous !== undefined) {
718
747
  throw new Error(`gjsify ship: in the ${layout.name} layout both ${previous} and ${from} install as ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.64.13",
3
+ "version": "0.64.14",
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.13",
146
- "@gjsify/buffer": "^0.64.13",
147
- "@gjsify/create-app": "^0.64.13",
148
- "@gjsify/node-globals": "^0.64.13",
149
- "@gjsify/node-polyfills": "^0.64.13",
150
- "@gjsify/npm-registry": "^0.64.13",
151
- "@gjsify/resolve-npm": "^0.64.13",
152
- "@gjsify/rolldown-plugin-gjsify": "^0.64.13",
153
- "@gjsify/vite-plugin-blueprint": "^0.64.13",
154
- "@gjsify/rolldown-plugin-pnp": "^0.64.13",
155
- "@gjsify/semver": "^0.64.13",
156
- "@gjsify/tar": "^0.64.13",
157
- "@gjsify/tsc": "^0.64.13",
158
- "@gjsify/utils": "^0.64.13",
159
- "@gjsify/web-polyfills": "^0.64.13",
160
- "@gjsify/workspace": "^0.64.13",
145
+ "@gjsify/blueprint": "^0.64.14",
146
+ "@gjsify/buffer": "^0.64.14",
147
+ "@gjsify/create-app": "^0.64.14",
148
+ "@gjsify/node-globals": "^0.64.14",
149
+ "@gjsify/node-polyfills": "^0.64.14",
150
+ "@gjsify/npm-registry": "^0.64.14",
151
+ "@gjsify/resolve-npm": "^0.64.14",
152
+ "@gjsify/rolldown-plugin-gjsify": "^0.64.14",
153
+ "@gjsify/vite-plugin-blueprint": "^0.64.14",
154
+ "@gjsify/rolldown-plugin-pnp": "^0.64.14",
155
+ "@gjsify/semver": "^0.64.14",
156
+ "@gjsify/tar": "^0.64.14",
157
+ "@gjsify/tsc": "^0.64.14",
158
+ "@gjsify/utils": "^0.64.14",
159
+ "@gjsify/web-polyfills": "^0.64.14",
160
+ "@gjsify/workspace": "^0.64.14",
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.13",
169
+ "@gjsify/unit": "^0.64.14",
170
170
  "@types/yargs": "^17.0.35",
171
171
  "typescript": "^6.0.3"
172
172
  },
173
173
  "peerDependencies": {
174
- "@gjsify/oxfmt-native": "^0.64.13",
175
- "@gjsify/rolldown-native": "^0.64.13"
174
+ "@gjsify/oxfmt-native": "^0.64.14",
175
+ "@gjsify/rolldown-native": "^0.64.14"
176
176
  },
177
177
  "peerDependenciesMeta": {
178
178
  "@gjsify/oxfmt-native": {