@vmz/core 0.1.16 → 0.1.17
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/dist/dom-core.d.ts +7 -0
- package/dist/dom-core.js +24 -3
- package/dist/dom-ssr.js +3 -2
- package/dist/serve-host.mjs +33 -11
- package/dist/server.js +82 -5
- package/package.json +7 -7
package/dist/dom-core.d.ts
CHANGED
|
@@ -172,6 +172,13 @@ export declare const directApi: {
|
|
|
172
172
|
*/
|
|
173
173
|
eachBlock(inst: any, bindingId: any, deps: any, spec: any, regionId?: any): DocumentFragment;
|
|
174
174
|
};
|
|
175
|
+
/**
|
|
176
|
+
* Vue-familiar `@click` / legacy `onClick` → canonical `onXxx` prop name.
|
|
177
|
+
* Function props that are not events are treated as getters (`v.call(host)`).
|
|
178
|
+
* @param {unknown} name
|
|
179
|
+
* @returns {string | null}
|
|
180
|
+
*/
|
|
181
|
+
export declare function eventPropHandlerName(name: any): string;
|
|
175
182
|
export declare function isEventPropName(name: any): boolean;
|
|
176
183
|
/** HTML boolean attributes: presence means true; `false`/`null` must remove the attr. */
|
|
177
184
|
export declare const BOOLEAN_HTML_ATTRS: Set<string>;
|
package/dist/dom-core.js
CHANGED
|
@@ -428,8 +428,9 @@ export const directApi = {
|
|
|
428
428
|
/** @type {Record<string, any>} */
|
|
429
429
|
const resolved = {};
|
|
430
430
|
for (const [k, v] of Object.entries(props || {})) {
|
|
431
|
-
|
|
432
|
-
|
|
431
|
+
const onKey = typeof v === 'function' ? eventPropHandlerName(k) : null;
|
|
432
|
+
if (onKey)
|
|
433
|
+
resolved[onKey] = v;
|
|
433
434
|
else if (typeof v === 'function')
|
|
434
435
|
resolved[k] = v.call(hostInst);
|
|
435
436
|
else
|
|
@@ -2616,8 +2617,28 @@ function wireDirectBind(inst, bindingId, deps, get, write, cf) {
|
|
|
2616
2617
|
apply();
|
|
2617
2618
|
trackDirectBind(inst, liveDeps, apply, bindingId);
|
|
2618
2619
|
}
|
|
2620
|
+
/**
|
|
2621
|
+
* Vue-familiar `@click` / legacy `onClick` → canonical `onXxx` prop name.
|
|
2622
|
+
* Function props that are not events are treated as getters (`v.call(host)`).
|
|
2623
|
+
* @param {unknown} name
|
|
2624
|
+
* @returns {string | null}
|
|
2625
|
+
*/
|
|
2626
|
+
export function eventPropHandlerName(name) {
|
|
2627
|
+
if (typeof name !== 'string' || !name)
|
|
2628
|
+
return null;
|
|
2629
|
+
if (/^on[A-Z]/.test(name))
|
|
2630
|
+
return name;
|
|
2631
|
+
// `@input` → `onInput`; reject `@update:modelValue` until that surface exists.
|
|
2632
|
+
if (name.charAt(0) === '@' && name.length > 1 && !name.includes(':')) {
|
|
2633
|
+
const ev = name.slice(1);
|
|
2634
|
+
if (!ev || !/^[A-Za-z]/.test(ev))
|
|
2635
|
+
return null;
|
|
2636
|
+
return `on${ev.charAt(0).toUpperCase()}${ev.slice(1)}`;
|
|
2637
|
+
}
|
|
2638
|
+
return null;
|
|
2639
|
+
}
|
|
2619
2640
|
export function isEventPropName(name) {
|
|
2620
|
-
return
|
|
2641
|
+
return eventPropHandlerName(name) != null;
|
|
2621
2642
|
}
|
|
2622
2643
|
/** Monotonic id for `bindComponentProp` BindingIds (per process). */
|
|
2623
2644
|
let directPropBindSeq = 0;
|
package/dist/dom-ssr.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* VMZ DOM SSR / hydrate / resume — precise patches, no VDOM diff.
|
|
4
4
|
* Imports client DOM primitives from ./dom-core.js for tree-shakeable browser entry.
|
|
5
5
|
*/
|
|
6
|
-
import { applyDomAttr, applyPreservedState, BOOLEAN_HTML_ATTRS, createInstance, destroy, directApi, getRegisteredComponent, hasMeaningfulChild, isEventEntryStrategy,
|
|
6
|
+
import { applyDomAttr, applyPreservedState, BOOLEAN_HTML_ATTRS, createInstance, destroy, directApi, eventPropHandlerName, getRegisteredComponent, hasMeaningfulChild, isEventEntryStrategy, mount, noteDomCreate, resolveComponent, runDirectCreate, scheduleClientOn, settlePendingChildMounts, snapshotInstanceState, stripFns, } from './dom-core.js';
|
|
7
7
|
/** @type {Error | null} last linkedom resolve failure (for clear SSR errors) */
|
|
8
8
|
let _ssrDocumentLastError = null;
|
|
9
9
|
/**
|
|
@@ -755,7 +755,8 @@ const serializeApi = {
|
|
|
755
755
|
/** @type {Record<string, any>} */
|
|
756
756
|
const resolved = {};
|
|
757
757
|
for (const [k, v] of Object.entries(props || {})) {
|
|
758
|
-
|
|
758
|
+
const onKey = typeof v === 'function' ? eventPropHandlerName(k) : null;
|
|
759
|
+
if (onKey)
|
|
759
760
|
continue;
|
|
760
761
|
else if (typeof v === 'function')
|
|
761
762
|
resolved[k] = v.call(hostInst);
|
package/dist/serve-host.mjs
CHANGED
|
@@ -211,6 +211,22 @@ async function renderPage(pathname, opts = {}) {
|
|
|
211
211
|
* @returns {Promise<{ status: number, stream?: AsyncGenerator<string, void, void>, redirect?: string, headers?: Record<string, string> } | null>}
|
|
212
212
|
*/
|
|
213
213
|
async function renderPageStream(pathname, opts = {}) {
|
|
214
|
+
try {
|
|
215
|
+
return await renderPageStreamInner(pathname, opts);
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
const normalized = normalizeDevError(err);
|
|
219
|
+
lastDevError = normalized;
|
|
220
|
+
console.error('vmz serve: renderPageStream failed', normalized.message);
|
|
221
|
+
return { status: 500, stream: emitDevErrorHtml(normalized) };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* @param {string} pathname
|
|
226
|
+
* @param {{ signal?: AbortSignal, searchParams?: URLSearchParams, cookieHeader?: string, method?: string, body?: unknown }} [opts]
|
|
227
|
+
* @returns {Promise<{ status: number, stream?: AsyncGenerator<string, void, void>, redirect?: string, headers?: Record<string, string> } | null>}
|
|
228
|
+
*/
|
|
229
|
+
async function renderPageStreamInner(pathname, opts = {}) {
|
|
214
230
|
if (isDev && lastDevError && pageCtors.size === 0) {
|
|
215
231
|
return { status: 500, stream: emitDevErrorHtml(lastDevError) };
|
|
216
232
|
}
|
|
@@ -945,7 +961,7 @@ async function* emitDevErrorHtml(err) {
|
|
|
945
961
|
.hint{opacity:.65;font-size:12px}
|
|
946
962
|
</style>`;
|
|
947
963
|
const body = `${style}
|
|
948
|
-
<main>
|
|
964
|
+
<main data-vmz-error="500">
|
|
949
965
|
<h1>Dev Error</h1>
|
|
950
966
|
<pre>${msg}</pre>
|
|
951
967
|
${stack ? `<pre style="opacity:.7;font-size:12px">${stack}</pre>` : ''}
|
|
@@ -961,17 +977,23 @@ async function* emitDevErrorHtml(err) {
|
|
|
961
977
|
};
|
|
962
978
|
})();
|
|
963
979
|
</script>`;
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
980
|
+
try {
|
|
981
|
+
const native = loadNativeAddon();
|
|
982
|
+
if (typeof native.generateHtmlShell === 'function') {
|
|
983
|
+
yield native.generateHtmlShell({
|
|
984
|
+
title: 'Dev Error',
|
|
985
|
+
lang: 'en',
|
|
986
|
+
cssHrefs: [],
|
|
987
|
+
bodyHtml: body,
|
|
988
|
+
bodyAttrs: [],
|
|
989
|
+
});
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
967
992
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
bodyHtml: body,
|
|
973
|
-
bodyAttrs: [],
|
|
974
|
-
});
|
|
993
|
+
catch {
|
|
994
|
+
/* fall through to plain HTML — never throw from the error page itself */
|
|
995
|
+
}
|
|
996
|
+
yield `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8" /><title>Dev Error</title></head><body>${body}</body></html>`;
|
|
975
997
|
}
|
|
976
998
|
/** @param {string} s */
|
|
977
999
|
function escapeHtml(s) {
|
package/dist/server.js
CHANGED
|
@@ -328,7 +328,12 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
330
|
if (!res.headersSent) {
|
|
331
|
-
|
|
331
|
+
if (wantsBrowserHtml(req)) {
|
|
332
|
+
sendHtml(res, 404, browserErrorHtml(404, 'Not Found', `No page matched ${url.pathname}`));
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
sendJson(res, 404, { error: 'not found', path: url.pathname });
|
|
336
|
+
}
|
|
332
337
|
}
|
|
333
338
|
}
|
|
334
339
|
catch (err) {
|
|
@@ -342,9 +347,14 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
342
347
|
}
|
|
343
348
|
return;
|
|
344
349
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
350
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
351
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
352
|
+
if (wantsBrowserHtml(req)) {
|
|
353
|
+
sendHtml(res, 500, browserErrorHtml(500, 'Dev Error', message, stack));
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
sendJson(res, 500, { error: message });
|
|
357
|
+
}
|
|
348
358
|
}
|
|
349
359
|
}
|
|
350
360
|
/**
|
|
@@ -585,7 +595,7 @@ function parseMultipartBuffer(buf, contentType) {
|
|
|
585
595
|
const splitAt = indexOfBuffer(part, Buffer.from('\r\n\r\n'), 0);
|
|
586
596
|
if (splitAt >= 0) {
|
|
587
597
|
const headerText = part.subarray(0, splitAt).toString('utf8');
|
|
588
|
-
|
|
598
|
+
const body = part.subarray(splitAt + 4);
|
|
589
599
|
const nameM = /content-disposition:[^\r\n]*;\s*name="([^"]*)"/i.exec(headerText);
|
|
590
600
|
const fileM = /content-disposition:[^\r\n]*;\s*filename="([^"]*)"/i.exec(headerText);
|
|
591
601
|
const typeM = /content-type:\s*([^\r\n]+)/i.exec(headerText);
|
|
@@ -657,6 +667,73 @@ function parseFormBody(raw, contentType) {
|
|
|
657
667
|
return { raw };
|
|
658
668
|
}
|
|
659
669
|
}
|
|
670
|
+
/**
|
|
671
|
+
* @param {import('node:http').IncomingMessage} req
|
|
672
|
+
* @returns {boolean}
|
|
673
|
+
*/
|
|
674
|
+
function wantsBrowserHtml(req) {
|
|
675
|
+
const accept = String(req.headers?.accept || '');
|
|
676
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
677
|
+
if (method !== 'GET' && method !== 'HEAD')
|
|
678
|
+
return false;
|
|
679
|
+
// Document navigations: browsers send text/html and/or Sec-Fetch-Dest: document.
|
|
680
|
+
// Bare `*/*` / empty Accept (Node fetch, many API clients) must stay JSON.
|
|
681
|
+
const dest = String(req.headers?.['sec-fetch-dest'] || '');
|
|
682
|
+
const mode = String(req.headers?.['sec-fetch-mode'] || '');
|
|
683
|
+
if (dest === 'document' || mode === 'navigate')
|
|
684
|
+
return true;
|
|
685
|
+
if (!accept || accept === '*/*')
|
|
686
|
+
return false;
|
|
687
|
+
const html = accept.includes('text/html');
|
|
688
|
+
const json = accept.includes('application/json');
|
|
689
|
+
if (html && !json)
|
|
690
|
+
return true;
|
|
691
|
+
if (html && json) {
|
|
692
|
+
const hi = accept.indexOf('text/html');
|
|
693
|
+
const ji = accept.indexOf('application/json');
|
|
694
|
+
return hi <= ji;
|
|
695
|
+
}
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Browser-facing error document (no N-API). Used when SSR returns null or throws
|
|
700
|
+
* on a document navigation — must not fall back to API-shaped JSON.
|
|
701
|
+
* @param {number} status
|
|
702
|
+
* @param {string} title
|
|
703
|
+
* @param {string} message
|
|
704
|
+
* @param {string} [stack]
|
|
705
|
+
*/
|
|
706
|
+
function browserErrorHtml(status, title, message, stack) {
|
|
707
|
+
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
708
|
+
const stackBlock = stack ? `<pre class="stack">${esc(stack)}</pre>` : '';
|
|
709
|
+
return `<!DOCTYPE html>
|
|
710
|
+
<html lang="en">
|
|
711
|
+
<head>
|
|
712
|
+
<meta charset="utf-8" />
|
|
713
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
714
|
+
<title>${esc(title)}</title>
|
|
715
|
+
<style>
|
|
716
|
+
body{margin:0;background:#0f1115;color:#f4f4f5;font:14px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
717
|
+
main{max-width:56rem;margin:0 auto;padding:2rem 1.25rem}
|
|
718
|
+
h1{margin:0 0 .75rem;color:#f87171;font-size:1.1rem}
|
|
719
|
+
.code{opacity:.65;font-size:12px;margin-bottom:.75rem}
|
|
720
|
+
pre{white-space:pre-wrap;margin:0 0 1rem}
|
|
721
|
+
.stack{opacity:.7;font-size:12px}
|
|
722
|
+
.hint{opacity:.65;font-size:12px}
|
|
723
|
+
</style>
|
|
724
|
+
</head>
|
|
725
|
+
<body data-vmz-error="${status}">
|
|
726
|
+
<main>
|
|
727
|
+
<p class="code">HTTP ${status}</p>
|
|
728
|
+
<h1>${esc(title)}</h1>
|
|
729
|
+
<pre>${esc(message)}</pre>
|
|
730
|
+
${stackBlock}
|
|
731
|
+
<p class="hint">Document navigation returns HTML errors — not API JSON.</p>
|
|
732
|
+
</main>
|
|
733
|
+
</body>
|
|
734
|
+
</html>
|
|
735
|
+
`;
|
|
736
|
+
}
|
|
660
737
|
/**
|
|
661
738
|
* @param {import('node:http').ServerResponse} res
|
|
662
739
|
* @param {number} status
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vmz/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "VMZ production runtime core — DOM / SSR / HTTP / WriteBarrier (no compiler)",
|
|
6
6
|
"exports": {
|
|
@@ -58,12 +58,12 @@
|
|
|
58
58
|
"linkedom": "^0.18.13"
|
|
59
59
|
},
|
|
60
60
|
"optionalDependencies": {
|
|
61
|
-
"@vmz/vmz-win32-x64": "0.1.
|
|
62
|
-
"@vmz/vmz-win32-arm64": "0.1.
|
|
63
|
-
"@vmz/vmz-darwin-x64": "0.1.
|
|
64
|
-
"@vmz/vmz-darwin-arm64": "0.1.
|
|
65
|
-
"@vmz/vmz-linux-x64": "0.1.
|
|
66
|
-
"@vmz/vmz-linux-arm64": "0.1.
|
|
61
|
+
"@vmz/vmz-win32-x64": "0.1.17",
|
|
62
|
+
"@vmz/vmz-win32-arm64": "0.1.17",
|
|
63
|
+
"@vmz/vmz-darwin-x64": "0.1.17",
|
|
64
|
+
"@vmz/vmz-darwin-arm64": "0.1.17",
|
|
65
|
+
"@vmz/vmz-linux-x64": "0.1.17",
|
|
66
|
+
"@vmz/vmz-linux-arm64": "0.1.17"
|
|
67
67
|
},
|
|
68
68
|
"publishConfig": {
|
|
69
69
|
"access": "public"
|