@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.
Files changed (186) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/astro.mjs +2 -1
  3. package/defaults/blockTexts.ts +9 -9
  4. package/defaults/collections.ts +257 -257
  5. package/defaults/colors.ts +74 -74
  6. package/defaults/company.ts +9 -9
  7. package/defaults/editor.ts +65 -65
  8. package/defaults/i18n/da-DK/blockTexts.ts +12 -0
  9. package/defaults/i18n/da-DK/collections.ts +220 -0
  10. package/defaults/i18n/da-DK/colors.ts +102 -0
  11. package/defaults/i18n/da-DK/company.ts +7 -0
  12. package/defaults/i18n/da-DK/editor.ts +13 -0
  13. package/defaults/i18n/da-DK/index.ts +31 -0
  14. package/defaults/i18n/da-DK/internalLinks.ts +12 -0
  15. package/defaults/i18n/da-DK/opens.ts +7 -0
  16. package/defaults/i18n/da-DK/popup.ts +9 -0
  17. package/defaults/i18n/da-DK/privacy.ts +38 -0
  18. package/defaults/i18n/da-DK/rules.ts +56 -0
  19. package/defaults/i18n/da-DK/sitedata.ts +8 -0
  20. package/defaults/i18n/da-DK/standard.ts +8 -0
  21. package/defaults/i18n/da-DK/standardPages.ts +17 -0
  22. package/defaults/internalLinks.ts +8 -8
  23. package/defaults/opens.ts +2 -2
  24. package/defaults/popup.tsx +7 -7
  25. package/defaults/privacy.ts +37 -37
  26. package/defaults/rules.ts +68 -66
  27. package/defaults/schema.ts +1 -1
  28. package/defaults/sitedata.ts +4 -4
  29. package/defaults/standard.ts +9 -6
  30. package/defaults/standardPages.ts +14 -14
  31. package/defaults/texts.ts +7 -510
  32. package/lib/appId.d.mts +1 -0
  33. package/lib/appId.mjs +6 -0
  34. package/lib/appRoute.d.mts +2 -2
  35. package/lib/appRoute.mjs +3 -3
  36. package/lib/appRoute.test.mjs +8 -8
  37. package/lib/apps.mjs +13 -3
  38. package/lib/apps.test.mjs +10 -1
  39. package/lib/contracts.d.mts +3 -2
  40. package/lib/contracts.mjs +3 -2
  41. package/lib/folderChanges.d.mts +29 -0
  42. package/lib/folderChanges.mjs +34 -0
  43. package/lib/licenses.mjs +2 -2
  44. package/lib/licenses.owner.test.mjs +9 -0
  45. package/lib/menu.mjs +4 -4
  46. package/lib/menu.test.mjs +101 -101
  47. package/lib/scheduled.mjs +1 -1
  48. package/lib/section-ranges.d.mts +1 -1
  49. package/lib/section-ranges.mjs +8 -14
  50. package/lib/section-ranges.test.mjs +26 -5
  51. package/lib/storeEntries.d.mts +3 -1
  52. package/lib/storeEntries.mjs +22 -8
  53. package/lib/storeEntries.test.mjs +59 -7
  54. package/lib/text-sections.d.mts +11 -1
  55. package/lib/text-sections.mjs +18 -8
  56. package/lib/text-sections.test.mjs +34 -1
  57. package/lib/translate.d.mts +11 -0
  58. package/lib/translate.mjs +13 -1
  59. package/lib/translate.test.mjs +23 -2
  60. package/lib/video-id.d.mts +6 -2
  61. package/lib/video-id.mjs +5 -1
  62. package/lib/video.d.mts +6 -2
  63. package/lib/video.mjs +5 -1
  64. package/lib/without-h1.d.mts +1 -0
  65. package/lib/without-h1.mjs +19 -4
  66. package/lib/without-h1.test.mjs +30 -1
  67. package/package.json +3 -2
  68. package/scripts/app-screenshots.mjs +9 -8
  69. package/scripts/check-content.mjs +12 -2
  70. package/scripts/check-language.mjs +0 -1
  71. package/scripts/check-site-types.mjs +26 -0
  72. package/scripts/editor-screens.mjs +3 -3
  73. package/scripts/site-types.mjs +14 -0
  74. package/scripts/site-types.test.mjs +21 -0
  75. package/site/activeTheme.test.mjs +52 -0
  76. package/site/appSelf.d.mts +2 -0
  77. package/site/appSelf.mjs +23 -0
  78. package/site/appSelf.test.mjs +35 -0
  79. package/site/content.ts +5 -2
  80. package/site/defineSite.mjs +2 -2
  81. package/site/schema.ts +3 -3
  82. package/site/siteModules.mjs +2 -1
  83. package/site/theme.mjs +19 -8
  84. package/src/Advanced.tsx +2 -2
  85. package/src/App.tsx +79 -78
  86. package/src/AppAdds.tsx +1 -1
  87. package/src/AppList.tsx +39 -24
  88. package/src/BlockDialog.tsx +39 -18
  89. package/src/BlockParts.tsx +6 -6
  90. package/src/BlockSettings.tsx +2 -2
  91. package/src/BrandForm.tsx +2 -2
  92. package/src/Colors.tsx +6 -6
  93. package/src/CompanyForm.tsx +1 -1
  94. package/src/Consents.tsx +3 -3
  95. package/src/CreateFab.tsx +8 -0
  96. package/src/DataForm.tsx +10 -20
  97. package/src/Deleted.tsx +3 -3
  98. package/src/EditorFooter.tsx +2 -2
  99. package/src/EntryForm.tsx +3 -3
  100. package/src/FooterSettings.tsx +2 -2
  101. package/src/FormFrame.tsx +4 -3
  102. package/src/HeaderSettings.tsx +3 -3
  103. package/src/Keywords.tsx +5 -5
  104. package/src/Library.tsx +75 -82
  105. package/src/Licenses.tsx +8 -8
  106. package/src/ListFab.tsx +13 -0
  107. package/src/MainDrawerParts.tsx +3 -3
  108. package/src/MainMenu.tsx +1 -1
  109. package/src/MarkdownEditor.tsx +88 -47
  110. package/src/Media.tsx +11 -20
  111. package/src/MediaTags.tsx +2 -2
  112. package/src/Notifications.tsx +2 -2
  113. package/src/Parts.tsx +17 -17
  114. package/src/Redirects.tsx +14 -22
  115. package/src/Screen.tsx +16 -6
  116. package/src/SeoScores.tsx +6 -6
  117. package/src/SideColumn.tsx +2 -2
  118. package/src/StartFab.tsx +6 -8
  119. package/src/ThemeStore.tsx +37 -16
  120. package/src/Tokens.tsx +2 -2
  121. package/src/apps.ts +7 -8
  122. package/src/checkSite.ts +86 -20
  123. package/src/drawerYield.d.mts +3 -0
  124. package/src/drawerYield.mjs +10 -0
  125. package/src/drawerYield.test.mjs +22 -0
  126. package/src/editor.css +59 -38
  127. package/src/examples.ts +1 -1
  128. package/src/fabScreens.test.mjs +28 -0
  129. package/src/fabScreens.ts +16 -0
  130. package/src/i18n/da-DK/library.ts +2 -2
  131. package/src/i18n/da-DK/text.ts +1 -1
  132. package/src/i18n/en-US/apps.ts +1 -1
  133. package/src/i18n/en-US/common.ts +8 -8
  134. package/src/i18n/en-US/library.ts +5 -5
  135. package/src/i18n/en-US/parts.ts +3 -3
  136. package/src/i18n/en-US/text.ts +1 -1
  137. package/src/i18n/index.ts +4 -8
  138. package/src/images.ts +2 -2
  139. package/src/legacyMove.test.mjs +137 -0
  140. package/src/legacyMove.ts +131 -0
  141. package/src/licenseData.ts +3 -3
  142. package/src/markdown.mjs +10 -7
  143. package/src/markdown.test.mjs +16 -2
  144. package/src/menuChildren.ts +5 -5
  145. package/src/menuOpen.ts +20 -4
  146. package/src/movedRoutes.d.mts +8 -1
  147. package/src/movedRoutes.mjs +102 -21
  148. package/src/movedRoutes.test.mjs +128 -6
  149. package/src/nav.ts +1 -1
  150. package/src/notificationData.ts +1 -1
  151. package/src/pageH1Panel.d.mts +4 -0
  152. package/src/pageH1Panel.mjs +31 -0
  153. package/src/pageH1Panel.test.mjs +56 -0
  154. package/src/pagePreview.tsx +22 -12
  155. package/src/schema.ts +2 -2
  156. package/src/sectionFrame.ts +3 -3
  157. package/src/sectionState.d.mts +3 -3
  158. package/src/sectionState.mjs +6 -6
  159. package/src/sectionState.test.mjs +14 -2
  160. package/src/sheetPlacement.d.mts +6 -4
  161. package/src/sheetPlacement.mjs +15 -10
  162. package/src/sheetPlacement.test.mjs +16 -9
  163. package/src/sidePanel.ts +38 -0
  164. package/src/site.ts +37 -13
  165. package/src/standard/accordion/index.tsx +1 -1
  166. package/src/standard/accordion/shared.ts +1 -1
  167. package/src/standard/buttons/index.tsx +1 -1
  168. package/src/standard/cards/index.tsx +1 -1
  169. package/src/standard/cards/shared.ts +1 -1
  170. package/src/standard/cookies/index.tsx +1 -1
  171. package/src/standard/posts/index.tsx +1 -1
  172. package/src/standard/testimonials/index.tsx +1 -1
  173. package/src/standard/testimonials/list.ts +1 -1
  174. package/src/store.ts +36 -31
  175. package/src/tagUses.d.mts +1 -1
  176. package/src/tagUses.mjs +2 -2
  177. package/src/tagUses.test.mjs +1 -1
  178. package/src/topBarSlot.tsx +7 -15
  179. package/src/ui.tsx +33 -22
  180. package/src/virtual.d.ts +6 -0
  181. package/starter/package.json +1 -1
  182. package/theme/components/PageHeading.astro +22 -0
  183. package/theme/layouts/Base.astro +2 -2
  184. package/theme/layouts/PageLayout.astro +37 -20
  185. package/theme/styles/global.css +2 -1
  186. 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`: #udseende/dele/blokke, #udseende/dele/liste/<list>) by the part's name; `editor`: a screen of the editor (`at`: from the top of what is
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: 'blokke',
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: `liste/${key}`, part: variant ? `${kind.label} · ${variant.label}` : kind.label });
71
- } else if (what === 'liste') shots[id].push({ name, editor: `liste/${key}` });
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 ? `virksomheden/${company.id}` : `apps/${id}`;
77
- shots[id].push({ name, editor: `${at}/${home || company ? id : 'indstillinger'}` });
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(`udseende/dele/${shot.parts}`);
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
  }
@@ -13,7 +13,6 @@ const PENDING = [
13
13
  'AGENTS.md',
14
14
  'README.md',
15
15
  'astro.mjs',
16
- 'defaults/',
17
16
  'demo/',
18
17
  'lib/',
19
18
  'pages/',
@@ -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", #udseende/dele, "Temaer", #udseende/temaer, "Søgemaskineoptimering", #seo, "Appstore", #indstillinger/appstore, and "Apps", #apps, which the menu no longer links to), and
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 = ['', 'udseende/dele', 'udseende/temaer', 'seo', 'indstillinger/appstore', 'apps'];
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 = 'indstillinger/appstore'));
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
+ });
@@ -0,0 +1,2 @@
1
+ /** @returns {import('vite').Plugin} */
2
+ export function appSelf(): import("vite").Plugin;
@@ -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
- export { stateOf, withoutH1 };
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.
@@ -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, #liste/<id>): the
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: 'udtalelser', tags: 'kategorier', cookies: 'cookies' },
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}#broedkrummer` } } : {}),
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}#broedkrummer`,
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}#indlaeg`,
178
+ '@id': `${url}#post`,
179
179
  headline: title,
180
180
  description,
181
181
  datePublished: date.toISOString(),
@@ -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
- export const installedThemes = () => [
57
- STANDARD_THEME,
58
- ...(existsSync(new URL('src/themes/', root)) ? readdirSync(new URL('src/themes/', root), { withFileTypes: true }) : [])
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 (typeof theme !== 'string' || !/^[a-z0-9-]+$/.test(theme) || !existsSync(new URL(`${themeFolder(theme)}/theme.json`, root)))
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" (#avanceret), under "Indstillinger": R2 caching, the copy of the site's files in Cloudflare R2 the Worker
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 = 'avanceret';
35
+ export const ADVANCED_ROUTE = 'advanced';
36
36
 
37
37
  // "Placering" when Cloudflare places the bucket (no jurisdiction).
38
38
  const AUTO = 'auto';