@morphemecms/cms 0.11.0 → 0.13.0-next-20261106203613
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 +31 -0
- package/astro.mjs +2 -1
- package/defaults/blockTexts.ts +9 -9
- package/defaults/collections.ts +257 -257
- package/defaults/colors.ts +74 -74
- package/defaults/company.ts +9 -9
- package/defaults/editor.ts +65 -65
- package/defaults/i18n/da-DK/blockTexts.ts +12 -0
- package/defaults/i18n/da-DK/collections.ts +220 -0
- package/defaults/i18n/da-DK/colors.ts +102 -0
- package/defaults/i18n/da-DK/company.ts +7 -0
- package/defaults/i18n/da-DK/editor.ts +13 -0
- package/defaults/i18n/da-DK/index.ts +31 -0
- package/defaults/i18n/da-DK/internalLinks.ts +12 -0
- package/defaults/i18n/da-DK/opens.ts +7 -0
- package/defaults/i18n/da-DK/popup.ts +9 -0
- package/defaults/i18n/da-DK/privacy.ts +38 -0
- package/defaults/i18n/da-DK/rules.ts +56 -0
- package/defaults/i18n/da-DK/sitedata.ts +8 -0
- package/defaults/i18n/da-DK/standard.ts +8 -0
- package/defaults/i18n/da-DK/standardPages.ts +17 -0
- package/defaults/internalLinks.ts +8 -8
- package/defaults/opens.ts +2 -2
- package/defaults/popup.tsx +7 -7
- package/defaults/privacy.ts +37 -37
- package/defaults/rules.ts +68 -66
- package/defaults/schema.ts +1 -1
- package/defaults/sitedata.ts +4 -4
- package/defaults/standard.ts +9 -6
- package/defaults/standardPages.ts +14 -14
- package/defaults/texts.ts +7 -510
- package/lib/appId.d.mts +1 -0
- package/lib/appId.mjs +6 -0
- package/lib/appRoute.d.mts +2 -2
- package/lib/appRoute.mjs +3 -3
- package/lib/appRoute.test.mjs +8 -8
- package/lib/apps.mjs +13 -3
- package/lib/apps.test.mjs +10 -1
- package/lib/contracts.d.mts +3 -2
- package/lib/contracts.mjs +3 -2
- package/lib/folderChanges.d.mts +29 -0
- package/lib/folderChanges.mjs +34 -0
- package/lib/licenses.mjs +2 -2
- package/lib/licenses.owner.test.mjs +9 -0
- package/lib/menu.mjs +4 -4
- package/lib/menu.test.mjs +101 -101
- package/lib/scheduled.mjs +1 -1
- package/lib/section-ranges.d.mts +1 -1
- package/lib/section-ranges.mjs +8 -14
- package/lib/section-ranges.test.mjs +26 -5
- package/lib/storeEntries.d.mts +3 -1
- package/lib/storeEntries.mjs +22 -8
- package/lib/storeEntries.test.mjs +59 -7
- package/lib/text-sections.d.mts +11 -1
- package/lib/text-sections.mjs +18 -8
- package/lib/text-sections.test.mjs +34 -1
- package/lib/translate.d.mts +11 -0
- package/lib/translate.mjs +13 -1
- package/lib/translate.test.mjs +23 -2
- package/lib/video-id.d.mts +6 -2
- package/lib/video-id.mjs +5 -1
- package/lib/video.d.mts +6 -2
- package/lib/video.mjs +5 -1
- package/lib/without-h1.d.mts +1 -0
- package/lib/without-h1.mjs +19 -4
- package/lib/without-h1.test.mjs +30 -1
- package/package.json +3 -2
- package/scripts/app-screenshots.mjs +9 -8
- package/scripts/check-content.mjs +12 -2
- package/scripts/check-language.mjs +0 -1
- package/scripts/check-site-types.mjs +26 -0
- package/scripts/editor-screens.mjs +3 -3
- package/scripts/site-types.mjs +14 -0
- package/scripts/site-types.test.mjs +21 -0
- package/site/activeTheme.test.mjs +52 -0
- package/site/appSelf.d.mts +2 -0
- package/site/appSelf.mjs +23 -0
- package/site/appSelf.test.mjs +35 -0
- package/site/content.ts +5 -2
- package/site/defineSite.mjs +2 -2
- package/site/schema.ts +3 -3
- package/site/siteModules.mjs +2 -1
- package/site/theme.mjs +19 -8
- package/src/Advanced.tsx +2 -2
- package/src/App.tsx +79 -78
- package/src/AppAdds.tsx +1 -1
- package/src/AppList.tsx +39 -24
- package/src/BlockDialog.tsx +39 -18
- package/src/BlockParts.tsx +6 -6
- package/src/BlockSettings.tsx +2 -2
- package/src/BrandForm.tsx +2 -2
- package/src/Colors.tsx +6 -6
- package/src/CompanyForm.tsx +1 -1
- package/src/Consents.tsx +3 -3
- package/src/CreateFab.tsx +8 -0
- package/src/DataForm.tsx +10 -20
- package/src/Deleted.tsx +3 -3
- package/src/EditorFooter.tsx +2 -2
- package/src/EntryForm.tsx +3 -3
- package/src/FooterSettings.tsx +2 -2
- package/src/FormFrame.tsx +4 -3
- package/src/HeaderSettings.tsx +3 -3
- package/src/Keywords.tsx +5 -5
- package/src/Library.tsx +75 -82
- package/src/Licenses.tsx +8 -8
- package/src/ListFab.tsx +13 -0
- package/src/MainDrawerParts.tsx +3 -3
- package/src/MainMenu.tsx +1 -1
- package/src/MarkdownEditor.tsx +88 -47
- package/src/Media.tsx +11 -20
- package/src/MediaTags.tsx +2 -2
- package/src/Notifications.tsx +2 -2
- package/src/Parts.tsx +17 -17
- package/src/Redirects.tsx +14 -22
- package/src/Screen.tsx +16 -6
- package/src/SeoScores.tsx +6 -6
- package/src/SideColumn.tsx +2 -2
- package/src/StartFab.tsx +6 -8
- package/src/ThemeStore.tsx +37 -16
- package/src/Tokens.tsx +2 -2
- package/src/apps.ts +7 -8
- package/src/checkSite.ts +86 -20
- package/src/drawerYield.d.mts +3 -0
- package/src/drawerYield.mjs +10 -0
- package/src/drawerYield.test.mjs +22 -0
- package/src/editor.css +59 -38
- package/src/examples.ts +1 -1
- package/src/fabScreens.test.mjs +28 -0
- package/src/fabScreens.ts +16 -0
- package/src/i18n/da-DK/library.ts +2 -2
- package/src/i18n/da-DK/text.ts +1 -1
- package/src/i18n/en-US/apps.ts +1 -1
- package/src/i18n/en-US/common.ts +8 -8
- package/src/i18n/en-US/library.ts +5 -5
- package/src/i18n/en-US/parts.ts +3 -3
- package/src/i18n/en-US/text.ts +1 -1
- package/src/i18n/index.ts +4 -8
- package/src/images.ts +2 -2
- package/src/legacyMove.test.mjs +137 -0
- package/src/legacyMove.ts +131 -0
- package/src/licenseData.ts +3 -3
- package/src/markdown.mjs +10 -7
- package/src/markdown.test.mjs +16 -2
- package/src/menuChildren.ts +5 -5
- package/src/menuOpen.ts +20 -4
- package/src/movedRoutes.d.mts +8 -1
- package/src/movedRoutes.mjs +102 -21
- package/src/movedRoutes.test.mjs +128 -6
- package/src/nav.ts +1 -1
- package/src/notificationData.ts +1 -1
- package/src/pageH1Panel.d.mts +4 -0
- package/src/pageH1Panel.mjs +31 -0
- package/src/pageH1Panel.test.mjs +56 -0
- package/src/pagePreview.tsx +22 -12
- package/src/schema.ts +2 -2
- package/src/sectionFrame.ts +3 -3
- package/src/sectionState.d.mts +3 -3
- package/src/sectionState.mjs +6 -6
- package/src/sectionState.test.mjs +14 -2
- package/src/sheetPlacement.d.mts +6 -4
- package/src/sheetPlacement.mjs +15 -10
- package/src/sheetPlacement.test.mjs +16 -9
- package/src/sidePanel.ts +38 -0
- package/src/site.ts +37 -13
- package/src/standard/accordion/index.tsx +1 -1
- package/src/standard/accordion/shared.ts +1 -1
- package/src/standard/buttons/index.tsx +1 -1
- package/src/standard/cards/index.tsx +1 -1
- package/src/standard/cards/shared.ts +1 -1
- package/src/standard/cookies/index.tsx +1 -1
- package/src/standard/posts/index.tsx +1 -1
- package/src/standard/testimonials/index.tsx +1 -1
- package/src/standard/testimonials/list.ts +1 -1
- package/src/store.ts +36 -31
- package/src/tagUses.d.mts +1 -1
- package/src/tagUses.mjs +2 -2
- package/src/tagUses.test.mjs +1 -1
- package/src/topBarSlot.tsx +7 -15
- package/src/ui.tsx +33 -22
- package/src/virtual.d.ts +6 -0
- package/starter/package.json +1 -1
- package/theme/components/PageHeading.astro +22 -0
- package/theme/layouts/Base.astro +2 -2
- package/theme/layouts/PageLayout.astro +37 -20
- package/theme/styles/global.css +2 -1
- package/theme/styles/ui.css +40 -10
|
@@ -17,6 +17,7 @@ import { chromium } from 'playwright';
|
|
|
17
17
|
import { createServer } from 'vite';
|
|
18
18
|
import { aliases, themePlugin } from '@morphemecms/cms/site/theme.mjs';
|
|
19
19
|
import { appFoldersPlugin } from '@morphemecms/cms/lib/apps.mjs';
|
|
20
|
+
import { appSelf } from '@morphemecms/cms/site/appSelf.mjs';
|
|
20
21
|
import { binOf } from './bin.mjs';
|
|
21
22
|
import { openEditor, serveBuilt } from './built-site.mjs';
|
|
22
23
|
|
|
@@ -30,13 +31,13 @@ const BLUE = { source: '#1a73e8', variant: 'tonalSpot', contrast: 'standard' };
|
|
|
30
31
|
const ids = process.argv.length > 2 ? process.argv.slice(2) : readdirSync(APPS).filter((id) => existsSync(join(APPS, id, 'index.ts')));
|
|
31
32
|
|
|
32
33
|
// The pictures each app picks, read as store-release.mjs does, in their order. `part`: its drawing under "Dele" ›
|
|
33
|
-
// "Blokke" or the list's screen (`parts`: #
|
|
34
|
+
// "Blokke" or the list's screen (`parts`: #appearance/parts/blocks, #appearance/parts/list/<list>) by the part's name; `editor`: a screen of the editor (`at`: from the top of what is
|
|
34
35
|
// there). And each app's settings as the pictures show them: those it starts with, and the made-up ones of its picture.
|
|
35
36
|
const vite = await createServer({
|
|
36
37
|
configFile: false,
|
|
37
38
|
logLevel: 'error',
|
|
38
39
|
appType: 'custom',
|
|
39
|
-
plugins: [themePlugin(), appFoldersPlugin(ROOT)],
|
|
40
|
+
plugins: [themePlugin(), appFoldersPlugin(ROOT), appSelf()],
|
|
40
41
|
resolve: { alias: aliases() },
|
|
41
42
|
server: { middlewareMode: true, hmr: false, ws: false },
|
|
42
43
|
});
|
|
@@ -61,20 +62,20 @@ try {
|
|
|
61
62
|
if (what === 'blok')
|
|
62
63
|
shots[id].push({
|
|
63
64
|
name,
|
|
64
|
-
parts: '
|
|
65
|
+
parts: 'blocks',
|
|
65
66
|
part: (await load(`/src/apps/${id}/blocks/${key}/index.tsx`)).default.label,
|
|
66
67
|
});
|
|
67
68
|
else if (what === 'slags') {
|
|
68
69
|
const kind = app.kinds[key];
|
|
69
70
|
const variant = kind.variants?.[Number(n)];
|
|
70
|
-
shots[id].push({ name, parts: `
|
|
71
|
-
} else if (what === 'liste') shots[id].push({ name, editor: `
|
|
71
|
+
shots[id].push({ name, parts: `list/${key}`, part: variant ? `${kind.label} · ${variant.label}` : kind.label });
|
|
72
|
+
} else if (what === 'liste') shots[id].push({ name, editor: `list/${key}` });
|
|
72
73
|
else if (what === 'indstillinger') {
|
|
73
74
|
// Where they are, as settingsRoute in AppList.tsx: on the screen or the part of "Virksomheden" with its kind.
|
|
74
75
|
const home = app.category && partScreens.find((s) => s.apps?.includes(app.category));
|
|
75
76
|
const company = app.category && companyParts.find((p) => p.parts && p.apps?.includes(app.category));
|
|
76
|
-
const at = home ? home.id : company ? `
|
|
77
|
-
shots[id].push({ name, editor: `${at}/${home || company ? id : '
|
|
77
|
+
const at = home ? home.id : company ? `company/${company.id}` : `apps/${id}`;
|
|
78
|
+
shots[id].push({ name, editor: `${at}/${home || company ? id : 'settings'}` });
|
|
78
79
|
Object.assign(settings[id], shot.values);
|
|
79
80
|
} else shots[id].push({ name, editor: `apps/${id}`, at: `[data-app-page="${id}"]` });
|
|
80
81
|
}
|
|
@@ -135,7 +136,7 @@ try {
|
|
|
135
136
|
for (const [i, shot] of shots[id].entries()) {
|
|
136
137
|
const options = { path: join(folder, shot.name), type: 'jpeg', quality: 80 };
|
|
137
138
|
if (shot.part) {
|
|
138
|
-
await go(`
|
|
139
|
+
await go(`appearance/parts/${shot.parts}`);
|
|
139
140
|
// Tall enough for the whole drawing, under the screen's top bar.
|
|
140
141
|
await editor.setViewportSize({ width: phone.width, height: 1600 });
|
|
141
142
|
const drawing = editor.locator(`section[data-part="${shot.part}"] > :last-child`).first();
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// the system's icon packs and the choice of one (iconPackProblems), and so an icon the site or the editor draws that a
|
|
6
6
|
// pack has none for, and the choice of motion scheme (motionProblems), and names in a folder that differ only in case
|
|
7
7
|
// (casingProblems). Says too which of the site's texts the
|
|
8
|
-
// editor has no translation of (@site/texts, a block's texts.ts); that does not stop the build. The editor's
|
|
8
|
+
// editor has no translation of (@site/texts, a block's texts.ts); that does not stop the build, but a standard part's text with no Danish (defaults/i18n/da-DK/) does. The editor's
|
|
9
9
|
// code is TypeScript and TSX, so it is loaded with Vite, as the site's own build does. Exits early, without checking, when
|
|
10
10
|
// MORPHEME_CONTENT_CHECKED names the commit being built (morpheme ci-build already ran it; already-checked.mjs).
|
|
11
11
|
import { execFileSync } from 'node:child_process';
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
themeProblems,
|
|
26
26
|
} from '@morphemecms/cms/site/theme.mjs';
|
|
27
27
|
import { appFoldersPlugin, appIds, appWorkersProblems } from '@morphemecms/cms/lib/apps.mjs';
|
|
28
|
+
import { appSelf } from '@morphemecms/cms/site/appSelf.mjs';
|
|
28
29
|
import { caseCollisionProblems } from '@morphemecms/cms/lib/caseCollisions.mjs';
|
|
29
30
|
import { codePackageProblems, declarationProblems, ownerId } from '@morphemecms/cms/lib/licenses.mjs';
|
|
30
31
|
import { breakpointProblems } from '@morphemecms/cms/site/breakpoints.mjs';
|
|
@@ -70,7 +71,7 @@ const server = await createServer({
|
|
|
70
71
|
logLevel: 'error',
|
|
71
72
|
appType: 'custom',
|
|
72
73
|
// As in packages/editor/astro.mjs: the editor takes the site from src/site/ as @site, and the theme in use as @theme.
|
|
73
|
-
plugins: [themePlugin(), appFoldersPlugin(process.cwd())],
|
|
74
|
+
plugins: [themePlugin(), appFoldersPlugin(process.cwd()), appSelf()],
|
|
74
75
|
resolve: { alias: aliases() },
|
|
75
76
|
server: { middlewareMode: true, hmr: false, ws: false },
|
|
76
77
|
});
|
|
@@ -122,6 +123,15 @@ try {
|
|
|
122
123
|
|
|
123
124
|
for (const [locale, files] of Object.entries(missing)) {
|
|
124
125
|
for (const [file, texts] of Object.entries(files)) {
|
|
126
|
+
// A standard part's text with no translation stops the check; the site's own only warns.
|
|
127
|
+
if (file.startsWith('defaults/i18n/')) {
|
|
128
|
+
result.problems.push(
|
|
129
|
+
...texts.map(
|
|
130
|
+
(text) => `${file}: standardteksten ${JSON.stringify(text)} mangler på ${locale}. Skriv den i defaults/i18n/${locale}/.`,
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
125
135
|
console.warn(`\nEditoren viser ${texts.length} af sitets tekster uoversat på ${locale}. Skriv dem i ${file}:\n`);
|
|
126
136
|
for (const text of texts) console.warn(` ${JSON.stringify(text)}`);
|
|
127
137
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Type-checks the package's whole TS source in the demo site, with the site's own tsc, as a site that has it from npm does.
|
|
2
|
+
// Run by `npm run check:site-types`, after the build (.morpheme/demo/site, node_modules/@morphemecms/cms). Wiring only.
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join, relative } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { binOf } from './bin.mjs';
|
|
8
|
+
import { siteTypesConfig } from './site-types.mjs';
|
|
9
|
+
|
|
10
|
+
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
11
|
+
const site = join(root, '.morpheme/demo/site');
|
|
12
|
+
const config = join(site, 'tsconfig.package.json');
|
|
13
|
+
const packageDir = relative(site, join(root, 'node_modules/@morphemecms/cms')).replaceAll('\\', '/');
|
|
14
|
+
|
|
15
|
+
writeFileSync(config, JSON.stringify(siteTypesConfig(packageDir), null, 2));
|
|
16
|
+
try {
|
|
17
|
+
execFileSync(process.execPath, [binOf('typescript', 'tsc', site), '--noEmit', '-p', 'tsconfig.package.json'], {
|
|
18
|
+
cwd: site,
|
|
19
|
+
stdio: 'inherit',
|
|
20
|
+
});
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('The package source does not type-check as a site type-checks it: sites will fail the same way.');
|
|
23
|
+
process.exitCode = error.status ?? 1;
|
|
24
|
+
} finally {
|
|
25
|
+
rmSync(config, { force: true });
|
|
26
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// packages/editor/src/icon.tsx), there or on one of the built site's pages. An icon drawn from Material Symbols instead
|
|
5
5
|
// of the icon pack (data-icon-fallback) is only listed, with where it is: it does not fail.
|
|
6
6
|
// The screens are found as the editor's own links: from the start screen (the drawer's links are in the page also while it is
|
|
7
|
-
// closed) and "Dele", #
|
|
7
|
+
// closed) and "Dele", #appearance/parts, "Temaer", #appearance/themes, "Søgemaskineoptimering", #seo, "Appstore", #settings/appstore, and "Apps", #apps, which the menu no longer links to), and
|
|
8
8
|
// from each screen it opens, two steps down. GitHub is answered from the files in this repository, so it needs no login
|
|
9
9
|
// and changes nothing. It runs on the built site: npm run build && npm run check:screens (or on another
|
|
10
10
|
// folder with a built site, its path after the command: packages/editor/scripts/check-themes.mjs).
|
|
@@ -71,7 +71,7 @@ for (const [width, height] of [
|
|
|
71
71
|
// From the start screen (the drawer's links), and from "Dele", "Temaer", "Søgemaskineoptimering" and "Appstore"
|
|
72
72
|
// (the apps' pages and their developers'), whose screens are a step further down than the others. "Apps" (the list of
|
|
73
73
|
// the apps installed) has no row in the menu, so it is a start of its own.
|
|
74
|
-
const starts = ['', '
|
|
74
|
+
const starts = ['', 'appearance/parts', 'appearance/themes', 'seo', 'settings/appstore', 'apps'];
|
|
75
75
|
const seen = new Set(starts);
|
|
76
76
|
let next = starts;
|
|
77
77
|
for (let depth = 0; depth <= 2 && next.length; depth++) {
|
|
@@ -117,7 +117,7 @@ for (const [state, { shows, hides }] of Object.entries(keyStates)) {
|
|
|
117
117
|
const page = await browser.newPage({ viewport: { width: 390, height: 844 }, locale: 'da-DK' });
|
|
118
118
|
page.on('pageerror', (e) => problems.push(`Appstore (${state}): ${e.message}`));
|
|
119
119
|
await openEditor(page, site, {}, state);
|
|
120
|
-
await page.evaluate(() => (location.hash = '
|
|
120
|
+
await page.evaluate(() => (location.hash = 'settings/appstore'));
|
|
121
121
|
await page.waitForTimeout(500);
|
|
122
122
|
const text = await page.evaluate(() => document.body.innerText);
|
|
123
123
|
for (const x of shows) if (!text.includes(x)) problems.push(`Appstore (${state}): "${x}" mangler`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The tsconfig of the site-style type check (check-site-types.mjs): the demo site's own, plus the package's whole TS source.
|
|
2
|
+
// Exposes siteTypesConfig(packageDir). No dependencies.
|
|
3
|
+
|
|
4
|
+
const DIRS = ['src', 'site', 'lib', 'defaults', 'theme'];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} packageDir the package's folder, relative to the site, with forward slashes
|
|
8
|
+
* @returns {{ extends: string, include: string[], exclude: string[] }}
|
|
9
|
+
*/
|
|
10
|
+
export const siteTypesConfig = (packageDir) => ({
|
|
11
|
+
extends: './tsconfig.json',
|
|
12
|
+
include: ['.astro/types.d.ts', '**/*', ...DIRS.flatMap((dir) => [`${packageDir}/${dir}/**/*.ts`, `${packageDir}/${dir}/**/*.tsx`])],
|
|
13
|
+
exclude: ['dist', `${packageDir}/**/*.test.*`],
|
|
14
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Tests siteTypesConfig (site-types.mjs): the tsconfig that type-checks the package's source as a site does.
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
import { siteTypesConfig } from './site-types.mjs';
|
|
5
|
+
|
|
6
|
+
const PACKAGE = '../../../node_modules/@morphemecms/cms';
|
|
7
|
+
const config = siteTypesConfig(PACKAGE);
|
|
8
|
+
|
|
9
|
+
test('extends the site tsconfig', () => {
|
|
10
|
+
assert.equal(config.extends, './tsconfig.json');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('includes the site first, then the package source', () => {
|
|
14
|
+
assert.deepEqual(config.include.slice(0, 2), ['.astro/types.d.ts', '**/*']);
|
|
15
|
+
assert.ok(config.include.includes(`${PACKAGE}/src/**/*.tsx`));
|
|
16
|
+
assert.ok(config.include.includes(`${PACKAGE}/lib/**/*.ts`));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('excludes dist and the package tests', () => {
|
|
20
|
+
assert.deepEqual(config.exclude, ['dist', `${PACKAGE}/**/*.test.*`]);
|
|
21
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Tests of activeTheme and installedThemes (site/theme.mjs) with permanent theme ids.
|
|
2
|
+
// Run with `node --test site/activeTheme.test.mjs`; the site's root is the working directory, set before the import.
|
|
3
|
+
import { strict as assert } from 'node:assert';
|
|
4
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { registerHooks } from 'node:module';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
|
|
10
|
+
const PACKAGE = '@morphemecms/cms/';
|
|
11
|
+
const pkg = new URL('../', import.meta.url);
|
|
12
|
+
registerHooks({
|
|
13
|
+
resolve: (specifier, context, next) =>
|
|
14
|
+
specifier.startsWith(PACKAGE) ? next(new URL(specifier.slice(PACKAGE.length), pkg).href, context) : next(specifier, context),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const site = mkdtempSync(join(tmpdir(), 'site-'));
|
|
18
|
+
const theme = (name) => {
|
|
19
|
+
mkdirSync(join(site, 'src/themes', name), { recursive: true });
|
|
20
|
+
writeFileSync(join(site, 'src/themes', name, 'theme.json'), '{}');
|
|
21
|
+
};
|
|
22
|
+
const choose = (value) => {
|
|
23
|
+
mkdirSync(join(site, 'src/data'), { recursive: true });
|
|
24
|
+
writeFileSync(join(site, 'src/data/theme.json'), JSON.stringify({ theme: value }));
|
|
25
|
+
};
|
|
26
|
+
theme('theme_fa395ad84690acbd');
|
|
27
|
+
theme('own-theme');
|
|
28
|
+
process.chdir(site);
|
|
29
|
+
const { activeTheme, installedThemes } = await import('./theme.mjs');
|
|
30
|
+
|
|
31
|
+
test('a permanent theme id is accepted', () => {
|
|
32
|
+
choose('theme_fa395ad84690acbd');
|
|
33
|
+
assert.equal(activeTheme(), 'theme_fa395ad84690acbd');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('the standard theme and a site-own theme are accepted', () => {
|
|
37
|
+
choose('saturn');
|
|
38
|
+
assert.equal(activeTheme(), 'saturn');
|
|
39
|
+
choose('own-theme');
|
|
40
|
+
assert.equal(activeTheme(), 'own-theme');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('a theme id that is not a store id stops the build', () => {
|
|
44
|
+
choose('theme_xyz');
|
|
45
|
+
assert.throws(() => activeTheme(), /theme_xyz/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('a theme folder that looks like a store id but is not one stops the build, naming the folder', () => {
|
|
49
|
+
assert.deepEqual(installedThemes(), ['saturn', 'own-theme', 'theme_fa395ad84690acbd']);
|
|
50
|
+
theme('theme_xyz');
|
|
51
|
+
assert.throws(() => installedThemes(), /src\/themes\/theme_xyz/);
|
|
52
|
+
});
|
package/site/appSelf.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The Vite plugin behind `import self from '@app/self'` (app contract 3): the id of the app whose folder the importing
|
|
2
|
+
// file is in. Exposes appSelf. Depends on appIdOf (lib/appId.mjs).
|
|
3
|
+
import { appIdOf } from '../lib/appId.mjs';
|
|
4
|
+
|
|
5
|
+
const SELF = '@app/self';
|
|
6
|
+
const PREFIX = '\0app-self:';
|
|
7
|
+
|
|
8
|
+
/** @returns {import('vite').Plugin} */
|
|
9
|
+
export function appSelf() {
|
|
10
|
+
return {
|
|
11
|
+
name: 'app-self',
|
|
12
|
+
enforce: 'pre',
|
|
13
|
+
resolveId(source, importer) {
|
|
14
|
+
if (source !== SELF) return null;
|
|
15
|
+
const path = (importer ?? '').split('?')[0].replaceAll('\\', '/');
|
|
16
|
+
if (!/\/src\/apps\/[^/]+\//.test(path)) throw new Error(`${SELF} is only for app files: ${path}`);
|
|
17
|
+
return PREFIX + appIdOf(path);
|
|
18
|
+
},
|
|
19
|
+
load(id) {
|
|
20
|
+
return id.startsWith(PREFIX) ? `export default { id: ${JSON.stringify(id.slice(PREFIX.length)).replaceAll('"', "'")} };` : null;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Tests for the @app/self Vite plugin (site/appSelf.mjs): the id an app file gets, and the error outside apps.
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { appSelf } from './appSelf.mjs';
|
|
5
|
+
|
|
6
|
+
/** The module text the plugin makes for `@app/self` imported from `importer`. */
|
|
7
|
+
const moduleFor = (importer) => {
|
|
8
|
+
const plugin = appSelf();
|
|
9
|
+
const id = plugin.resolveId('@app/self', importer);
|
|
10
|
+
return plugin.load(id);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
test('gives a store id', () => {
|
|
14
|
+
assert.equal(moduleFor('/x/src/apps/app_a030dfba69793996/link.ts'), "export default { id: 'app_a030dfba69793996' };");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('gives a plain folder name', () => {
|
|
18
|
+
assert.equal(moduleFor('/x/src/apps/cookiescanner/worker.js'), "export default { id: 'cookiescanner' };");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('gives the app of a nested file', () => {
|
|
22
|
+
assert.equal(moduleFor('/x/src/apps/mobilepay/blocks/mobilepay/load.ts'), "export default { id: 'mobilepay' };");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('throws outside an app, with the path', () => {
|
|
26
|
+
assert.throws(() => moduleFor('/x/src/themes/saturn/a.ts'), /@app\/self is only for app files: \/x\/src\/themes\/saturn\/a\.ts/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('gives the same id for a Windows path', () => {
|
|
30
|
+
assert.equal(moduleFor(String.raw`C:\x\src\apps\app_a030dfba69793996\link.ts`), moduleFor('/x/src/apps/app_a030dfba69793996/link.ts'));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('ignores other imports', () => {
|
|
34
|
+
assert.equal(appSelf().resolveId('react', '/x/src/apps/a/b.ts'), null);
|
|
35
|
+
});
|
package/site/content.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
// The site's content collections (pages and blog posts): their status, paths, headings, and a page's text without its H1.
|
|
2
|
+
// Exposes getPages, getPosts, pageHeading, pageHeadingHtml, withoutH1 and h1SectionColor. Depends on astro:content and lib/.
|
|
1
3
|
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
2
4
|
import { categories, isBlogCategory } from './categories';
|
|
3
5
|
import { displayTitle } from './text';
|
|
4
6
|
import { isPreview } from './noindex.mjs';
|
|
5
7
|
import { stateOf } from '@morphemecms/cms/lib/status.mjs';
|
|
6
8
|
import { pageH1 } from '@morphemecms/cms/src/sections.mjs';
|
|
7
|
-
import { withoutH1 } from '@morphemecms/cms/lib/without-h1.mjs';
|
|
9
|
+
import { h1SectionColor, withoutH1 } from '@morphemecms/cms/lib/without-h1.mjs';
|
|
8
10
|
|
|
9
11
|
// From text.ts, which the editor uses too (it has no content collections).
|
|
10
12
|
export { displayTitle, openingText } from './text';
|
|
11
|
-
|
|
13
|
+
// withoutH1 and h1SectionColor: a page's text without its H1 (and its marker), and the colour of the H1's section (PageLayout.astro).
|
|
14
|
+
export { h1SectionColor, stateOf, withoutH1 };
|
|
12
15
|
|
|
13
16
|
export type Post = CollectionEntry<'blog'>;
|
|
14
17
|
// Every page has a path: its own, or /<file name>/ for pages created in the editor.
|
package/site/defineSite.mjs
CHANGED
|
@@ -99,10 +99,10 @@ const STANDARD = {
|
|
|
99
99
|
menu: 'src/data/menu.json',
|
|
100
100
|
/** Addresses of the site that are not pages, which a new page or post cannot have. */
|
|
101
101
|
reserved: ['/editor/', '/admin/', '/oauth/', '/images/', '/filer/', '/ny/', '/404/'],
|
|
102
|
-
/** The lists the standard blocks use, by what they hold, and their ids (their addresses, #
|
|
102
|
+
/** The lists the standard blocks use, by what they hold, and their ids (their addresses, #list/<id>): the
|
|
103
103
|
* testimonials, the list the block "Udtalelser" brings (src/standard/testimonials/list.ts), the tags, which a
|
|
104
104
|
* testimonial's "Opret et tag" makes a new one of, and the cookies, the list the block "Cookieliste" brings. */
|
|
105
|
-
lists: { testimonials: '
|
|
105
|
+
lists: { testimonials: 'testimonials', tags: 'tags', cookies: 'cookies' },
|
|
106
106
|
/** The folder of the site's cookie list, "Cookies" under "Lister" (the standard block "Cookieliste",
|
|
107
107
|
* src/standard/cookies/list.ts): a file per cookie, those an app sets added when it is installed and taken out when it
|
|
108
108
|
* is uninstalled (src/cookieList.ts). */
|
package/site/schema.ts
CHANGED
|
@@ -131,7 +131,7 @@ export function webPage({
|
|
|
131
131
|
isPartOf: { '@id': ids.website },
|
|
132
132
|
about: { '@id': ids.business },
|
|
133
133
|
...(image ? { primaryImageOfPage: abs(image) } : {}),
|
|
134
|
-
...(trail.length ? { breadcrumb: { '@id': `${url}#
|
|
134
|
+
...(trail.length ? { breadcrumb: { '@id': `${url}#breadcrumbs` } } : {}),
|
|
135
135
|
...(questions.length
|
|
136
136
|
? {
|
|
137
137
|
mainEntity: questions.map((q) => ({
|
|
@@ -146,7 +146,7 @@ export function webPage({
|
|
|
146
146
|
? [
|
|
147
147
|
{
|
|
148
148
|
'@type': 'BreadcrumbList',
|
|
149
|
-
'@id': `${url}#
|
|
149
|
+
'@id': `${url}#breadcrumbs`,
|
|
150
150
|
itemListElement: trail.map((c, i) => ({ '@type': 'ListItem', position: i + 1, name: c.name, item: abs(c.href) })),
|
|
151
151
|
},
|
|
152
152
|
]
|
|
@@ -175,7 +175,7 @@ export function blogPosting({
|
|
|
175
175
|
const url = abs(path);
|
|
176
176
|
return {
|
|
177
177
|
'@type': 'BlogPosting',
|
|
178
|
-
'@id': `${url}#
|
|
178
|
+
'@id': `${url}#post`,
|
|
179
179
|
headline: title,
|
|
180
180
|
description,
|
|
181
181
|
datePublished: date.toISOString(),
|
package/site/siteModules.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { createServer } from 'vite';
|
|
4
4
|
import { appFoldersPlugin } from '../lib/apps.mjs';
|
|
5
5
|
import { aliases, themePlugin } from './theme.mjs';
|
|
6
|
+
import { appSelf } from './appSelf.mjs';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Runs `use` with a loader of the site's modules (`@site/collections`, `/src/apps/<id>/index.ts`), then closes it.
|
|
@@ -15,7 +16,7 @@ export async function withSiteModules(use) {
|
|
|
15
16
|
configFile: false,
|
|
16
17
|
logLevel: 'error',
|
|
17
18
|
appType: 'custom',
|
|
18
|
-
plugins: [themePlugin(), appFoldersPlugin(`${process.cwd()}/`)],
|
|
19
|
+
plugins: [themePlugin(), appFoldersPlugin(`${process.cwd()}/`), appSelf()],
|
|
19
20
|
resolve: { alias: aliases() },
|
|
20
21
|
server: { middlewareMode: true, hmr: false, ws: false },
|
|
21
22
|
});
|
package/site/theme.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// theme or the look of a block, is the standard theme's, styled by the theme in use's CSS. The site and the editor
|
|
5
5
|
// reach the theme as @theme (themePlugin: the theme in use's file, else the standard theme's), and the site's own
|
|
6
6
|
// parts (src/site/) as @site (aliases): for packages/editor/astro.mjs and packages/editor/scripts/check-content.mjs. Stops with what is
|
|
7
|
-
// wrong, when the theme named is not installed. Also the virtual modules with all Phosphor's icons (allIconsModule) and with the theme's coverage of Material's parts
|
|
7
|
+
// wrong, when the theme named is not installed or a folder in src/themes/ is a malformed store id (a store id is theme_ + 16 hex characters). Also the virtual modules with all Phosphor's icons (allIconsModule) and with the theme's coverage of Material's parts
|
|
8
8
|
// (coverageModule, site/themeCoverageFiles.mjs).
|
|
9
9
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
10
10
|
import { join, relative } from 'node:path';
|
|
@@ -16,6 +16,7 @@ import { MATERIAL_ALL, MATERIAL_SSR, materialModule, materialSsrModule } from '.
|
|
|
16
16
|
import { ICON_PACK_FILE, ICON_PACK_LICENSE, SYSTEM_PACKS, THEME_PACKS_FOLDER, packInUse, packTheme, themePackId } from './iconPacks.mjs';
|
|
17
17
|
import { MOTION_SCHEMES } from '@morphemecms/cms/lib/motion-schemes.mjs';
|
|
18
18
|
import { templateProblems } from '@morphemecms/cms/lib/page-template.mjs';
|
|
19
|
+
import { strayStoreId } from '@morphemecms/cms/lib/storeEntries.mjs';
|
|
19
20
|
import { storeImportProblems } from './storeTheme.mjs';
|
|
20
21
|
import { coverageModule } from './themeCoverageFiles.mjs';
|
|
21
22
|
|
|
@@ -52,13 +53,19 @@ export const THEME_FILES = [
|
|
|
52
53
|
'editor/looks.mjs',
|
|
53
54
|
];
|
|
54
55
|
|
|
55
|
-
/** The themes installed: the standard theme and the folders in src/themes/ with a theme.json.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
/** The themes installed: the standard theme and the folders in src/themes/ with a theme.json. Stops on a folder that
|
|
57
|
+
* starts with `app_` or `theme_` but is not a store id of a theme. */
|
|
58
|
+
export const installedThemes = () => {
|
|
59
|
+
const names = (existsSync(new URL('src/themes/', root)) ? readdirSync(new URL('src/themes/', root), { withFileTypes: true }) : [])
|
|
59
60
|
.filter((e) => e.isDirectory() && existsSync(new URL(`src/themes/${e.name}/theme.json`, root)))
|
|
60
|
-
.map((e) => e.name)
|
|
61
|
-
|
|
61
|
+
.map((e) => e.name);
|
|
62
|
+
const stray = names.find((name) => strayStoreId(name, 'theme'));
|
|
63
|
+
if (stray)
|
|
64
|
+
throw new Error(
|
|
65
|
+
`src/themes/${stray}: the folder name looks like a store id but is not one. A store theme id is theme_ and 16 lowercase hex characters; rename the folder.`,
|
|
66
|
+
);
|
|
67
|
+
return [STANDARD_THEME, ...names];
|
|
68
|
+
};
|
|
62
69
|
|
|
63
70
|
/** What every theme has itself. */
|
|
64
71
|
export const OWN_FILES = ['theme.json', 'styles/global.css'];
|
|
@@ -455,7 +462,11 @@ export function siteFile(file) {
|
|
|
455
462
|
/** The folder of the theme in use, under src/themes/. */
|
|
456
463
|
export function activeTheme() {
|
|
457
464
|
const { theme } = JSON.parse(readFileSync(new URL('src/data/theme.json', root), 'utf8'));
|
|
458
|
-
if (
|
|
465
|
+
if (
|
|
466
|
+
typeof theme !== 'string' ||
|
|
467
|
+
!/^([a-z0-9-]+|theme_[0-9a-f]{16})$/.test(theme) ||
|
|
468
|
+
!existsSync(new URL(`${themeFolder(theme)}/theme.json`, root))
|
|
469
|
+
)
|
|
459
470
|
throw new Error(`src/data/theme.json: temaet ${JSON.stringify(theme)} er ikke installeret (src/themes/<tema>/theme.json).`);
|
|
460
471
|
return theme;
|
|
461
472
|
}
|
package/src/Advanced.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "Avanceret" (#
|
|
1
|
+
// "Avanceret" (#advanced), under "Indstillinger": R2 caching, the copy of the site's files in Cloudflare R2 the Worker
|
|
2
2
|
// answers the editor from (packages/editor/worker/files.js), turned on and off with "Aktiveret" in the Worker's settings (r2Caching.ts),
|
|
3
3
|
// saved at once in its own commit. Off, the Worker asks GitHub for everything. It can only be turned on when Cloudflare
|
|
4
4
|
// has the bucket (the Worker asks, /api/r2), else a deploy would fail; under the switch, why not, and, until the site is
|
|
@@ -32,7 +32,7 @@ import { buildSettingsOf, withBuildSetting, type BuildSetting } from './buildSet
|
|
|
32
32
|
import { BUILD_SETTINGS_PATH } from '../lib/build-settings.mjs';
|
|
33
33
|
import { siteT, t } from './i18n';
|
|
34
34
|
|
|
35
|
-
export const ADVANCED_ROUTE = '
|
|
35
|
+
export const ADVANCED_ROUTE = 'advanced';
|
|
36
36
|
|
|
37
37
|
// "Placering" when Cloudflare places the bucket (no jurisdiction).
|
|
38
38
|
const AUTO = 'auto';
|