@morphemecms/cms 0.1.1-next-20261103223426 → 0.2.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 +6 -1
- package/bin/morpheme.mjs +1 -1
- package/lib/apps.mjs +5 -2
- package/lib/apps.test.d.mts +1 -0
- package/lib/apps.test.mjs +50 -0
- package/lib/caseCollisions.d.mts +4 -0
- package/lib/caseCollisions.mjs +62 -0
- package/lib/caseCollisions.test.d.mts +1 -0
- package/lib/caseCollisions.test.mjs +74 -0
- package/lib/contracts.d.mts +15 -0
- package/lib/contracts.mjs +17 -0
- package/lib/licenses.d.mts +1 -1
- package/lib/licenses.mjs +2 -2
- package/lib/storeEntries.d.mts +18 -0
- package/lib/storeEntries.mjs +64 -0
- package/lib/storeEntries.test.d.mts +1 -0
- package/lib/storeEntries.test.mjs +71 -0
- package/package.json +1 -1
- package/pages/editor/sw.js +1 -1
- package/scripts/built-site.mjs +37 -2
- package/scripts/check-content.mjs +4 -2
- package/scripts/check-store.mjs +160 -0
- package/site/defineSite.d.mts +2 -14
- package/site/defineSite.mjs +2 -7
- package/site/storeTheme.d.mts +6 -0
- package/site/storeTheme.mjs +29 -0
- package/site/storeTheme.test.d.mts +1 -0
- package/site/storeTheme.test.mjs +53 -0
- package/site/theme.mjs +14 -3
- package/src/App.tsx +13 -16
- package/src/AppList.tsx +22 -5
- package/src/Consents.tsx +2 -2
- package/src/Deleted.tsx +1 -1
- package/src/EntryForm.tsx +3 -3
- package/src/FormFrame.tsx +3 -3
- package/src/Library.tsx +2 -2
- package/src/Licenses.tsx +1 -1
- package/src/Media.tsx +2 -2
- package/src/Notifications.tsx +2 -2
- package/src/Redirects.tsx +2 -2
- package/src/Screen.tsx +1 -1
- package/src/ThemeStore.tsx +1 -1
- package/src/apps.ts +2 -2
- package/src/checkSite.ts +2 -2
- package/src/deploy.ts +1 -1
- package/src/i18n/da-DK/consents.ts +1 -1
- package/src/i18n/en-US/apps.ts +1 -1
- package/src/i18n/en-US/consents.ts +1 -1
- package/src/i18n/en-US/library.ts +1 -1
- package/src/i18n/en-US/licenses.ts +1 -1
- package/src/i18n/en-US/redirects.ts +2 -2
- package/src/store.ts +74 -94
- package/src/themes.ts +1 -1
- package/starter/README.md +1 -0
- package/theme/styles/ui.css +4 -4
- package/theme/ui/BottomSheet.tsx +3 -3
- package/theme/ui/Dialog.tsx +1 -1
- package/theme/ui/SideSheet.tsx +1 -1
- package/theme/ui/bottomSheet.script.ts +2 -2
- package/worker/consent.js +1 -1
- package/worker/index.js +4 -0
- package/worker/push.js +1 -1
- package/worker/store.js +46 -0
- /package/src/{consents.ts → consentData.ts} +0 -0
- /package/src/{licenses.ts → licenseData.ts} +0 -0
- /package/src/{notifications.ts → notificationData.ts} +0 -0
- /package/src/{redirects.ts → redirectData.ts} +0 -0
- /package/theme/ui/{bottomSheet.ts → bottomSheetControl.ts} +0 -0
- /package/theme/ui/{dialog.ts → dialogControl.ts} +0 -0
- /package/theme/ui/{sideSheet.ts → sideSheetControl.ts} +0 -0
package/src/Media.tsx
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
X,
|
|
24
24
|
} from '@phosphor-icons/react';
|
|
25
25
|
import { moveRedirects, REDIRECTS } from './addresses';
|
|
26
|
-
import { changesOf, flatten, readAll, REDIRECT_FILES } from './
|
|
26
|
+
import { changesOf, flatten, readAll, REDIRECT_FILES } from './redirectData';
|
|
27
27
|
import { ask } from './ask';
|
|
28
28
|
import { dateName, headed, letterOf } from './Sheet';
|
|
29
29
|
import Screen from './Screen';
|
|
@@ -501,7 +501,7 @@ export default function Media({ files, env }: { files: RepoFile[]; env: Env }) {
|
|
|
501
501
|
setRenaming(true);
|
|
502
502
|
env.notify({ text: t.common.saving });
|
|
503
503
|
try {
|
|
504
|
-
// The redirects (
|
|
504
|
+
// The redirects (redirectData.ts): the old address sends on to the new one, and those that sent to
|
|
505
505
|
// the old address send straight to the new one (no chain).
|
|
506
506
|
const loaded = await env.load(REDIRECT_FILES);
|
|
507
507
|
const all = readAll(loaded);
|
package/src/Notifications.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// "Notifikationer" (#notifikationer), which the bell in the top bar opens (Screen.tsx NotificationsLink): the
|
|
2
|
-
// notifications of the editor and the apps (
|
|
2
|
+
// notifications of the editor and the apps (notificationData.ts), newest first, each with when it came and, when it had
|
|
3
3
|
// one, a link to what it is about; an app's with the app's icon. Opening it marks them seen; at the top the bin clears
|
|
4
4
|
// them and the sliders open "Konto", where each type is turned on or off.
|
|
5
5
|
import { useEffect } from 'react';
|
|
@@ -8,7 +8,7 @@ import { siteName } from '@site/editor';
|
|
|
8
8
|
import Screen from './Screen';
|
|
9
9
|
import { EmptyNote, Group, IconButton, IconLink, ListItem } from './ui';
|
|
10
10
|
import { apps } from './registry';
|
|
11
|
-
import { clearNotifications, markSeen, useNotifications, type Notification } from './
|
|
11
|
+
import { clearNotifications, markSeen, useNotifications, type Notification } from './notificationData';
|
|
12
12
|
import { when } from './when';
|
|
13
13
|
import { t } from './i18n';
|
|
14
14
|
|
package/src/Redirects.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// "Videresendelser" under Indstillinger › "Søgemaskineoptimering" (#seo/videresendelser): two rows with how many
|
|
2
2
|
// each has, each opening a screen of its own.
|
|
3
3
|
// "Alle videresendelser" (#seo/videresendelser/alle): every address of a page the site sends on elsewhere
|
|
4
|
-
// (
|
|
4
|
+
// (redirectData.ts), by address, with where it sends to, and search. Tapping one opens a sheet where
|
|
5
5
|
// where it sends to can be changed, or the redirect deleted; "+" adds a new one (in the editor's own,
|
|
6
6
|
// src/data/redirects.json). Each save is one commit, and none sends on through another afterwards
|
|
7
7
|
// (flatten): a redirect to an address that is sent on goes straight to where that ends, and those
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
} from './ui';
|
|
38
38
|
import type { Env } from './App';
|
|
39
39
|
import type { RepoFile } from './github';
|
|
40
|
-
import { changesOf, entriesOf, flatten, lookup, readAll, REDIRECT_FILES, setRedirect, type AllRedirects } from './
|
|
40
|
+
import { changesOf, entriesOf, flatten, lookup, readAll, REDIRECT_FILES, setRedirect, type AllRedirects } from './redirectData';
|
|
41
41
|
import type { Missing } from './notFound';
|
|
42
42
|
import { when } from './when';
|
|
43
43
|
import { headed, letterOf } from './Sheet';
|
package/src/Screen.tsx
CHANGED
|
@@ -16,7 +16,7 @@ import { settings, siteName } from '@site/editor';
|
|
|
16
16
|
import { t } from './i18n';
|
|
17
17
|
import { IconButton, IconLink, TabPanel, TabRow, TopAppBar, type TabChoice } from './ui';
|
|
18
18
|
import { up } from './nav';
|
|
19
|
-
import { unseen, useNotifications } from './
|
|
19
|
+
import { unseen, useNotifications } from './notificationData';
|
|
20
20
|
|
|
21
21
|
// `label` is the screen back goes to, named by the back arrow and the breadcrumbs. `guard` takes every
|
|
22
22
|
// way out of the screen (back, the breadcrumbs and the logo) and goes it, after warning about changes
|
package/src/ThemeStore.tsx
CHANGED
|
@@ -110,7 +110,7 @@ const canUninstall = (id: string, files: RepoFile[]) =>
|
|
|
110
110
|
* Each says true when it was saved. */
|
|
111
111
|
function useThemeActions(env: Env) {
|
|
112
112
|
const [saving, setSaving] = useState(false);
|
|
113
|
-
const recordOf = (x: ShownTheme): StoreRecord => ({ version: x.version, ref: x.
|
|
113
|
+
const recordOf = (x: ShownTheme): StoreRecord => ({ version: x.version, ref: x.version, files: x.files });
|
|
114
114
|
|
|
115
115
|
async function change(
|
|
116
116
|
id: string,
|
package/src/apps.ts
CHANGED
|
@@ -44,7 +44,7 @@ import type { BlockData } from './blocks';
|
|
|
44
44
|
import type { Field, FolderCollection, KindPart, Opens, OwnedList, PreviewVariant } from './schema';
|
|
45
45
|
import type { ConsentCategory } from '../lib/cookieCategories.mjs';
|
|
46
46
|
import type { AppCategory, AppCookie, AppNeeds, AppShot, StoreRecord } from './store';
|
|
47
|
-
import type { ThirdParty } from './
|
|
47
|
+
import type { ThirdParty } from './licenseData';
|
|
48
48
|
import { config } from '@site/config';
|
|
49
49
|
|
|
50
50
|
/** An app's settings, on its page once it is installed (#apps/<app>, AppList.tsx). */
|
|
@@ -101,7 +101,7 @@ export type AppScreen = { title: string; page: ComponentType<AppPageProps> };
|
|
|
101
101
|
* the editor's parts and given the same `api`. */
|
|
102
102
|
export type AppTab = { label: string; icon: Icon; page: ComponentType<AppPageProps> };
|
|
103
103
|
|
|
104
|
-
/** The notifications an app sends ("Notifikationer",
|
|
104
|
+
/** The notifications an app sends ("Notifikationer", notificationData.ts). `types`: each kind it sends, by its id, with its
|
|
105
105
|
* name (in the site's language, shown with siteText), under "Konto" › "Notifikationer", where each can be turned off; it
|
|
106
106
|
* sends no other. `check`: what it has to say now, asked with its `api` when the editor opens and every 15 minutes while
|
|
107
107
|
* it is open; each comes once (by its `id`). */
|
package/src/checkSite.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// screens under "Virksomheden" and "Blokke", the apps installed and their settings (src/apps/), and the
|
|
6
6
|
// colours ("Farver": colours, readable together). A hidden block is not on the site, so it may be unfinished.
|
|
7
7
|
// The rules of a page's template (collections.ts) too: what may be there, how many, and what must.
|
|
8
|
-
// The redirects (
|
|
8
|
+
// The redirects (redirectData.ts): none sends on through another, and none in a loop. And the site's own rules
|
|
9
9
|
// (siteProblems in its rules.ts: a button that opens a promotion not on its page).
|
|
10
10
|
// And the site's texts the editor has no translation of (untranslated).
|
|
11
11
|
import { check } from './check';
|
|
@@ -37,7 +37,7 @@ import { appFolders } from './appFiles';
|
|
|
37
37
|
import { colorPairs as shownPairs, sharedParts, translator, writtenApps, writtenBlockParts, writtenCollections } from './site';
|
|
38
38
|
import { splitBlocks } from '../lib/blocks.mjs';
|
|
39
39
|
import { appTexts, DEFAULTS_LOCALE, locales, siteLocale, t, type Texts } from './i18n';
|
|
40
|
-
import { flatten, readAll, REDIRECT_FILES, type AllRedirects } from './
|
|
40
|
+
import { flatten, readAll, REDIRECT_FILES, type AllRedirects } from './redirectData';
|
|
41
41
|
|
|
42
42
|
const ratio = (n: number) => n.toLocaleString(config.locale, { maximumFractionDigits: 1 });
|
|
43
43
|
|
package/src/deploy.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// After a save: tells when the site has been rebuilt with it. Each build writes the commit it was
|
|
2
2
|
// made from to /editor/version.json (packages/editor/pages/editor/version.json.js), and the editor asks for that
|
|
3
|
-
// until it is the saved commit or a later one. The message is a notification of the type `site` (
|
|
3
|
+
// until it is the saved commit or a later one. The message is a notification of the type `site` (notificationData.ts),
|
|
4
4
|
// which also comes on the phone or computer when the editor is not on screen.
|
|
5
5
|
// Each save says what it changed ("farver ændret", "side “Om” ændret", t.app.changes): right after it (savedText),
|
|
6
6
|
// and, with the other saves since, when the website has been updated with them (changesText, App.tsx save).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "Consents" under "Privacy" (Consents.tsx,
|
|
1
|
+
// "Consents" under "Privacy" (Consents.tsx, consentData.ts), as in Danish (../da-DK/consents.ts).
|
|
2
2
|
import type { Messages } from '../en-US';
|
|
3
3
|
|
|
4
4
|
export const consents: Messages['consents'] = {
|
package/src/i18n/en-US/apps.ts
CHANGED
|
@@ -78,7 +78,7 @@ export const apps = {
|
|
|
78
78
|
searchStore: 'Search for apps',
|
|
79
79
|
loadingStore: 'Loading apps',
|
|
80
80
|
storeFailed: 'Couldn’t load the Appstore',
|
|
81
|
-
/** On "Appstore" when the site has
|
|
81
|
+
/** On "Appstore" when the site's Worker has no store key: only the site's own are there. */
|
|
82
82
|
noStore: 'The site has no Appstore',
|
|
83
83
|
/** An app's files did not come from the store as its catalog says. */
|
|
84
84
|
storeBroken: (app: string) => `${app} couldn’t be loaded correctly from the Appstore`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "Samtykker" under "Privatliv" (Consents.tsx,
|
|
1
|
+
// "Samtykker" under "Privatliv" (Consents.tsx, consentData.ts): the visitors' answers in the cookie banner.
|
|
2
2
|
export const consents = {
|
|
3
3
|
title: 'Consents',
|
|
4
4
|
intro: 'The visitors’ answers in the cookie banner, with the consent ID the banner shows them. They are deleted after 2 years.',
|
|
@@ -10,7 +10,7 @@ export const library = {
|
|
|
10
10
|
notificationsNew: (count: number) => `Notifications, ${count} new`,
|
|
11
11
|
noNotifications: 'No notifications',
|
|
12
12
|
clearNotifications: 'Clear notifications',
|
|
13
|
-
/** On "Konto" › "Notifikationer" (Library.tsx NotificationChoices,
|
|
13
|
+
/** On "Konto" › "Notifikationer" (Library.tsx NotificationChoices, notificationData.ts). */
|
|
14
14
|
/** The sliders on "Notifikationer": to "Konto", where they are turned on or off. */
|
|
15
15
|
notificationChoices: 'Choose notifications',
|
|
16
16
|
onDevice: 'On the phone',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// "Licenser" under "Indstillinger" (Licenses.tsx): the third-party work the site and the editor are made with, and the
|
|
2
2
|
// licences on an app's and a theme's page.
|
|
3
|
-
import type { LicenseKind } from '../../
|
|
3
|
+
import type { LicenseKind } from '../../licenseData';
|
|
4
4
|
|
|
5
5
|
export const licenses = {
|
|
6
6
|
/** The groups: the icon packs, what the editor and the site's own code bring, and each app and theme in use. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "Videresendelser" (Redirects.tsx,
|
|
1
|
+
// "Videresendelser" (Redirects.tsx, redirectData.ts).
|
|
2
2
|
export const redirects = {
|
|
3
3
|
notLoaded: (error: string) => `Couldn’t load the redirects: ${error}`,
|
|
4
4
|
noFrom: 'Type the address that should redirect.',
|
|
@@ -8,7 +8,7 @@ export const redirects = {
|
|
|
8
8
|
alreadyRedirects: (from: string) => `There’s already a redirect from ${from}. Edit it in the list.`,
|
|
9
9
|
/** Deleting one; `via`: how many other addresses send on through it. */
|
|
10
10
|
deleteAsk: (from: string, via: number) => `Delete the redirect from ${from}? The address then shows the not-found page.`,
|
|
11
|
-
/**
|
|
11
|
+
/** redirectData.ts: the addresses that come back to one, joined by arrows. */
|
|
12
12
|
loop: (addresses: string) => `The redirects would go in a loop (${addresses}).`,
|
|
13
13
|
/** checkSite.ts: a redirect that sends on through another, before the site is built. */
|
|
14
14
|
chain: (from: string, to: string, end: string) =>
|
package/src/store.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// The app store ("Indstillinger" › "Appstore", #indstillinger/appstore, AppList.tsx): the apps that are not in the site
|
|
2
|
-
// and can be installed. The store is
|
|
2
|
+
// and can be installed. The store is reached through the site's Worker (/api/store/catalog, /api/store/files/<oid>;
|
|
3
|
+
// the Worker holds the site's key to it, and the editor never calls the store itself): each app of its catalog with
|
|
3
4
|
// its id, its version and the version of the contract it is written to (APP_API), its name, who made it, its icon
|
|
4
5
|
// (Phosphor's name), what it does (in a sentence, and more fully), its category and the words it is found by, what it
|
|
5
6
|
// adds (its blocks, the lists it brings and its kinds of the site's lists), what it needs (needsOf), the cookies it
|
|
6
7
|
// sets, its pictures, what is new in each version (its release notes), its texts in the editor's other languages (as an app's texts.ts), its
|
|
7
|
-
// licence and those of the third-party work it brings (StoreLicenses), and
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// against its id), with a note of where it came from (store.json, StoreRecord), and puts it among the apps installed,
|
|
8
|
+
// licence and those of the third-party work it brings (StoreLicenses), and its folder file by file with their blob ids.
|
|
9
|
+
// Installing one copies its folder into the site's (config.apps.folder/<id>/, each file checked against its id), with a
|
|
10
|
+
// note of where it came from (store.json, StoreRecord; its `ref` is the version), and puts it among the apps installed,
|
|
11
11
|
// in one commit (appChanges); the site's next build has it. Updating copies the newer version over it, and
|
|
12
12
|
// uninstalling deletes the folder, also in one commit each. What was made with it (its settings, its entries) is the
|
|
13
13
|
// site's data and stays.
|
|
@@ -17,26 +17,19 @@
|
|
|
17
17
|
// with its store.json), and it can then be chosen under "Udseende" › "Temaer"; updating and uninstalling are as for
|
|
18
18
|
// an app (themeChanges), and the theme in use cannot be uninstalled.
|
|
19
19
|
// The catalog is what is released: an app's or a theme's entry is written from its folder by `npm run store:release
|
|
20
|
-
// -- <id>` (scripts/store-release.mjs) when the owner releases it, not by each merge (packages/editor/scripts/releases.mjs).
|
|
21
|
-
//
|
|
20
|
+
// -- <id>` (scripts/store-release.mjs) when the owner releases it, not by each merge (packages/editor/scripts/releases.mjs).
|
|
21
|
+
// A site whose Worker has no store key has no store (storeConnected): "Appstore" has only the site's own.
|
|
22
22
|
import { config } from '@site/config';
|
|
23
|
-
import {
|
|
23
|
+
import type { Change, RepoFile } from './github';
|
|
24
|
+
import { blobId } from '../lib/git.mjs';
|
|
24
25
|
import { locale, t } from './i18n';
|
|
25
26
|
import { hooksOf, type AppHook } from '../lib/hooks';
|
|
26
|
-
import type { StoreLicenses } from './
|
|
27
|
+
import type { StoreLicenses } from './licenseData';
|
|
28
|
+
import { APP_API, THEME_API } from '../lib/contracts.mjs';
|
|
29
|
+
import { RECORD, STANDARD, isText, validApp, validTheme } from '../lib/storeEntries.mjs';
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
* @morphemecms/cms): an app in the store says which it is written to, and only those the editor can run are
|
|
31
|
-
* offered. Raised when an app written to the one before no longer works. */
|
|
32
|
-
export const APP_API = 1;
|
|
33
|
-
|
|
34
|
-
/** The version of the contract between the editor and a theme (what a theme must and may have, "Hvad et tema må" in
|
|
35
|
-
* CLAUDE.md, themeProblems in packages/editor/site/theme.mjs, the standard theme's UI parts with Material 3's names and
|
|
36
|
-
* variants, the motion scheme's --motion-* springs and motionSpring/motionTiming (@morphemecms/cms/lib/motion),
|
|
37
|
-
* and the classes it styles): only themes written to it are offered. Raised when a theme written to the one before no
|
|
38
|
-
* longer works. */
|
|
39
|
-
export const THEME_API = 2;
|
|
31
|
+
// The contracts this editor runs (lib/contracts.mjs, also read by the site's Worker).
|
|
32
|
+
export { APP_API, THEME_API };
|
|
40
33
|
|
|
41
34
|
/** The kinds of apps, a filter in the store (t.apps.categories). */
|
|
42
35
|
export const appCategories = ['booking', 'newsletter', 'payment', 'analytics', 'content', 'design'] as const;
|
|
@@ -142,14 +135,12 @@ export type StoreApp = {
|
|
|
142
135
|
adds: { blocks: StoreAdd[]; lists: StoreAdd[]; kinds: (StoreAdd & { list: string })[] };
|
|
143
136
|
/** Its texts in the editor's other languages, by the text as it is written. */
|
|
144
137
|
texts?: Partial<Record<string, Record<string, string>>>;
|
|
145
|
-
/**
|
|
146
|
-
path: string;
|
|
147
|
-
ref: string;
|
|
138
|
+
/** Its folder, file by file. */
|
|
148
139
|
files: StoreFile[];
|
|
149
140
|
};
|
|
150
141
|
|
|
151
142
|
/** A theme in the store's catalog: as its theme.json has it (its texts by the editor's language), its pictures of a
|
|
152
|
-
* site in it (screenshots/<n>-<key>.jpg, t.library.themeShots) and
|
|
143
|
+
* site in it (screenshots/<n>-<key>.jpg, t.library.themeShots) and its folder, file by file. */
|
|
153
144
|
export type StoreTheme = {
|
|
154
145
|
id: string;
|
|
155
146
|
version: string;
|
|
@@ -168,8 +159,6 @@ export type StoreTheme = {
|
|
|
168
159
|
/** The icon packs it brings (its icon-packs/<id>/, with their pack.json: name, who made it, licence), installed with it
|
|
169
160
|
* and chosen under "Udseende" › "Ikonpakker". */
|
|
170
161
|
iconPacks?: { id: string; name: string; author: string; license: string }[];
|
|
171
|
-
path: string;
|
|
172
|
-
ref: string;
|
|
173
162
|
files: StoreFile[];
|
|
174
163
|
};
|
|
175
164
|
|
|
@@ -185,7 +174,8 @@ export type ShownTheme = Omit<StoreTheme, 'description' | 'highlights' | 'releas
|
|
|
185
174
|
* it (for the commits), and all its words in both, for the search. */
|
|
186
175
|
export type ShownApp = StoreApp & { written: string; words: string };
|
|
187
176
|
|
|
188
|
-
/** Where an app installed from the store came from: its version, the
|
|
177
|
+
/** Where an app installed from the store came from: its version, the version again as `ref` (an older store.json has
|
|
178
|
+
* the store's commit there) and its files (store.json). */
|
|
189
179
|
export type StoreRecord = { version: string; ref: string; files: StoreFile[] };
|
|
190
180
|
|
|
191
181
|
/** The store's address in the editor, under "Indstillinger" (its apps; its themes at STORE_THEMES), its address
|
|
@@ -200,8 +190,8 @@ export const DEVELOPER_ROUTE = 'apps/udvikler';
|
|
|
200
190
|
export const appFolder = (id: string) => `${config.apps.folder}/${id}`;
|
|
201
191
|
/** A theme's folder in the site; the standard theme's is always there. */
|
|
202
192
|
export const themeFolder = (id: string) => `${config.themes}/${id}`;
|
|
203
|
-
export const STANDARD_THEME =
|
|
204
|
-
export
|
|
193
|
+
export const STANDARD_THEME = STANDARD;
|
|
194
|
+
export { RECORD };
|
|
205
195
|
|
|
206
196
|
/** Whether a version is newer than another ("1.10.0" than "1.9.2"). */
|
|
207
197
|
export function newer(a: string, b: string): boolean {
|
|
@@ -230,27 +220,8 @@ const notesOf = (x: unknown) =>
|
|
|
230
220
|
? Object.fromEntries(Object.entries(x).filter((e): e is [string, string] => isText(e[1])))
|
|
231
221
|
: undefined;
|
|
232
222
|
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
const ID = /^[a-z0-9][a-z0-9-]*$/;
|
|
236
|
-
// A file's path in an app's folder: only within it.
|
|
237
|
-
const isPath = (v: unknown) => isText(v) && v.split('/').every((part) => part !== '' && part !== '.' && part !== '..');
|
|
238
|
-
|
|
239
|
-
/** Whether an entry of the catalog has what an app needs, written to the contract the editor runs, with files only in
|
|
240
|
-
* its own folder, and not the store's own address. */
|
|
241
|
-
function valid(a: any): a is StoreApp {
|
|
242
|
-
return (
|
|
243
|
-
ID.test(a?.id ?? '') &&
|
|
244
|
-
![STORE_BEFORE, DEVELOPER_ROUTE].includes(`apps/${a.id}`) &&
|
|
245
|
-
isText(a.version) &&
|
|
246
|
-
a.api === APP_API &&
|
|
247
|
-
[a.label, a.publisher, a.icon, a.hint, a.path, a.ref].every(isText) &&
|
|
248
|
-
Array.isArray(a.files) &&
|
|
249
|
-
a.files.length > 0 &&
|
|
250
|
-
a.files.every((f: any) => isText(f?.oid) && isPath(f?.path) && f.path !== RECORD) &&
|
|
251
|
-
['blocks', 'lists', 'kinds'].every((k) => Array.isArray(a.adds?.[k]) && a.adds[k].every(isAdd))
|
|
252
|
-
);
|
|
253
|
-
}
|
|
223
|
+
const validStoreApp = (a: any): a is StoreApp => validApp(a, APP_API);
|
|
224
|
+
const validStoreTheme = (x: any): x is StoreTheme => validTheme(x, THEME_API);
|
|
254
225
|
|
|
255
226
|
/** The apps in a catalog (catalog.json, as text) the editor can install, with their texts in its language. */
|
|
256
227
|
export function catalogApps(text: string): ShownApp[] {
|
|
@@ -261,7 +232,7 @@ export function catalogApps(text: string): ShownApp[] {
|
|
|
261
232
|
return [];
|
|
262
233
|
}
|
|
263
234
|
const apps: unknown[] = Array.isArray(json?.apps) ? json.apps : [];
|
|
264
|
-
return apps.filter(
|
|
235
|
+
return apps.filter(validStoreApp).map((a) => {
|
|
265
236
|
const own = a.texts?.[locale];
|
|
266
237
|
const say = (s: string) => own?.[s] ?? s;
|
|
267
238
|
const add = <A extends StoreAdd>(x: A): A => ({ ...x, label: say(x.label), ...(x.hint ? { hint: say(x.hint) } : {}) });
|
|
@@ -291,20 +262,6 @@ export function catalogApps(text: string): ShownApp[] {
|
|
|
291
262
|
});
|
|
292
263
|
}
|
|
293
264
|
|
|
294
|
-
/** Whether an entry of the catalog has what a theme needs, written to the contract the editor runs, with files only
|
|
295
|
-
* in its own folder; never the standard theme, which every site has. */
|
|
296
|
-
function validTheme(x: any): x is StoreTheme {
|
|
297
|
-
return (
|
|
298
|
-
ID.test(x?.id ?? '') &&
|
|
299
|
-
x.id !== STANDARD_THEME &&
|
|
300
|
-
x.api === THEME_API &&
|
|
301
|
-
[x.version, x.name, x.author, x.icon, x.path, x.ref].every(isText) &&
|
|
302
|
-
Array.isArray(x.files) &&
|
|
303
|
-
x.files.length > 0 &&
|
|
304
|
-
x.files.every((f: any) => isText(f?.oid) && isPath(f?.path) && f.path !== RECORD)
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
265
|
/** The themes in a catalog (catalog.json, as text) the editor can install, with their texts in its language (else the
|
|
309
266
|
* first they have). */
|
|
310
267
|
export function catalogThemes(text: string): ShownTheme[] {
|
|
@@ -316,7 +273,7 @@ export function catalogThemes(text: string): ShownTheme[] {
|
|
|
316
273
|
}
|
|
317
274
|
const themes: unknown[] = Array.isArray(json?.themes) ? json.themes : [];
|
|
318
275
|
const inLocale = <T>(x?: Record<string, T>) => x && (x[locale] ?? Object.values(x)[0]);
|
|
319
|
-
return themes.filter(
|
|
276
|
+
return themes.filter(validStoreTheme).map((x) => {
|
|
320
277
|
const description = inLocale(x.description);
|
|
321
278
|
const highlights = inLocale(x.highlights) ?? [];
|
|
322
279
|
return {
|
|
@@ -338,49 +295,72 @@ export function catalogThemes(text: string): ShownTheme[] {
|
|
|
338
295
|
// The catalog, read once while the editor is open (again after it could not be read).
|
|
339
296
|
let catalog: Promise<string> | null = null;
|
|
340
297
|
|
|
341
|
-
|
|
342
|
-
export const hasStore = !!config.store;
|
|
343
|
-
|
|
344
|
-
// The store's repository; only asked for what is in its catalog, so never without one.
|
|
345
|
-
const storeRepo = () => {
|
|
346
|
-
if (!config.store) throw new Error(t.apps.noStore);
|
|
347
|
-
return config.store;
|
|
348
|
-
};
|
|
349
|
-
|
|
298
|
+
// The catalog as the site's Worker passes it on: `{ connected: false }` when the site has no key to the store.
|
|
350
299
|
function catalogText(): Promise<string> {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
300
|
+
catalog ??= fetch('/api/store/catalog').then(
|
|
301
|
+
(res) => {
|
|
302
|
+
if (!res.ok) throw new Error(t.apps.storeFailed);
|
|
303
|
+
return res.text();
|
|
304
|
+
},
|
|
305
|
+
() => {
|
|
306
|
+
throw new Error(t.apps.storeFailed);
|
|
307
|
+
},
|
|
308
|
+
);
|
|
356
309
|
catalog.catch(() => (catalog = null));
|
|
357
310
|
return catalog;
|
|
358
311
|
}
|
|
359
312
|
|
|
313
|
+
/** Whether the site's Worker has a store key; without one, "Appstore" has only the site's own apps and themes. Fails when
|
|
314
|
+
* the catalog cannot be read. */
|
|
315
|
+
export const storeConnected = (): Promise<boolean> =>
|
|
316
|
+
catalogText().then((text) => {
|
|
317
|
+
try {
|
|
318
|
+
return JSON.parse(text)?.connected !== false;
|
|
319
|
+
} catch {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
360
324
|
/** The store's apps. Fails when the catalog cannot be read. */
|
|
361
325
|
export const readCatalog = (): Promise<ShownApp[]> => catalogText().then(catalogApps);
|
|
362
326
|
|
|
363
327
|
/** The store's themes. Fails when the catalog cannot be read. */
|
|
364
328
|
export const readThemes = (): Promise<ShownTheme[]> => catalogText().then(catalogThemes);
|
|
365
329
|
|
|
366
|
-
|
|
330
|
+
// A file from the store by its blob id (through the site's Worker), checked against the id.
|
|
331
|
+
async function readFile(oid: string): Promise<Uint8Array> {
|
|
332
|
+
const res = await fetch(`/api/store/files/${oid}`);
|
|
333
|
+
if (!res.ok) throw new Error(t.apps.storeFailed);
|
|
334
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
335
|
+
if ((await blobId(bytes)) !== oid) throw new Error(t.apps.storeFailed);
|
|
336
|
+
return bytes;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// The text of a file, or null when it is binary (a NUL, or not UTF-8), as GitHub told them apart before.
|
|
340
|
+
function textOf(bytes: Uint8Array): string | null {
|
|
341
|
+
if (bytes.includes(0)) return null;
|
|
342
|
+
try {
|
|
343
|
+
// ignoreBOM keeps a leading BOM in the text, so it encodes back to the same bytes (and the same blob id).
|
|
344
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
345
|
+
} catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** An app's (or a theme's) files from the store, each checked against its blob id; text files as text. */
|
|
367
351
|
export async function readApp(app: {
|
|
368
|
-
path: string;
|
|
369
|
-
ref: string;
|
|
370
352
|
files: StoreFile[];
|
|
371
353
|
label?: string;
|
|
372
354
|
name?: string;
|
|
373
355
|
}): Promise<{ path: string; content: string | Uint8Array }[]> {
|
|
374
|
-
const found = await loadFrom(
|
|
375
|
-
storeRepo(),
|
|
376
|
-
app.ref,
|
|
377
|
-
app.files.map((f) => `${app.path}/${f.path}`),
|
|
378
|
-
);
|
|
379
356
|
return Promise.all(
|
|
380
357
|
app.files.map(async (f) => {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
358
|
+
try {
|
|
359
|
+
const bytes = await readFile(f.oid);
|
|
360
|
+
return { path: f.path, content: textOf(bytes) ?? bytes };
|
|
361
|
+
} catch {
|
|
362
|
+
throw new Error(t.apps.storeBroken(app.label ?? app.name ?? f.path));
|
|
363
|
+
}
|
|
384
364
|
}),
|
|
385
365
|
);
|
|
386
366
|
}
|
|
@@ -395,7 +375,7 @@ export async function readScreenshots(app: StoreApp): Promise<{ src: string; lab
|
|
|
395
375
|
const type = types[x.file.split('.').pop()!.toLowerCase()];
|
|
396
376
|
if (!oid || !type) return null;
|
|
397
377
|
try {
|
|
398
|
-
const bytes = await
|
|
378
|
+
const bytes = await readFile(oid);
|
|
399
379
|
return { src: URL.createObjectURL(new Blob([bytes as BlobPart], { type })), label: x.label };
|
|
400
380
|
} catch {
|
|
401
381
|
return null;
|
|
@@ -413,7 +393,7 @@ export async function readThemeScreenshots(theme: Pick<StoreTheme, 'screenshots'
|
|
|
413
393
|
const oid = theme.files.find((f) => f.path === `${SCREENSHOTS}/${x.file}`)?.oid;
|
|
414
394
|
if (!oid) return null;
|
|
415
395
|
try {
|
|
416
|
-
const bytes = await
|
|
396
|
+
const bytes = await readFile(oid);
|
|
417
397
|
return { key: x.key, src: URL.createObjectURL(new Blob([bytes as BlobPart], { type: 'image/jpeg' })) };
|
|
418
398
|
} catch {
|
|
419
399
|
return null;
|
package/src/themes.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// from the store (packages/editor/src/store.ts), where it came from (store.json) are read: their code is not loaded.
|
|
6
6
|
import chosen from '@data/theme.json';
|
|
7
7
|
import { STANDARD_THEME, type StoreRecord } from './store';
|
|
8
|
-
import type { ThirdParty } from './
|
|
8
|
+
import type { ThirdParty } from './licenseData';
|
|
9
9
|
|
|
10
10
|
export type ThemeInfo = {
|
|
11
11
|
/** Its folder in src/themes/, as saved in src/data/theme.json. */
|
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 `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.
|
|
12
13
|
- `src/content/`: siderne og indlæggene, `src/data/`: sitets indstillinger, `public/`: billeder, logo og ikoner.
|
|
13
14
|
- `src/themes/` og `src/apps/`: temaer og apps fra appbutikken, når der er installeret nogen.
|
|
14
15
|
|
package/theme/styles/ui.css
CHANGED
|
@@ -1272,7 +1272,7 @@
|
|
|
1272
1272
|
/* ---- Containment: Dialog, BottomSheet, Carousel ---- */
|
|
1273
1273
|
/*
|
|
1274
1274
|
Dialogs (Dialog.tsx, dialogClass, dialogClassOf, dialogModalClass, dialogFullScreenModalClass, and dialog() in
|
|
1275
|
-
|
|
1275
|
+
dialogControl.ts for what a modal one does in a browser; m3.material.io/components/dialogs): .dialog is the container (a
|
|
1276
1276
|
pop-up's box, the cookie question), and .dialog-modal the <dialog> it is shown in over the page, in the middle with
|
|
1277
1277
|
the scrim behind it (a pop-up, Popup.astro). With its parts it is Material's basic dialog, .dialog-basic: evt. its
|
|
1278
1278
|
icon (.dialog-icon, centred over the headline with .dialog-with-icon), .dialog-headline, .dialog-supporting-text,
|
|
@@ -1290,7 +1290,7 @@
|
|
|
1290
1290
|
--dialog-full-screen-max-width, --dialog-full-screen-headline-size a full-screen dialog from 48rem, and its
|
|
1291
1291
|
headline in its header
|
|
1292
1292
|
Bottom sheets (BottomSheet.tsx, bottomSheetClass, bottomSheetContainerClass, and bottomSheet() and
|
|
1293
|
-
standardBottomSheet() in
|
|
1293
|
+
standardBottomSheet() in bottomSheetControl.ts for what it does in a browser, bottomSheet.script.ts on the site;
|
|
1294
1294
|
m3.material.io/components/bottom-sheets): a modal one's <dialog> is .bottom-sheet (an app's: Calendly's calendar), at
|
|
1295
1295
|
most 44rem wide and nearly as high as the screen, from the bottom on a phone and in the middle of the screen from
|
|
1296
1296
|
48rem (a dialog there, rounded all round, without the drag handle; with .bottom-sheet-fit as high as what is in
|
|
@@ -1669,7 +1669,7 @@
|
|
|
1669
1669
|
}
|
|
1670
1670
|
|
|
1671
1671
|
/* A standard bottom sheet: at the bottom of the screen with the page usable behind it, no scrim; as high as the state
|
|
1672
|
-
it rests in (data-state), its height followed by
|
|
1672
|
+
it rests in (data-state), its height followed by bottomSheetControl.ts while it moves. 100cqh is the screen's height, or
|
|
1673
1673
|
the frame's that is a size container (the editor's drawing, which holds it with contain: layout). */
|
|
1674
1674
|
.bottom-sheet-standard {
|
|
1675
1675
|
position: fixed;
|
|
@@ -3866,7 +3866,7 @@
|
|
|
3866
3866
|
.divider-inset (in from the start) or .divider-middle-inset (in from both sides), and .divider-vertical (top to
|
|
3867
3867
|
bottom, between things side by side).
|
|
3868
3868
|
--divider-color, --divider-thickness, --divider-inset the line, and how far in an inset one starts
|
|
3869
|
-
Side sheets (SideSheet.tsx, sideSheetClass, sideSheetContainerClass, and sideSheet() in
|
|
3869
|
+
Side sheets (SideSheet.tsx, sideSheetClass, sideSheetContainerClass, and sideSheet() in sideSheetControl.ts for what a modal
|
|
3870
3870
|
one does in a browser; m3.material.io/components/side-sheets): .side-sheet-container with its kind,
|
|
3871
3871
|
.side-sheet-standard (beside what it is about, with a line between) or .side-sheet-modal (over the page, in the
|
|
3872
3872
|
<dialog> .side-sheet at the side of the screen, over the scrim like a dialog's); in it .side-sheet-header
|
package/theme/ui/BottomSheet.tsx
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
// close button, `closeLabel`), what it shows (`children`, filling the rest; what stands in place of a frame while it
|
|
4
4
|
// loads has bottomSheetCoverClass) and evt. a footer (`footer`). Its kind (`kind`) is Material's:
|
|
5
5
|
// - 'modal' (the default): over the page and its scrim, in a <dialog> with bottomSheetClass (an app's: Calendly's
|
|
6
|
-
// Sheet.astro), which bottomSheet() in
|
|
6
|
+
// Sheet.astro), which bottomSheet() in bottomSheetControl.ts opens, closes and lets be dragged down; in the editor only
|
|
7
7
|
// drawn, as high as `height` says.
|
|
8
8
|
// - 'standard': at the bottom of the screen with the page usable behind it, without a scrim, a <section> named by its
|
|
9
9
|
// headline. It rests in one of its states (`state`, where it starts: 'collapsed', only the handle and the header;
|
|
10
10
|
// 'half-expanded', at most half the screen's height; 'expanded', as high as what it shows, nearly to the top; or
|
|
11
11
|
// 'hidden'), and its handle is a button (`dragHandleLabel`, what it says to a screen reader) that is dragged between
|
|
12
12
|
// them or tapped to expand and collapse it; with a close button it can be hidden. On the site bottomSheet.script.ts
|
|
13
|
-
// lets it be dragged (standardBottomSheet() in
|
|
13
|
+
// lets it be dragged (standardBottomSheet() in bottomSheetControl.ts, which a block's or an app's script can also use); in
|
|
14
14
|
// the editor it is drawn in its state and dragged here, at the bottom of its frame (which holds it with
|
|
15
15
|
// contain: layout, and is a size container).
|
|
16
16
|
// The editor's own sheets (choices, fields) are modal ones in a <dialog> with bottomSheetFitClass, as high as what is in
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { useEffect, useRef, type ReactNode } from 'react';
|
|
22
22
|
import { useOnSite } from '@morphemecms/cms/src/standard/view';
|
|
23
23
|
import IconButton from '@theme/ui/IconButton';
|
|
24
|
-
import { standardBottomSheet } from '@theme/ui/
|
|
24
|
+
import { standardBottomSheet } from '@theme/ui/bottomSheetControl';
|
|
25
25
|
|
|
26
26
|
/** The kinds of a bottom sheet, and the states a standard one rests in, as Material 3 names them. */
|
|
27
27
|
export type BottomSheetKind = 'modal' | 'standard';
|
package/theme/ui/Dialog.tsx
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// (`divider`), and under them what it shows (`supportingText` and `children`, scrolling). From 48rem, where Material
|
|
11
11
|
// shows it as a dialog, it is in the middle of the screen over the scrim, with the same header.
|
|
12
12
|
// Shown modally it is in a <dialog> with dialogModalClass (a full-screen one with dialogFullScreenModalClass), which
|
|
13
|
-
// dialog() in
|
|
13
|
+
// dialog() in dialogControl.ts (or the script that shows it: Popup.astro) opens and closes. How it looks is the theme's
|
|
14
14
|
// (--dialog-*). Other attributes go on it, and `className` after its own classes (its place). An Astro component
|
|
15
15
|
// without React gives an element the same classes with dialogClass.
|
|
16
16
|
import type { ReactNode } from 'react';
|
package/theme/ui/SideSheet.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// close button, `closeLabel`, and what it does in the editor, `onClose`; none without it), what it shows (`children`, scrolling when it is long) and evt. its
|
|
4
4
|
// actions at the bottom (`actions`: buttons). Its kind (`kind`): 'standard', beside what it is about on the page, an
|
|
5
5
|
// <aside> named by its headline, its place the page's; or 'modal', over the page in a <dialog> with sideSheetClass over
|
|
6
|
-
// the scrim, which sideSheet() in
|
|
6
|
+
// the scrim, which sideSheet() in sideSheetControl.ts opens, closes and lets slide in from the side (the <dialog> gets
|
|
7
7
|
// aria-labelledby with `headlineId`). How it looks is the theme's (--side-sheet-*). In the editor only drawn, as high as
|
|
8
8
|
// its place.
|
|
9
9
|
import type { ReactNode } from 'react';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// What a standard bottom sheet (BottomSheet.tsx with kind 'standard') does on the site: it can be dragged between its
|
|
2
|
-
// states and tapped open and closed (standardBottomSheet in
|
|
2
|
+
// states and tapped open and closed (standardBottomSheet in bottomSheetControl.ts). Run on every page by the theme's Base.astro
|
|
3
3
|
// (blockScripts, packages/editor/site/blockScripts.ts); without a standard bottom sheet on the page it does nothing.
|
|
4
|
-
import { standardBottomSheet } from '@theme/ui/
|
|
4
|
+
import { standardBottomSheet } from '@theme/ui/bottomSheetControl';
|
|
5
5
|
|
|
6
6
|
export default function bottomSheets() {
|
|
7
7
|
document.querySelectorAll<HTMLElement>('[data-bottom-sheet-standard]').forEach((sheet) => standardBottomSheet(sheet));
|
package/worker/consent.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The visitors' answers in the cookie banner, so the site can show that a visitor said yes (GDPR article 7(1)): the
|
|
2
|
-
// consent log, "Samtykker" under "Privatliv" in the editor (packages/editor/src/
|
|
2
|
+
// consent log, "Samtykker" under "Privatliv" in the editor (packages/editor/src/consentData.ts, Consents.tsx).
|
|
3
3
|
// Kept in Cloudflare D1 (the binding CONSENT in wrangler.jsonc), never in the repository: one row per answer, with the
|
|
4
4
|
// random consent id the visitor's browser keeps (consentId in src/scripts/tracking.ts, shown in the banner under
|
|
5
5
|
// "Cookieindstillinger"), when, the yes or no to each category the banner asked about, and the banner it was given in
|
package/worker/index.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
// - GitHub sign-in for the editor at /editor/ (/oauth/*, packages/editor/worker/oauth.js), kept in a cookie only the Worker reads
|
|
14
14
|
// (packages/editor/worker/session.js), and what the editor asks of GitHub with it (/api/github/*, packages/editor/worker/editor-github.js), the site's
|
|
15
15
|
// files through the cache in R2 (packages/editor/worker/files.js).
|
|
16
|
+
// - what the editor asks of the app store (/api/store/catalog and /api/store/files/<oid>, packages/editor/worker/store.js), with the
|
|
17
|
+
// site's key (MORPHEME_STORE_KEY) that never leaves the Worker.
|
|
16
18
|
// - the addresses visitors came to that do not exist, for the editor (/api/not-found, packages/editor/worker/not-found.js).
|
|
17
19
|
// - the visitors' answers in the cookie banner, the consent log (/api/consent, packages/editor/worker/consent.js, in D1), and each hour
|
|
18
20
|
// those older than two years deleted.
|
|
@@ -32,6 +34,7 @@ import { handleNotFound, isVisit, json, recordNotFound } from './not-found.js';
|
|
|
32
34
|
import { forgetOldConsents, handleConsent } from './consent.js';
|
|
33
35
|
import { access, mayEdit } from './session.js';
|
|
34
36
|
import { handleGitHub } from './editor-github.js';
|
|
37
|
+
import { handleStore } from './store.js';
|
|
35
38
|
import { handleR2 } from './r2-status.js';
|
|
36
39
|
import { publishScheduled, SCHEDULED_LIST } from './publish.js';
|
|
37
40
|
import { commitFiles, readFiles } from './github.js';
|
|
@@ -143,6 +146,7 @@ export default {
|
|
|
143
146
|
if (url.pathname === '/api/consent') return handleConsent(request, env);
|
|
144
147
|
if (url.pathname === '/api/r2') return handleR2(request, env);
|
|
145
148
|
if (url.pathname.startsWith('/api/github/')) return handleGitHub(request, env, ctx);
|
|
149
|
+
if (url.pathname.startsWith('/api/store/')) return handleStore(request, env, ctx);
|
|
146
150
|
if (/^\/api\/push(\/wait)?\/?$/.test(url.pathname)) return handlePush(request, env);
|
|
147
151
|
const answer = appAnswer(request, url, env, ctx);
|
|
148
152
|
if (answer) return answer;
|