@morphemecms/cms 0.3.0 → 0.4.0-next-20261104182038
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 +14 -0
- package/README.md +1 -1
- package/bin/morpheme.mjs +1 -1
- package/defaults/editor.ts +14 -1
- package/icons/lucide/LICENSE-material-design-icons +19 -19
- package/icons/material-symbols/LICENSE-material-design-icons +19 -19
- package/icons/phosphor/LICENSE +20 -20
- package/lib/changelog.d.mts +24 -0
- package/lib/changelog.mjs +65 -0
- package/lib/changelog.test.d.mts +1 -0
- package/lib/changelog.test.mjs +67 -0
- package/lib/cms-version.d.mts +56 -0
- package/lib/cms-version.mjs +85 -0
- package/lib/cms-version.test.d.mts +1 -0
- package/lib/cms-version.test.mjs +68 -0
- package/lib/lockfile.d.mts +19 -0
- package/lib/lockfile.mjs +135 -0
- package/lib/lockfile.test.d.mts +1 -0
- package/lib/lockfile.test.mjs +88 -0
- package/lib/range.d.mts +4 -0
- package/lib/range.mjs +104 -0
- package/lib/range.test.d.mts +1 -0
- package/lib/range.test.mjs +46 -0
- package/lib/side-block.test.mjs +1 -1
- package/package.json +7 -1
- package/scripts/built-site.mjs +14 -0
- package/scripts/check-cms-update.mjs +416 -0
- package/site/icons.mjs +3 -1
- package/site/theme.d.mts +1 -1
- package/site/theme.mjs +2 -2
- package/src/App.tsx +3 -0
- package/src/CmsPage.tsx +140 -0
- package/src/EntryForm.tsx +6 -2
- package/src/Library.tsx +2 -0
- package/src/SideColumn.tsx +6 -1
- package/src/appFiles.ts +2 -2
- package/src/apps.ts +2 -2
- package/src/blocks.ts +1 -1
- package/src/cmsView.d.mts +15 -0
- package/src/cmsView.mjs +81 -0
- package/src/cmsView.test.d.mts +1 -0
- package/src/cmsView.test.mjs +152 -0
- package/src/frontmatter.mjs +4 -1
- package/src/frontmatter.test.mjs +6 -0
- package/src/i18n/da-DK/cms.ts +31 -0
- package/src/i18n/da-DK/common.ts +1 -0
- package/src/i18n/da-DK/index.ts +2 -0
- package/src/i18n/en-US/cms.ts +35 -0
- package/src/i18n/en-US/common.ts +2 -0
- package/src/i18n/en-US/index.ts +2 -0
- package/src/registry.tsx +3 -3
- package/src/schema.ts +1 -1
- package/starter/.prettierignore +1 -0
- package/starter/README.md +1 -0
- package/starter/package.json +1 -1
- package/starter/wrangler.jsonc +1 -1
- package/theme/layouts/PageLayout.astro +2 -1
- package/worker/cms-update.js +246 -0
- package/worker/github.js +8 -6
- package/worker/index.js +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @morphemecms/cms
|
|
2
2
|
|
|
3
|
+
## 0.4.0-next-20261104182038
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- cf46cb1: An app's block and pop-up name their view module `viewOf.ts` (it was `view.ts`), so it no longer collides with `View.tsx` on Windows.
|
|
8
|
+
- 1fcbd56: The site updates Morpheme by itself: a new minor or patch version on npm is installed within the hour and undone when the site cannot be built. See Settings › Morpheme.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- dd9ab22: Settings › Morpheme says when the site has no KV storage (PUSH), and no longer fails to open when the site runs a pre-release of Morpheme.
|
|
13
|
+
- cf9f144: Keep the line breaks of a file's front matter (CRLF or LF) when the editor writes it back.
|
|
14
|
+
- 6cb2c17: `check:worker` passes when the site runs a pre-release of Morpheme (as the staging site does).
|
|
15
|
+
- 682da97: New sites from the starter can reach the Morpheme store on the same Cloudflare account, and Prettier skips store theme files.
|
|
16
|
+
|
|
3
17
|
## 0.3.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ Hele vejledningen står i `docs/editor.md` i sitets repo. Her er kun det, et sit
|
|
|
41
41
|
- `@site/config`: sitets navn, domæne, GitHub-repo og mapper.
|
|
42
42
|
- `@site/worker` (til Workeren): `apps`, `contact`, `moved`, `standard`, `oldAddresses` (sitets videresendelser fra gamle adresser, `{ ids, paths }`) og appenes del af Workeren (`appWorkers`, `appTexts`).
|
|
43
43
|
- **`@site`** for `src/site/`, ellers `defaults/`: i Vite (`siteFile` i `themePlugin`, `site/theme.mjs`, brugt af `astro.mjs` og `scripts/check-content.mjs`, der indlæser reglerne med Vite) og i TypeScript (`paths` i `tsconfig.json`: `"@site/*": ["./src/site/*", "./node_modules/@morphemecms/cms/defaults/*"]`); til Workeren `@site/config` og `@site/worker` i `alias` i `wrangler.jsonc`, med `main` sat til pakkens `worker/index.js`.
|
|
44
|
-
- **Sitets egne blokke** i appenes `blocks/<type>/` (`src/apps/<app>/blocks/<type>/`), hver med `index.tsx` (`defineBlock` fra `@morphemecms/cms/src/blocks`), `
|
|
44
|
+
- **Sitets egne blokke** i appenes `blocks/<type>/` (`src/apps/<app>/blocks/<type>/`), hver med `index.tsx` (`defineBlock` fra `@morphemecms/cms/src/blocks`), `viewOf.ts` og `load.ts` (det, blokken viser, i editoren og på sitet) og `View.tsx` (tegningen, begge steder). Pakken finder dem selv.
|
|
45
45
|
- **Et standardtema**, som alle sitets temaer bygger på, med standardblokkenes udseende (`blocks/<type>/View.tsx`, én tegning til sitet og editoren), som pakken selv finder (dette site: `packages/editor/theme/`, `packages/editor/src/standard/siteLooks.ts` og `packages/editor/src/standard/themeBlocks.ts`).
|
|
46
46
|
- **Siderne** er pakkens (`pages/`, sat ind af `siteConfig()` i `astro.mjs`): sitets sider, bloggen, 404, `/ny/` og editoren (siden, der viser `src/App.tsx`, manifestet, `version.json` og billederne, sitets egen kode bruger). Sitets `astro.config.mjs` er kun `export default await siteConfig()`, og `src/content.config.ts` henter samlingerne fra `site/content.config`.
|
|
47
47
|
- **Standardblokkenes stil:** `blocks.css` i standardtemaet (dette site: `packages/editor/theme/styles/blocks.css`), importeret i hvert temas CSS, og variablerne `--block-*` sat til temaets farver og former.
|
package/bin/morpheme.mjs
CHANGED
|
@@ -12,7 +12,7 @@ const COMMANDS = {
|
|
|
12
12
|
'after-build': ['scheduled-list', 'link-counts', 'page-colors'],
|
|
13
13
|
'check:content': ['check-content'],
|
|
14
14
|
'check:editor': ['editor-roundtrip'],
|
|
15
|
-
'check:worker': ['check-worker', 'check-consent', 'check-store'],
|
|
15
|
+
'check:worker': ['check-worker', 'check-consent', 'check-store', 'check-cms-update'],
|
|
16
16
|
'check:screens': ['editor-screens'],
|
|
17
17
|
'check:themes': ['check-themes'],
|
|
18
18
|
'check:demo': ['check-demo'],
|
package/defaults/editor.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
Signpost,
|
|
44
44
|
Smiley,
|
|
45
45
|
SlidersHorizontal,
|
|
46
|
+
Sparkle,
|
|
46
47
|
SquareHalfBottom,
|
|
47
48
|
SquaresFour,
|
|
48
49
|
Sticker,
|
|
@@ -126,6 +127,7 @@ export const icons: Record<string, Icon> = {
|
|
|
126
127
|
blokvalg: ToggleRight,
|
|
127
128
|
apps: PuzzlePiece,
|
|
128
129
|
appstore: Storefront,
|
|
130
|
+
morpheme: Sparkle,
|
|
129
131
|
avanceret: Wrench,
|
|
130
132
|
licenser: Certificate,
|
|
131
133
|
// The screens under "Udseende" (Library.tsx Theme).
|
|
@@ -156,7 +158,18 @@ export const groups: { title: string; ids: string[] }[] = [
|
|
|
156
158
|
|
|
157
159
|
/** The settings of the whole site, not on the start screen but on "Indstillinger" (#indstillinger, Library.tsx
|
|
158
160
|
* Settings), which the gear in the top bar opens (Screen.tsx SettingsLink); A–Å as a group's. */
|
|
159
|
-
export const settings: string[] = [
|
|
161
|
+
export const settings: string[] = [
|
|
162
|
+
'udseende',
|
|
163
|
+
'apps',
|
|
164
|
+
'appstore',
|
|
165
|
+
'morpheme',
|
|
166
|
+
'maaling',
|
|
167
|
+
'promovering',
|
|
168
|
+
'privatliv',
|
|
169
|
+
'seo',
|
|
170
|
+
'avanceret',
|
|
171
|
+
'licenser',
|
|
172
|
+
];
|
|
160
173
|
|
|
161
174
|
/** The site's own short lists, on a screen of their own under "Lister" (#lister), with those the standard blocks
|
|
162
175
|
* bring ("Udtalelser") and the apps (their lists.ts). */
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
Pictogrammers Free License
|
|
2
|
-
--------------------------
|
|
3
|
-
|
|
4
|
-
This icon collection is released as free, open source, and GPL friendly by
|
|
5
|
-
the [Pictogrammers](http://pictogrammers.com/) icon group. You may use it
|
|
6
|
-
for commercial projects, open source projects, or anything really.
|
|
7
|
-
|
|
8
|
-
# Icons: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
|
|
9
|
-
Some of the icons are redistributed under the Apache 2.0 license. All other
|
|
10
|
-
icons are either redistributed under their respective licenses or are
|
|
11
|
-
distributed under the Apache 2.0 license.
|
|
12
|
-
|
|
13
|
-
# Fonts: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
|
|
14
|
-
All web and desktop fonts are distributed under the Apache 2.0 license. Web
|
|
15
|
-
and desktop fonts contain some icons that are redistributed under the Apache
|
|
16
|
-
2.0 license. All other icons are either redistributed under their respective
|
|
17
|
-
licenses or are distributed under the Apache 2.0 license.
|
|
18
|
-
|
|
19
|
-
# Code: MIT (https://opensource.org/licenses/MIT)
|
|
1
|
+
Pictogrammers Free License
|
|
2
|
+
--------------------------
|
|
3
|
+
|
|
4
|
+
This icon collection is released as free, open source, and GPL friendly by
|
|
5
|
+
the [Pictogrammers](http://pictogrammers.com/) icon group. You may use it
|
|
6
|
+
for commercial projects, open source projects, or anything really.
|
|
7
|
+
|
|
8
|
+
# Icons: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
|
|
9
|
+
Some of the icons are redistributed under the Apache 2.0 license. All other
|
|
10
|
+
icons are either redistributed under their respective licenses or are
|
|
11
|
+
distributed under the Apache 2.0 license.
|
|
12
|
+
|
|
13
|
+
# Fonts: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
|
|
14
|
+
All web and desktop fonts are distributed under the Apache 2.0 license. Web
|
|
15
|
+
and desktop fonts contain some icons that are redistributed under the Apache
|
|
16
|
+
2.0 license. All other icons are either redistributed under their respective
|
|
17
|
+
licenses or are distributed under the Apache 2.0 license.
|
|
18
|
+
|
|
19
|
+
# Code: MIT (https://opensource.org/licenses/MIT)
|
|
20
20
|
The MIT license applies to all non-font and non-icon files.
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
Pictogrammers Free License
|
|
2
|
-
--------------------------
|
|
3
|
-
|
|
4
|
-
This icon collection is released as free, open source, and GPL friendly by
|
|
5
|
-
the [Pictogrammers](http://pictogrammers.com/) icon group. You may use it
|
|
6
|
-
for commercial projects, open source projects, or anything really.
|
|
7
|
-
|
|
8
|
-
# Icons: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
|
|
9
|
-
Some of the icons are redistributed under the Apache 2.0 license. All other
|
|
10
|
-
icons are either redistributed under their respective licenses or are
|
|
11
|
-
distributed under the Apache 2.0 license.
|
|
12
|
-
|
|
13
|
-
# Fonts: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
|
|
14
|
-
All web and desktop fonts are distributed under the Apache 2.0 license. Web
|
|
15
|
-
and desktop fonts contain some icons that are redistributed under the Apache
|
|
16
|
-
2.0 license. All other icons are either redistributed under their respective
|
|
17
|
-
licenses or are distributed under the Apache 2.0 license.
|
|
18
|
-
|
|
19
|
-
# Code: MIT (https://opensource.org/licenses/MIT)
|
|
1
|
+
Pictogrammers Free License
|
|
2
|
+
--------------------------
|
|
3
|
+
|
|
4
|
+
This icon collection is released as free, open source, and GPL friendly by
|
|
5
|
+
the [Pictogrammers](http://pictogrammers.com/) icon group. You may use it
|
|
6
|
+
for commercial projects, open source projects, or anything really.
|
|
7
|
+
|
|
8
|
+
# Icons: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
|
|
9
|
+
Some of the icons are redistributed under the Apache 2.0 license. All other
|
|
10
|
+
icons are either redistributed under their respective licenses or are
|
|
11
|
+
distributed under the Apache 2.0 license.
|
|
12
|
+
|
|
13
|
+
# Fonts: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
|
|
14
|
+
All web and desktop fonts are distributed under the Apache 2.0 license. Web
|
|
15
|
+
and desktop fonts contain some icons that are redistributed under the Apache
|
|
16
|
+
2.0 license. All other icons are either redistributed under their respective
|
|
17
|
+
licenses or are distributed under the Apache 2.0 license.
|
|
18
|
+
|
|
19
|
+
# Code: MIT (https://opensource.org/licenses/MIT)
|
|
20
20
|
The MIT license applies to all non-font and non-icon files.
|
package/icons/phosphor/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2023 Phosphor Icons
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Phosphor Icons
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unzips gzip bytes.
|
|
3
|
+
* @param {Uint8Array} bytes
|
|
4
|
+
* @returns {Promise<Uint8Array>}
|
|
5
|
+
*/
|
|
6
|
+
export function gunzip(bytes: Uint8Array): Promise<Uint8Array>;
|
|
7
|
+
/**
|
|
8
|
+
* The UTF-8 text of the entry named exactly `path` in a tar, or null when there is none.
|
|
9
|
+
* @param {Uint8Array} tar
|
|
10
|
+
* @param {string} path
|
|
11
|
+
* @returns {string | null}
|
|
12
|
+
*/
|
|
13
|
+
export function fileFromTar(tar: Uint8Array, path: string): string | null;
|
|
14
|
+
/**
|
|
15
|
+
* The changelog's sections for versions after `from` up to and including `to`, newest first, without empty ones.
|
|
16
|
+
* @param {string} changelog
|
|
17
|
+
* @param {string} from
|
|
18
|
+
* @param {string} to
|
|
19
|
+
* @returns {{ version: string, text: string }[]}
|
|
20
|
+
*/
|
|
21
|
+
export function sectionsBetween(changelog: string, from: string, to: string): {
|
|
22
|
+
version: string;
|
|
23
|
+
text: string;
|
|
24
|
+
}[];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// A version's release notes, read from its npm tarball: unzips the tarball, finds CHANGELOG.md in the tar and cuts out the
|
|
2
|
+
// sections between two versions. Exposes gunzip, fileFromTar and sectionsBetween. Depends on ./cms-version.mjs; no node: imports
|
|
3
|
+
// (runs in a Cloudflare Worker and in Node 18+).
|
|
4
|
+
import { compareVersions } from './cms-version.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Unzips gzip bytes.
|
|
8
|
+
* @param {Uint8Array} bytes
|
|
9
|
+
* @returns {Promise<Uint8Array>}
|
|
10
|
+
*/
|
|
11
|
+
export async function gunzip(bytes) {
|
|
12
|
+
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'));
|
|
13
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The text of a NUL-terminated field of a tar header. */
|
|
17
|
+
function field(header, start, end) {
|
|
18
|
+
const bytes = header.subarray(start, end);
|
|
19
|
+
const nul = bytes.indexOf(0);
|
|
20
|
+
return new TextDecoder().decode(nul === -1 ? bytes : bytes.subarray(0, nul));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The UTF-8 text of the entry named exactly `path` in a tar, or null when there is none.
|
|
25
|
+
* @param {Uint8Array} tar
|
|
26
|
+
* @param {string} path
|
|
27
|
+
* @returns {string | null}
|
|
28
|
+
*/
|
|
29
|
+
export function fileFromTar(tar, path) {
|
|
30
|
+
let offset = 0;
|
|
31
|
+
while (offset + 512 <= tar.length) {
|
|
32
|
+
const header = tar.subarray(offset, offset + 512);
|
|
33
|
+
if (header.every((byte) => byte === 0)) return null;
|
|
34
|
+
const name = field(header, 0, 100);
|
|
35
|
+
const prefix = field(header, 345, 500);
|
|
36
|
+
const size = parseInt(field(header, 124, 136).trim(), 8) || 0;
|
|
37
|
+
const data = tar.subarray(offset + 512, offset + 512 + size);
|
|
38
|
+
if ((prefix ? prefix + '/' + name : name) === path) return new TextDecoder().decode(data);
|
|
39
|
+
offset += 512 + Math.ceil(size / 512) * 512;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The changelog's sections for versions after `from` up to and including `to`, newest first, without empty ones.
|
|
46
|
+
* @param {string} changelog
|
|
47
|
+
* @param {string} from
|
|
48
|
+
* @param {string} to
|
|
49
|
+
* @returns {{ version: string, text: string }[]}
|
|
50
|
+
*/
|
|
51
|
+
export function sectionsBetween(changelog, from, to) {
|
|
52
|
+
const sections = [];
|
|
53
|
+
let current = null;
|
|
54
|
+
for (const line of changelog.split(/\r?\n/)) {
|
|
55
|
+
if (line.startsWith('## ')) {
|
|
56
|
+
current = { version: line.slice(3).trim(), lines: [] };
|
|
57
|
+
sections.push(current);
|
|
58
|
+
} else if (current) current.lines.push(line);
|
|
59
|
+
}
|
|
60
|
+
return sections
|
|
61
|
+
.filter(({ version }) => /^\d+\.\d+\.\d+$/.test(version) && compareVersions(version, from) === 1 && compareVersions(version, to) <= 0)
|
|
62
|
+
.map(({ version, lines }) => ({ version, text: lines.join('\n').trim() }))
|
|
63
|
+
.filter(({ text }) => text !== '')
|
|
64
|
+
.sort((a, b) => compareVersions(b.version, a.version));
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Tests for lib/changelog.mjs: a version's release notes read from its npm tarball.
|
|
2
|
+
// Run: node --test packages/editor/lib/changelog.test.mjs
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { gzipSync } from 'node:zlib';
|
|
6
|
+
import { gunzip, fileFromTar, sectionsBetween } from './changelog.mjs';
|
|
7
|
+
|
|
8
|
+
/** One tar entry: a 512-byte header, then the data padded to a multiple of 512. */
|
|
9
|
+
function tarEntry(name, text, prefix = '') {
|
|
10
|
+
const data = Buffer.from(text);
|
|
11
|
+
const header = Buffer.alloc(512);
|
|
12
|
+
header.write(name, 0, 'ascii');
|
|
13
|
+
header.write('0000644\0', 100, 'ascii');
|
|
14
|
+
header.write('0000000\0', 108, 'ascii');
|
|
15
|
+
header.write('0000000\0', 116, 'ascii');
|
|
16
|
+
header.write(data.length.toString(8).padStart(11, '0') + '\0', 124, 'ascii');
|
|
17
|
+
header.write('00000000000\0', 136, 'ascii');
|
|
18
|
+
header.write('0', 156, 'ascii');
|
|
19
|
+
header.write('ustar\0', 257, 'ascii');
|
|
20
|
+
header.write('00', 263, 'ascii');
|
|
21
|
+
header.write(prefix, 345, 'ascii');
|
|
22
|
+
header.fill(' ', 148, 156);
|
|
23
|
+
const sum = header.reduce((total, byte) => total + byte, 0);
|
|
24
|
+
header.write(sum.toString(8).padStart(6, '0') + '\0 ', 148, 'ascii');
|
|
25
|
+
return Buffer.concat([header, data, Buffer.alloc(Math.ceil(data.length / 512) * 512 - data.length)]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const tarball = (...entries) => new Uint8Array(Buffer.concat([...entries, Buffer.alloc(1024)]));
|
|
29
|
+
|
|
30
|
+
const changelogText = '# x\n\n## 0.1.1\n';
|
|
31
|
+
const tar = tarball(tarEntry('package/package.json', '{"name":"x"}'), tarEntry('package/CHANGELOG.md', changelogText));
|
|
32
|
+
|
|
33
|
+
test('fileFromTar finds a file by its name', () => assert.equal(fileFromTar(tar, 'package/CHANGELOG.md'), changelogText));
|
|
34
|
+
test('fileFromTar returns null for a missing file', () => assert.equal(fileFromTar(tar, 'package/missing.md'), null));
|
|
35
|
+
test('fileFromTar joins the prefix of a long name', () => {
|
|
36
|
+
const prefix = 'package/' + 'a'.repeat(120);
|
|
37
|
+
const long = tarball(tarEntry('CHANGELOG.md', 'long one', prefix));
|
|
38
|
+
assert.equal(fileFromTar(long, prefix + '/CHANGELOG.md'), 'long one');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('gunzip gives the input back', async () => {
|
|
42
|
+
const bytes = await gunzip(new Uint8Array(gzipSync(Buffer.from('hello tar'))));
|
|
43
|
+
assert.equal(new TextDecoder().decode(bytes), 'hello tar');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** A changelog with one `### Patch Changes` line and one bullet under each version. */
|
|
47
|
+
const changelogOf = (...versions) =>
|
|
48
|
+
['# @morphemecms/cms', '', ...versions.flatMap((v) => [`## ${v}`, '', '### Patch Changes', '', `- Change ${v}`, ''])].join('\n');
|
|
49
|
+
|
|
50
|
+
const full = changelogOf('0.3.0', '0.2.1', '0.2.0', '0.1.1');
|
|
51
|
+
const versionsOf = (sections) => sections.map((section) => section.version);
|
|
52
|
+
|
|
53
|
+
test('sectionsBetween returns the versions after from up to to, newest first', () => {
|
|
54
|
+
const sections = sectionsBetween(full, '0.1.1', '0.3.0');
|
|
55
|
+
assert.deepEqual(versionsOf(sections), ['0.3.0', '0.2.1', '0.2.0']);
|
|
56
|
+
assert.equal(sections[0].text, '### Patch Changes\n\n- Change 0.3.0');
|
|
57
|
+
});
|
|
58
|
+
test('sectionsBetween is empty when from equals to', () => assert.deepEqual(sectionsBetween(full, '0.2.1', '0.2.1'), []));
|
|
59
|
+
test('sectionsBetween includes to but not from', () => assert.deepEqual(versionsOf(sectionsBetween(full, '0.1.0', '0.1.1')), ['0.1.1']));
|
|
60
|
+
test('sectionsBetween drops a section with only blank lines', () => {
|
|
61
|
+
const text = '# x\n\n## 0.2.0\n\n\n';
|
|
62
|
+
assert.deepEqual(sectionsBetween(text, '0.1.0', '0.2.0'), []);
|
|
63
|
+
});
|
|
64
|
+
test('sectionsBetween ignores pre-release headings', () => {
|
|
65
|
+
const text = `## 0.2.0-next-1\n\n- Next\n\n${changelogOf('0.1.1')}`;
|
|
66
|
+
assert.deepEqual(versionsOf(sectionsBetween(text, '0.1.0', '0.2.0')), ['0.1.1']);
|
|
67
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compares two plain X.Y.Z versions number by number; throws Error('pre-release') for a version with a pre-release part.
|
|
3
|
+
* @param {string} a
|
|
4
|
+
* @param {string} b
|
|
5
|
+
* @returns {-1 | 0 | 1}
|
|
6
|
+
*/
|
|
7
|
+
export function compareVersions(a: string, b: string): -1 | 0 | 1;
|
|
8
|
+
/**
|
|
9
|
+
* The version of the CMS a site's package.json pins exactly, or why the site does not let the CMS manage it.
|
|
10
|
+
* @param {{ dependencies?: Record<string, string>, devDependencies?: Record<string, string> }} packageJson
|
|
11
|
+
* @param {{ packages?: Record<string, { link?: boolean }> } | null | undefined} lock
|
|
12
|
+
* @returns {{ version: string } | { reason: 'missing' | 'workspace' | 'tag' | 'range' | 'link' }}
|
|
13
|
+
*/
|
|
14
|
+
export function managedVersion(packageJson: {
|
|
15
|
+
dependencies?: Record<string, string>;
|
|
16
|
+
devDependencies?: Record<string, string>;
|
|
17
|
+
}, lock: {
|
|
18
|
+
packages?: Record<string, {
|
|
19
|
+
link?: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
} | null | undefined): {
|
|
22
|
+
version: string;
|
|
23
|
+
} | {
|
|
24
|
+
reason: "missing" | "workspace" | "tag" | "range" | "link";
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The version to move to, and whether it is taken by itself; null when there is nothing newer to take.
|
|
28
|
+
* @param {string} current
|
|
29
|
+
* @param {string} latest
|
|
30
|
+
* @param {string[]} skipped
|
|
31
|
+
* @returns {null | { version: string, auto: boolean, why?: 'major' | 'skipped' }}
|
|
32
|
+
*/
|
|
33
|
+
export function nextVersion(current: string, latest: string, skipped: string[]): null | {
|
|
34
|
+
version: string;
|
|
35
|
+
auto: boolean;
|
|
36
|
+
why?: "major" | "skipped";
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* What to do next with an update being watched (state 'waiting') or an undo being watched (state 'undoing').
|
|
40
|
+
* @param {{ state: string, to: string, from: string, at: string, undoAt?: string }} update times are ISO strings
|
|
41
|
+
* @param {string} running the version the site runs now
|
|
42
|
+
* @param {'success' | 'failure' | null} check the result of the commit's check run
|
|
43
|
+
* @param {number} now milliseconds
|
|
44
|
+
* @returns {'wait' | 'done' | 'undo' | 'undone' | 'stuck'}
|
|
45
|
+
*/
|
|
46
|
+
export function watchStep(update: {
|
|
47
|
+
state: string;
|
|
48
|
+
to: string;
|
|
49
|
+
from: string;
|
|
50
|
+
at: string;
|
|
51
|
+
undoAt?: string;
|
|
52
|
+
}, running: string, check: "success" | "failure" | null, now: number): "wait" | "done" | "undo" | "undone" | "stuck";
|
|
53
|
+
/** The npm package of the CMS. */
|
|
54
|
+
export const PACKAGE: "@morphemecms/cms";
|
|
55
|
+
/** How long an update or its undo is watched before it counts as over, in milliseconds (20 minutes). */
|
|
56
|
+
export const WATCH_LIMIT: number;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Which version of the CMS a site takes, and when an update that did not build is undone: reads the site's package.json,
|
|
2
|
+
// compares versions and steps the update's watch. Exposes PACKAGE, WATCH_LIMIT, compareVersions, managedVersion,
|
|
3
|
+
// nextVersion and watchStep. No dependencies, no I/O (runs in a Worker and in Node).
|
|
4
|
+
|
|
5
|
+
/** The npm package of the CMS. */
|
|
6
|
+
export const PACKAGE = '@morphemecms/cms';
|
|
7
|
+
|
|
8
|
+
/** How long an update or its undo is watched before it counts as over, in milliseconds (20 minutes). */
|
|
9
|
+
export const WATCH_LIMIT = 20 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
const PLAIN = /^\d+\.\d+\.\d+$/;
|
|
12
|
+
|
|
13
|
+
/** True when a version has anything after X.Y.Z. */
|
|
14
|
+
const isPreRelease = (version) => !PLAIN.test(version);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compares two plain X.Y.Z versions number by number; throws Error('pre-release') for a version with a pre-release part.
|
|
18
|
+
* @param {string} a
|
|
19
|
+
* @param {string} b
|
|
20
|
+
* @returns {-1 | 0 | 1}
|
|
21
|
+
*/
|
|
22
|
+
export function compareVersions(a, b) {
|
|
23
|
+
if (isPreRelease(a) || isPreRelease(b)) throw new Error('pre-release');
|
|
24
|
+
const x = a.split('.').map(Number);
|
|
25
|
+
const y = b.split('.').map(Number);
|
|
26
|
+
for (let i = 0; i < 3; i++) {
|
|
27
|
+
if (x[i] !== y[i]) return x[i] < y[i] ? -1 : 1;
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The version of the CMS a site's package.json pins exactly, or why the site does not let the CMS manage it.
|
|
34
|
+
* @param {{ dependencies?: Record<string, string>, devDependencies?: Record<string, string> }} packageJson
|
|
35
|
+
* @param {{ packages?: Record<string, { link?: boolean }> } | null | undefined} lock
|
|
36
|
+
* @returns {{ version: string } | { reason: 'missing' | 'workspace' | 'tag' | 'range' | 'link' }}
|
|
37
|
+
*/
|
|
38
|
+
export function managedVersion(packageJson, lock) {
|
|
39
|
+
const value = packageJson?.dependencies?.[PACKAGE] ?? packageJson?.devDependencies?.[PACKAGE];
|
|
40
|
+
if (value === undefined) return { reason: 'missing' };
|
|
41
|
+
if (value === '*' || value.startsWith('workspace:') || value.startsWith('file:')) return { reason: 'workspace' };
|
|
42
|
+
if (lock?.packages?.['node_modules/@morphemecms/cms']?.link === true) return { reason: 'link' };
|
|
43
|
+
if (!/\d+\.\d+/.test(value)) return { reason: 'tag' };
|
|
44
|
+
if (!PLAIN.test(value)) return { reason: 'range' };
|
|
45
|
+
return { version: value };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The version to move to, and whether it is taken by itself; null when there is nothing newer to take.
|
|
50
|
+
* @param {string} current
|
|
51
|
+
* @param {string} latest
|
|
52
|
+
* @param {string[]} skipped
|
|
53
|
+
* @returns {null | { version: string, auto: boolean, why?: 'major' | 'skipped' }}
|
|
54
|
+
*/
|
|
55
|
+
export function nextVersion(current, latest, skipped) {
|
|
56
|
+
if (isPreRelease(latest)) return null;
|
|
57
|
+
if (compareVersions(latest, current) !== 1) return null;
|
|
58
|
+
if (latest.split('.')[0] !== current.split('.')[0]) return { version: latest, auto: false, why: 'major' };
|
|
59
|
+
if (skipped.includes(latest)) return { version: latest, auto: false, why: 'skipped' };
|
|
60
|
+
return { version: latest, auto: true };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* What to do next with an update being watched (state 'waiting') or an undo being watched (state 'undoing').
|
|
65
|
+
* @param {{ state: string, to: string, from: string, at: string, undoAt?: string }} update times are ISO strings
|
|
66
|
+
* @param {string} running the version the site runs now
|
|
67
|
+
* @param {'success' | 'failure' | null} check the result of the commit's check run
|
|
68
|
+
* @param {number} now milliseconds
|
|
69
|
+
* @returns {'wait' | 'done' | 'undo' | 'undone' | 'stuck'}
|
|
70
|
+
*/
|
|
71
|
+
export function watchStep(update, running, check, now) {
|
|
72
|
+
if (update.state === 'waiting') {
|
|
73
|
+
if (running === update.to) return 'done';
|
|
74
|
+
if (check === 'failure') return 'undo';
|
|
75
|
+
if (now - Date.parse(update.at) > WATCH_LIMIT) return 'undo';
|
|
76
|
+
return 'wait';
|
|
77
|
+
}
|
|
78
|
+
if (update.state === 'undoing') {
|
|
79
|
+
if (check === 'success') return 'undone';
|
|
80
|
+
if (check === 'failure') return 'stuck';
|
|
81
|
+
if (now - Date.parse(update.undoAt) > WATCH_LIMIT) return 'undone';
|
|
82
|
+
return 'wait';
|
|
83
|
+
}
|
|
84
|
+
return 'wait';
|
|
85
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Tests for lib/cms-version.mjs: which CMS version a site takes, and when an update is undone.
|
|
2
|
+
// Run: node --test packages/editor/lib/cms-version.test.mjs
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { PACKAGE, WATCH_LIMIT, compareVersions, managedVersion, nextVersion, watchStep } from './cms-version.mjs';
|
|
6
|
+
|
|
7
|
+
test('the package is the CMS', () => assert.equal(PACKAGE, '@morphemecms/cms'));
|
|
8
|
+
test('the watch limit is 20 minutes', () => assert.equal(WATCH_LIMIT, 20 * 60 * 1000));
|
|
9
|
+
|
|
10
|
+
test('compareVersions: older is -1', () => assert.equal(compareVersions('0.1.0', '0.1.1'), -1));
|
|
11
|
+
test('compareVersions: compares number by number', () => assert.equal(compareVersions('0.10.0', '0.9.9'), 1));
|
|
12
|
+
test('compareVersions: equal is 0', () => assert.equal(compareVersions('0.1.1', '0.1.1'), 0));
|
|
13
|
+
test('compareVersions: a pre-release throws', () =>
|
|
14
|
+
assert.throws(() => compareVersions('0.2.0-next-1', '0.1.0'), { message: 'pre-release' }));
|
|
15
|
+
|
|
16
|
+
const lock = { packages: {} };
|
|
17
|
+
const dep = (value) => ({ dependencies: { [PACKAGE]: value } });
|
|
18
|
+
|
|
19
|
+
test('managedVersion: an exact version is managed', () => assert.deepEqual(managedVersion(dep('0.1.1'), lock), { version: '0.1.1' }));
|
|
20
|
+
test('managedVersion: * is a workspace', () => assert.deepEqual(managedVersion(dep('*'), lock), { reason: 'workspace' }));
|
|
21
|
+
test('managedVersion: workspace:* is a workspace', () =>
|
|
22
|
+
assert.deepEqual(managedVersion(dep('workspace:*'), lock), { reason: 'workspace' }));
|
|
23
|
+
test('managedVersion: a tag is a tag', () => assert.deepEqual(managedVersion(dep('next'), lock), { reason: 'tag' }));
|
|
24
|
+
test('managedVersion: ^ is a range', () => assert.deepEqual(managedVersion(dep('^0.1.0'), lock), { reason: 'range' }));
|
|
25
|
+
test('managedVersion: ~ is a range', () => assert.deepEqual(managedVersion(dep('~0.1.0'), lock), { reason: 'range' }));
|
|
26
|
+
test('managedVersion: a linked lock entry is a link', () => {
|
|
27
|
+
const linked = { packages: { 'node_modules/@morphemecms/cms': { link: true } } };
|
|
28
|
+
assert.deepEqual(managedVersion(dep('0.1.1'), linked), { reason: 'link' });
|
|
29
|
+
});
|
|
30
|
+
test('managedVersion: no dependency is missing', () => assert.deepEqual(managedVersion({}, lock), { reason: 'missing' }));
|
|
31
|
+
test('managedVersion: devDependencies count', () => {
|
|
32
|
+
assert.deepEqual(managedVersion({ devDependencies: { [PACKAGE]: '0.1.0' } }, lock), { version: '0.1.0' });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('nextVersion: a patch is taken by itself', () =>
|
|
36
|
+
assert.deepEqual(nextVersion('0.1.0', '0.1.1', []), { version: '0.1.1', auto: true }));
|
|
37
|
+
test('nextVersion: a minor is taken by itself', () =>
|
|
38
|
+
assert.deepEqual(nextVersion('0.1.1', '0.2.0', []), { version: '0.2.0', auto: true }));
|
|
39
|
+
test('nextVersion: a major is not', () => {
|
|
40
|
+
assert.deepEqual(nextVersion('0.9.0', '1.0.0', []), { version: '1.0.0', auto: false, why: 'major' });
|
|
41
|
+
});
|
|
42
|
+
test('nextVersion: a skipped version is not', () => {
|
|
43
|
+
assert.deepEqual(nextVersion('0.1.0', '0.1.1', ['0.1.1']), { version: '0.1.1', auto: false, why: 'skipped' });
|
|
44
|
+
});
|
|
45
|
+
test('nextVersion: the same version is null', () => assert.equal(nextVersion('0.1.1', '0.1.1', []), null));
|
|
46
|
+
test('nextVersion: an older version is null', () => assert.equal(nextVersion('0.2.0', '0.1.9', []), null));
|
|
47
|
+
test('nextVersion: a pre-release is null', () => assert.equal(nextVersion('0.1.0', '0.2.0-next-20261103', []), null));
|
|
48
|
+
|
|
49
|
+
const at = '2026-11-03T12:00:00.000Z';
|
|
50
|
+
const min = 60 * 1000;
|
|
51
|
+
const t0 = Date.parse(at);
|
|
52
|
+
const waiting = { state: 'waiting', from: '0.1.0', to: '0.1.1', at };
|
|
53
|
+
const undoing = { state: 'undoing', from: '0.1.0', to: '0.1.1', at, undoAt: at };
|
|
54
|
+
|
|
55
|
+
test('watchStep waiting: the running version wins', () => assert.equal(watchStep(waiting, '0.1.1', null, t0 + 5 * min), 'done'));
|
|
56
|
+
test('watchStep waiting: the running version wins over a failure', () => assert.equal(watchStep(waiting, '0.1.1', 'failure', t0), 'done'));
|
|
57
|
+
test('watchStep waiting: a failed check undoes', () => assert.equal(watchStep(waiting, '0.1.0', 'failure', t0 + 5 * min), 'undo'));
|
|
58
|
+
test('watchStep waiting: 19 minutes waits', () => assert.equal(watchStep(waiting, '0.1.0', null, t0 + 19 * min), 'wait'));
|
|
59
|
+
test('watchStep waiting: 21 minutes undoes', () => assert.equal(watchStep(waiting, '0.1.0', null, t0 + 21 * min), 'undo'));
|
|
60
|
+
test('watchStep waiting: a success that is not online yet waits', () =>
|
|
61
|
+
assert.equal(watchStep(waiting, '0.1.0', 'success', t0 + 5 * min), 'wait'));
|
|
62
|
+
|
|
63
|
+
test('watchStep undoing: success is undone', () => assert.equal(watchStep(undoing, '0.1.0', 'success', t0), 'undone'));
|
|
64
|
+
test('watchStep undoing: failure is stuck', () => assert.equal(watchStep(undoing, '0.1.0', 'failure', t0), 'stuck'));
|
|
65
|
+
test('watchStep undoing: 21 minutes is undone', () => assert.equal(watchStep(undoing, '0.1.0', null, t0 + 21 * min), 'undone'));
|
|
66
|
+
test('watchStep undoing: 5 minutes waits', () => assert.equal(watchStep(undoing, '0.1.0', null, t0 + 5 * min), 'wait'));
|
|
67
|
+
test('watchStep: any other state waits', () =>
|
|
68
|
+
assert.equal(watchStep({ ...waiting, state: 'done' }, '0.1.0', 'failure', t0 + 30 * min), 'wait'));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package.json and package-lock.json (parsed) with the version the manifest describes (from the store's registry,
|
|
3
|
+
* with its dist), as new objects. The version is written exactly ("1.4.0", not "^1.4.0"): the editor decides when to
|
|
4
|
+
* update. Throws NeedsNpm when only `npm install` can do it.
|
|
5
|
+
*/
|
|
6
|
+
export function addToLock(packageJson: any, lock: any, manifest: any): {
|
|
7
|
+
packageJson: any;
|
|
8
|
+
lock: any;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* package.json and package-lock.json (parsed) without the package, as new objects: what `npm uninstall` writes for a
|
|
12
|
+
* package from the store, which has nothing of its own in the lock. Throws NeedsNpm when another package still needs it.
|
|
13
|
+
*/
|
|
14
|
+
export function removeFromLock(packageJson: any, lock: any, name: any): {
|
|
15
|
+
packageJson: any;
|
|
16
|
+
lock: any;
|
|
17
|
+
};
|
|
18
|
+
export class NeedsNpm extends Error {
|
|
19
|
+
}
|