@morphemecms/cms 0.14.2 → 0.14.3-next-20261107160925
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/CHANGELOG.md +9 -0
- package/lib/license-texts/Unsplash.txt +7 -0
- package/lib/licenses.d.mts +8 -0
- package/lib/licenses.editions.test.mjs +45 -0
- package/lib/licenses.mjs +40 -0
- package/lib/releaseNotes.d.mts +79 -9
- package/lib/releaseNotes.mjs +136 -21
- package/lib/releaseNotes.test.mjs +187 -5
- package/package.json +1 -1
- package/release-notes/editions/0.14.json +12 -0
- package/release-notes/editions/0.14.webp +0 -0
- package/release-notes/i18n/da-DK/0.14.0.json +12 -11
- package/release-notes/i18n/da-DK/0.14.1.json +3 -2
- package/release-notes/i18n/da-DK/0.14.2.json +5 -4
- package/release-notes/i18n/da-DK/pending/edition-photo-card.json +6 -0
- package/release-notes/i18n/da-DK/pending/edition-photo-card.webp +0 -0
- package/release-notes/i18n/da-DK/pending/short-notification-buttons.json +1 -0
- package/release-notes/i18n/da-DK/pending/version-heading-date-photo.json +5 -0
- package/release-notes/i18n/en-US/0.14.0.json +13 -12
- package/release-notes/i18n/en-US/0.14.1.json +3 -2
- package/release-notes/i18n/en-US/0.14.2.json +5 -4
- package/release-notes/i18n/en-US/pending/edition-photo-card.json +6 -0
- package/release-notes/i18n/en-US/pending/edition-photo-card.webp +0 -0
- package/release-notes/i18n/en-US/pending/short-notification-buttons.json +1 -0
- package/release-notes/i18n/en-US/pending/version-heading-date-photo.json +5 -0
- package/scripts/release-notes.mjs +2 -1
- package/src/App.tsx +4 -1
- package/src/CmsPage.tsx +7 -2
- package/src/Licenses.tsx +4 -0
- package/src/Notifications.tsx +1 -1
- package/src/ReleaseNoteLines.tsx +17 -0
- package/src/ReleaseNoteSection.tsx +12 -0
- package/src/ReleaseNotes.tsx +49 -27
- package/src/Screen.tsx +2 -2
- package/src/UpdatesNotice.tsx +104 -69
- package/src/VersionLinePage.tsx +42 -0
- package/src/bundledNotes.ts +40 -5
- package/src/editor.css +4 -1
- package/src/i18n/da-DK/cms.ts +4 -0
- package/src/i18n/da-DK/library.ts +7 -4
- package/src/i18n/en-US/cms.ts +7 -0
- package/src/i18n/en-US/library.ts +12 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @morphemecms/cms
|
|
2
2
|
|
|
3
|
+
## 0.14.3-next-20261107160925
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ed6e436: The release notes on the Morpheme page and the start screen are rewritten in plainer words, in English and Danish.
|
|
8
|
+
- aa478c9: After an update, the start screen shows a small card with the edition's photo and name instead of the large list of news.
|
|
9
|
+
- 8b7e313: The buttons under Notifications are now Clear and Settings.
|
|
10
|
+
- b1e7393: The Morpheme page names each version after its edition, with its date and photo, and a test version shows the changes merged since the last release.
|
|
11
|
+
|
|
3
12
|
## 0.14.2
|
|
4
13
|
|
|
5
14
|
### Patch Changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Unsplash grants you an irrevocable, nonexclusive, worldwide copyright license to
|
|
2
|
+
download, copy, modify, distribute, perform, and use photos from Unsplash for free,
|
|
3
|
+
including for commercial purposes, without permission from or attributing the photographer
|
|
4
|
+
or Unsplash. This license does not include the right to compile photos from Unsplash to
|
|
5
|
+
replicate a similar or competing service.
|
|
6
|
+
|
|
7
|
+
https://unsplash.com/license
|
package/lib/licenses.d.mts
CHANGED
|
@@ -98,6 +98,12 @@ export function declarationProblems({ license, thirdParty, folder, where, what,
|
|
|
98
98
|
* checked with what it gives in place of what a package lacks (declarationProblems); those they depend on are not.
|
|
99
99
|
* @param {string} root @param {string[]} folders @param {Set<string>} [skip] */
|
|
100
100
|
export function codePackageProblems(root: string, folders: string[], skip?: Set<string>): any[];
|
|
101
|
+
/** The photo of each edition the CMS carries (release-notes/editions/<line>.json and .webp, one per minor line, shown on
|
|
102
|
+
* the start screen after an update) as a work in "Licenser": a picture in the editor, its id `editions/<line>`, named after its place
|
|
103
|
+
* (en-US), by its photographer under the Unsplash License (license-texts/Unsplash.txt). An edition without a photographer
|
|
104
|
+
* gives a work without an author, which workProblems stops the build on.
|
|
105
|
+
* @param {string} cmsRoot the CMS package's folder @returns {object[]} */
|
|
106
|
+
export function editionWorks(cmsRoot: string): object[];
|
|
101
107
|
/** An app or a theme as licensesIntegration takes it: its kind and id, its name, version and who made it, its manifest
|
|
102
108
|
* (its defineApp or its theme.json, with `license` and `thirdParty`), its folder, and whether the site is built with it
|
|
103
109
|
* (an app installed; the theme in use, and the standard theme it builds on).
|
|
@@ -147,6 +153,8 @@ export const UNLICENSED: "UNLICENSED";
|
|
|
147
153
|
export const OWN_LICENSE_FILE: "LICENSE";
|
|
148
154
|
export function packageName(specifier: any): any;
|
|
149
155
|
export function ownerId(kind: any, id: any): string;
|
|
156
|
+
/** The licence of the editions' photos (release-notes/editions/), as "Licenser" shows it. */
|
|
157
|
+
export const UNSPLASH_LICENSE: "Unsplash License";
|
|
150
158
|
/**
|
|
151
159
|
* An app or a theme as licensesIntegration takes it: its kind and id, its name, version and who made it, its manifest
|
|
152
160
|
* (its defineApp or its theme.json, with `license` and `thirdParty`), its folder, and whether the site is built with it
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Tests editionWorks in licenses.mjs: each edition's photo as a picture in the editor, under the Unsplash License.
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { editionWorks, UNSPLASH_LICENSE, workProblems } from './licenses.mjs';
|
|
8
|
+
|
|
9
|
+
const root = (editions) => {
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), 'editions-'));
|
|
11
|
+
mkdirSync(join(dir, 'release-notes', 'editions'), { recursive: true });
|
|
12
|
+
for (const [version, edition] of Object.entries(editions))
|
|
13
|
+
writeFileSync(join(dir, 'release-notes', 'editions', `${version}.json`), JSON.stringify(edition));
|
|
14
|
+
return dir;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
test("an edition's photo is a picture in the editor, by its photographer, under the Unsplash License", () => {
|
|
18
|
+
const [work] = editionWorks(
|
|
19
|
+
root({ 0.14: { name: 'Braies', place: { 'en-US': 'Lago di Braies, Italy' }, photo: { by: 'A Photographer' } } }),
|
|
20
|
+
);
|
|
21
|
+
assert.equal(work.id, 'editions/0.14');
|
|
22
|
+
assert.equal(work.name, 'Lago di Braies, Italy');
|
|
23
|
+
assert.equal(work.author, 'A Photographer');
|
|
24
|
+
assert.equal(work.license, UNSPLASH_LICENSE);
|
|
25
|
+
assert.equal(work.kind, 'image');
|
|
26
|
+
assert.deepEqual(work.where, ['editor']);
|
|
27
|
+
assert.match(work.text, /^Unsplash grants you an irrevocable/);
|
|
28
|
+
assert.deepEqual(workProblems(work, 'x'), []);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('an edition without a photographer is a problem', () => {
|
|
32
|
+
const [work] = editionWorks(root({ 0.14: { name: 'Braies' } }));
|
|
33
|
+
assert.equal(work.name, 'Braies');
|
|
34
|
+
assert.equal(workProblems(work, 'x').length, 1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('no editions folder gives no works', () => {
|
|
38
|
+
assert.deepEqual(editionWorks(mkdtempSync(join(tmpdir(), 'none-'))), []);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("the CMS's own editions all have a photographer", () => {
|
|
42
|
+
const works = editionWorks(new URL('..', import.meta.url).pathname);
|
|
43
|
+
assert.ok(works.length > 0);
|
|
44
|
+
for (const work of works) assert.deepEqual(workProblems(work, work.id), []);
|
|
45
|
+
});
|
package/lib/licenses.mjs
CHANGED
|
@@ -433,6 +433,40 @@ const HELPERS = [
|
|
|
433
433
|
];
|
|
434
434
|
const OWN_VIRTUAL = [/^\0virtual:(app-folders|all-icons|material-symbols|theme-coverage)$/, /^\0site-theme-icons?(:|$)/, /^\0app-self:/];
|
|
435
435
|
|
|
436
|
+
// ---------------------------------------------------------------------------------------------------
|
|
437
|
+
// The editions' photos
|
|
438
|
+
|
|
439
|
+
/** The licence of the editions' photos (release-notes/editions/), as "Licenser" shows it. */
|
|
440
|
+
export const UNSPLASH_LICENSE = 'Unsplash License';
|
|
441
|
+
|
|
442
|
+
/** The photo of each edition the CMS carries (release-notes/editions/<line>.json and .webp, one per minor line, shown on
|
|
443
|
+
* the start screen after an update) as a work in "Licenser": a picture in the editor, its id `editions/<line>`, named after its place
|
|
444
|
+
* (en-US), by its photographer under the Unsplash License (license-texts/Unsplash.txt). An edition without a photographer
|
|
445
|
+
* gives a work without an author, which workProblems stops the build on.
|
|
446
|
+
* @param {string} cmsRoot the CMS package's folder @returns {object[]} */
|
|
447
|
+
export function editionWorks(cmsRoot) {
|
|
448
|
+
const dir = join(cmsRoot, 'release-notes', 'editions');
|
|
449
|
+
if (!existsSync(dir)) return [];
|
|
450
|
+
const text = read(fileURLToPath(new URL('./license-texts/Unsplash.txt', import.meta.url)));
|
|
451
|
+
return readdirSync(dir)
|
|
452
|
+
.filter((f) => f.endsWith('.json'))
|
|
453
|
+
.sort()
|
|
454
|
+
.map((f) => {
|
|
455
|
+
const line = f.slice(0, -'.json'.length);
|
|
456
|
+
const edition = JSON.parse(readFileSync(join(dir, f), 'utf8'));
|
|
457
|
+
return {
|
|
458
|
+
id: `editions/${line}`,
|
|
459
|
+
name: edition.place?.['en-US'] ?? edition.name,
|
|
460
|
+
version: null,
|
|
461
|
+
author: edition.photo?.by ?? null,
|
|
462
|
+
license: UNSPLASH_LICENSE,
|
|
463
|
+
text,
|
|
464
|
+
kind: 'image',
|
|
465
|
+
where: ['editor'],
|
|
466
|
+
};
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
436
470
|
/** An app or a theme as licensesIntegration takes it: its kind and id, its name, version and who made it, its manifest
|
|
437
471
|
* (its defineApp or its theme.json, with `license` and `thirdParty`), its folder, and whether the site is built with it
|
|
438
472
|
* (an app installed; the theme in use, and the standard theme it builds on).
|
|
@@ -628,6 +662,12 @@ export function licensesIntegration({ root, iconPacks, owners }) {
|
|
|
628
662
|
if (pack.theme) packWorks.set(pack.theme, [...(packWorks.get(pack.theme) ?? []), ...own.map((w) => w.id)]);
|
|
629
663
|
}
|
|
630
664
|
|
|
665
|
+
// The editions' photos, which the editor shows after an update.
|
|
666
|
+
for (const work of editionWorks(fileURLToPath(new URL('..', import.meta.url)))) {
|
|
667
|
+
problems.push(...workProblems(work, `Udgaven ${work.id}`));
|
|
668
|
+
add(work);
|
|
669
|
+
}
|
|
670
|
+
|
|
631
671
|
// The apps' and the themes' own licences and what they declare (checked by npm run check:content, with what they
|
|
632
672
|
// give in place of what a package lacks), shipped or not; and what is shipped through them alone.
|
|
633
673
|
const owned = [];
|
package/lib/releaseNotes.d.mts
CHANGED
|
@@ -16,9 +16,10 @@ export function problemsOf(pending: Pending): string[];
|
|
|
16
16
|
* ids of the cards that have a screenshot. A locale without notes is left out.
|
|
17
17
|
* @param {Pending} pending
|
|
18
18
|
* @param {string} version
|
|
19
|
+
* @param {string} [date] the day it is released (YYYY-MM-DD)
|
|
19
20
|
* @returns {Record<string, { file: ReleasedNotes, images: string[] }>}
|
|
20
21
|
*/
|
|
21
|
-
export function released(pending: Pending, version: string): Record<string, {
|
|
22
|
+
export function released(pending: Pending, version: string, date?: string): Record<string, {
|
|
22
23
|
file: ReleasedNotes;
|
|
23
24
|
images: string[];
|
|
24
25
|
}>;
|
|
@@ -37,10 +38,18 @@ export function imagePath(version: string, locale: string, id: string): string;
|
|
|
37
38
|
* @returns {ReleaseNote[] | undefined}
|
|
38
39
|
*/
|
|
39
40
|
export function notesFor(notes: Record<string, ReleaseNote[]> | undefined, locale: string): ReleaseNote[] | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* The day a test version was made, from its snapshot id (0.14.3-next-20261107145215 → 2026-11-07); undefined for another
|
|
43
|
+
* version.
|
|
44
|
+
* @param {string} version
|
|
45
|
+
* @returns {string | undefined}
|
|
46
|
+
*/
|
|
47
|
+
export function testVersionDate(version: string): string | undefined;
|
|
40
48
|
/**
|
|
41
49
|
* The notes that are new since `from` (the version seen last), in `locale` (else English): those of the entries newer than
|
|
42
50
|
* it, the newest version first, its cards before its lines. A test version (0.14.1-next.2) has not had the notes of its
|
|
43
|
-
* own version yet.
|
|
51
|
+
* own version yet. An entry of a test version (its merged, not yet released notes) counts as its version without the
|
|
52
|
+
* pre-release. Nothing when `from` is not a version.
|
|
44
53
|
* @param {{ version: string, notes?: Record<string, ReleaseNote[]> }[]} entries
|
|
45
54
|
* @param {string | undefined} from
|
|
46
55
|
* @param {string} locale
|
|
@@ -51,21 +60,67 @@ export function notesSince(entries: {
|
|
|
51
60
|
notes?: Record<string, ReleaseNote[]>;
|
|
52
61
|
}[], from: string | undefined, locale: string): ReleaseNote[];
|
|
53
62
|
/**
|
|
54
|
-
* The
|
|
55
|
-
* (
|
|
56
|
-
*
|
|
57
|
-
*
|
|
63
|
+
* The changelog's versions (CHANGELOG.md, as Changesets writes it), each with its changes as lines: a bullet's text
|
|
64
|
+
* without its commit ("- 3fbb65f: The start screen …" → "The start screen …", its id the commit) and code marks, with the
|
|
65
|
+
* lines it runs on to. Newest first; a version that is not x.y.z is left out.
|
|
66
|
+
* @param {string} changelog
|
|
67
|
+
* @returns {{ version: string, lines: ReleaseNote[] }[]}
|
|
68
|
+
*/
|
|
69
|
+
export function changelogSections(changelog: string): {
|
|
70
|
+
version: string;
|
|
71
|
+
lines: ReleaseNote[];
|
|
72
|
+
}[];
|
|
73
|
+
/**
|
|
74
|
+
* The notes of every released version up to `version`, from the files the package carries
|
|
75
|
+
* (release-notes/i18n/<locale>/<version>.json), and before them from its changelog, newest first. A card gets the address
|
|
76
|
+
* of its screenshot (<locale>/<version>/<id>.webp) when that file exists. Newer versions and versions without notes are left out.
|
|
77
|
+
* A test version (0.14.2-next.0, the npm tag next) has not had the notes of its own version yet: the line up to the one before,
|
|
78
|
+
* then, first, its merged notes (`pending`). Each released version has the day it was released (its file's `date`).
|
|
58
79
|
* It runs as the editor loads (bundledNotes.ts), so it never throws: nothing when `version` is not a version.
|
|
59
80
|
* @param {Record<string, ReleasedNotes>} files path -> parsed notes file, e.g. '../release-notes/i18n/en-US/0.14.0.json'
|
|
60
81
|
* @param {Record<string, string>} images path -> address, e.g. '../release-notes/i18n/en-US/0.14.0/<id>.webp'
|
|
61
82
|
* @param {string} version the running version
|
|
62
|
-
* @
|
|
83
|
+
* @param {Record<string, ReleaseNote>} [pending] path -> a note not released yet, e.g. '../release-notes/i18n/en-US/pending/<id>.json':
|
|
84
|
+
* on a test version, the notes merged since the last release, as the first entry, under the running version, dated by
|
|
85
|
+
* its snapshot id (testVersionDate); a card's screenshot <locale>/pending/<id>.webp
|
|
86
|
+
* @param {string} [changelog] the package's CHANGELOG.md: the versions without a notes file (those before the notes),
|
|
87
|
+
* with their changes as lines, in English (changelogSections)
|
|
88
|
+
* @returns {{ version: string, text: string, date?: string, notes: Record<string, ReleaseNote[]> }[]}
|
|
63
89
|
*/
|
|
64
|
-
export function bundledNotes(files: Record<string, ReleasedNotes>, images: Record<string, string>, version: string): {
|
|
90
|
+
export function bundledNotes(files: Record<string, ReleasedNotes>, images: Record<string, string>, version: string, pending?: Record<string, ReleaseNote>, changelog?: string): {
|
|
65
91
|
version: string;
|
|
66
92
|
text: string;
|
|
93
|
+
date?: string;
|
|
67
94
|
notes: Record<string, ReleaseNote[]>;
|
|
68
95
|
}[];
|
|
96
|
+
/**
|
|
97
|
+
* The entries grouped by minor line, newest line first, each line's entries in their order (bundledNotes'): one page each.
|
|
98
|
+
* @template {{ version: string }} T
|
|
99
|
+
* @param {T[]} entries
|
|
100
|
+
* @returns {{ line: string, entries: T[] }[]}
|
|
101
|
+
*/
|
|
102
|
+
export function versionLines<T extends {
|
|
103
|
+
version: string;
|
|
104
|
+
}>(entries: T[]): {
|
|
105
|
+
line: string;
|
|
106
|
+
entries: T[];
|
|
107
|
+
}[];
|
|
108
|
+
/** @typedef {{ line: string, name: string, place: Record<string, string>, by: string | null, image: string }} Edition line: its minor line ("0.14"); name: the place its photo is from, short ("Braies"); place: in full, per locale; by: its photographer; image: the photo's address */
|
|
109
|
+
/**
|
|
110
|
+
* The edition of a version's minor line: one photo per line (release-notes/editions/<line>.json with its photo
|
|
111
|
+
* <line>.webp, e.g. 0.14.json for every 0.14 version, a test version 0.14.3-next-… too).
|
|
112
|
+
* @param {Record<string, { name?: string, place?: Record<string, string>, photo?: { by?: string } }>} files path -> the edition's file
|
|
113
|
+
* @param {Record<string, string>} images path -> the photo's address
|
|
114
|
+
* @param {string} version
|
|
115
|
+
* @returns {Edition | undefined} undefined when its line has none, or it has no name or no photo
|
|
116
|
+
*/
|
|
117
|
+
export function editionOf(files: Record<string, {
|
|
118
|
+
name?: string;
|
|
119
|
+
place?: Record<string, string>;
|
|
120
|
+
photo?: {
|
|
121
|
+
by?: string;
|
|
122
|
+
};
|
|
123
|
+
}>, images: Record<string, string>, version: string): Edition | undefined;
|
|
69
124
|
/**
|
|
70
125
|
* The release notes and screenshots of `version` from an unzipped npm tar. Notes: package/release-notes/i18n/<locale>/<version>.json
|
|
71
126
|
* per locale. Screenshots: one per card, at imagePath(). A missing or unreadable notes file leaves its locale out; a missing
|
|
@@ -109,7 +164,7 @@ export function parseImagePath(pathname: string): {
|
|
|
109
164
|
*/
|
|
110
165
|
export function withImageUrls(version: string, notes: Record<string, ReleaseNote[]> | undefined): Record<string, ReleaseNote[]> | undefined;
|
|
111
166
|
/** @typedef {{ id: string, kind: 'card', title: string, text: string, image?: string } | { id: string, kind: 'line', text: string }} ReleaseNote */
|
|
112
|
-
/** @typedef {{ version: string, notes: ReleaseNote[] }} ReleasedNotes */
|
|
167
|
+
/** @typedef {{ version: string, date?: string, notes: ReleaseNote[] }} ReleasedNotes date: the day it was released (YYYY-MM-DD) */
|
|
113
168
|
/** @typedef {{ width: number, height: number }} ImageSize */
|
|
114
169
|
/** @typedef {{ note: object, imageBytes: number | null, imageSize: ImageSize | null }} PendingEntry imageBytes: size of <id>.webp in bytes, null when there is none; imageSize: its pixels, null when it is not a WebP */
|
|
115
170
|
/** @typedef {Record<string, Record<string, PendingEntry>>} Pending locale -> id -> entry */
|
|
@@ -118,8 +173,19 @@ export const MAX_IMAGE_BYTES: number;
|
|
|
118
173
|
/** The width of a card's screenshot in pixels (375 CSS pixels at scale 2) and its greatest height (400 CSS pixels at scale 2). */
|
|
119
174
|
export const IMAGE_WIDTH: 750;
|
|
120
175
|
export const MAX_IMAGE_HEIGHT: 800;
|
|
176
|
+
export function minorOf(version: string): string;
|
|
121
177
|
export function staleImageDirs(dirs: string[], version: string): string[];
|
|
122
178
|
export function imageKey(version: string, locale: string, id: string): string;
|
|
179
|
+
/**
|
|
180
|
+
* line: its minor line ("0.14"); name: the place its photo is from, short ("Braies"); place: in full, per locale; by: its photographer; image: the photo's address
|
|
181
|
+
*/
|
|
182
|
+
export type Edition = {
|
|
183
|
+
line: string;
|
|
184
|
+
name: string;
|
|
185
|
+
place: Record<string, string>;
|
|
186
|
+
by: string | null;
|
|
187
|
+
image: string;
|
|
188
|
+
};
|
|
123
189
|
export type ReleaseNote = {
|
|
124
190
|
id: string;
|
|
125
191
|
kind: "card";
|
|
@@ -131,8 +197,12 @@ export type ReleaseNote = {
|
|
|
131
197
|
kind: "line";
|
|
132
198
|
text: string;
|
|
133
199
|
};
|
|
200
|
+
/**
|
|
201
|
+
* date: the day it was released (YYYY-MM-DD)
|
|
202
|
+
*/
|
|
134
203
|
export type ReleasedNotes = {
|
|
135
204
|
version: string;
|
|
205
|
+
date?: string;
|
|
136
206
|
notes: ReleaseNote[];
|
|
137
207
|
};
|
|
138
208
|
export type ImageSize = {
|
package/lib/releaseNotes.mjs
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// picks a locale's notes and those new since a version, builds the notes of the running minor line from the package files and reads a version's notes and screenshots from its tar.
|
|
3
3
|
// Also builds the Worker's screenshot address and KV key, parses the address and adds the address to a version's cards.
|
|
4
4
|
// Exposes MAX_IMAGE_BYTES, IMAGE_WIDTH, MAX_IMAGE_HEIGHT, webpSize, problemsOf, released, imagePath, notesFor, notesSince, bundledNotes, staleImageDirs, notesFromTar, imageUrl, imageKey,
|
|
5
|
-
// parseImagePath and
|
|
5
|
+
// parseImagePath, withImageUrls, editionOf, testVersionDate, changelogSections, minorOf and versionLines. Depends on ./cms-version.mjs and ./changelog.mjs; no node: imports.
|
|
6
6
|
import { compareVersions } from './cms-version.mjs';
|
|
7
7
|
import { bytesFromTar, fileFromTar } from './changelog.mjs';
|
|
8
8
|
|
|
9
9
|
/** @typedef {{ id: string, kind: 'card', title: string, text: string, image?: string } | { id: string, kind: 'line', text: string }} ReleaseNote */
|
|
10
|
-
/** @typedef {{ version: string, notes: ReleaseNote[] }} ReleasedNotes */
|
|
10
|
+
/** @typedef {{ version: string, date?: string, notes: ReleaseNote[] }} ReleasedNotes date: the day it was released (YYYY-MM-DD) */
|
|
11
11
|
/** @typedef {{ width: number, height: number }} ImageSize */
|
|
12
12
|
/** @typedef {{ note: object, imageBytes: number | null, imageSize: ImageSize | null }} PendingEntry imageBytes: size of <id>.webp in bytes, null when there is none; imageSize: its pixels, null when it is not a WebP */
|
|
13
13
|
/** @typedef {Record<string, Record<string, PendingEntry>>} Pending locale -> id -> entry */
|
|
@@ -118,15 +118,16 @@ export function problemsOf(pending) {
|
|
|
118
118
|
* ids of the cards that have a screenshot. A locale without notes is left out.
|
|
119
119
|
* @param {Pending} pending
|
|
120
120
|
* @param {string} version
|
|
121
|
+
* @param {string} [date] the day it is released (YYYY-MM-DD)
|
|
121
122
|
* @returns {Record<string, { file: ReleasedNotes, images: string[] }>}
|
|
122
123
|
*/
|
|
123
|
-
export function released(pending, version) {
|
|
124
|
+
export function released(pending, version, date) {
|
|
124
125
|
const result = {};
|
|
125
126
|
for (const [locale, entries] of Object.entries(pending)) {
|
|
126
127
|
const ids = Object.keys(entries).sort();
|
|
127
128
|
if (ids.length === 0) continue;
|
|
128
129
|
result[locale] = {
|
|
129
|
-
file: { version, notes: ids.map((id) => entries[id].note) },
|
|
130
|
+
file: { version, ...(date ? { date } : {}), notes: ids.map((id) => entries[id].note) },
|
|
130
131
|
images: ids.filter((id) => entries[id].note.kind === 'card'),
|
|
131
132
|
};
|
|
132
133
|
}
|
|
@@ -155,11 +156,25 @@ export function notesFor(notes, locale) {
|
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
const PLAIN = /^\d+\.\d+\.\d+$/;
|
|
159
|
+
/** A version without its pre-release (0.14.3-next-20261107145215 → 0.14.3). */
|
|
160
|
+
const baseOf = (version) => String(version ?? '').split('-')[0];
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The day a test version was made, from its snapshot id (0.14.3-next-20261107145215 → 2026-11-07); undefined for another
|
|
164
|
+
* version.
|
|
165
|
+
* @param {string} version
|
|
166
|
+
* @returns {string | undefined}
|
|
167
|
+
*/
|
|
168
|
+
export function testVersionDate(version) {
|
|
169
|
+
const m = /-next-(\d{4})(\d{2})(\d{2})\d*$/.exec(String(version ?? ''));
|
|
170
|
+
return m ? `${m[1]}-${m[2]}-${m[3]}` : undefined;
|
|
171
|
+
}
|
|
158
172
|
|
|
159
173
|
/**
|
|
160
174
|
* The notes that are new since `from` (the version seen last), in `locale` (else English): those of the entries newer than
|
|
161
175
|
* it, the newest version first, its cards before its lines. A test version (0.14.1-next.2) has not had the notes of its
|
|
162
|
-
* own version yet.
|
|
176
|
+
* own version yet. An entry of a test version (its merged, not yet released notes) counts as its version without the
|
|
177
|
+
* pre-release. Nothing when `from` is not a version.
|
|
163
178
|
* @param {{ version: string, notes?: Record<string, ReleaseNote[]> }[]} entries
|
|
164
179
|
* @param {string | undefined} from
|
|
165
180
|
* @param {string} locale
|
|
@@ -169,43 +184,143 @@ export function notesSince(entries, from, locale) {
|
|
|
169
184
|
const [base, pre] = String(from ?? '').split('-');
|
|
170
185
|
if (!PLAIN.test(base)) return [];
|
|
171
186
|
const notes = entries
|
|
172
|
-
.filter((entry) => PLAIN.test(entry.version) && compareVersions(entry.version, base) >= (pre === undefined ? 1 : 0))
|
|
173
|
-
.sort((a, b) => compareVersions(b.version, a.version))
|
|
187
|
+
.filter((entry) => PLAIN.test(baseOf(entry.version)) && compareVersions(baseOf(entry.version), base) >= (pre === undefined ? 1 : 0))
|
|
188
|
+
.sort((a, b) => compareVersions(baseOf(b.version), baseOf(a.version)))
|
|
174
189
|
.flatMap((entry) => notesFor(entry.notes, locale) ?? []);
|
|
175
190
|
return [...notes.filter((note) => note.kind === 'card'), ...notes.filter((note) => note.kind === 'line')];
|
|
176
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* The changelog's versions (CHANGELOG.md, as Changesets writes it), each with its changes as lines: a bullet's text
|
|
194
|
+
* without its commit ("- 3fbb65f: The start screen …" → "The start screen …", its id the commit) and code marks, with the
|
|
195
|
+
* lines it runs on to. Newest first; a version that is not x.y.z is left out.
|
|
196
|
+
* @param {string} changelog
|
|
197
|
+
* @returns {{ version: string, lines: ReleaseNote[] }[]}
|
|
198
|
+
*/
|
|
199
|
+
export function changelogSections(changelog) {
|
|
200
|
+
// Markdown's code marks are not shown: the page writes plain text.
|
|
201
|
+
const plain = (text) => text.replace(/`([^`]*)`/g, '$1').trim();
|
|
202
|
+
const sections = [];
|
|
203
|
+
let current = null;
|
|
204
|
+
let last = null;
|
|
205
|
+
for (const raw of String(changelog ?? '').split(/\r?\n/)) {
|
|
206
|
+
if (raw.startsWith('## ')) {
|
|
207
|
+
const version = raw.slice(3).trim();
|
|
208
|
+
current = PLAIN.test(version) ? { version, lines: [] } : null;
|
|
209
|
+
if (current) sections.push(current);
|
|
210
|
+
last = null;
|
|
211
|
+
} else if (current && /^- /.test(raw)) {
|
|
212
|
+
const m = /^- (?:([0-9a-f]{7,40}): )?(.*)$/.exec(raw);
|
|
213
|
+
last = { id: `${current.version}-${m[1] ?? current.lines.length}`, kind: 'line', text: plain(m[2]) };
|
|
214
|
+
current.lines.push(last);
|
|
215
|
+
} else if (last && /^\s+\S/.test(raw)) {
|
|
216
|
+
last.text = `${last.text} ${plain(raw)}`;
|
|
217
|
+
} else if (raw.trim() === '' || raw.startsWith('#')) {
|
|
218
|
+
last = null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return sections.sort((a, b) => compareVersions(b.version, a.version));
|
|
222
|
+
}
|
|
223
|
+
|
|
177
224
|
const sameMinor = (a, b) => a.split('.').slice(0, 2).join('.') === b.split('.').slice(0, 2).join('.');
|
|
178
225
|
|
|
179
226
|
/**
|
|
180
|
-
* The notes of every released version
|
|
181
|
-
* (release-notes/i18n/<locale>/<version>.json), newest first. A card gets the address
|
|
182
|
-
* (<locale>/<version>/<id>.webp) when that file exists.
|
|
183
|
-
* A test version (0.14.2-next.0, the npm tag next) has not had the notes of its own version yet: the line up to the one before
|
|
227
|
+
* The notes of every released version up to `version`, from the files the package carries
|
|
228
|
+
* (release-notes/i18n/<locale>/<version>.json), and before them from its changelog, newest first. A card gets the address
|
|
229
|
+
* of its screenshot (<locale>/<version>/<id>.webp) when that file exists. Newer versions and versions without notes are left out.
|
|
230
|
+
* A test version (0.14.2-next.0, the npm tag next) has not had the notes of its own version yet: the line up to the one before,
|
|
231
|
+
* then, first, its merged notes (`pending`). Each released version has the day it was released (its file's `date`).
|
|
184
232
|
* It runs as the editor loads (bundledNotes.ts), so it never throws: nothing when `version` is not a version.
|
|
185
233
|
* @param {Record<string, ReleasedNotes>} files path -> parsed notes file, e.g. '../release-notes/i18n/en-US/0.14.0.json'
|
|
186
234
|
* @param {Record<string, string>} images path -> address, e.g. '../release-notes/i18n/en-US/0.14.0/<id>.webp'
|
|
187
235
|
* @param {string} version the running version
|
|
188
|
-
* @
|
|
236
|
+
* @param {Record<string, ReleaseNote>} [pending] path -> a note not released yet, e.g. '../release-notes/i18n/en-US/pending/<id>.json':
|
|
237
|
+
* on a test version, the notes merged since the last release, as the first entry, under the running version, dated by
|
|
238
|
+
* its snapshot id (testVersionDate); a card's screenshot <locale>/pending/<id>.webp
|
|
239
|
+
* @param {string} [changelog] the package's CHANGELOG.md: the versions without a notes file (those before the notes),
|
|
240
|
+
* with their changes as lines, in English (changelogSections)
|
|
241
|
+
* @returns {{ version: string, text: string, date?: string, notes: Record<string, ReleaseNote[]> }[]}
|
|
189
242
|
*/
|
|
190
|
-
export function bundledNotes(files, images, version) {
|
|
243
|
+
export function bundledNotes(files, images, version, pending = {}, changelog = '') {
|
|
191
244
|
const [base, pre] = String(version ?? '').split('-');
|
|
192
245
|
if (!PLAIN.test(base)) return [];
|
|
193
246
|
const byVersion = {};
|
|
247
|
+
const dates = {};
|
|
248
|
+
const withImage = (note, locale, folder) => {
|
|
249
|
+
if (note?.kind !== 'card') return note;
|
|
250
|
+
const image = Object.entries(images).find(([imagePath]) => imagePath.endsWith(`/i18n/${locale}/${folder}/${note.id}.webp`));
|
|
251
|
+
return image ? { ...note, image: image[1] } : note;
|
|
252
|
+
};
|
|
194
253
|
for (const [path, file] of Object.entries(files)) {
|
|
195
254
|
const match = /\/i18n\/([^/]+)\/([^/]+)\.json$/.exec(path);
|
|
196
255
|
if (!match || !Array.isArray(file?.notes)) continue;
|
|
197
256
|
const [, locale, v] = match;
|
|
198
|
-
if (!PLAIN.test(v) ||
|
|
257
|
+
if (!PLAIN.test(v) || compareVersions(v, base) >= (pre === undefined ? 1 : 0)) continue;
|
|
199
258
|
byVersion[v] ??= {};
|
|
200
|
-
byVersion[v][locale] = file.notes.map((note) =>
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
259
|
+
byVersion[v][locale] = file.notes.map((note) => withImage(note, locale, v));
|
|
260
|
+
if (typeof file.date === 'string') dates[v] ??= file.date;
|
|
261
|
+
}
|
|
262
|
+
// The versions from before the notes files: their changelog lines, in English.
|
|
263
|
+
for (const section of changelogSections(changelog)) {
|
|
264
|
+
const v = section.version;
|
|
265
|
+
if (byVersion[v] || compareVersions(v, base) >= (pre === undefined ? 1 : 0) || !section.lines.length) continue;
|
|
266
|
+
byVersion[v] = { 'en-US': section.lines };
|
|
205
267
|
}
|
|
206
|
-
|
|
268
|
+
const entries = Object.keys(byVersion)
|
|
207
269
|
.sort((a, b) => compareVersions(b, a))
|
|
208
|
-
.map((v) => ({ version: v, text: '', notes: byVersion[v] }));
|
|
270
|
+
.map((v) => ({ version: v, text: '', ...(dates[v] ? { date: dates[v] } : {}), notes: byVersion[v] }));
|
|
271
|
+
if (pre === undefined) return entries;
|
|
272
|
+
// A test version: the notes merged since the last release, by locale, sorted by id as release:version will.
|
|
273
|
+
const merged = {};
|
|
274
|
+
for (const [path, note] of Object.entries(pending).sort(([a], [b]) => a.localeCompare(b))) {
|
|
275
|
+
const locale = /\/i18n\/([^/]+)\/pending\/[^/]+\.json$/.exec(path)?.[1];
|
|
276
|
+
if (!locale || !note?.kind) continue;
|
|
277
|
+
(merged[locale] ??= []).push(withImage(note, locale, 'pending'));
|
|
278
|
+
}
|
|
279
|
+
if (!Object.keys(merged).length) return entries;
|
|
280
|
+
const date = testVersionDate(version);
|
|
281
|
+
return [{ version, text: '', ...(date ? { date } : {}), notes: merged }, ...entries];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A version's minor line, as its page names it (0.14.3-next-… → "0.14").
|
|
286
|
+
* @param {string} version
|
|
287
|
+
* @returns {string}
|
|
288
|
+
*/
|
|
289
|
+
export const minorOf = (version) => baseOf(version).split('.').slice(0, 2).join('.');
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The entries grouped by minor line, newest line first, each line's entries in their order (bundledNotes'): one page each.
|
|
293
|
+
* @template {{ version: string }} T
|
|
294
|
+
* @param {T[]} entries
|
|
295
|
+
* @returns {{ line: string, entries: T[] }[]}
|
|
296
|
+
*/
|
|
297
|
+
export function versionLines(entries) {
|
|
298
|
+
const lines = new Map();
|
|
299
|
+
for (const entry of entries) {
|
|
300
|
+
const line = minorOf(entry.version);
|
|
301
|
+
if (!lines.has(line)) lines.set(line, []);
|
|
302
|
+
lines.get(line).push(entry);
|
|
303
|
+
}
|
|
304
|
+
return [...lines].map(([line, list]) => ({ line, entries: list })).sort((a, b) => compareVersions(`${b.line}.0`, `${a.line}.0`));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** @typedef {{ line: string, name: string, place: Record<string, string>, by: string | null, image: string }} Edition line: its minor line ("0.14"); name: the place its photo is from, short ("Braies"); place: in full, per locale; by: its photographer; image: the photo's address */
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* The edition of a version's minor line: one photo per line (release-notes/editions/<line>.json with its photo
|
|
311
|
+
* <line>.webp, e.g. 0.14.json for every 0.14 version, a test version 0.14.3-next-… too).
|
|
312
|
+
* @param {Record<string, { name?: string, place?: Record<string, string>, photo?: { by?: string } }>} files path -> the edition's file
|
|
313
|
+
* @param {Record<string, string>} images path -> the photo's address
|
|
314
|
+
* @param {string} version
|
|
315
|
+
* @returns {Edition | undefined} undefined when its line has none, or it has no name or no photo
|
|
316
|
+
*/
|
|
317
|
+
export function editionOf(files, images, version) {
|
|
318
|
+
if (!PLAIN.test(baseOf(version))) return undefined;
|
|
319
|
+
const line = minorOf(version);
|
|
320
|
+
const file = Object.entries(files).find(([path]) => path.endsWith(`/editions/${line}.json`))?.[1];
|
|
321
|
+
const image = Object.entries(images).find(([path]) => path.endsWith(`/editions/${line}.webp`))?.[1];
|
|
322
|
+
if (!file || !image || !isFilled(file.name)) return undefined;
|
|
323
|
+
return { line, name: file.name.trim(), place: file.place ?? {}, by: file.photo?.by ?? null, image };
|
|
209
324
|
}
|
|
210
325
|
|
|
211
326
|
/**
|