@gjsify/cli 0.64.14 → 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 +82 -82
- package/lib/commands/release-notes.d.ts +1 -0
- package/lib/commands/release-notes.js +24 -6
- package/lib/types/config-data.d.ts +7 -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 +20 -1
- package/lib/utils/release-notes.js +12 -3
- package/package.json +22 -21
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `gjsify release-notes [release] [--compare]`
|
|
1
|
+
// `gjsify release-notes [release] [--compare] [--metainfo <path>]`
|
|
2
2
|
//
|
|
3
3
|
// Prints one `releases` entry as GitHub Markdown, so a release workflow can pass
|
|
4
4
|
// it to `gh release create --notes-file -` instead of an empty body. Exits 1 when
|
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
// rather than publish nothing.
|
|
7
7
|
import { Config } from '../config.js';
|
|
8
8
|
import { readPackageJson } from '../utils/pkg-json.js';
|
|
9
|
-
import { renderReleaseNotes, stripVersionPrefix } from '../utils/release-notes.js';
|
|
10
|
-
import {
|
|
9
|
+
import { renderReleaseNotes, resolveReleases, stripVersionPrefix } from '../utils/release-notes.js';
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { join, resolve } from 'node:path';
|
|
11
12
|
export const releaseNotesCommand = {
|
|
12
13
|
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
|
+
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.',
|
|
14
15
|
builder: (yargs) => yargs
|
|
15
16
|
.positional('release', {
|
|
16
17
|
description: 'Version or tag (a leading `v` is accepted). Default: the version in package.json.',
|
|
@@ -20,6 +21,11 @@ export const releaseNotesCommand = {
|
|
|
20
21
|
description: 'Append a `**Full changelog:**` compare link to the previous entry (needs `vcsBrowserUrl`).',
|
|
21
22
|
type: 'boolean',
|
|
22
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',
|
|
23
29
|
}),
|
|
24
30
|
handler: async (args) => {
|
|
25
31
|
const projectDir = process.cwd();
|
|
@@ -30,9 +36,21 @@ export const releaseNotesCommand = {
|
|
|
30
36
|
const version = args.release ?? readPackageJson(join(projectDir, 'package.json'))?.version;
|
|
31
37
|
if (!version)
|
|
32
38
|
throw new Error('no version given and package.json has none');
|
|
33
|
-
const releases
|
|
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
|
+
});
|
|
34
52
|
const repoUrl = args.compare === true ? (ship.vcsBrowserUrl ?? flatpak.vcsBrowserUrl) : undefined;
|
|
35
|
-
process.stdout.write(renderReleaseNotes({ releases, version: stripVersionPrefix(version), repoUrl }));
|
|
53
|
+
process.stdout.write(renderReleaseNotes({ releases, source, version: stripVersionPrefix(version), repoUrl }));
|
|
36
54
|
}
|
|
37
55
|
catch (err) {
|
|
38
56
|
process.stderr.write(`gjsify release-notes: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
@@ -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.
|
|
@@ -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
|
+
}
|
|
@@ -8,12 +8,31 @@ export interface ReleaseEntry {
|
|
|
8
8
|
export declare function descriptionToMarkdown(description: string | DescriptionBlock[]): string;
|
|
9
9
|
/** `v0.1.7` and `0.1.7` name the same release. */
|
|
10
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;
|
|
11
28
|
export interface ReleaseNotesInput {
|
|
12
29
|
releases: readonly ReleaseEntry[] | undefined;
|
|
30
|
+
/** Where `releases` came from, for the error text. Default: the gjsify config. */
|
|
31
|
+
source?: string;
|
|
13
32
|
/** Requested version, with or without a leading `v`. */
|
|
14
33
|
version: string;
|
|
15
34
|
/** Append the compare link when set and a previous release exists. */
|
|
16
35
|
repoUrl?: string;
|
|
17
36
|
}
|
|
18
37
|
/** Throws an `Error` whose message is meant to be printed as-is. */
|
|
19
|
-
export declare function renderReleaseNotes({ releases, version, repoUrl }: ReleaseNotesInput): string;
|
|
38
|
+
export declare function renderReleaseNotes({ releases, source, version, repoUrl }: ReleaseNotesInput): string;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// release, so both read the one `releases` list in the config. The string →
|
|
5
5
|
// block normalisation is `stringToBlocks` from app-metadata.ts, not a second copy.
|
|
6
6
|
import { stringToBlocks } from './app-metadata.js';
|
|
7
|
+
import { parseMetainfoReleases } from './metainfo-releases.js';
|
|
7
8
|
/** Paragraphs → paragraphs, lists → `- ` items. Translator hints are dropped. */
|
|
8
9
|
export function descriptionToMarkdown(description) {
|
|
9
10
|
const blocks = typeof description === 'string' ? stringToBlocks(description) : description;
|
|
@@ -26,14 +27,22 @@ export function descriptionToMarkdown(description) {
|
|
|
26
27
|
export function stripVersionPrefix(version) {
|
|
27
28
|
return version.trim().replace(/^v/i, '');
|
|
28
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
|
+
}
|
|
29
37
|
/** Throws an `Error` whose message is meant to be printed as-is. */
|
|
30
|
-
export function renderReleaseNotes({ releases, version, repoUrl }) {
|
|
38
|
+
export function renderReleaseNotes({ releases, source, version, repoUrl }) {
|
|
31
39
|
const wanted = stripVersionPrefix(version);
|
|
32
40
|
const list = releases ?? [];
|
|
33
41
|
const index = list.findIndex((r) => stripVersionPrefix(r.version) === wanted);
|
|
34
42
|
if (index < 0) {
|
|
35
|
-
|
|
36
|
-
|
|
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})`));
|
|
37
46
|
}
|
|
38
47
|
const entry = list[index];
|
|
39
48
|
const body = entry.description === undefined ? '' : descriptionToMarkdown(entry.description);
|
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
|
}
|