@zubra/server 5.2.0 → 5.3.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.
Files changed (2) hide show
  1. package/package.json +11 -4
  2. package/src/index.js +36 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zubra/server",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "Zubra on the server: render components into any template engine's pages, serve their browser modules, and give frameworks' HTML its components' shadow roots.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -27,16 +27,23 @@
27
27
  "test": "node --test \"test/*.test.js\""
28
28
  },
29
29
  "peerDependencies": {
30
- "@zubra/compiler": "^5.2.0",
31
- "@zubra/runtime": "^5.2.0"
30
+ "@zubra/compiler": "^5.3.0",
31
+ "@zubra/runtime": "^5.3.0"
32
32
  },
33
33
  "devDependencies": {
34
+ "@minify-html/node": "0.18.1",
35
+ "@swc/html": "1.16.22",
34
36
  "@zubra/compiler": "file:../compiler",
35
37
  "@zubra/runtime": "file:../runtime",
38
+ "dompurify": "3.4.17",
36
39
  "express": "5.3.0",
37
40
  "fastify": "5.12.8",
38
41
  "hono": "4.13.17",
39
- "playwright": "1.63.0"
42
+ "html-minifier-terser": "7.2.0",
43
+ "htmlnano": "3.6.1",
44
+ "jsdom": "31.0.0",
45
+ "playwright": "1.63.0",
46
+ "sanitize-html": "2.18.1"
40
47
  },
41
48
  "files": [
42
49
  "src"
package/src/index.js CHANGED
@@ -16,7 +16,8 @@
16
16
  // form-associated ones a hidden input, so the page is complete before JavaScript.
17
17
  import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
18
18
  import { dirname, extname, join, relative, resolve, sep } from 'node:path';
19
- import { fileURLToPath } from 'node:url';
19
+ import { fileURLToPath, pathToFileURL } from 'node:url';
20
+ import { createRequire } from 'node:module';
20
21
  import { StringDecoder } from 'node:string_decoder';
21
22
  import * as nodeStream from 'node:stream';
22
23
  const { Transform } = nodeStream;
@@ -36,9 +37,11 @@ const TYPES = { '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; char
36
37
  * `components`: the folder of `.zb` (and `.zb.jsx`/`.zb.tsx`) files. `prefix`: the URL path
37
38
  * the browser modules are served under. `cache`: keep compiled output (turn it off in
38
39
  * development to pick up edits). `production`: serve the runtime's production build, without hot
39
- * reloading (by default when `NODE_ENV` is `production`).
40
+ * reloading (by default when `NODE_ENV` is `production`). `imports`: more import map entries,
41
+ * for packages the components import beyond `@zubra/runtime` and `@zubra/sync` (both of which
42
+ * are served and mapped by themselves).
40
43
  */
41
- export function createZubra({ components = 'components', prefix = '/_zubra', cache = true, production = process.env.NODE_ENV === 'production' } = {}) {
44
+ export function createZubra({ components = 'components', prefix = '/_zubra', cache = true, production = process.env.NODE_ENV === 'production', imports = {} } = {}) {
42
45
  const root = resolve(components);
43
46
  if (!existsSync(root)) throw new Error(`@zubra/server: no components folder at ${root}`);
44
47
  const sourceDir = dirname(fileURLToPath(import.meta.resolve('@zubra/runtime')));
@@ -46,6 +49,24 @@ export function createZubra({ components = 'components', prefix = '/_zubra', cac
46
49
  const runtimeDir = production && existsSync(productionDir) ? productionDir : sourceDir;
47
50
  const base = prefix.replace(/\/$/, '');
48
51
 
52
+ // @zubra/sync, for components that start with 'use collection': found from the components
53
+ // folder (the app's own node_modules), served under <prefix>/sync/ and put in the import map,
54
+ // so a page needs no setup for it. Not installed: such a component says so (`needs()`).
55
+ const syncDir = (() => {
56
+ for (const from of [join(root, 'noop.js'), join(process.cwd(), 'noop.js')]) {
57
+ try {
58
+ return dirname(createRequire(pathToFileURL(from)).resolve('@zubra/sync'));
59
+ } catch {}
60
+ }
61
+ return null;
62
+ })();
63
+ const needs = (name, code) => {
64
+ if (!syncDir && code.includes('from "@zubra/sync"')) {
65
+ throw new Error(`@zubra/server: ${relative(process.cwd(), pathOf(name))} starts with 'use collection', which needs @zubra/sync. Install it: npm install @zubra/sync`);
66
+ }
67
+ return code;
68
+ };
69
+
49
70
  // name (the path without its extension, "/"-separated) → file
50
71
  const files = new Map();
51
72
  (function walk(dir) {
@@ -88,11 +109,15 @@ export function createZubra({ components = 'components', prefix = '/_zubra', cac
88
109
  const js = (name) => once(`js:${name}`, () => {
89
110
  const response = need().compileFile(pathOf(name), { emit: 'js', runtime: '@zubra/runtime' });
90
111
  if (!response.ok) throw new Error(response.error);
91
- return response.output;
112
+ return needs(name, response.output);
92
113
  });
93
114
 
94
- const importMap = () => `<script type="importmap">${JSON.stringify({ imports: { '@zubra/runtime': `${base}/runtime/index.js`, '@zubra/runtime/spread': `${base}/runtime/spread.js`, '@zubra/runtime/primitives': `${base}/runtime/primitives.js` } })}</script>`;
115
+ const importMap = () => `<script type="importmap">${JSON.stringify({ imports: { '@zubra/runtime': `${base}/runtime/index.js`, '@zubra/runtime/spread': `${base}/runtime/spread.js`, '@zubra/runtime/primitives': `${base}/runtime/primitives.js`, ...syncImports, ...imports } })}</script>`;
116
+ const syncImports = syncDir ? { '@zubra/sync': `${base}/sync/index.js`, '@zubra/sync/connect': `${base}/sync/connect.js` } : {};
95
117
  const head = (names) => {
118
+ // A component that needs a package that isn't installed fails here, while the page is built,
119
+ // with the reason, rather than later in the browser as an unresolved import.
120
+ for (const name of names) js(name);
96
121
  const styles = names.map(css).join('\n');
97
122
  return `${styles ? `<style data-zb-components>${styles}</style>\n` : ''}${importMap()}`;
98
123
  };
@@ -107,6 +132,11 @@ export function createZubra({ components = 'components', prefix = '/_zubra', cac
107
132
  if (!files.has(name)) return { status: 404, type: 'text/plain', body: 'Not found' };
108
133
  return { status: 200, type: TYPES['.js'], body: js(name) };
109
134
  }
135
+ if (rest.startsWith('sync/') && syncDir) {
136
+ const file = resolve(syncDir, rest.slice('sync/'.length));
137
+ if (!file.startsWith(syncDir + sep) || extname(file) !== '.js' || !existsSync(file)) return { status: 404, type: 'text/plain', body: 'Not found' };
138
+ return { status: 200, type: TYPES['.js'], body: readFileSync(file) };
139
+ }
110
140
  if (rest.startsWith('runtime/')) {
111
141
  const file = resolve(runtimeDir, rest.slice('runtime/'.length));
112
142
  if (!file.startsWith(runtimeDir + sep) || !existsSync(file) || statSync(file).isDirectory()) return { status: 404, type: 'text/plain', body: 'Not found' };
@@ -296,6 +326,7 @@ export function createZubra({ components = 'components', prefix = '/_zubra', cac
296
326
  writeFileSync(file, js(name));
297
327
  }
298
328
  cpSync(runtimeDir, join(out, 'runtime'), { recursive: true });
329
+ if (syncDir) cpSync(syncDir, join(out, 'sync'), { recursive: true, filter: (from) => !from.endsWith('.d.ts') });
299
330
  }
300
331
 
301
332
  /**