@zubra/server 5.2.0 → 5.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zubra/server",
3
- "version": "5.2.0",
3
+ "version": "5.4.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.4.0",
31
+ "@zubra/runtime": "^5.4.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.d.ts CHANGED
@@ -61,6 +61,11 @@ export interface ZubraOptions {
61
61
  cache?: boolean;
62
62
  /** Serve the runtime's production build, without hot reloading (default: `NODE_ENV` is `production`). */
63
63
  production?: boolean;
64
+ /**
65
+ * Compile for the Zubra DevTools extension and serve the runtime build it reads (default: on,
66
+ * except in production; `ZUBRA_DEVTOOLS=1` turns it on there too).
67
+ */
68
+ devtools?: boolean;
64
69
  }
65
70
 
66
71
  export declare function createZubra(options?: ZubraOptions): Zubra;
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,16 +37,39 @@ 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`). `devtools`: compile for the Zubra DevTools
41
+ * extension and serve the runtime build it reads (on by default except in production;
42
+ * `ZUBRA_DEVTOOLS=1` turns it on there too, for a demo or a staging site). `imports`: more import map entries,
43
+ * for packages the components import beyond `@zubra/runtime` and `@zubra/sync` (both of which
44
+ * are served and mapped by themselves).
40
45
  */
41
- export function createZubra({ components = 'components', prefix = '/_zubra', cache = true, production = process.env.NODE_ENV === 'production' } = {}) {
46
+ export function createZubra({ components = 'components', prefix = '/_zubra', cache = true, production = process.env.NODE_ENV === 'production', devtools = process.env.ZUBRA_DEVTOOLS === '1' || !production, imports = {} } = {}) {
42
47
  const root = resolve(components);
43
48
  if (!existsSync(root)) throw new Error(`@zubra/server: no components folder at ${root}`);
44
49
  const sourceDir = dirname(fileURLToPath(import.meta.resolve('@zubra/runtime')));
45
50
  const productionDir = join(sourceDir, '..', 'dist', 'production');
46
- const runtimeDir = production && existsSync(productionDir) ? productionDir : sourceDir;
51
+ // The Zubra DevTools need the runtime's development build, whose hook the production one leaves out.
52
+ const runtimeDir = production && !devtools && existsSync(productionDir) ? productionDir : sourceDir;
47
53
  const base = prefix.replace(/\/$/, '');
48
54
 
55
+ // @zubra/sync, for components that start with 'use collection': found from the components
56
+ // folder (the app's own node_modules), served under <prefix>/sync/ and put in the import map,
57
+ // so a page needs no setup for it. Not installed: such a component says so (`needs()`).
58
+ const syncDir = (() => {
59
+ for (const from of [join(root, 'noop.js'), join(process.cwd(), 'noop.js')]) {
60
+ try {
61
+ return dirname(createRequire(pathToFileURL(from)).resolve('@zubra/sync'));
62
+ } catch {}
63
+ }
64
+ return null;
65
+ })();
66
+ const needs = (name, code) => {
67
+ if (!syncDir && code.includes('from "@zubra/sync"')) {
68
+ throw new Error(`@zubra/server: ${relative(process.cwd(), pathOf(name))} starts with 'use collection', which needs @zubra/sync. Install it: npm install @zubra/sync`);
69
+ }
70
+ return code;
71
+ };
72
+
49
73
  // name (the path without its extension, "/"-separated) → file
50
74
  const files = new Map();
51
75
  (function walk(dir) {
@@ -86,13 +110,17 @@ export function createZubra({ components = 'components', prefix = '/_zubra', cac
86
110
  call(pathOf(name), { emit: 'html', props, slots, slotSource: 'html', partial: true, ...(pending.length ? { pending } : {}) });
87
111
  const css = (name) => once(`css:${name}`, () => call(pathOf(name), { emit: 'css' }));
88
112
  const js = (name) => once(`js:${name}`, () => {
89
- const response = need().compileFile(pathOf(name), { emit: 'js', runtime: '@zubra/runtime' });
113
+ const response = need().compileFile(pathOf(name), { emit: 'js', runtime: '@zubra/runtime', devtools });
90
114
  if (!response.ok) throw new Error(response.error);
91
- return response.output;
115
+ return needs(name, response.output);
92
116
  });
93
117
 
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>`;
118
+ 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>`;
119
+ const syncImports = syncDir ? { '@zubra/sync': `${base}/sync/index.js`, '@zubra/sync/connect': `${base}/sync/connect.js` } : {};
95
120
  const head = (names) => {
121
+ // A component that needs a package that isn't installed fails here, while the page is built,
122
+ // with the reason, rather than later in the browser as an unresolved import.
123
+ for (const name of names) js(name);
96
124
  const styles = names.map(css).join('\n');
97
125
  return `${styles ? `<style data-zb-components>${styles}</style>\n` : ''}${importMap()}`;
98
126
  };
@@ -107,6 +135,11 @@ export function createZubra({ components = 'components', prefix = '/_zubra', cac
107
135
  if (!files.has(name)) return { status: 404, type: 'text/plain', body: 'Not found' };
108
136
  return { status: 200, type: TYPES['.js'], body: js(name) };
109
137
  }
138
+ if (rest.startsWith('sync/') && syncDir) {
139
+ const file = resolve(syncDir, rest.slice('sync/'.length));
140
+ if (!file.startsWith(syncDir + sep) || extname(file) !== '.js' || !existsSync(file)) return { status: 404, type: 'text/plain', body: 'Not found' };
141
+ return { status: 200, type: TYPES['.js'], body: readFileSync(file) };
142
+ }
110
143
  if (rest.startsWith('runtime/')) {
111
144
  const file = resolve(runtimeDir, rest.slice('runtime/'.length));
112
145
  if (!file.startsWith(runtimeDir + sep) || !existsSync(file) || statSync(file).isDirectory()) return { status: 404, type: 'text/plain', body: 'Not found' };
@@ -296,6 +329,7 @@ export function createZubra({ components = 'components', prefix = '/_zubra', cac
296
329
  writeFileSync(file, js(name));
297
330
  }
298
331
  cpSync(runtimeDir, join(out, 'runtime'), { recursive: true });
332
+ if (syncDir) cpSync(syncDir, join(out, 'sync'), { recursive: true, filter: (from) => !from.endsWith('.d.ts') });
299
333
  }
300
334
 
301
335
  /**