@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/src/SideColumn.tsx
CHANGED
|
@@ -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
|
|
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/appFiles.ts
CHANGED
|
@@ -24,6 +24,6 @@ export const appPictures = import.meta.glob<string>('/src/apps/*/screenshots/*.{
|
|
|
24
24
|
});
|
|
25
25
|
export const appBlocks = import.meta.glob<{ default: BlockDefinition }>('/src/apps/*/blocks/*/index.tsx', { eager: true });
|
|
26
26
|
export const appShared = import.meta.glob<{ default: SharedTexts }>('/src/apps/*/blocks/*/shared.ts', { eager: true });
|
|
27
|
-
/** The apps' blocks drawn from their view: their drawing (View.tsx) and what makes their view in the editor (
|
|
27
|
+
/** The apps' blocks drawn from their view: their drawing (View.tsx) and what makes their view in the editor (viewOf.ts). */
|
|
28
28
|
export const appViews = import.meta.glob<{ default: View<any> }>('/src/apps/*/blocks/*/View.tsx', { eager: true });
|
|
29
|
-
export const appViewOfs = import.meta.glob<{ viewOf: ViewOf<any> }>('/src/apps/*/blocks/*/
|
|
29
|
+
export const appViewOfs = import.meta.glob<{ viewOf: ViewOf<any> }>('/src/apps/*/blocks/*/viewOf.ts', { eager: true });
|
package/src/apps.ts
CHANGED
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
// screenshots/ the pictures it is shown with in the store, picked to give an idea of it (`screenshots` in
|
|
8
8
|
// index.ts, packages/editor/scripts/app-screenshots.mjs), on its page;
|
|
9
9
|
// texts.ts its texts in the editor's other languages, as a block's;
|
|
10
|
-
// blocks/<type>/ the blocks it adds, each a folder like a standard block's (index.tsx,
|
|
10
|
+
// blocks/<type>/ the blocks it adds, each a folder like a standard block's (index.tsx, viewOf.ts, load.ts,
|
|
11
11
|
// View.tsx, texts.ts, evt. shared.ts and script.ts);
|
|
12
12
|
// lists in index.ts the lists it brings (`lists`, a FolderCollection each, with its icon): under "Lister" while
|
|
13
13
|
// it is installed, and on its page; the entries are files in the site's data (src/data/), so
|
|
14
14
|
// they stay, as the rest of the content, when it is uninstalled;
|
|
15
15
|
// <list>/ a kind of entry it adds to one of the site's lists (`kinds` in index.ts), e.g. popups/: a
|
|
16
16
|
// pop-up with a signup form, chosen under "+" with the list's own kinds (FolderCollection
|
|
17
|
-
// .kinds); its part (View.tsx, given its view by
|
|
17
|
+
// .kinds); its part (View.tsx, given its view by viewOf.ts in the editor and load.ts on the site,
|
|
18
18
|
// and script.ts for what it does in a browser) is drawn in the pop-up under its text, on the site
|
|
19
19
|
// (Popup.astro finds them) and in the editor (`part` in index.ts, kindDraw);
|
|
20
20
|
// slots/head.astro, slots/body-end.astro what it puts on every page of the site, in <head> or at the
|
package/src/blocks.ts
CHANGED
|
@@ -44,7 +44,7 @@ export type SiteBlock = {
|
|
|
44
44
|
/** Before its name where it is picked (BlockDialog.tsx). */
|
|
45
45
|
icon: Icon;
|
|
46
46
|
/** How it looks on the site, with what is in it now (the card in the text, its sheet): its View.tsx given the view
|
|
47
|
-
* its view.ts makes (registry.tsx). Without it, the card has the lines of describe. */
|
|
47
|
+
* its view.ts (an app's: viewOf.ts) makes (registry.tsx). Without it, the card has the lines of describe. */
|
|
48
48
|
draw?: (props: Record<string, any>, data: BlockData) => ReactNode;
|
|
49
49
|
/** Gone from the site by itself (a notice whose time is over): its card is greyed, like a hidden one. */
|
|
50
50
|
over?: (props: BlockProps) => boolean;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function cmsView(status: any, now?: number): {
|
|
2
|
+
running: any;
|
|
3
|
+
checkedAt: any;
|
|
4
|
+
latest: any;
|
|
5
|
+
notes: any[];
|
|
6
|
+
action: string | null;
|
|
7
|
+
npmCommand: string | null;
|
|
8
|
+
update: {
|
|
9
|
+
state: any;
|
|
10
|
+
to: any;
|
|
11
|
+
at: any;
|
|
12
|
+
} | null;
|
|
13
|
+
problems: any[];
|
|
14
|
+
};
|
|
15
|
+
export const DONE_SHOWN: number;
|
package/src/cmsView.mjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Transforms CMS status into a view for the update page.
|
|
2
|
+
// Exposes cmsView and DONE_SHOWN.
|
|
3
|
+
// Depends on cms-version for version comparison.
|
|
4
|
+
|
|
5
|
+
import { compareVersions } from '../lib/cms-version.mjs';
|
|
6
|
+
|
|
7
|
+
export const DONE_SHOWN = 24 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
export function cmsView(status, now = Date.now()) {
|
|
10
|
+
const running = status.running;
|
|
11
|
+
const checkedAt = status.checked?.at ?? null;
|
|
12
|
+
|
|
13
|
+
let latest = null;
|
|
14
|
+
if (status.checked?.latest) {
|
|
15
|
+
try {
|
|
16
|
+
if (compareVersions(status.checked.latest, running) === 1) {
|
|
17
|
+
latest = status.checked.latest;
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
// Pre-release or invalid version, ignore
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let notes = [];
|
|
25
|
+
if (latest !== null && status.notes && status.notes.length > 0) {
|
|
26
|
+
notes = [...status.notes];
|
|
27
|
+
try {
|
|
28
|
+
notes.sort((a, b) => compareVersions(b.version, a.version));
|
|
29
|
+
} catch {
|
|
30
|
+
// Keep original order if sorting fails
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const busy = status.update?.state === 'waiting' || status.update?.state === 'undoing';
|
|
35
|
+
|
|
36
|
+
let action = null;
|
|
37
|
+
if (latest !== null && !busy) {
|
|
38
|
+
if (status.checked?.problem?.kind === 'major') {
|
|
39
|
+
action = 'update';
|
|
40
|
+
} else if (status.checked?.problem?.kind === 'skipped') {
|
|
41
|
+
action = 'try-again';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let npmCommand = null;
|
|
46
|
+
if (latest !== null && status.checked?.problem?.kind === 'needs-npm') {
|
|
47
|
+
npmCommand = `npm install @morphemecms/cms@${latest}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let update = null;
|
|
51
|
+
if (status.update) {
|
|
52
|
+
const state = status.update.state;
|
|
53
|
+
if (state === 'waiting' || state === 'undoing' || state === 'undone' || state === 'stuck') {
|
|
54
|
+
update = { state, to: status.update.to, at: status.update.at };
|
|
55
|
+
} else if (state === 'done') {
|
|
56
|
+
const at = Date.parse(status.update.at);
|
|
57
|
+
if (now - at < DONE_SHOWN) {
|
|
58
|
+
update = { state, to: status.update.to, at: status.update.at };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const problems = [];
|
|
64
|
+
if (status.token === false) {
|
|
65
|
+
problems.push('no-token');
|
|
66
|
+
}
|
|
67
|
+
if (status.kv === false) {
|
|
68
|
+
problems.push('no-kv');
|
|
69
|
+
}
|
|
70
|
+
if (status.checked?.problem) {
|
|
71
|
+
const kind = status.checked.problem.kind;
|
|
72
|
+
if (kind === 'unreachable' || kind === 'cannot-write' || kind === 'not-managed') {
|
|
73
|
+
problems.push(kind);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (status.checked?.noChecks === true) {
|
|
77
|
+
problems.push('slow-undo');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { running, checkedAt, latest, notes, action, npmCommand, update, problems };
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Tests for cmsView.
|
|
2
|
+
// Verifies status transformation and output shape.
|
|
3
|
+
// Run: node --test packages/editor/src/cmsView.test.mjs
|
|
4
|
+
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { cmsView } from './cmsView.mjs';
|
|
8
|
+
|
|
9
|
+
const baseStatus = {
|
|
10
|
+
running: '0.1.1',
|
|
11
|
+
token: true,
|
|
12
|
+
kv: true,
|
|
13
|
+
checked: { at: '2026-11-04T10:00:00.000Z', latest: '0.1.1' },
|
|
14
|
+
update: null,
|
|
15
|
+
skipped: [],
|
|
16
|
+
notes: [],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const now = Date.parse('2026-11-04T12:00:00.000Z');
|
|
20
|
+
|
|
21
|
+
function makeStatus(overrides) {
|
|
22
|
+
const result = { ...baseStatus, ...overrides };
|
|
23
|
+
|
|
24
|
+
if (!('checked' in overrides)) {
|
|
25
|
+
result.checked = baseStatus.checked;
|
|
26
|
+
} else if (overrides.checked === null) {
|
|
27
|
+
result.checked = null;
|
|
28
|
+
} else {
|
|
29
|
+
result.checked = { ...baseStatus.checked, ...overrides.checked };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!('update' in overrides)) {
|
|
33
|
+
result.update = baseStatus.update;
|
|
34
|
+
} else if (overrides.update === null) {
|
|
35
|
+
result.update = null;
|
|
36
|
+
} else {
|
|
37
|
+
result.update = { ...baseStatus.update, ...overrides.update };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test('up to date', () => {
|
|
44
|
+
const view = cmsView(baseStatus, now);
|
|
45
|
+
assert.equal(view.latest, null);
|
|
46
|
+
assert.equal(view.action, null);
|
|
47
|
+
assert.equal(view.npmCommand, null);
|
|
48
|
+
assert.equal(view.update, null);
|
|
49
|
+
assert.deepEqual(view.problems, []);
|
|
50
|
+
assert.equal(view.checkedAt, '2026-11-04T10:00:00.000Z');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('not checked', () => {
|
|
54
|
+
const status = makeStatus({ checked: null });
|
|
55
|
+
const view = cmsView(status, now);
|
|
56
|
+
assert.equal(view.checkedAt, null);
|
|
57
|
+
assert.equal(view.latest, null);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('patch waiting', () => {
|
|
61
|
+
const status = makeStatus({
|
|
62
|
+
checked: { latest: '0.1.2' },
|
|
63
|
+
update: { commit: 'a1', from: '0.1.1', to: '0.1.2', at: '2026-11-04T11:00:00.000Z', state: 'waiting' },
|
|
64
|
+
});
|
|
65
|
+
const view = cmsView(status, now);
|
|
66
|
+
assert.equal(view.update.state, 'waiting');
|
|
67
|
+
assert.equal(view.update.to, '0.1.2');
|
|
68
|
+
assert.equal(view.action, null);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('major', () => {
|
|
72
|
+
const status = makeStatus({ checked: { latest: '1.0.0', problem: { kind: 'major' } } });
|
|
73
|
+
const view = cmsView(status, now);
|
|
74
|
+
assert.equal(view.latest, '1.0.0');
|
|
75
|
+
assert.equal(view.action, 'update');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('major while undoing', () => {
|
|
79
|
+
const status = makeStatus({
|
|
80
|
+
checked: { latest: '1.0.0', problem: { kind: 'major' } },
|
|
81
|
+
update: { commit: 'a1', from: '1.0.0', to: '0.1.1', at: '2026-11-04T11:00:00.000Z', state: 'undoing' },
|
|
82
|
+
});
|
|
83
|
+
const view = cmsView(status, now);
|
|
84
|
+
assert.equal(view.action, null);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('skipped', () => {
|
|
88
|
+
const status = makeStatus({ checked: { latest: '0.1.3', problem: { kind: 'skipped' } } });
|
|
89
|
+
const view = cmsView(status, now);
|
|
90
|
+
assert.equal(view.action, 'try-again');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('needs npm', () => {
|
|
94
|
+
const status = makeStatus({ checked: { latest: '0.1.2', problem: { kind: 'needs-npm', detail: 'x' } } });
|
|
95
|
+
const view = cmsView(status, now);
|
|
96
|
+
assert.equal(view.npmCommand, 'npm install @morphemecms/cms@0.1.2');
|
|
97
|
+
assert.equal(view.action, null);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('no token', () => {
|
|
101
|
+
const status = makeStatus({ token: false });
|
|
102
|
+
const view = cmsView(status, now);
|
|
103
|
+
assert.deepEqual(view.problems, ['no-token']);
|
|
104
|
+
});
|
|
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
|
+
|
|
112
|
+
test('cannot write', () => {
|
|
113
|
+
const status = makeStatus({ checked: { problem: { kind: 'cannot-write' } } });
|
|
114
|
+
const view = cmsView(status, now);
|
|
115
|
+
assert.deepEqual(view.problems, ['cannot-write']);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('slow undo', () => {
|
|
119
|
+
const status = makeStatus({ checked: { noChecks: true } });
|
|
120
|
+
const view = cmsView(status, now);
|
|
121
|
+
assert.ok(view.problems.includes('slow-undo'));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('done two days ago', () => {
|
|
125
|
+
const status = makeStatus({ update: { commit: 'a1', from: '0.1.1', to: '0.1.2', at: '2026-11-02T12:00:00.000Z', state: 'done' } });
|
|
126
|
+
const view = cmsView(status, now);
|
|
127
|
+
assert.equal(view.update, null);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('done an hour ago', () => {
|
|
131
|
+
const status = makeStatus({ update: { commit: 'a1', from: '0.1.1', to: '0.1.2', at: '2026-11-04T11:00:00.000Z', state: 'done' } });
|
|
132
|
+
const view = cmsView(status, now);
|
|
133
|
+
assert.equal(view.update.state, 'done');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('notes order', () => {
|
|
137
|
+
const status = makeStatus({
|
|
138
|
+
checked: { latest: '0.1.3' },
|
|
139
|
+
notes: [
|
|
140
|
+
{ version: '0.1.2', text: 'b' },
|
|
141
|
+
{ version: '0.1.3', text: 'c' },
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
const view = cmsView(status, now);
|
|
145
|
+
assert.equal(view.notes[0].version, '0.1.3');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('pre-release running', () => {
|
|
149
|
+
const status = makeStatus({ running: '0.2.0-next.1', checked: { latest: '0.2.0' } });
|
|
150
|
+
const view = cmsView(status, now);
|
|
151
|
+
assert.equal(view.latest, null);
|
|
152
|
+
});
|
package/src/frontmatter.mjs
CHANGED
|
@@ -28,7 +28,10 @@ export function writeFields(yaml, changes) {
|
|
|
28
28
|
if (value !== '' && value != null) doc.set(key, toNode(doc, key, value));
|
|
29
29
|
else if (doc.has(key)) doc.delete(key);
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
if (!doc.contents?.items?.length) return '';
|
|
32
|
+
const text = doc.toString(STRINGIFY).replace(/\n$/, '');
|
|
33
|
+
// The yaml package writes \n; a file with \r\n keeps its line breaks (a checkout on Windows).
|
|
34
|
+
return yaml.includes('\r\n') ? text.replace(/\n/g, '\r\n') : text;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
function toNode(doc, key, value) {
|
package/src/frontmatter.test.mjs
CHANGED
|
@@ -25,3 +25,9 @@ test('a switch without a missing value is written as it is', () => {
|
|
|
25
25
|
assert.equal(booleanWrite(true, undefined), true);
|
|
26
26
|
assert.equal(booleanWrite(false, undefined), false);
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
test('front matter with CRLF line breaks is written back unchanged, and added fields use CRLF too', () => {
|
|
30
|
+
const crlf = 'title: "Om os"\r\nwide: false';
|
|
31
|
+
assert.equal(writeFields(crlf, {}), crlf);
|
|
32
|
+
assert.equal(writeFields(crlf, { side: false }), `${crlf}\r\nside: false`);
|
|
33
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Messages } from '../en-US';
|
|
2
|
+
|
|
3
|
+
export const cms: Messages['cms'] = {
|
|
4
|
+
running: (v) => `Version ${v}`,
|
|
5
|
+
checked: (when) => `Tjekket ${when}`,
|
|
6
|
+
notChecked: 'Ikke tjekket endnu',
|
|
7
|
+
upToDate: 'Opdateret',
|
|
8
|
+
available: (v) => `Version ${v} er klar`,
|
|
9
|
+
whatsNew: 'Nyheder',
|
|
10
|
+
major: 'En ny hovedversion installeres ikke af sig selv.',
|
|
11
|
+
skipped: (v) => `${v} blev fortrudt, fordi siden ikke kunne bygges.`,
|
|
12
|
+
needsNpm: 'Den kræver npm. Kør dette i sitets repo:',
|
|
13
|
+
update: 'Opdater',
|
|
14
|
+
tryAgain: 'Prøv igen',
|
|
15
|
+
waiting: (v) => `Opdaterer til ${v}. Venter på, at siden bygges.`,
|
|
16
|
+
undoing: (v) => `Fortryder opdateringen til ${v}.`,
|
|
17
|
+
done: (v) => `Opdateret til ${v}`,
|
|
18
|
+
undone: (v) => `Opdateringen til ${v} blev fortrudt, fordi siden ikke kunne bygges.`,
|
|
19
|
+
stuck: (v) => `Hverken opdateringen til ${v} eller fortrydelsen kunne bygges. Siden skal rettes i hånden.`,
|
|
20
|
+
unreachable: 'npm eller GitHub kunne ikke nås.',
|
|
21
|
+
cannotWrite: 'Sitets GitHub-nøgle kan ikke gemme. Giv den Contents: Read and write.',
|
|
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.',
|
|
24
|
+
notManaged: 'Sitets package.json bestemmer versionen.',
|
|
25
|
+
slowUndo: 'Nøglen kan ikke læse check runs, så en fejlet opdatering fortrydes først efter 20 minutter.',
|
|
26
|
+
notifyDone: (v) => `Morpheme er opdateret til ${v}`,
|
|
27
|
+
notifyUndone: (v) => `Opdateringen af Morpheme til ${v} er fortrudt, fordi siden ikke kunne bygges`,
|
|
28
|
+
notifyStuck: (v) => `Morpheme ${v} kunne ikke bygges, og det kunne fortrydelsen heller ikke. Ret siden i hånden.`,
|
|
29
|
+
commitUpdate: (v) => `Opdater Morpheme til ${v}`,
|
|
30
|
+
commitUndo: (v) => `Fortryd opdateringen af Morpheme til ${v}`,
|
|
31
|
+
};
|
package/src/i18n/da-DK/common.ts
CHANGED
|
@@ -110,6 +110,7 @@ export const common: Messages['common'] = {
|
|
|
110
110
|
settings: 'Indstillinger',
|
|
111
111
|
advanced: 'Avanceret',
|
|
112
112
|
licenses: 'Licenser',
|
|
113
|
+
morpheme: 'Morpheme',
|
|
113
114
|
},
|
|
114
115
|
saved: (change) => `${change}. Du får besked, når hjemmesiden er opdateret.`,
|
|
115
116
|
today: 'i dag',
|
package/src/i18n/da-DK/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { apps } from './apps';
|
|
|
6
6
|
import { blocks } from './blocks';
|
|
7
7
|
import { blockSettings } from './blockSettings';
|
|
8
8
|
import { check } from './check';
|
|
9
|
+
import { cms } from './cms';
|
|
9
10
|
import { colors } from './colors';
|
|
10
11
|
import { common } from './common';
|
|
11
12
|
import { company } from './company';
|
|
@@ -41,6 +42,7 @@ export const daDK: Messages = {
|
|
|
41
42
|
seo,
|
|
42
43
|
keywords,
|
|
43
44
|
licenses,
|
|
45
|
+
cms,
|
|
44
46
|
parts,
|
|
45
47
|
history,
|
|
46
48
|
preview,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// "Morpheme" under "Indstillinger" (the CMS's update page, #morpheme): the running version, a new one and its notes,
|
|
2
|
+
// and an update's progress; also the Worker's notifications and commit messages (worker/cms-update.js).
|
|
3
|
+
|
|
4
|
+
export const cms = {
|
|
5
|
+
running: (v: string) => `Version ${v}`,
|
|
6
|
+
/** When the Worker last asked npm, as when.ts writes it. */
|
|
7
|
+
checked: (when: string) => `Checked ${when}`,
|
|
8
|
+
notChecked: 'Not checked yet',
|
|
9
|
+
upToDate: 'Up to date',
|
|
10
|
+
available: (v: string) => `Version ${v} is available`,
|
|
11
|
+
whatsNew: 'What’s new',
|
|
12
|
+
major: 'A new major version isn’t installed by itself.',
|
|
13
|
+
skipped: (v: string) => `${v} was undone because the site couldn’t be built.`,
|
|
14
|
+
/** Followed by the command, in a code block. */
|
|
15
|
+
needsNpm: 'It needs npm. Run this in the site’s repository:',
|
|
16
|
+
update: 'Update',
|
|
17
|
+
tryAgain: 'Try again',
|
|
18
|
+
waiting: (v: string) => `Updating to ${v}. Waiting for the site to be built.`,
|
|
19
|
+
undoing: (v: string) => `Undoing the update to ${v}.`,
|
|
20
|
+
done: (v: string) => `Updated to ${v}`,
|
|
21
|
+
undone: (v: string) => `The update to ${v} was undone because the site couldn’t be built.`,
|
|
22
|
+
stuck: (v: string) => `The update to ${v} and its undo couldn’t be built. The site must be fixed by hand.`,
|
|
23
|
+
unreachable: 'npm or GitHub couldn’t be reached.',
|
|
24
|
+
cannotWrite: 'The site’s GitHub token can’t save. Give it Contents: Read and write.',
|
|
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.',
|
|
27
|
+
notManaged: 'The site’s package.json decides the version.',
|
|
28
|
+
slowUndo: 'The token can’t read check runs, so a failed update is undone after 20 minutes.',
|
|
29
|
+
/** The site's notifications (Notifications.tsx) and the commits the Worker writes, in the site's language. */
|
|
30
|
+
notifyDone: (v: string) => `Morpheme was updated to ${v}`,
|
|
31
|
+
notifyUndone: (v: string) => `The update of Morpheme to ${v} was undone because the site couldn’t be built`,
|
|
32
|
+
notifyStuck: (v: string) => `Morpheme ${v} couldn’t be built, and neither could its undo. Fix the site by hand.`,
|
|
33
|
+
commitUpdate: (v: string) => `Update Morpheme to ${v}`,
|
|
34
|
+
commitUndo: (v: string) => `Undo the update of Morpheme to ${v}`,
|
|
35
|
+
};
|
package/src/i18n/en-US/common.ts
CHANGED
|
@@ -134,6 +134,8 @@ export const common = {
|
|
|
134
134
|
advanced: 'Advanced',
|
|
135
135
|
/** Under Indstillinger: the licences of what the site and the editor are made with (#licenser, Licenses.tsx). */
|
|
136
136
|
licenses: 'Licenses',
|
|
137
|
+
/** Under Indstillinger: the CMS's version and its updates (#morpheme). */
|
|
138
|
+
morpheme: 'Morpheme',
|
|
137
139
|
},
|
|
138
140
|
/** Right after a save, with what it changed (savedText in deploy.ts); the site is rebuilt in 1-2 minutes. */
|
|
139
141
|
saved: (change: string) => `${change}. You’ll get a message when the website is updated.`,
|
package/src/i18n/en-US/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { apps } from './apps';
|
|
|
6
6
|
import { blocks } from './blocks';
|
|
7
7
|
import { blockSettings } from './blockSettings';
|
|
8
8
|
import { check } from './check';
|
|
9
|
+
import { cms } from './cms';
|
|
9
10
|
import { colors } from './colors';
|
|
10
11
|
import { common } from './common';
|
|
11
12
|
import { company } from './company';
|
|
@@ -41,6 +42,7 @@ export const enUS = {
|
|
|
41
42
|
seo,
|
|
42
43
|
keywords,
|
|
43
44
|
licenses,
|
|
45
|
+
cms,
|
|
44
46
|
parts,
|
|
45
47
|
history,
|
|
46
48
|
preview,
|
package/src/registry.tsx
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// and its look is the theme's (packages/editor/theme/blocks/<type>/, or the theme in use's): View.tsx, which draws its
|
|
10
10
|
// view in the editor (blockViews, from standard/themeBlocks.ts) and on the site. A new folder is a new block, in the editor and on
|
|
11
11
|
// the site; nothing else lists them. The apps (src/apps/<app>/, apps.ts) add blocks the same way, in their folder's
|
|
12
|
-
// blocks/, each with its own View.tsx
|
|
13
|
-
// only the look of the standard ones.
|
|
12
|
+
// blocks/, each with its own View.tsx and viewOf.ts for view.ts (it would differ from View.tsx only in case); they
|
|
13
|
+
// are offered once the app is installed. A theme has no blocks of its own, only the look of the standard ones.
|
|
14
14
|
import type { ReactNode } from 'react';
|
|
15
15
|
import { appIdOf, installedApps } from './apps';
|
|
16
16
|
import { htmlBlock, type BlockData, type BlockDefinition, type BlockProps, type DrawBlock, type SiteBlock } from './blocks';
|
|
@@ -60,7 +60,7 @@ export const siteBlocks: SiteBlock[] = [
|
|
|
60
60
|
...Object.entries(fromApps).map(([path, m]) =>
|
|
61
61
|
localSiteBlock({
|
|
62
62
|
...m.default,
|
|
63
|
-
draw: drawn(appViews[beside(path, 'View.tsx')]?.default, appViewOfs[beside(path, '
|
|
63
|
+
draw: drawn(appViews[beside(path, 'View.tsx')]?.default, appViewOfs[beside(path, 'viewOf.ts')]?.viewOf),
|
|
64
64
|
type: typeOf(path),
|
|
65
65
|
app: appIdOf(path),
|
|
66
66
|
}),
|
package/src/schema.ts
CHANGED
|
@@ -264,7 +264,7 @@ export const entryName = (collection: FolderCollection, data: Record<string, unk
|
|
|
264
264
|
|
|
265
265
|
/** An app's part of an entry of its kind, drawn in the list's own drawing (a Mailchimp signup in a pop-up's box), on the
|
|
266
266
|
* site and in the editor alike: its drawing (`View`, given its view and the entry's own button to show where it wants)
|
|
267
|
-
* and what makes its view in the editor (`viewOf`, as
|
|
267
|
+
* and what makes its view in the editor (`viewOf`, in its viewOf.ts, as an app's block). */
|
|
268
268
|
export type KindPart = {
|
|
269
269
|
View: ComponentType<{ view: any; button: ReactNode }>;
|
|
270
270
|
viewOf: (values: Record<string, any>, data: BlockData) => unknown;
|
package/starter/.prettierignore
CHANGED
package/starter/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Editoren, standardtemaet *Material* og sitets Worker er pakken [`@morphemecms/cm
|
|
|
9
9
|
|
|
10
10
|
- `src/site/config.ts`: sitets navn, domæne, sprog og GitHub-repo; resten er pakkens standard (`defineSite`).
|
|
11
11
|
- `src/site/worker.js`: det, Workeren har fra sitet.
|
|
12
|
+
- Hemmeligheden `GITHUB_PUBLISH_TOKEN` (`npx wrangler secret put GITHUB_PUBLISH_TOKEN`) lader Workeren skrive til sitets GitHub-repo, også når den opdaterer CMS'et. Tokenet skal have **Contents: Read and write** og **Checks: Read**.
|
|
12
13
|
- Hemmeligheden `MORPHEME_STORE_KEY` (`npx wrangler secret put MORPHEME_STORE_KEY`) forbinder sitet til Morpheme-butikken; uden den har *Appstore* kun sitets egne apps og temaer.
|
|
13
14
|
- `src/content/`: siderne og indlæggene, `src/data/`: sitets indstillinger, `public/`: billeder, logo og ikoner.
|
|
14
15
|
- `src/themes/` og `src/apps/`: temaer og apps fra appbutikken, når der er installeret nogen.
|
package/starter/package.json
CHANGED
|
@@ -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": "
|
|
34
|
+
"@morphemecms/cms": "0.3.0",
|
|
35
35
|
"@phosphor-icons/core": "^2.1.1",
|
|
36
36
|
"@phosphor-icons/react": "^2.1.10",
|
|
37
37
|
"@tailwindcss/typography": "^0.5.20",
|
package/starter/wrangler.jsonc
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"$schema": "node_modules/wrangler/config-schema.json",
|
|
6
6
|
"name": "morpheme-site",
|
|
7
7
|
"compatibility_date": "2026-09-01",
|
|
8
|
-
"compatibility_flags": ["nodejs_compat"],
|
|
8
|
+
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"], // public fetch: the store is on this account
|
|
9
9
|
"main": "node_modules/@morphemecms/cms/worker/index.js",
|
|
10
10
|
// What the Worker has from this site: its settings and src/site/worker.js (its redirects, apps and contact).
|
|
11
11
|
"alias": { "@site/config": "./src/site/config.ts", "@site/worker": "./src/site/worker.js" },
|
|
@@ -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 &&
|
|
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
|
---
|