@morphemecms/cms 0.3.0 → 0.4.0-next-20261104164754
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 +12 -0
- package/README.md +1 -1
- package/bin/morpheme.mjs +1 -1
- package/defaults/editor.ts +3 -1
- 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/package.json +2 -1
- package/scripts/built-site.mjs +14 -0
- package/scripts/check-cms-update.mjs +405 -0
- package/site/theme.d.mts +1 -1
- package/site/theme.mjs +2 -2
- package/src/App.tsx +3 -0
- package/src/CmsPage.tsx +139 -0
- package/src/Library.tsx +2 -0
- 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 +78 -0
- package/src/cmsView.test.d.mts +1 -0
- package/src/cmsView.test.mjs +146 -0
- package/src/frontmatter.mjs +4 -1
- package/src/frontmatter.test.mjs +6 -0
- package/src/i18n/da-DK/cms.ts +30 -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 +34 -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/worker/cms-update.js +245 -0
- package/worker/github.js +8 -6
- package/worker/index.js +6 -1
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
// Checks the Worker's own update of the CMS (packages/editor/worker/cms-update.js): the hourly check of npm, the update's commit, the
|
|
2
|
+
// minute watch of its check run with the undo, the 20 minutes' fallback, and /api/cms. GitHub, npm, KV and the notifications are
|
|
3
|
+
// played here (a fake KV, an in-memory package.json and lock, a fake fetch); nothing goes to GitHub, npm or Cloudflare.
|
|
4
|
+
// Run: npm run check:worker
|
|
5
|
+
import { gzipSync } from 'node:zlib';
|
|
6
|
+
import { config } from '../site/siteConfig.mjs';
|
|
7
|
+
import { workerModules } from './worker-modules.mjs';
|
|
8
|
+
|
|
9
|
+
const [{ RUNNING, checkForUpdate, cmsStatus, handleCms, startUpdate, watchUpdate }, { GitHubError }] = await workerModules(
|
|
10
|
+
'cms-update.js',
|
|
11
|
+
'files.js',
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const problems = [];
|
|
15
|
+
const expect = (ok, what) => ok || problems.push(what);
|
|
16
|
+
|
|
17
|
+
const T0 = Date.parse('2026-11-04T10:00:00Z');
|
|
18
|
+
const MINUTE = 60 * 1000;
|
|
19
|
+
const PACKAGE = '@morphemecms/cms';
|
|
20
|
+
const danish = String(config.locale).startsWith('da');
|
|
21
|
+
const commitUpdate = (v) => (danish ? `Opdater Morpheme til ${v}` : `Update Morpheme to ${v}`);
|
|
22
|
+
const commitUndo = (v) => (danish ? `Fortryd opdateringen af Morpheme til ${v}` : `Undo the update of Morpheme to ${v}`);
|
|
23
|
+
|
|
24
|
+
/** Workers KV, as much of it as the Worker uses; `reads` and `writes` count the calls. */
|
|
25
|
+
function kvStore() {
|
|
26
|
+
const map = new Map();
|
|
27
|
+
const kv = {
|
|
28
|
+
reads: 0,
|
|
29
|
+
writes: 0,
|
|
30
|
+
map,
|
|
31
|
+
get: async (key, type) => {
|
|
32
|
+
kv.reads++;
|
|
33
|
+
const value = map.get(key);
|
|
34
|
+
return value === undefined ? null : type === 'json' ? JSON.parse(value) : value;
|
|
35
|
+
},
|
|
36
|
+
put: async (key, value) => {
|
|
37
|
+
kv.writes++;
|
|
38
|
+
map.set(key, String(value));
|
|
39
|
+
},
|
|
40
|
+
delete: async (key) => {
|
|
41
|
+
kv.writes++;
|
|
42
|
+
map.delete(key);
|
|
43
|
+
},
|
|
44
|
+
list: async ({ prefix = '' } = {}) => ({ keys: [...map.keys()].filter((k) => k.startsWith(prefix)).map((name) => ({ name })) }),
|
|
45
|
+
};
|
|
46
|
+
return kv;
|
|
47
|
+
}
|
|
48
|
+
const stored = (kv, key) => (kv.map.has(key) ? JSON.parse(kv.map.get(key)) : null);
|
|
49
|
+
|
|
50
|
+
/** A gzipped tar with the files (`{ path: text }`), as npm packs a package. */
|
|
51
|
+
function tarball(files) {
|
|
52
|
+
const blocks = [];
|
|
53
|
+
for (const [path, text] of Object.entries(files)) {
|
|
54
|
+
const data = new TextEncoder().encode(text);
|
|
55
|
+
const header = new Uint8Array(512);
|
|
56
|
+
header.set(new TextEncoder().encode(path), 0);
|
|
57
|
+
header.set(new TextEncoder().encode(data.length.toString(8).padStart(11, '0')), 124);
|
|
58
|
+
blocks.push(header, data, new Uint8Array(Math.ceil(data.length / 512) * 512 - data.length));
|
|
59
|
+
}
|
|
60
|
+
blocks.push(new Uint8Array(1024));
|
|
61
|
+
return gzipSync(Buffer.concat(blocks));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const CHANGELOG = '# Changelog\n\n## 0.1.1\n\n- Fixed a thing.\n\n## 0.1.0\n\n- The first version.\n';
|
|
65
|
+
const manifestOf = (version, extra = {}) => ({
|
|
66
|
+
name: PACKAGE,
|
|
67
|
+
version,
|
|
68
|
+
license: 'MIT',
|
|
69
|
+
dist: { tarball: `https://registry.npmjs.org/@morphemecms/cms/-/cms-${version}.tgz`, integrity: `sha512-${version}` },
|
|
70
|
+
...extra,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The site and the world around it: package.json and the lock in memory, npm, the check runs, and the recorded commits and
|
|
75
|
+
* notifications. `checks`: a commit's check run by its id ('success' | 'failure'; none: no run yet), `forbidden`: GitHub answers
|
|
76
|
+
* the check runs with 403, `commitFails`: from which commit on `commitFiles` throws.
|
|
77
|
+
*/
|
|
78
|
+
function world({ range = '0.1.0', latest = '0.1.1', manifestExtra = {}, lock = true } = {}) {
|
|
79
|
+
const files = {
|
|
80
|
+
'package.json': JSON.stringify({ name: 'site', dependencies: { [PACKAGE]: range } }, null, 2) + '\n',
|
|
81
|
+
'package-lock.json':
|
|
82
|
+
JSON.stringify(
|
|
83
|
+
{
|
|
84
|
+
name: 'site',
|
|
85
|
+
lockfileVersion: 3,
|
|
86
|
+
packages: {
|
|
87
|
+
'': { name: 'site', dependencies: { [PACKAGE]: range } },
|
|
88
|
+
[`node_modules/${PACKAGE}`]: { version: '0.1.0', resolved: manifestOf('0.1.0').dist.tarball, integrity: 'sha512-0.1.0' },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
null,
|
|
92
|
+
2,
|
|
93
|
+
) + '\n',
|
|
94
|
+
};
|
|
95
|
+
if (!lock) delete files['package-lock.json'];
|
|
96
|
+
const w = {
|
|
97
|
+
kv: kvStore(),
|
|
98
|
+
files,
|
|
99
|
+
commits: [],
|
|
100
|
+
notices: [],
|
|
101
|
+
urls: [],
|
|
102
|
+
checks: {},
|
|
103
|
+
forbidden: false,
|
|
104
|
+
commitFails: null,
|
|
105
|
+
may: true,
|
|
106
|
+
latest,
|
|
107
|
+
readCount: 0,
|
|
108
|
+
};
|
|
109
|
+
w.env = { GITHUB_PUBLISH_TOKEN: 'token', PUSH: w.kv };
|
|
110
|
+
w.deps = {
|
|
111
|
+
readFiles: async (env, paths) => {
|
|
112
|
+
w.readCount++;
|
|
113
|
+
return { head: `head${w.commits.length}`, texts: paths.map((p) => files[p]) };
|
|
114
|
+
},
|
|
115
|
+
commitFiles: async (env, head, headline, changes) => {
|
|
116
|
+
if (w.failure && w.commits.length >= (w.commitFails ?? 0)) throw w.failure;
|
|
117
|
+
for (const { path, text } of changes) files[path] = text;
|
|
118
|
+
w.commits.push({ headline, changes, head });
|
|
119
|
+
return `c${w.commits.length}`;
|
|
120
|
+
},
|
|
121
|
+
fetch: async (url) => {
|
|
122
|
+
url = String(url);
|
|
123
|
+
w.urls.push(url);
|
|
124
|
+
const check = /api\.github\.com\/repos\/[^/]+\/[^/]+\/commits\/(\w+)\/check-runs$/.exec(url);
|
|
125
|
+
if (check) {
|
|
126
|
+
if (w.forbidden) return new Response('{}', { status: 403 });
|
|
127
|
+
const conclusion = w.checks[check[1]];
|
|
128
|
+
const runs = [{ app: { slug: 'some-other-app' }, conclusion: 'failure', started_at: '2026-11-04T10:00:00Z' }];
|
|
129
|
+
if (conclusion) runs.push({ app: { slug: 'cloudflare-workers-and-pages' }, conclusion, started_at: '2026-11-04T10:01:00Z' });
|
|
130
|
+
return Response.json({ check_runs: runs });
|
|
131
|
+
}
|
|
132
|
+
if (url === 'https://registry.npmjs.org/-/package/@morphemecms%2fcms/dist-tags') return Response.json({ latest: w.latest });
|
|
133
|
+
const manifest = /^https:\/\/registry\.npmjs\.org\/@morphemecms%2fcms\/(\d+\.\d+\.\d+)$/.exec(url);
|
|
134
|
+
if (manifest) return Response.json(manifestOf(manifest[1], manifest[1] === w.latest ? manifestExtra : {}));
|
|
135
|
+
if (url.endsWith('.tgz')) return new Response(tarball({ 'package/CHANGELOG.md': CHANGELOG }));
|
|
136
|
+
return new Response('{}', { status: 404 });
|
|
137
|
+
},
|
|
138
|
+
notify: async (env, notice, texts) => void w.notices.push({ notice, texts }),
|
|
139
|
+
mayEdit: async () => w.may,
|
|
140
|
+
};
|
|
141
|
+
return w;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const request = (path, init) => new Request(`https://site.example${path}`, init);
|
|
145
|
+
const post = (version) =>
|
|
146
|
+
request('/api/cms/update', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ version }) });
|
|
147
|
+
|
|
148
|
+
// 1. An exact version, a newer one on npm: one commit, the files written, the update kept, the notes kept.
|
|
149
|
+
{
|
|
150
|
+
const w = world();
|
|
151
|
+
await checkForUpdate(w.env, w.deps, T0);
|
|
152
|
+
expect(w.commits.length === 1, `opdateringen lavede ${w.commits.length} commits, ikke 1`);
|
|
153
|
+
expect(w.commits[0]?.headline === commitUpdate('0.1.1'), `commitbeskeden var "${w.commits[0]?.headline}"`);
|
|
154
|
+
expect(JSON.parse(w.files['package.json']).dependencies[PACKAGE] === '0.1.1', 'package.json sagde ikke 0.1.1');
|
|
155
|
+
const entry = JSON.parse(w.files['package-lock.json']).packages[`node_modules/${PACKAGE}`];
|
|
156
|
+
expect(
|
|
157
|
+
entry.version === '0.1.1' && entry.resolved === manifestOf('0.1.1').dist.tarball && entry.integrity === 'sha512-0.1.1',
|
|
158
|
+
'låsen fik ikke 0.1.1s resolved og integrity',
|
|
159
|
+
);
|
|
160
|
+
expect(w.files['package.json'].endsWith('}\n') && w.files['package-lock.json'].endsWith('}\n'), 'filerne endte ikke på en linjeskift');
|
|
161
|
+
const update = stored(w.kv, 'cms:update');
|
|
162
|
+
expect(
|
|
163
|
+
update?.state === 'waiting' && update.from === '0.1.0' && update.to === '0.1.1' && update.commit === 'c1',
|
|
164
|
+
`cms:update var ${JSON.stringify(update)}`,
|
|
165
|
+
);
|
|
166
|
+
expect(stored(w.kv, 'cms:notes:0.1.1')?.text === '- Fixed a thing.', 'versionens noter blev ikke gemt');
|
|
167
|
+
expect(stored(w.kv, 'cms:notes:0.1.0') === null, 'noter for en version, sitet allerede har, blev gemt');
|
|
168
|
+
expect(stored(w.kv, 'cms:checked')?.latest === '0.1.1', 'cms:checked fik ikke latest');
|
|
169
|
+
|
|
170
|
+
// 2. The hourly step while it waits: nothing.
|
|
171
|
+
const before = w.urls.length;
|
|
172
|
+
await checkForUpdate(w.env, w.deps, T0 + 60 * MINUTE);
|
|
173
|
+
expect(w.commits.length === 1 && w.urls.length === before, 'timetjekket gjorde noget, mens en opdatering ventede');
|
|
174
|
+
|
|
175
|
+
// 3. The update's check run fails: the undo, the skip, then the undo's build is fine.
|
|
176
|
+
w.checks.c1 = 'failure';
|
|
177
|
+
await watchUpdate(w.env, w.deps, T0 + 2 * MINUTE);
|
|
178
|
+
expect(w.commits.length === 2 && w.commits[1].headline === commitUndo('0.1.1'), 'en fejlet opdatering blev ikke fortrudt med en commit');
|
|
179
|
+
expect(JSON.parse(w.files['package.json']).dependencies[PACKAGE] === '0.1.0', 'fortrydelsen skrev ikke 0.1.0 tilbage');
|
|
180
|
+
expect(
|
|
181
|
+
JSON.parse(w.files['package-lock.json']).packages[`node_modules/${PACKAGE}`].integrity === 'sha512-0.1.0',
|
|
182
|
+
'fortrydelsen skrev ikke 0.1.0 tilbage i låsen',
|
|
183
|
+
);
|
|
184
|
+
expect(stored(w.kv, 'cms:skip:0.1.1')?.commit === 'c1', 'den fortrudte version blev ikke sprunget over');
|
|
185
|
+
const undoing = stored(w.kv, 'cms:update');
|
|
186
|
+
expect(undoing?.state === 'undoing' && undoing.undoCommit === 'c2', `cms:update var ${JSON.stringify(undoing)}`);
|
|
187
|
+
expect(w.notices.length === 0, 'der blev sendt en besked, før fortrydelsen var bygget');
|
|
188
|
+
await watchUpdate(w.env, w.deps, T0 + 3 * MINUTE);
|
|
189
|
+
expect(stored(w.kv, 'cms:update')?.state === 'undoing', 'fortrydelsen blev regnet for færdig uden et resultat');
|
|
190
|
+
w.checks.c2 = 'success';
|
|
191
|
+
await watchUpdate(w.env, w.deps, T0 + 4 * MINUTE);
|
|
192
|
+
expect(stored(w.kv, 'cms:update')?.state === 'undone', 'fortrydelsen blev ikke til undone');
|
|
193
|
+
expect(w.notices.length === 1 && w.notices[0].notice.error === true, 'undone gav ikke én besked med error');
|
|
194
|
+
const notice = w.notices[0]?.notice;
|
|
195
|
+
expect(notice?.type === 'updates' && notice.id === 'cms' && notice.text.includes('0.1.1'), 'beskeden havde ikke typen updates');
|
|
196
|
+
expect(
|
|
197
|
+
Object.keys(w.notices[0]?.texts ?? {})
|
|
198
|
+
.sort()
|
|
199
|
+
.join() === 'da-DK,en-US' && w.notices[0].texts['da-DK'][notice.text]?.includes('fortrudt'),
|
|
200
|
+
'beskeden havde ikke den danske tekst',
|
|
201
|
+
);
|
|
202
|
+
// The skipped version is not taken by itself again.
|
|
203
|
+
await checkForUpdate(w.env, w.deps, T0 + 70 * MINUTE);
|
|
204
|
+
expect(w.commits.length === 2 && stored(w.kv, 'cms:checked')?.problem?.kind === 'skipped', 'en fortrudt version blev taget igen');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 4. The site runs the new version: done, one notification.
|
|
208
|
+
{
|
|
209
|
+
const w = world();
|
|
210
|
+
w.files['package.json'] = JSON.stringify({ dependencies: { [PACKAGE]: RUNNING } });
|
|
211
|
+
await w.kv.put(
|
|
212
|
+
'cms:update',
|
|
213
|
+
JSON.stringify({ commit: 'c1', from: '0.0.0', to: RUNNING, at: new Date(T0).toISOString(), state: 'waiting' }),
|
|
214
|
+
);
|
|
215
|
+
await watchUpdate(w.env, w.deps, T0 + 5 * MINUTE);
|
|
216
|
+
expect(stored(w.kv, 'cms:update')?.state === 'done', 'en opdatering, der kører, blev ikke done');
|
|
217
|
+
expect(w.notices.length === 1 && !w.notices[0].notice.error, 'done gav ikke én besked uden error');
|
|
218
|
+
await watchUpdate(w.env, w.deps, T0 + 6 * MINUTE);
|
|
219
|
+
expect(w.notices.length === 1, 'done blev meldt to gange');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 5. A version the site does not pin exactly is left alone.
|
|
223
|
+
for (const range of ['*', '^0.1.0', 'next']) {
|
|
224
|
+
const w = world({ range });
|
|
225
|
+
await checkForUpdate(w.env, w.deps, T0);
|
|
226
|
+
expect(w.commits.length === 0, `"${range}" blev opdateret`);
|
|
227
|
+
expect(stored(w.kv, 'cms:checked')?.problem?.kind === 'not-managed', `"${range}" gav ikke not-managed`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 6. A new first number, and a skipped version, are not taken by themselves.
|
|
231
|
+
{
|
|
232
|
+
const w = world({ latest: '1.0.0' });
|
|
233
|
+
await checkForUpdate(w.env, w.deps, T0);
|
|
234
|
+
expect(
|
|
235
|
+
w.commits.length === 0 && stored(w.kv, 'cms:checked')?.problem?.kind === 'major',
|
|
236
|
+
'en ny hovedversion blev taget, eller meldt forkert',
|
|
237
|
+
);
|
|
238
|
+
expect(stored(w.kv, 'cms:checked')?.latest === '1.0.0', 'latest blev ikke gemt ved en hovedversion');
|
|
239
|
+
const s = world();
|
|
240
|
+
await s.kv.put('cms:skip:0.1.1', JSON.stringify({ version: '0.1.1', commit: 'x', at: new Date(T0).toISOString() }));
|
|
241
|
+
await checkForUpdate(s.env, s.deps, T0);
|
|
242
|
+
expect(
|
|
243
|
+
s.commits.length === 0 && stored(s.kv, 'cms:checked')?.problem?.kind === 'skipped',
|
|
244
|
+
'en fortrudt version blev taget, eller meldt forkert',
|
|
245
|
+
);
|
|
246
|
+
const n = world({ latest: '0.1.0' });
|
|
247
|
+
await checkForUpdate(n.env, n.deps, T0);
|
|
248
|
+
expect(n.commits.length === 0 && !stored(n.kv, 'cms:checked')?.problem, 'der var et problem, selv om sitet er opdateret');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 7. A token that cannot write: nothing is committed or kept.
|
|
252
|
+
for (const error of [new GitHubError('Forbidden', 403), new GitHubError('Resource not accessible by personal access token', 200)]) {
|
|
253
|
+
const w = world();
|
|
254
|
+
w.failure = error;
|
|
255
|
+
await checkForUpdate(w.env, w.deps, T0);
|
|
256
|
+
expect(stored(w.kv, 'cms:update') === null, `cms:update blev gemt, selv om gemningen fejlede (${error.status})`);
|
|
257
|
+
expect(
|
|
258
|
+
stored(w.kv, 'cms:checked')?.problem?.kind === 'cannot-write',
|
|
259
|
+
`en nøgle uden skriverettighed gav ikke cannot-write (${error.status})`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 8. Without the token (or KV) nothing is asked and nothing is kept.
|
|
264
|
+
for (const [what, patch] of [
|
|
265
|
+
['uden nøgle', { GITHUB_PUBLISH_TOKEN: undefined }],
|
|
266
|
+
['uden KV', { PUSH: undefined }],
|
|
267
|
+
]) {
|
|
268
|
+
const w = world();
|
|
269
|
+
const env = { ...w.env, ...patch };
|
|
270
|
+
await checkForUpdate(env, w.deps, T0);
|
|
271
|
+
await watchUpdate(env, w.deps, T0);
|
|
272
|
+
expect(w.urls.length === 0 && w.readCount === 0 && w.kv.writes === 0 && w.commits.length === 0, `tjekket gjorde noget ${what}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 9. Check runs GitHub does not give: the update is undone after 20 minutes.
|
|
276
|
+
{
|
|
277
|
+
const w = world();
|
|
278
|
+
await checkForUpdate(w.env, w.deps, T0);
|
|
279
|
+
w.forbidden = true;
|
|
280
|
+
await watchUpdate(w.env, w.deps, T0 + MINUTE);
|
|
281
|
+
expect(stored(w.kv, 'cms:checked')?.noChecks === true, 'noChecks blev ikke sat, da check runs gav 403');
|
|
282
|
+
await watchUpdate(w.env, w.deps, T0 + 19 * MINUTE);
|
|
283
|
+
expect(stored(w.kv, 'cms:update')?.state === 'waiting' && w.commits.length === 1, 'opdateringen blev fortrudt efter 19 minutter');
|
|
284
|
+
await watchUpdate(w.env, w.deps, T0 + 21 * MINUTE);
|
|
285
|
+
expect(stored(w.kv, 'cms:update')?.state === 'undoing' && w.commits.length === 2, 'opdateringen blev ikke fortrudt efter 21 minutter');
|
|
286
|
+
await watchUpdate(w.env, w.deps, T0 + 30 * MINUTE);
|
|
287
|
+
expect(stored(w.kv, 'cms:update')?.state === 'undoing', 'fortrydelsen blev regnet for færdig efter 9 minutter');
|
|
288
|
+
await watchUpdate(w.env, w.deps, T0 + 42 * MINUTE);
|
|
289
|
+
expect(stored(w.kv, 'cms:update')?.state === 'undone' && w.notices.length === 1, 'fortrydelsen blev ikke undone efter 20 minutter');
|
|
290
|
+
expect(stored(w.kv, 'cms:checked')?.noChecks === true, 'noChecks blev væk');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 10. The undo cannot be committed: stuck, one notification, then nothing.
|
|
294
|
+
{
|
|
295
|
+
const w = world();
|
|
296
|
+
await checkForUpdate(w.env, w.deps, T0);
|
|
297
|
+
w.checks.c1 = 'failure';
|
|
298
|
+
w.failure = new GitHubError('Conflict', 409);
|
|
299
|
+
w.commitFails = 1;
|
|
300
|
+
await watchUpdate(w.env, w.deps, T0 + 2 * MINUTE);
|
|
301
|
+
expect(stored(w.kv, 'cms:update')?.state === 'stuck', 'en fortrydelse, der ikke kunne gemmes, gav ikke stuck');
|
|
302
|
+
expect(w.notices.length === 1 && w.notices[0].notice.error === true, 'stuck gav ikke én besked med error');
|
|
303
|
+
const urls = w.urls.length;
|
|
304
|
+
await watchUpdate(w.env, w.deps, T0 + 3 * MINUTE);
|
|
305
|
+
expect(w.notices.length === 1 && w.urls.length === urls && w.commits.length === 1, 'minuttet efter stuck gjorde noget');
|
|
306
|
+
// Its undo's build failing is stuck as well.
|
|
307
|
+
const u = world();
|
|
308
|
+
await checkForUpdate(u.env, u.deps, T0);
|
|
309
|
+
u.checks.c1 = 'failure';
|
|
310
|
+
await watchUpdate(u.env, u.deps, T0 + 2 * MINUTE);
|
|
311
|
+
u.checks.c2 = 'failure';
|
|
312
|
+
await watchUpdate(u.env, u.deps, T0 + 3 * MINUTE);
|
|
313
|
+
expect(
|
|
314
|
+
stored(u.kv, 'cms:update')?.state === 'stuck' && u.notices.length === 1 && u.notices[0].notice.error,
|
|
315
|
+
'en fortrydelse, der ikke byggede, gav ikke stuck',
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 11. Nothing waiting: one KV read, no request.
|
|
320
|
+
{
|
|
321
|
+
const w = world();
|
|
322
|
+
await watchUpdate(w.env, w.deps, T0);
|
|
323
|
+
expect(
|
|
324
|
+
w.kv.reads === 1 && w.urls.length === 0 && w.kv.writes === 0,
|
|
325
|
+
`minuttet uden en opdatering læste ${w.kv.reads} gange og gjorde ${w.urls.length} kald`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 12. /api/cms.
|
|
330
|
+
{
|
|
331
|
+
const w = world({ latest: '1.0.0' });
|
|
332
|
+
w.may = false;
|
|
333
|
+
expect((await handleCms(request('/api/cms'), w.env, undefined, w.deps))?.status === 403, 'en, der ikke kan gemme, fik status');
|
|
334
|
+
expect((await handleCms(post('1.0.0'), w.env, undefined, w.deps))?.status === 403, 'en, der ikke kan gemme, kunne opdatere');
|
|
335
|
+
expect((await handleCms(request('/api/other'), w.env, undefined, w.deps)) === null, 'en anden adresse blev ikke givet videre');
|
|
336
|
+
w.may = true;
|
|
337
|
+
await checkForUpdate(w.env, w.deps, T0);
|
|
338
|
+
const status = await (await handleCms(request('/api/cms'), w.env, undefined, w.deps)).json();
|
|
339
|
+
expect(
|
|
340
|
+
status.running === RUNNING &&
|
|
341
|
+
status.token === true &&
|
|
342
|
+
status.kv === true &&
|
|
343
|
+
status.checked?.latest === '1.0.0' &&
|
|
344
|
+
status.update === null &&
|
|
345
|
+
Array.isArray(status.skipped) &&
|
|
346
|
+
Array.isArray(status.notes),
|
|
347
|
+
`statussen havde ikke formen: ${JSON.stringify(status)}`,
|
|
348
|
+
);
|
|
349
|
+
expect((await handleCms(post('0.9.9'), w.env, undefined, w.deps)).status === 400, 'en version, der ikke er latest, blev taget imod');
|
|
350
|
+
|
|
351
|
+
// A waiting update: 409.
|
|
352
|
+
const k = world();
|
|
353
|
+
await checkForUpdate(k.env, k.deps, T0);
|
|
354
|
+
expect((await handleCms(post('0.1.1'), k.env, undefined, k.deps)).status === 409, 'en opdatering, mens en anden venter, gav ikke 409');
|
|
355
|
+
|
|
356
|
+
// A skipped version, tried again: its skip is gone and a commit is made.
|
|
357
|
+
const s = world();
|
|
358
|
+
await s.kv.put('cms:skip:0.1.1', JSON.stringify({ version: '0.1.1', commit: 'x', at: new Date(T0).toISOString() }));
|
|
359
|
+
await checkForUpdate(s.env, s.deps, T0);
|
|
360
|
+
const again = await handleCms(post('0.1.1'), s.env, undefined, s.deps);
|
|
361
|
+
expect(again.status === 200 && (await again.json()).update?.state === 'waiting', 'Prøv igen gav ikke en ventende opdatering');
|
|
362
|
+
expect(stored(s.kv, 'cms:skip:0.1.1') === null && s.commits.length === 1, 'Prøv igen fjernede ikke spring-over-nøglen og committede');
|
|
363
|
+
|
|
364
|
+
// A package that wants npm: 422.
|
|
365
|
+
const n = world({ manifestExtra: { dependencies: { left: '1.0.0' } } });
|
|
366
|
+
await checkForUpdate(n.env, n.deps, T0);
|
|
367
|
+
expect(n.commits.length === 0 && stored(n.kv, 'cms:checked')?.problem?.kind === 'needs-npm', 'needs-npm blev ikke meldt af timetjekket');
|
|
368
|
+
const npm = await handleCms(post('0.1.1'), n.env, undefined, n.deps);
|
|
369
|
+
expect(npm.status === 422 && (await npm.json()).error === 'needs-npm', 'en pakke, der kræver npm, gav ikke 422');
|
|
370
|
+
|
|
371
|
+
// A token that cannot write: 502; no token: 503.
|
|
372
|
+
const c = world();
|
|
373
|
+
await checkForUpdate({ ...c.env, GITHUB_PUBLISH_TOKEN: undefined }, c.deps, T0);
|
|
374
|
+
c.kv.map.set('cms:checked', JSON.stringify({ at: new Date(T0).toISOString(), latest: '0.1.1' }));
|
|
375
|
+
c.failure = new GitHubError('Forbidden', 403);
|
|
376
|
+
expect((await handleCms(post('0.1.1'), c.env, undefined, c.deps)).status === 502, 'en nøgle, der ikke kan gemme, gav ikke 502');
|
|
377
|
+
expect(
|
|
378
|
+
(await handleCms(post('0.1.1'), { ...c.env, GITHUB_PUBLISH_TOKEN: undefined }, undefined, c.deps)).status === 503,
|
|
379
|
+
'uden nøgle gav ikke 503',
|
|
380
|
+
);
|
|
381
|
+
const direct = await startUpdate(world().env, '0.1.1', world().deps, T0).catch((e) => e);
|
|
382
|
+
expect(direct?.to === '0.1.1' && direct.state === 'waiting', 'startUpdate gav ikke opdateringen');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// The page's notes: from the running version up to the newest, newest first.
|
|
386
|
+
{
|
|
387
|
+
const [major, minor, patch] = RUNNING.split('-')[0].split('.').map(Number);
|
|
388
|
+
const next1 = `${major}.${minor}.${patch + 1}`;
|
|
389
|
+
const next2 = `${major}.${minor}.${patch + 2}`;
|
|
390
|
+
const w = world();
|
|
391
|
+
for (const v of ['0.0.0', RUNNING, next1, next2, '9.9.9'])
|
|
392
|
+
await w.kv.put(`cms:notes:${v}`, JSON.stringify({ version: v, text: `Notes ${v}` }));
|
|
393
|
+
await w.kv.put('cms:checked', JSON.stringify({ at: new Date(T0).toISOString(), latest: next2 }));
|
|
394
|
+
const { notes } = await cmsStatus(w.env);
|
|
395
|
+
expect(
|
|
396
|
+
notes.map((n) => n.version).join() === `${next2},${next1}`,
|
|
397
|
+
`noterne var ${notes.map((n) => n.version).join()}, ikke ${next2},${next1}`,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (problems.length) {
|
|
402
|
+
console.error(problems.map((p) => `- ${p}`).join('\n'));
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
console.log('Workerens opdatering af CMS’et er i orden.');
|
package/site/theme.d.mts
CHANGED
|
@@ -65,7 +65,7 @@ export function themePlugin(): {
|
|
|
65
65
|
load(id: any): string | null;
|
|
66
66
|
};
|
|
67
67
|
/** What a block is missing to be drawn from its view (packages/editor/src/standard/view.tsx): a standard block's
|
|
68
|
-
* contract its view.ts and load.ts, an app's block (src/apps/<app>/blocks/<type>/)
|
|
68
|
+
* contract its view.ts and load.ts, an app's block (src/apps/<app>/blocks/<type>/) its viewOf.ts, load.ts and View.tsx; and the
|
|
69
69
|
* files of how a block looked before (Site.astro, draw.tsx), which nothing uses now. */
|
|
70
70
|
export function blockProblems(): string[];
|
|
71
71
|
/** The theme every theme builds on, Material (this package's theme/, id 'saturn'): it has all of THEME_FILES and the look of every
|
package/site/theme.mjs
CHANGED
|
@@ -465,7 +465,7 @@ export function themePlugin() {
|
|
|
465
465
|
}
|
|
466
466
|
|
|
467
467
|
/** What a block is missing to be drawn from its view (packages/editor/src/standard/view.tsx): a standard block's
|
|
468
|
-
* contract its view.ts and load.ts, an app's block (src/apps/<app>/blocks/<type>/)
|
|
468
|
+
* contract its view.ts and load.ts, an app's block (src/apps/<app>/blocks/<type>/) its viewOf.ts, load.ts and View.tsx; and the
|
|
469
469
|
* files of how a block looked before (Site.astro, draw.tsx), which nothing uses now. */
|
|
470
470
|
export function blockProblems() {
|
|
471
471
|
const problems = [];
|
|
@@ -475,7 +475,7 @@ export function blockProblems() {
|
|
|
475
475
|
for (const app of folders(new URL('src/apps/', root)))
|
|
476
476
|
for (const type of folders(new URL(`src/apps/${app}/blocks/`, root))) {
|
|
477
477
|
const dir = `src/apps/${app}/blocks/${type}`;
|
|
478
|
-
for (const f of ['index.tsx', '
|
|
478
|
+
for (const f of ['index.tsx', 'viewOf.ts', 'load.ts', 'View.tsx']) if (!has(`${dir}/${f}`)) problems.push(`${dir}/${f} mangler.`);
|
|
479
479
|
for (const f of OLD_LOOK)
|
|
480
480
|
if (has(`${dir}/${f}`)) problems.push(`${dir}/${f}: blokken tegnes af View.tsx, både på sitet og i editoren.`);
|
|
481
481
|
}
|
package/src/App.tsx
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
// theme's design tokens, "Værdier" (Tokens.tsx)
|
|
32
32
|
// blokvalg the blocks that can be put in, "Blokindstillinger" (BlockSettings.tsx)
|
|
33
33
|
// avanceret "Avanceret", under "Indstillinger": the copy of the site's files in R2 (Advanced.tsx)
|
|
34
|
+
// morpheme "Morpheme", under "Indstillinger": the CMS's version, a newer one and its notes, an update's progress (CmsPage.tsx)
|
|
34
35
|
// licenser "Licenser", under "Indstillinger": the licences of what the site and the editor are made with, and
|
|
35
36
|
// licenser/<id> one with its text (Licenses.tsx)
|
|
36
37
|
// apps the apps that come with the site, apps/<app> one with its button to install it and, while it
|
|
@@ -92,6 +93,7 @@ import Tokens from './Tokens';
|
|
|
92
93
|
import BrandForm from './BrandForm';
|
|
93
94
|
import BlockSettings from './BlockSettings';
|
|
94
95
|
import Advanced, { ADVANCED_ROUTE } from './Advanced';
|
|
96
|
+
import CmsPage, { CMS_ROUTE } from './CmsPage';
|
|
95
97
|
import Licenses, { LICENSES_ROUTE, LicensePage } from './Licenses';
|
|
96
98
|
import { StoreThemePage } from './ThemeStore';
|
|
97
99
|
import {
|
|
@@ -841,6 +843,7 @@ function Page({ route, files, env, user, builtAt, onLogout }: PageProps) {
|
|
|
841
843
|
if (route === 'udseende/logo') return <BrandForm env={env} />;
|
|
842
844
|
if (route === 'blokvalg') return <BlockSettings env={env} files={files} />;
|
|
843
845
|
if (route === ADVANCED_ROUTE) return <Advanced env={env} />;
|
|
846
|
+
if (route === CMS_ROUTE) return <CmsPage />;
|
|
844
847
|
if (route === LICENSES_ROUTE) return <Licenses />;
|
|
845
848
|
if (route.startsWith(`${LICENSES_ROUTE}/`)) return <LicensePage key={route} id={route.slice(LICENSES_ROUTE.length + 1)} />;
|
|
846
849
|
if (route === 'apps') return <Apps env={env} files={files} />;
|
package/src/CmsPage.tsx
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// "Morpheme" (#morpheme), under "Indstillinger": the CMS version the site runs, a newer one with its notes, why it is
|
|
2
|
+
// not taken by itself, and an update's progress. Exposes CMS_ROUTE and the page. Depends on cmsView.mjs and the Worker's /api/cms.
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
4
|
+
import { CheckCircle, Clock, Info, Sparkle, WarningCircle } from '@phosphor-icons/react';
|
|
5
|
+
import Screen from './Screen';
|
|
6
|
+
import { Button, EmptyNote, Group, ListItem, ProblemNote } from './ui';
|
|
7
|
+
import { backFrom } from './site';
|
|
8
|
+
import { cmsView } from './cmsView.mjs';
|
|
9
|
+
import { snackbar } from '../lib/snackbar';
|
|
10
|
+
import { t } from './i18n';
|
|
11
|
+
import { when } from './when';
|
|
12
|
+
|
|
13
|
+
export const CMS_ROUTE = 'morpheme';
|
|
14
|
+
|
|
15
|
+
type Problem = 'unreachable' | 'cannot-write' | 'no-token' | 'not-managed' | 'slow-undo';
|
|
16
|
+
type UpdateState = 'waiting' | 'undoing' | 'done' | 'undone' | 'stuck';
|
|
17
|
+
type View = {
|
|
18
|
+
running: string;
|
|
19
|
+
checkedAt: string | null;
|
|
20
|
+
latest: string | null;
|
|
21
|
+
notes: { version: string; text: string }[];
|
|
22
|
+
action: 'update' | 'try-again' | null;
|
|
23
|
+
npmCommand: string | null;
|
|
24
|
+
update: { state: UpdateState; to: string; at: string } | null;
|
|
25
|
+
problems: Problem[];
|
|
26
|
+
};
|
|
27
|
+
type Status = object;
|
|
28
|
+
|
|
29
|
+
/** The Worker's error code in words; any other answer says what status it was. */
|
|
30
|
+
function errorText(code: unknown, status: number, version = '') {
|
|
31
|
+
if (code === 'not-set-up') return t.cms.noToken;
|
|
32
|
+
if (code === 'waiting') return t.cms.waiting(version);
|
|
33
|
+
return t.app.githubAnswered(status);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The Worker's answer (GET /api/cms) as the page shows it, or why it could not be read. */
|
|
37
|
+
async function readStatus(): Promise<Status> {
|
|
38
|
+
const res = await fetch('/api/cms', { cache: 'no-store' });
|
|
39
|
+
const json = await res.json().catch(() => ({}));
|
|
40
|
+
if (!res.ok) throw new Error(errorText(json.error, res.status));
|
|
41
|
+
return json;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Why the site does not take the newer version by itself, or nothing. */
|
|
45
|
+
const reasonOf = (view: View) =>
|
|
46
|
+
view.action === 'update' ? t.cms.major : view.action === 'try-again' && view.latest ? t.cms.skipped(view.latest) : null;
|
|
47
|
+
|
|
48
|
+
function UpdateRow({ update }: { update: NonNullable<View['update']> }) {
|
|
49
|
+
const text = t.cms[update.state](update.to);
|
|
50
|
+
const bad = update.state === 'undone' || update.state === 'stuck';
|
|
51
|
+
return (
|
|
52
|
+
<ListItem
|
|
53
|
+
icon={bad ? WarningCircle : update.state === 'done' ? CheckCircle : Clock}
|
|
54
|
+
tone={bad ? 'accent' : undefined}
|
|
55
|
+
headline={text}
|
|
56
|
+
supporting={when(new Date(update.at))}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default function CmsPage() {
|
|
62
|
+
const [view, setView] = useState<View | null>(null);
|
|
63
|
+
const [problem, setProblem] = useState('');
|
|
64
|
+
const [busy, setBusy] = useState(false);
|
|
65
|
+
|
|
66
|
+
const load = useCallback(async () => {
|
|
67
|
+
try {
|
|
68
|
+
setView(cmsView(await readStatus()) as View);
|
|
69
|
+
setProblem('');
|
|
70
|
+
} catch (e) {
|
|
71
|
+
setProblem((e as Error).message);
|
|
72
|
+
}
|
|
73
|
+
}, []);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
void load();
|
|
76
|
+
}, [load]);
|
|
77
|
+
|
|
78
|
+
async function update(version: string) {
|
|
79
|
+
setBusy(true);
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetch('/api/cms/update', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'content-type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({ version }),
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
const json = await res.json().catch(() => ({}));
|
|
88
|
+
throw new Error(errorText(json.error, res.status, version));
|
|
89
|
+
}
|
|
90
|
+
await load();
|
|
91
|
+
} catch (e) {
|
|
92
|
+
snackbar({ text: (e as Error).message, error: true });
|
|
93
|
+
} finally {
|
|
94
|
+
setBusy(false);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const reason = view && reasonOf(view);
|
|
99
|
+
return (
|
|
100
|
+
<Screen title={t.common.parts.morpheme} back={backFrom(CMS_ROUTE)}>
|
|
101
|
+
{problem && <ProblemNote alert>{problem}</ProblemNote>}
|
|
102
|
+
{!view && !problem && <EmptyNote busy>{t.app.loading}</EmptyNote>}
|
|
103
|
+
{view && (
|
|
104
|
+
<>
|
|
105
|
+
<Group>
|
|
106
|
+
<ListItem icon={Info} headline={t.cms.running(view.running)} supporting={view.latest ? undefined : t.cms.upToDate} />
|
|
107
|
+
<ListItem icon={Clock} headline={view.checkedAt ? t.cms.checked(when(new Date(view.checkedAt))) : t.cms.notChecked} />
|
|
108
|
+
{view.update && <UpdateRow update={view.update} />}
|
|
109
|
+
{view.latest && <ListItem icon={Sparkle} tone="accent" headline={t.cms.available(view.latest)} supporting={reason} />}
|
|
110
|
+
{view.npmCommand && <ListItem headline={t.cms.needsNpm} supporting={<code>{view.npmCommand}</code>} />}
|
|
111
|
+
{view.problems.map((kind) => (
|
|
112
|
+
<ListItem key={kind} icon={WarningCircle} tone="accent" headline={t.cms[kindText[kind]]} />
|
|
113
|
+
))}
|
|
114
|
+
</Group>
|
|
115
|
+
{view.action && view.latest && (
|
|
116
|
+
<Button kind="filled" disabled={busy} onClick={() => void update(view.latest!)}>
|
|
117
|
+
{view.action === 'update' ? t.cms.update : t.cms.tryAgain}
|
|
118
|
+
</Button>
|
|
119
|
+
)}
|
|
120
|
+
{view.notes.length > 0 && (
|
|
121
|
+
<Group title={t.cms.whatsNew}>
|
|
122
|
+
{view.notes.map((n) => (
|
|
123
|
+
<ListItem key={n.version} headline={t.cms.running(n.version)} supporting={n.text} />
|
|
124
|
+
))}
|
|
125
|
+
</Group>
|
|
126
|
+
)}
|
|
127
|
+
</>
|
|
128
|
+
)}
|
|
129
|
+
</Screen>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const kindText: Record<Problem, 'unreachable' | 'cannotWrite' | 'noToken' | 'notManaged' | 'slowUndo'> = {
|
|
134
|
+
unreachable: 'unreachable',
|
|
135
|
+
'cannot-write': 'cannotWrite',
|
|
136
|
+
'no-token': 'noToken',
|
|
137
|
+
'not-managed': 'notManaged',
|
|
138
|
+
'slow-undo': 'slowUndo',
|
|
139
|
+
};
|
package/src/Library.tsx
CHANGED
|
@@ -111,6 +111,7 @@ import { MOTION_SCHEMES, schemeInUse, type MotionScheme } from '../lib/motion';
|
|
|
111
111
|
import { applyMotion } from './theme';
|
|
112
112
|
import { colorsOf } from './Colors';
|
|
113
113
|
import { OwnerLicenses } from './Licenses';
|
|
114
|
+
import { CMS_ROUTE } from './CmsPage';
|
|
114
115
|
import { apps } from './site';
|
|
115
116
|
import { offBlocks } from './registry';
|
|
116
117
|
import { sideChoicesOf, sideSettingOf } from './SideColumn';
|
|
@@ -133,6 +134,7 @@ export function partOf(id: string, files: RepoFile[]): { label: string; href: st
|
|
|
133
134
|
if (id === 'blokvalg') return { label: parts.blockSettings, href: '#blokvalg', count: null };
|
|
134
135
|
if (id === 'apps') return { label: parts.apps, href: '#apps', count: null };
|
|
135
136
|
if (id === 'appstore') return { label: t.apps.find, href: `#${STORE_ROUTE}`, count: null };
|
|
137
|
+
if (id === 'morpheme') return { label: parts.morpheme, href: `#${CMS_ROUTE}`, count: null };
|
|
136
138
|
if (id === 'avanceret') return { label: parts.advanced, href: '#avanceret', count: null };
|
|
137
139
|
if (id === 'licenser') return { label: parts.licenses, href: '#licenser', count: null };
|
|
138
140
|
const screen = partScreens.find((s) => s.id === id);
|
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;
|