@gjsify/cli 0.64.13 → 0.64.15-next.287
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 +225 -222
- package/lib/cli-app.js +2 -1
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/release-notes.d.ts +8 -0
- package/lib/commands/release-notes.js +60 -0
- package/lib/types/config-data.d.ts +7 -0
- package/lib/utils/app-metadata.d.ts +3 -1
- package/lib/utils/app-metadata.js +1 -1
- package/lib/utils/check-system-deps.js +1 -0
- package/lib/utils/metainfo-releases.d.ts +3 -0
- package/lib/utils/metainfo-releases.js +95 -0
- package/lib/utils/release-notes.d.ts +38 -0
- package/lib/utils/release-notes.js +58 -0
- package/lib/utils/ship/layout.d.ts +18 -0
- package/lib/utils/ship/layout.js +30 -1
- package/package.json +22 -21
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)
|
package/lib/commands/index.d.ts
CHANGED
package/lib/commands/index.js
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// `gjsify release-notes [release] [--compare] [--metainfo <path>]`
|
|
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, resolveReleases, stripVersionPrefix } from '../utils/release-notes.js';
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { join, resolve } from 'node:path';
|
|
12
|
+
export const releaseNotesCommand = {
|
|
13
|
+
command: 'release-notes [release]',
|
|
14
|
+
description: 'Print the `releases` entry for a version as GitHub Markdown (from the config or a metainfo file). Fails when there is no entry or no description.',
|
|
15
|
+
builder: (yargs) => yargs
|
|
16
|
+
.positional('release', {
|
|
17
|
+
description: 'Version or tag (a leading `v` is accepted). Default: the version in package.json.',
|
|
18
|
+
type: 'string',
|
|
19
|
+
})
|
|
20
|
+
.option('compare', {
|
|
21
|
+
description: 'Append a `**Full changelog:**` compare link to the previous entry (needs `vcsBrowserUrl`).',
|
|
22
|
+
type: 'boolean',
|
|
23
|
+
default: false,
|
|
24
|
+
})
|
|
25
|
+
.option('metainfo', {
|
|
26
|
+
description: 'Read the `<release>` entries from this AppStream metainfo file instead of the config. ' +
|
|
27
|
+
'Default without `releases` in the config: the file named by `gjsify.site.metainfo`.',
|
|
28
|
+
type: 'string',
|
|
29
|
+
}),
|
|
30
|
+
handler: async (args) => {
|
|
31
|
+
const projectDir = process.cwd();
|
|
32
|
+
try {
|
|
33
|
+
const config = await new Config().forCommand(projectDir);
|
|
34
|
+
const ship = config.ship ?? {};
|
|
35
|
+
const flatpak = config.flatpak ?? {};
|
|
36
|
+
const version = args.release ?? readPackageJson(join(projectDir, 'package.json'))?.version;
|
|
37
|
+
if (!version)
|
|
38
|
+
throw new Error('no version given and package.json has none');
|
|
39
|
+
const { releases, source } = resolveReleases({
|
|
40
|
+
metainfo: args.metainfo,
|
|
41
|
+
configReleases: ship.releases ?? flatpak.releases,
|
|
42
|
+
siteMetainfo: config.site?.metainfo,
|
|
43
|
+
readMetainfo: (path) => {
|
|
44
|
+
try {
|
|
45
|
+
return readFileSync(resolve(projectDir, path), 'utf8');
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error(`cannot read the metainfo file ${path}`);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const repoUrl = args.compare === true ? (ship.vcsBrowserUrl ?? flatpak.vcsBrowserUrl) : undefined;
|
|
53
|
+
process.stdout.write(renderReleaseNotes({ releases, source, version: stripVersionPrefix(version), repoUrl }));
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
process.stderr.write(`gjsify release-notes: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
57
|
+
return process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -216,6 +216,13 @@ export interface ConfigData {
|
|
|
216
216
|
* (ADR 0024 § 8) and no project should have to write it twice.
|
|
217
217
|
*/
|
|
218
218
|
ship?: ConfigDataShip;
|
|
219
|
+
/**
|
|
220
|
+
* Config for `gjsify site`, owned and validated by `@gjsify/site`. Only the metainfo path is
|
|
221
|
+
* typed here: `gjsify release-notes` reads releases from it when the config has none.
|
|
222
|
+
*/
|
|
223
|
+
site?: {
|
|
224
|
+
metainfo?: string;
|
|
225
|
+
};
|
|
219
226
|
/**
|
|
220
227
|
* Config for `gjsify format` / `lint` / `fix`. A thin shell: oxc's own
|
|
221
228
|
* `.oxfmtrc.json` / `.oxlintrc.json` are the real configuration files.
|
|
@@ -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,95 @@
|
|
|
1
|
+
// The `<release>` entries of a hand-written AppStream metainfo file, in the shape of the config's
|
|
2
|
+
// `releases`, for `gjsify release-notes`.
|
|
3
|
+
//
|
|
4
|
+
// Apps that keep the file by hand (`gjsify.site.metainfo`) have no `releases` in the config, but the
|
|
5
|
+
// file holds the same data. The tokeniser is the one `gjsify flatpak check` uses; this module only
|
|
6
|
+
// adds the part that one throws away: inline markup, which a release note needs as Markdown.
|
|
7
|
+
import { scanXml } from '@gjsify/utils/core';
|
|
8
|
+
/** Strip the legacy intltool `_` prefix (`<_description>`), which `.metainfo.xml.in` files still use. */
|
|
9
|
+
const normName = (name) => (name.startsWith('_') ? name.slice(1) : name);
|
|
10
|
+
function buildTree(xml) {
|
|
11
|
+
const root = { name: '#root', attrs: {}, nodes: [] };
|
|
12
|
+
const stack = [root];
|
|
13
|
+
for (const token of scanXml(xml)) {
|
|
14
|
+
const top = stack[stack.length - 1];
|
|
15
|
+
if (token.kind === 'text') {
|
|
16
|
+
top.nodes.push(token.text);
|
|
17
|
+
}
|
|
18
|
+
else if (token.kind === 'open') {
|
|
19
|
+
const element = { name: normName(token.name), attrs: token.attributes, nodes: [] };
|
|
20
|
+
top.nodes.push(element);
|
|
21
|
+
if (!token.selfClosing)
|
|
22
|
+
stack.push(element);
|
|
23
|
+
}
|
|
24
|
+
else if (stack.length > 1) {
|
|
25
|
+
stack.pop();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return root;
|
|
29
|
+
}
|
|
30
|
+
const elements = (parent, name) => parent.nodes.filter((node) => typeof node !== 'string' && (name === undefined || node.name === name));
|
|
31
|
+
/** A translation (`xml:lang`) of the untagged element before it; the untagged one is the source. */
|
|
32
|
+
const isTranslation = (element) => element.attrs['xml:lang'] !== undefined;
|
|
33
|
+
const INLINE_WRAP = { em: '*', i: '*', strong: '**', b: '**', code: '`', tt: '`' };
|
|
34
|
+
function inlineMarkdown(element) {
|
|
35
|
+
let out = '';
|
|
36
|
+
for (const node of element.nodes) {
|
|
37
|
+
if (typeof node === 'string') {
|
|
38
|
+
out += node;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (isTranslation(node))
|
|
42
|
+
continue;
|
|
43
|
+
const inner = inlineMarkdown(node);
|
|
44
|
+
const wrap = INLINE_WRAP[node.name];
|
|
45
|
+
if (wrap !== undefined)
|
|
46
|
+
out += inner.trim() === '' ? inner : `${wrap}${inner.trim()}${wrap}`;
|
|
47
|
+
else if (node.name === 'a' && node.attrs.href)
|
|
48
|
+
out += `[${inner}](${node.attrs.href})`;
|
|
49
|
+
else
|
|
50
|
+
out += inner;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
const clean = (text) => text.replace(/\s+/g, ' ').trim();
|
|
55
|
+
function parseBlocks(description) {
|
|
56
|
+
const blocks = [];
|
|
57
|
+
for (const element of elements(description)) {
|
|
58
|
+
if (isTranslation(element))
|
|
59
|
+
continue;
|
|
60
|
+
if (element.name === 'p') {
|
|
61
|
+
const text = clean(inlineMarkdown(element));
|
|
62
|
+
if (text)
|
|
63
|
+
blocks.push({ p: text });
|
|
64
|
+
}
|
|
65
|
+
else if (element.name === 'ul' || element.name === 'ol') {
|
|
66
|
+
const items = elements(element, 'li')
|
|
67
|
+
.filter((li) => !isTranslation(li))
|
|
68
|
+
.map((li) => clean(inlineMarkdown(li)))
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
if (items.length)
|
|
71
|
+
blocks.push({ ul: items });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return blocks;
|
|
75
|
+
}
|
|
76
|
+
/** Document order is kept: metainfo lists the newest release first, which `--compare` relies on. */
|
|
77
|
+
export function parseMetainfoReleases(xml) {
|
|
78
|
+
const component = elements(buildTree(xml), 'component')[0];
|
|
79
|
+
if (!component)
|
|
80
|
+
throw new Error('metainfo root must be <component>');
|
|
81
|
+
const releases = elements(component, 'releases')[0];
|
|
82
|
+
if (!releases)
|
|
83
|
+
return [];
|
|
84
|
+
return elements(releases, 'release').map((release) => {
|
|
85
|
+
const timestamp = release.attrs.timestamp;
|
|
86
|
+
const date = release.attrs.date ?? (timestamp ? new Date(Number(timestamp) * 1000).toISOString().slice(0, 10) : '');
|
|
87
|
+
const description = elements(release, 'description').find((candidate) => !isTranslation(candidate));
|
|
88
|
+
const blocks = description ? parseBlocks(description) : [];
|
|
89
|
+
return {
|
|
90
|
+
version: release.attrs.version ?? '',
|
|
91
|
+
date,
|
|
92
|
+
...(blocks.length ? { description: blocks } : {}),
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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 ReleasesSource {
|
|
12
|
+
/** `--metainfo <path>`: wins over everything. */
|
|
13
|
+
metainfo?: string;
|
|
14
|
+
/** `ship.releases ?? flatpak.releases` from the config. */
|
|
15
|
+
configReleases?: readonly ReleaseEntry[];
|
|
16
|
+
/** `site.metainfo` from the config: read only when nothing above names releases. */
|
|
17
|
+
siteMetainfo?: string;
|
|
18
|
+
/** Reads a metainfo file; throws when it does not exist. */
|
|
19
|
+
readMetainfo: (path: string) => string;
|
|
20
|
+
}
|
|
21
|
+
export interface ResolvedReleases {
|
|
22
|
+
releases: readonly ReleaseEntry[] | undefined;
|
|
23
|
+
/** Names the origin in error messages; unset for the config. */
|
|
24
|
+
source?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Lookup order: `--metainfo`, config `releases`, the metainfo the config names. */
|
|
27
|
+
export declare function resolveReleases(input: ReleasesSource): ResolvedReleases;
|
|
28
|
+
export interface ReleaseNotesInput {
|
|
29
|
+
releases: readonly ReleaseEntry[] | undefined;
|
|
30
|
+
/** Where `releases` came from, for the error text. Default: the gjsify config. */
|
|
31
|
+
source?: string;
|
|
32
|
+
/** Requested version, with or without a leading `v`. */
|
|
33
|
+
version: string;
|
|
34
|
+
/** Append the compare link when set and a previous release exists. */
|
|
35
|
+
repoUrl?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Throws an `Error` whose message is meant to be printed as-is. */
|
|
38
|
+
export declare function renderReleaseNotes({ releases, source, version, repoUrl }: ReleaseNotesInput): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
import { parseMetainfoReleases } from './metainfo-releases.js';
|
|
8
|
+
/** Paragraphs → paragraphs, lists → `- ` items. Translator hints are dropped. */
|
|
9
|
+
export function descriptionToMarkdown(description) {
|
|
10
|
+
const blocks = typeof description === 'string' ? stringToBlocks(description) : description;
|
|
11
|
+
const out = [];
|
|
12
|
+
for (const block of blocks) {
|
|
13
|
+
if ('p' in block) {
|
|
14
|
+
const text = block.p.trim().replace(/\s+/g, ' ');
|
|
15
|
+
if (text)
|
|
16
|
+
out.push(text);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
const items = block.ul.map((item) => (typeof item === 'string' ? item : item.item).trim()).filter(Boolean);
|
|
20
|
+
if (items.length)
|
|
21
|
+
out.push(items.map((item) => `- ${item}`).join('\n'));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return out.join('\n\n');
|
|
25
|
+
}
|
|
26
|
+
/** `v0.1.7` and `0.1.7` name the same release. */
|
|
27
|
+
export function stripVersionPrefix(version) {
|
|
28
|
+
return version.trim().replace(/^v/i, '');
|
|
29
|
+
}
|
|
30
|
+
/** Lookup order: `--metainfo`, config `releases`, the metainfo the config names. */
|
|
31
|
+
export function resolveReleases(input) {
|
|
32
|
+
const path = input.metainfo ?? (input.configReleases ? undefined : input.siteMetainfo);
|
|
33
|
+
if (path === undefined)
|
|
34
|
+
return { releases: input.configReleases };
|
|
35
|
+
return { releases: parseMetainfoReleases(input.readMetainfo(path)), source: path };
|
|
36
|
+
}
|
|
37
|
+
/** Throws an `Error` whose message is meant to be printed as-is. */
|
|
38
|
+
export function renderReleaseNotes({ releases, source, version, repoUrl }) {
|
|
39
|
+
const wanted = stripVersionPrefix(version);
|
|
40
|
+
const list = releases ?? [];
|
|
41
|
+
const index = list.findIndex((r) => stripVersionPrefix(r.version) === wanted);
|
|
42
|
+
if (index < 0) {
|
|
43
|
+
const none = source === undefined ? 'no `releases` configured' : 'no `<release>` entries';
|
|
44
|
+
throw new Error(`no \`releases\` entry for version ${wanted} in ${source ?? 'the gjsify config'}` +
|
|
45
|
+
(list.length ? ` (found: ${list.map((r) => r.version).join(', ')})` : ` (${none})`));
|
|
46
|
+
}
|
|
47
|
+
const entry = list[index];
|
|
48
|
+
const body = entry.description === undefined ? '' : descriptionToMarkdown(entry.description);
|
|
49
|
+
if (!body)
|
|
50
|
+
throw new Error(`the \`releases\` entry for version ${wanted} has no description`);
|
|
51
|
+
// Most recent first, so the previous release is the next entry.
|
|
52
|
+
const previous = list[index + 1];
|
|
53
|
+
if (!repoUrl || !previous)
|
|
54
|
+
return `${body}\n`;
|
|
55
|
+
const base = repoUrl.replace(/\/+$/, '');
|
|
56
|
+
const compare = `${base}/compare/v${stripVersionPrefix(previous.version)}...v${wanted}`;
|
|
57
|
+
return `${body}\n\n**Full changelog:** ${compare}\n`;
|
|
58
|
+
}
|
|
@@ -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
|
*
|
package/lib/utils/ship/layout.js
CHANGED
|
@@ -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 = (
|
|
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.
|
|
3
|
+
"version": "0.64.15-next.287",
|
|
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": "
|
|
146
|
-
"@gjsify/buffer": "
|
|
147
|
-
"@gjsify/create-app": "
|
|
148
|
-
"@gjsify/node-globals": "
|
|
149
|
-
"@gjsify/node-polyfills": "
|
|
150
|
-
"@gjsify/npm-registry": "
|
|
151
|
-
"@gjsify/resolve-npm": "
|
|
152
|
-
"@gjsify/rolldown-plugin-gjsify": "
|
|
153
|
-
"@gjsify/vite-plugin-blueprint": "
|
|
154
|
-
"@gjsify/rolldown-plugin-pnp": "
|
|
155
|
-
"@gjsify/semver": "
|
|
156
|
-
"@gjsify/tar": "
|
|
157
|
-
"@gjsify/tsc": "
|
|
158
|
-
"@gjsify/utils": "
|
|
159
|
-
"@gjsify/web-polyfills": "
|
|
160
|
-
"@gjsify/workspace": "
|
|
145
|
+
"@gjsify/blueprint": "0.64.15-next.287",
|
|
146
|
+
"@gjsify/buffer": "0.64.15-next.287",
|
|
147
|
+
"@gjsify/create-app": "0.64.15-next.287",
|
|
148
|
+
"@gjsify/node-globals": "0.64.15-next.287",
|
|
149
|
+
"@gjsify/node-polyfills": "0.64.15-next.287",
|
|
150
|
+
"@gjsify/npm-registry": "0.64.15-next.287",
|
|
151
|
+
"@gjsify/resolve-npm": "0.64.15-next.287",
|
|
152
|
+
"@gjsify/rolldown-plugin-gjsify": "0.64.15-next.287",
|
|
153
|
+
"@gjsify/vite-plugin-blueprint": "0.64.15-next.287",
|
|
154
|
+
"@gjsify/rolldown-plugin-pnp": "0.64.15-next.287",
|
|
155
|
+
"@gjsify/semver": "0.64.15-next.287",
|
|
156
|
+
"@gjsify/tar": "0.64.15-next.287",
|
|
157
|
+
"@gjsify/tsc": "0.64.15-next.287",
|
|
158
|
+
"@gjsify/utils": "0.64.15-next.287",
|
|
159
|
+
"@gjsify/web-polyfills": "0.64.15-next.287",
|
|
160
|
+
"@gjsify/workspace": "0.64.15-next.287",
|
|
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": "
|
|
169
|
+
"@gjsify/unit": "0.64.15-next.287",
|
|
170
170
|
"@types/yargs": "^17.0.35",
|
|
171
171
|
"typescript": "^6.0.3"
|
|
172
172
|
},
|
|
173
173
|
"peerDependencies": {
|
|
174
|
-
"@gjsify/oxfmt-native": "
|
|
175
|
-
"@gjsify/rolldown-native": "
|
|
174
|
+
"@gjsify/oxfmt-native": "0.64.15-next.287",
|
|
175
|
+
"@gjsify/rolldown-native": "0.64.15-next.287"
|
|
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": "5742b282a8a2bc8633dd8384ef436768447f6a83"
|
|
195
196
|
}
|