@vmz/core 0.1.15 → 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.
@@ -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
- if (typeof v === 'function' && isEventPropName(k))
432
- resolved[k] = v;
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 typeof name === 'string' && /^on[A-Z]/.test(name);
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, isEventPropName, mount, noteDomCreate, resolveComponent, runDirectCreate, scheduleClientOn, settlePendingChildMounts, snapshotInstanceState, stripFns, } from './dom-core.js';
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
- if (typeof v === 'function' && isEventPropName(k))
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);
@@ -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
- const native = loadNativeAddon();
965
- if (typeof native.generateHtmlShell !== 'function') {
966
- throw new Error('vmz native addon missing generateHtmlShell rebuild with `pnpm napi:build`');
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
- yield native.generateHtmlShell({
969
- title: 'Dev Error',
970
- lang: 'en',
971
- cssHrefs: [],
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
- sendJson(res, 404, { error: 'not found', path: url.pathname });
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
- sendJson(res, 500, {
346
- error: err instanceof Error ? err.message : String(err),
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
- let body = part.subarray(splitAt + 4);
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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.15",
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.15",
62
- "@vmz/vmz-win32-arm64": "0.1.15",
63
- "@vmz/vmz-darwin-x64": "0.1.15",
64
- "@vmz/vmz-darwin-arm64": "0.1.15",
65
- "@vmz/vmz-linux-x64": "0.1.15",
66
- "@vmz/vmz-linux-arm64": "0.1.15"
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"