@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 +11 -4
- package/src/index.d.ts +5 -0
- package/src/index.js +41 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zubra/server",
|
|
3
|
-
"version": "5.
|
|
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.
|
|
31
|
-
"@zubra/runtime": "^5.
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
|
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
|
/**
|