@morphemecms/cms 0.4.0-next-20261104164754 → 0.4.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @morphemecms/cms
2
2
 
3
- ## 0.4.0-next-20261104164754
3
+ ## 0.4.0
4
4
 
5
5
  ### Minor Changes
6
6
 
@@ -9,7 +9,9 @@
9
9
 
10
10
  ### Patch Changes
11
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.
12
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).
13
15
  - 682da97: New sites from the starter can reach the Morpheme store on the same Cloudflare account, and Prettier skips store theme files.
14
16
 
15
17
  ## 0.3.0
@@ -158,7 +158,18 @@ export const groups: { title: string; ids: string[] }[] = [
158
158
 
159
159
  /** The settings of the whole site, not on the start screen but on "Indstillinger" (#indstillinger, Library.tsx
160
160
  * Settings), which the gear in the top bar opens (Screen.tsx SettingsLink); A–Å as a group's. */
161
- export const settings: string[] = ['udseende', 'apps', 'appstore', 'morpheme', 'maaling', 'promovering', 'privatliv', 'seo', 'avanceret', 'licenser'];
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
+ ];
162
173
 
163
174
  /** The site's own short lists, on a screen of their own under "Lister" (#lister), with those the standard blocks
164
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.
@@ -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.
@@ -54,7 +54,7 @@ test('besideInSource reads export const beside = true', () => {
54
54
  assert.equal(besideInSource('export const beside = false;'), false);
55
55
  assert.equal(besideInSource('// export const beside = true;'), false);
56
56
  assert.equal(besideInSource('export const load = 1;'), false);
57
- });
57
+ });
58
58
 
59
59
  test('sideChoices lists installed blocks with beside, by label', () => {
60
60
  const blocks = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphemecms/cms",
3
- "version": "0.4.0-next-20261104164754",
3
+ "version": "0.4.0",
4
4
  "description": "Morpheme CMS: the editor for a site built with Astro, its screens, its blocks and the rules a site gives it. See README.md.",
5
5
  "license": "MIT",
6
6
  "author": "Lars Adolfsen",
@@ -41,6 +41,8 @@
41
41
  "scripts": {
42
42
  "build": "node scripts/demo-build.mjs",
43
43
  "icons": "node scripts/icon-packs.mjs",
44
+ "format": "prettier --write .",
45
+ "format:check": "prettier --check .",
44
46
  "release:version": "changeset version && node scripts/starter.mjs",
45
47
  "prepack": "node scripts/types.mjs write",
46
48
  "postpack": "node scripts/types.mjs clean"
@@ -118,6 +120,9 @@
118
120
  "jsdom": "^29.1.1",
119
121
  "motion": "^14.0.0",
120
122
  "playwright": "^1.64.1",
123
+ "prettier": "^3.9.10",
124
+ "prettier-plugin-astro": "^1.1.2",
125
+ "prettier-plugin-tailwindcss": "^0.8.1",
121
126
  "react": "^19.3.0",
122
127
  "react-dom": "^19.3.0",
123
128
  "rehype-raw": "^7.0.0",
@@ -384,20 +384,31 @@ for (const [what, patch] of [
384
384
 
385
385
  // The page's notes: from the running version up to the newest, newest first.
386
386
  {
387
- const [major, minor, patch] = RUNNING.split('-')[0].split('.').map(Number);
387
+ const running = RUNNING.split('-')[0];
388
+ const [major, minor, patch] = running.split('.').map(Number);
388
389
  const next1 = `${major}.${minor}.${patch + 1}`;
389
390
  const next2 = `${major}.${minor}.${patch + 2}`;
390
391
  const w = world();
391
- for (const v of ['0.0.0', RUNNING, next1, next2, '9.9.9'])
392
+ for (const v of ['0.0.0', running, next1, next2, '9.9.9'])
392
393
  await w.kv.put(`cms:notes:${v}`, JSON.stringify({ version: v, text: `Notes ${v}` }));
393
394
  await w.kv.put('cms:checked', JSON.stringify({ at: new Date(T0).toISOString(), latest: next2 }));
394
- const { notes } = await cmsStatus(w.env);
395
+ const { notes } = await cmsStatus(w.env, running);
395
396
  expect(
396
397
  notes.map((n) => n.version).join() === `${next2},${next1}`,
397
398
  `noterne var ${notes.map((n) => n.version).join()}, ikke ${next2},${next1}`,
398
399
  );
399
400
  }
400
401
 
402
+ // A pre-release running: the status answers, with no notes.
403
+ {
404
+ const w = world();
405
+ for (const v of ['0.1.0', '0.2.0']) await w.kv.put(`cms:notes:${v}`, JSON.stringify({ version: v, text: `Notes ${v}` }));
406
+ await w.kv.put('cms:checked', JSON.stringify({ at: new Date(T0).toISOString(), latest: '0.2.0' }));
407
+ const status = await cmsStatus(w.env, '0.2.0-next-1').catch((e) => e);
408
+ expect(status?.running === '0.2.0-next-1', `en forudgivelse gav ${status?.message ?? status?.running}, ikke status`);
409
+ expect(Array.isArray(status?.notes) && status.notes.length === 0, 'en forudgivelse gav noter');
410
+ }
411
+
401
412
  if (problems.length) {
402
413
  console.error(problems.map((p) => `- ${p}`).join('\n'));
403
414
  process.exit(1);
package/site/icons.mjs CHANGED
@@ -416,7 +416,9 @@ export const PACK_MAPS = { 'material-symbols': MATERIAL_SYMBOLS, lucide: LUCIDE
416
416
  /** The names of Phosphor's pack: the icons the site and the editor draw (`used`), and every name the other system
417
417
  * packs' maps name (an app's icon, which the CMS itself may not draw), sorted. */
418
418
  export const phosphorNames = (used) =>
419
- [...new Set([...used, ...Object.values(PACK_MAPS).flatMap(Object.keys), ...Object.values(MATERIAL_DESIGN_ICONS).flatMap(Object.keys)])].sort();
419
+ [
420
+ ...new Set([...used, ...Object.values(PACK_MAPS).flatMap(Object.keys), ...Object.values(MATERIAL_DESIGN_ICONS).flatMap(Object.keys)]),
421
+ ].sort();
420
422
 
421
423
  /** The names the system's pack has an icon by in its maps (its set's and Material Design Icons'); all for Phosphor's. */
422
424
  const mapped = (pack) => (PACK_MAPS[pack] ? [...Object.keys(PACK_MAPS[pack]), ...Object.keys(MATERIAL_DESIGN_ICONS[pack] ?? {})] : null);
package/src/CmsPage.tsx CHANGED
@@ -12,7 +12,7 @@ import { when } from './when';
12
12
 
13
13
  export const CMS_ROUTE = 'morpheme';
14
14
 
15
- type Problem = 'unreachable' | 'cannot-write' | 'no-token' | 'not-managed' | 'slow-undo';
15
+ type Problem = 'unreachable' | 'cannot-write' | 'no-token' | 'no-kv' | 'not-managed' | 'slow-undo';
16
16
  type UpdateState = 'waiting' | 'undoing' | 'done' | 'undone' | 'stuck';
17
17
  type View = {
18
18
  running: string;
@@ -130,10 +130,11 @@ export default function CmsPage() {
130
130
  );
131
131
  }
132
132
 
133
- const kindText: Record<Problem, 'unreachable' | 'cannotWrite' | 'noToken' | 'notManaged' | 'slowUndo'> = {
133
+ const kindText: Record<Problem, 'unreachable' | 'cannotWrite' | 'noToken' | 'noKv' | 'notManaged' | 'slowUndo'> = {
134
134
  unreachable: 'unreachable',
135
135
  'cannot-write': 'cannotWrite',
136
136
  'no-token': 'noToken',
137
+ 'no-kv': 'noKv',
137
138
  'not-managed': 'notManaged',
138
139
  'slow-undo': 'slowUndo',
139
140
  };
package/src/EntryForm.tsx CHANGED
@@ -134,7 +134,10 @@ const same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b)
134
134
  // "Sidekolonne") leaves the file when it is on (booleanWrite).
135
135
  const inForm = (fields: Field[], values: Record<string, unknown>) =>
136
136
  Object.fromEntries(
137
- fields.map((f) => [f.name, inUse(f, fields, values) ? (f.type === 'boolean' ? booleanWrite(values[f.name], f.missing) : values[f.name]) : '']),
137
+ fields.map((f) => [
138
+ f.name,
139
+ inUse(f, fields, values) ? (f.type === 'boolean' ? booleanWrite(values[f.name], f.missing) : values[f.name]) : '',
140
+ ]),
138
141
  );
139
142
 
140
143
  type Props = { collection: MarkdownCollection; file: RepoFile; env: Env };
@@ -642,7 +645,8 @@ function Form({
642
645
  // "Sidekolonne" only while src/data/side.json chooses a block and the page is not "Hele bredden" (sideSwitchShown).
643
646
  const sideShown = sideSwitchShown({ block: sideSettingOf(env.files) }, values);
644
647
  const field = (f: Field) =>
645
- inUse(f, collection.fields, values) && (f.name !== 'side' || sideShown) && <FieldInput key={f.name} field={f} value={values[f.name]} onChange={(v) => onChange(f, v)} />;
648
+ inUse(f, collection.fields, values) &&
649
+ (f.name !== 'side' || sideShown) && <FieldInput key={f.name} field={f} value={values[f.name]} onChange={(v) => onChange(f, v)} />;
646
650
 
647
651
  return (
648
652
  <Fields.Provider value={fields}>
@@ -72,7 +72,12 @@ export default function SideColumn({ env, files }: { env: Env; files: RepoFile[]
72
72
  return (
73
73
  <Screen title={t.common.parts.sideColumn} wide back={back}>
74
74
  <Group>
75
- <ChoiceRow selected={!chosen} onClick={() => choose(null, t.library.sideNone)} headline={t.library.sideNone} supporting={t.library.sideNoneHint} />
75
+ <ChoiceRow
76
+ selected={!chosen}
77
+ onClick={() => choose(null, t.library.sideNone)}
78
+ headline={t.library.sideNone}
79
+ supporting={t.library.sideNoneHint}
80
+ />
76
81
  {choices.map((x) => (
77
82
  <ChoiceRow key={x.type} selected={x.type === chosen} onClick={() => choose(x.type, x.label)} headline={x.label} />
78
83
  ))}
package/src/cmsView.mjs CHANGED
@@ -64,6 +64,9 @@ export function cmsView(status, now = Date.now()) {
64
64
  if (status.token === false) {
65
65
  problems.push('no-token');
66
66
  }
67
+ if (status.kv === false) {
68
+ problems.push('no-kv');
69
+ }
67
70
  if (status.checked?.problem) {
68
71
  const kind = status.checked.problem.kind;
69
72
  if (kind === 'unreachable' || kind === 'cannot-write' || kind === 'not-managed') {
@@ -103,6 +103,12 @@ test('no token', () => {
103
103
  assert.deepEqual(view.problems, ['no-token']);
104
104
  });
105
105
 
106
+ test('no kv', () => {
107
+ const status = makeStatus({ kv: false });
108
+ const view = cmsView(status, now);
109
+ assert.deepEqual(view.problems, ['no-kv']);
110
+ });
111
+
106
112
  test('cannot write', () => {
107
113
  const status = makeStatus({ checked: { problem: { kind: 'cannot-write' } } });
108
114
  const view = cmsView(status, now);
@@ -20,6 +20,7 @@ export const cms: Messages['cms'] = {
20
20
  unreachable: 'npm eller GitHub kunne ikke nås.',
21
21
  cannotWrite: 'Sitets GitHub-nøgle kan ikke gemme. Giv den Contents: Read and write.',
22
22
  noToken: 'Sitet har ingen GitHub-nøgle (GITHUB_PUBLISH_TOKEN).',
23
+ noKv: 'Sitet har intet KV-lager (PUSH), så det kan ikke se efter nye versioner.',
23
24
  notManaged: 'Sitets package.json bestemmer versionen.',
24
25
  slowUndo: 'Nøglen kan ikke læse check runs, så en fejlet opdatering fortrydes først efter 20 minutter.',
25
26
  notifyDone: (v) => `Morpheme er opdateret til ${v}`,
@@ -23,6 +23,7 @@ export const cms = {
23
23
  unreachable: 'npm or GitHub couldn’t be reached.',
24
24
  cannotWrite: 'The site’s GitHub token can’t save. Give it Contents: Read and write.',
25
25
  noToken: 'The site has no GitHub token (GITHUB_PUBLISH_TOKEN).',
26
+ noKv: 'The site has no KV storage (PUSH), so it can’t look for new versions.',
26
27
  notManaged: 'The site’s package.json decides the version.',
27
28
  slowUndo: 'The token can’t read check runs, so a failed update is undone after 20 minutes.',
28
29
  /** The site's notifications (Notifications.tsx) and the commits the Worker writes, in the site's language. */
@@ -31,7 +31,7 @@
31
31
  "@fontsource-variable/roboto": "^5.3.0",
32
32
  "@material/material-color-utilities": "^0.3.0",
33
33
  "@milkdown/kit": "^7.22.3",
34
- "@morphemecms/cms": "0.3.0",
34
+ "@morphemecms/cms": "0.4.0",
35
35
  "@phosphor-icons/core": "^2.1.1",
36
36
  "@phosphor-icons/react": "^2.1.10",
37
37
  "@tailwindcss/typography": "^0.5.20",
@@ -67,7 +67,8 @@ const review = (page.data.review ?? '').trim();
67
67
  const sideType = sideBlock(page.data);
68
68
  const sideText = end ? end.html : html;
69
69
  const side =
70
- sideType && (await blockLoads[sideType].load({}, { texts: blockTexts[sideType] ?? {}, index: 0, path: current, preview, html: sideText })) !== null
70
+ sideType &&
71
+ (await blockLoads[sideType].load({}, { texts: blockTexts[sideType] ?? {}, index: 0, path: current, preview, html: sideText })) !== null
71
72
  ? sideType
72
73
  : null;
73
74
  ---
@@ -200,18 +200,19 @@ export async function watchUpdate(env, deps = defaultDeps, now = Date.now()) {
200
200
  return tell(env, deps, 'notifyStuck', update.to, true);
201
201
  }
202
202
 
203
- /** What the page shows: the running version, the token, what npm said, the update, the skipped versions and the notes. */
204
- export async function cmsStatus(env) {
203
+ /** What the page shows: the running version, the token, what npm said, the update, the skipped versions and the notes; no notes when the running version is a pre-release. */
204
+ export async function cmsStatus(env, running = RUNNING) {
205
205
  const kv = env.PUSH;
206
- const status = { running: RUNNING, token: !!env.GITHUB_PUBLISH_TOKEN, kv: !!kv, checked: null, update: null, skipped: [], notes: [] };
206
+ const status = { running, token: !!env.GITHUB_PUBLISH_TOKEN, kv: !!kv, checked: null, update: null, skipped: [], notes: [] };
207
+ const plain = /^\d+\.\d+\.\d+$/;
207
208
  if (!kv) return status;
208
209
  status.checked = await kv.get('cms:checked', 'json');
209
210
  status.update = await kv.get('cms:update', 'json');
210
211
  status.skipped = await versionsUnder(kv, 'cms:skip:');
211
- const plain = /^\d+\.\d+\.\d+$/;
212
+ if (!plain.test(running)) return status;
212
213
  const wanted = (await versionsUnder(kv, 'cms:notes:')).filter(
213
214
  (v) =>
214
- plain.test(v) && compareVersions(v, RUNNING) === 1 && (!status.checked?.latest || compareVersions(v, status.checked.latest) <= 0),
215
+ plain.test(v) && compareVersions(v, running) === 1 && (!status.checked?.latest || compareVersions(v, status.checked.latest) <= 0),
215
216
  );
216
217
  wanted.sort((a, b) => compareVersions(b, a));
217
218
  const notes = await Promise.all(wanted.map((v) => kv.get(`cms:notes:${v}`, 'json')));