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