@morphemecms/cms 0.7.0 → 0.7.1

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @morphemecms/cms
2
2
 
3
+ ## 0.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 36e2e3b: Fix the Cloudflare build stopping with "Too many open files" by serving all Material Symbols icons as one module.
8
+
3
9
  ## 0.7.0
4
10
 
5
11
  ### Minor Changes
package/lib/licenses.mjs CHANGED
@@ -428,7 +428,7 @@ const HELPERS = [
428
428
  [/^\0vite\//, 'vite'],
429
429
  [/^\0rolldown\//, 'rolldown'],
430
430
  ];
431
- const OWN_VIRTUAL = [/^\0virtual:(app-folders|all-icons)$/, /^\0site-theme-icons?(:|$)/];
431
+ const OWN_VIRTUAL = [/^\0virtual:(app-folders|all-icons|material-symbols)$/, /^\0site-theme-icons?(:|$)/];
432
432
 
433
433
  /** An app or a theme as licensesIntegration takes it: its kind and id, its name, version and who made it, its manifest
434
434
  * (its defineApp or its theme.json, with `license` and `thirdParty`), its folder, and whether the site is built with it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphemecms/cms",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Morpheme CMS: the editor for a site built with Astro, its screens, its blocks and the rules a site gives it. See README.md.",
5
5
  "license": "MIT",
6
6
  "author": "Lars Adolfsen",
@@ -1,5 +1,11 @@
1
- /** The module's source: `{ '/node_modules/@material-symbols/svg-400/outlined/<name>.svg': '<svg …>' }`, the keys as
2
- * import.meta.glob names them (src/materialIcons.ts). With `ssr` false: an empty object. */
1
+ /** All Outlined files as text, read one at a time: `{ '/node_modules/@material-symbols/svg-400/outlined/<name>.svg': '<svg …>' }`,
2
+ * the keys as import.meta.glob named them. `dir` is the URL or the path of the outlined/ folder. */
3
+ export function materialFiles(dir: any): {};
4
+ /** The module's source: `export default` of all the files (materialFiles). */
5
+ export function materialModule(): string;
6
+ /** The SSR module's source: the same as materialModule. With `ssr` false: an empty object. */
3
7
  export function materialSsrModule(ssr: any): string;
8
+ /** The virtual module's id as imported (src/materialIcons.ts). */
9
+ export const MATERIAL_ALL: "virtual:material-symbols";
4
10
  /** The virtual module's id as imported (packages/editor/src/icon.tsx). */
5
11
  export const MATERIAL_SSR: "virtual:material-symbols-ssr";
@@ -1,19 +1,35 @@
1
- // The Material Symbols (Outlined) files as the module `virtual:material-symbols-ssr`, answered by themePlugin (theme.mjs):
2
- // all of them as text on the server render (so a backup icon, icon.tsx, is in the built page's HTML at once), nothing in the browser
3
- // (where src/materialIcons.ts is the one lazy chunk). Exposes MATERIAL_SSR and materialSsrModule(). Reads @material-symbols/svg-400.
1
+ // The Material Symbols (Outlined) files as two modules answered by themePlugin (theme.mjs): `virtual:material-symbols` (all of them
2
+ // as text, for the one lazy chunk of src/materialIcons.ts) and `virtual:material-symbols-ssr` (all of them on the server render, so
3
+ // a backup icon, icon.tsx, is in the built page's HTML at once; `{}` in the browser). Files are read one at a time, never globbed
4
+ // (7,874 open files at once stops Cloudflare's build with EMFILE). Exposes MATERIAL_ALL, MATERIAL_SSR, materialFiles, materialModule,
5
+ // materialSsrModule. Reads @material-symbols/svg-400.
4
6
  import { readdirSync, readFileSync } from 'node:fs';
5
7
  import { pathToFileURL } from 'node:url';
6
8
 
9
+ /** The virtual module's id as imported (src/materialIcons.ts). */
10
+ export const MATERIAL_ALL = 'virtual:material-symbols';
7
11
  /** The virtual module's id as imported (packages/editor/src/icon.tsx). */
8
12
  export const MATERIAL_SSR = 'virtual:material-symbols-ssr';
9
13
 
10
- /** The module's source: `{ '/node_modules/@material-symbols/svg-400/outlined/<name>.svg': '<svg …>' }`, the keys as
11
- * import.meta.glob names them (src/materialIcons.ts). With `ssr` false: an empty object. */
12
- export function materialSsrModule(ssr) {
13
- if (!ssr) return 'export default {};';
14
- const dir = new URL('node_modules/@material-symbols/svg-400/outlined/', pathToFileURL(`${process.cwd()}/`));
14
+ const PREFIX = '/node_modules/@material-symbols/svg-400/outlined/';
15
+
16
+ /** All Outlined files as text, read one at a time: `{ '/node_modules/@material-symbols/svg-400/outlined/<name>.svg': '<svg …>' }`,
17
+ * the keys as import.meta.glob named them. `dir` is the URL or the path of the outlined/ folder. */
18
+ export function materialFiles(dir) {
19
+ const base = dir instanceof URL ? dir : pathToFileURL(`${String(dir).replace(/[\/]+$/, '')}/`);
15
20
  const files = {};
16
- for (const name of readdirSync(dir))
17
- if (name.endsWith('.svg')) files[`/node_modules/@material-symbols/svg-400/outlined/${name}`] = readFileSync(new URL(name, dir), 'utf8');
18
- return `export default ${JSON.stringify(files)};`;
21
+ for (const name of readdirSync(base).sort()) if (name.endsWith('.svg')) files[PREFIX + name] = readFileSync(new URL(name, base), 'utf8');
22
+ return files;
23
+ }
24
+
25
+ const outlined = () => new URL(`node_modules/@material-symbols/svg-400/outlined/`, pathToFileURL(`${process.cwd()}/`));
26
+
27
+ /** The module's source: `export default` of all the files (materialFiles). */
28
+ export function materialModule() {
29
+ return `export default ${JSON.stringify(materialFiles(outlined()))};`;
30
+ }
31
+
32
+ /** The SSR module's source: the same as materialModule. With `ssr` false: an empty object. */
33
+ export function materialSsrModule(ssr) {
34
+ return ssr ? materialModule() : 'export default {};';
19
35
  }
@@ -0,0 +1,44 @@
1
+ // Tests of materialFiles, materialModule and materialSsrModule (site/materialSymbols.mjs): the Material Symbols files as one module.
2
+ // Run with `node --test site/materialSymbols.test.mjs`.
3
+ import { strict as assert } from 'node:assert';
4
+ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { test } from 'node:test';
8
+ import { materialFiles, materialModule, materialSsrModule } from './materialSymbols.mjs';
9
+
10
+ const PREFIX = '/node_modules/@material-symbols/svg-400/outlined/';
11
+ const A = '<svg viewBox="0 0 1 1"><path d="A"/></svg>';
12
+ const B = '<svg viewBox="0 0 2 2"><path d="B"/></svg>';
13
+
14
+ const folder = mkdtempSync(join(tmpdir(), 'material-'));
15
+ const dir = join(folder, 'node_modules/@material-symbols/svg-400/outlined');
16
+ mkdirSync(dir, { recursive: true });
17
+ writeFileSync(join(dir, 'b.svg'), B);
18
+ writeFileSync(join(dir, 'a.svg'), A);
19
+ writeFileSync(join(dir, 'c.txt'), 'not an icon');
20
+
21
+ test('one key per svg file, sorted, with the file text', () => {
22
+ assert.deepEqual(materialFiles(dir), { [`${PREFIX}a.svg`]: A, [`${PREFIX}b.svg`]: B });
23
+ assert.deepEqual(Object.keys(materialFiles(dir)), [`${PREFIX}a.svg`, `${PREFIX}b.svg`]);
24
+ });
25
+
26
+ test('a file that is not an svg has no key', () => {
27
+ assert.ok(!Object.keys(materialFiles(dir)).some((k) => k.endsWith('c.txt')));
28
+ });
29
+
30
+ test('the module source parses to the same object as materialFiles', () => {
31
+ const cwd = process.cwd();
32
+ process.chdir(folder);
33
+ try {
34
+ const code = materialModule();
35
+ assert.ok(code.startsWith('export default '));
36
+ assert.deepEqual(JSON.parse(code.slice('export default '.length, -1)), materialFiles(dir));
37
+ } finally {
38
+ process.chdir(cwd);
39
+ }
40
+ });
41
+
42
+ test('without ssr the SSR module is an empty object', () => {
43
+ assert.equal(materialSsrModule(false), 'export default {};');
44
+ });
package/site/theme.mjs CHANGED
@@ -11,7 +11,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
11
11
  import { svgParts } from '@morphemecms/cms/lib/svg.mjs';
12
12
  import { declarationProblems, ownerId } from '@morphemecms/cms/lib/licenses.mjs';
13
13
  import { installedPacks, isIconName, packFolder, packProblems, phosphorFile, svgProblems, usedIcons } from './icons.mjs';
14
- import { MATERIAL_SSR, materialSsrModule } from './materialSymbols.mjs';
14
+ import { MATERIAL_ALL, MATERIAL_SSR, materialModule, materialSsrModule } from './materialSymbols.mjs';
15
15
  import { ICON_PACK_FILE, ICON_PACK_LICENSE, SYSTEM_PACKS, THEME_PACKS_FOLDER, packInUse, packTheme, themePackId } from './iconPacks.mjs';
16
16
  import { MOTION_SCHEMES } from '@morphemecms/cms/lib/motion-schemes.mjs';
17
17
  import { storeImportProblems } from './storeTheme.mjs';
@@ -227,6 +227,7 @@ const PHOSPHOR_REACT = '@phosphor-icons/react';
227
227
  const PHOSPHOR_FILE = /@phosphor-icons\/core\/assets\/(?:regular|fill)\/([a-z0-9-]+)\.svg\?raw$/;
228
228
  const REACT_ICONS = '\0site-theme-icons';
229
229
  const FILE_ICON = '\0site-theme-icon:';
230
+ const MATERIAL_ALL_ID = `\0${MATERIAL_ALL}`;
230
231
  const MATERIAL_SSR_ID = `\0${MATERIAL_SSR}`;
231
232
 
232
233
  /** The React component's name of a Phosphor icon by its file's name (`caret-right` → CaretRight), when Phosphor has it. */
@@ -471,6 +472,7 @@ export function themePlugin() {
471
472
  // The theme's icons import Phosphor's own.
472
473
  if (source === PHOSPHOR_REACT) return own && importer !== REACT_ICONS ? REACT_ICONS : null;
473
474
  if (source === ALL_ICONS) return '\0' + ALL_ICONS;
475
+ if (source === MATERIAL_ALL) return MATERIAL_ALL_ID;
474
476
  if (source === MATERIAL_SSR) return MATERIAL_SSR_ID;
475
477
  const icon = source.match(PHOSPHOR_FILE)?.[1];
476
478
  if (icon) return svgParts(icons[icon]) ? FILE_ICON + icon : null;
@@ -485,6 +487,7 @@ export function themePlugin() {
485
487
  return null;
486
488
  },
487
489
  load(id, options) {
490
+ if (id === MATERIAL_ALL_ID) return materialModule();
488
491
  if (id === MATERIAL_SSR_ID) return materialSsrModule(options?.ssr);
489
492
  if (id === REACT_ICONS) return reactIcons(icons);
490
493
  if (id === '\0' + ALL_ICONS)
@@ -1,8 +1,6 @@
1
1
  // All of Material Symbols (Outlined, regular and filled) as text, by their file, for the icons icon.tsx draws that are not on
2
- // its list: one lazy chunk, loaded in the browser the first time one is drawn (the server render has them from
3
- // virtual:material-symbols-ssr instead, packages/editor/site/materialSymbols.mjs). Imported by icon.tsx only dynamically.
4
- export const materialIcons = import.meta.glob<string>('/node_modules/@material-symbols/svg-400/outlined/*.svg', {
5
- query: '?raw',
6
- import: 'default',
7
- eager: true,
8
- });
2
+ // its list: one lazy chunk, loaded in the browser the first time one is drawn. The set comes from virtual:material-symbols
3
+ // (packages/editor/site/materialSymbols.mjs), one module read file by file. Imported by icon.tsx only dynamically.
4
+ import all from 'virtual:material-symbols';
5
+
6
+ export const materialIcons: Record<string, string> = all;
package/src/virtual.d.ts CHANGED
@@ -1,3 +1,8 @@
1
+ declare module 'virtual:material-symbols' {
2
+ const files: Record<string, string>;
3
+ export default files;
4
+ }
5
+
1
6
  declare module 'virtual:material-symbols-ssr' {
2
7
  const files: Record<string, string>;
3
8
  export default files;
@@ -32,7 +32,7 @@
32
32
  "@material-symbols/svg-400": "0.48.2",
33
33
  "@material/material-color-utilities": "^0.3.0",
34
34
  "@milkdown/kit": "^7.22.3",
35
- "@morphemecms/cms": "0.7.0",
35
+ "@morphemecms/cms": "0.7.1",
36
36
  "@phosphor-icons/core": "^2.1.1",
37
37
  "@phosphor-icons/react": "^2.1.10",
38
38
  "@tailwindcss/typography": "^0.5.20",