@vmz/core 0.1.25 → 0.1.27

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.
@@ -161,6 +161,14 @@ export declare const directApi: {
161
161
  * @param {string | null} client
162
162
  */
163
163
  component(hostInst: any, name: any, props: any, client: any): any;
164
+ /**
165
+ * Subscribe to a child component event (`@submit` → event name `submit`).
166
+ * Orthogonal to function props (`:on-submit` → prop `onSubmit`).
167
+ * @param {HTMLElement} hostEl
168
+ * @param {string} eventName
169
+ * @param {(ev?: any, ...rest: any[]) => any} handler
170
+ */
171
+ onComponentEvent(hostEl: any, eventName: any, handler: any): void;
164
172
  /**
165
173
  * Keep nested Direct child props live with parent field writes.
166
174
  * @param {object} hostInst
@@ -198,12 +206,20 @@ export declare const directApi: {
198
206
  eachBlock(inst: any, bindingId: any, deps: any, spec: any, regionId?: any): DocumentFragment;
199
207
  };
200
208
  /**
201
- * Vue-familiar `@click` / legacy `onClick` → canonical `onXxx` prop name.
202
- * Function props that are not events are treated as getters (`v.call(host)`).
209
+ * Function prop wire: only `onXxx` camelCase is a handler prop.
210
+ * Component `@event` must never normalize here — that channel is `onComponentEvent`.
203
211
  * @param {unknown} name
204
212
  * @returns {string | null}
205
213
  */
206
214
  export declare function eventPropHandlerName(name: any): string;
215
+ /**
216
+ * Dispatch a component event to parent subscribers registered via `onComponentEvent`.
217
+ * Installed on every Direct instance as `inst.emit`.
218
+ * @param {object} inst
219
+ * @param {string} eventName
220
+ * @param {...any} payload
221
+ */
222
+ export declare function emitComponentEvent(inst: any, eventName: any, ...payload: any[]): void;
207
223
  export declare function isEventPropName(name: any): boolean;
208
224
  /** HTML boolean attributes: presence means true; `false`/`null` must remove the attr. */
209
225
  export declare const BOOLEAN_HTML_ATTRS: Set<string>;
package/dist/dom-core.js CHANGED
@@ -505,6 +505,8 @@ export const directApi = {
505
505
  /** @type {Record<string, any>} */
506
506
  const resolved = {};
507
507
  for (const [k, v] of Object.entries(props || {})) {
508
+ // Function props that already look like `onXxx` stay as handlers.
509
+ // Component `@event` never arrives here (emit uses onComponentEvent).
508
510
  const onKey = typeof v === 'function' ? eventPropHandlerName(k) : null;
509
511
  if (onKey)
510
512
  resolved[onKey] = v;
@@ -574,6 +576,23 @@ export const directApi = {
574
576
  }
575
577
  return host;
576
578
  },
579
+ /**
580
+ * Subscribe to a child component event (`@submit` → event name `submit`).
581
+ * Orthogonal to function props (`:on-submit` → prop `onSubmit`).
582
+ * @param {HTMLElement} hostEl
583
+ * @param {string} eventName
584
+ * @param {(ev?: any, ...rest: any[]) => any} handler
585
+ */
586
+ onComponentEvent(hostEl, eventName, handler) {
587
+ const child = hostEl && hostEl.__vmzInst;
588
+ if (!child || typeof eventName !== 'string' || !eventName)
589
+ return;
590
+ if (typeof handler !== 'function')
591
+ return;
592
+ const bag = child.__vmzComponentListeners || (child.__vmzComponentListeners = Object.create(null));
593
+ const list = bag[eventName] || (bag[eventName] = []);
594
+ list.push(handler);
595
+ },
577
596
  /**
578
597
  * Keep nested Direct child props live with parent field writes.
579
598
  * @param {object} hostInst
@@ -2736,8 +2755,8 @@ function wireDirectBind(inst, bindingId, deps, get, write, cf) {
2736
2755
  trackDirectBind(inst, liveDeps, apply, bindingId);
2737
2756
  }
2738
2757
  /**
2739
- * Vue-familiar `@click` / legacy `onClick` → canonical `onXxx` prop name.
2740
- * Function props that are not events are treated as getters (`v.call(host)`).
2758
+ * Function prop wire: only `onXxx` camelCase is a handler prop.
2759
+ * Component `@event` must never normalize here — that channel is `onComponentEvent`.
2741
2760
  * @param {unknown} name
2742
2761
  * @returns {string | null}
2743
2762
  */
@@ -2746,15 +2765,27 @@ export function eventPropHandlerName(name) {
2746
2765
  return null;
2747
2766
  if (/^on[A-Z]/.test(name))
2748
2767
  return name;
2749
- // `@input` → `onInput`; reject `@update:modelValue` until that surface exists.
2750
- if (name.charAt(0) === '@' && name.length > 1 && !name.includes(':')) {
2751
- const ev = name.slice(1);
2752
- if (!ev || !/^[A-Za-z]/.test(ev))
2753
- return null;
2754
- return `on${ev.charAt(0).toUpperCase()}${ev.slice(1)}`;
2755
- }
2756
2768
  return null;
2757
2769
  }
2770
+ /**
2771
+ * Dispatch a component event to parent subscribers registered via `onComponentEvent`.
2772
+ * Installed on every Direct instance as `inst.emit`.
2773
+ * @param {object} inst
2774
+ * @param {string} eventName
2775
+ * @param {...any} payload
2776
+ */
2777
+ export function emitComponentEvent(inst, eventName, ...payload) {
2778
+ if (!inst || typeof eventName !== 'string' || !eventName)
2779
+ return;
2780
+ const bag = inst.__vmzComponentListeners;
2781
+ const list = bag && bag[eventName];
2782
+ if (!Array.isArray(list) || list.length === 0)
2783
+ return;
2784
+ for (const fn of list) {
2785
+ if (typeof fn === 'function')
2786
+ fn(...payload);
2787
+ }
2788
+ }
2758
2789
  export function isEventPropName(name) {
2759
2790
  return eventPropHandlerName(name) != null;
2760
2791
  }
@@ -3137,6 +3168,11 @@ export function createInstance(Component, props = {}) {
3137
3168
  inst.__vmzBinders = Object.create(null);
3138
3169
  inst.__vmzBindings = Object.create(null);
3139
3170
  inst.__vmzDepToBindings = Object.create(null);
3171
+ inst.__vmzComponentListeners = Object.create(null);
3172
+ // Compiler intrinsic surface: `this.emit('submit', payload)` — not a string bus.
3173
+ inst.emit = function emit(eventName, ...payload) {
3174
+ return emitComponentEvent(inst, eventName, ...payload);
3175
+ };
3140
3176
  makeReactive(inst, Component.__vmzState || []);
3141
3177
  makeReactive(inst, Component.__vmzProps || []);
3142
3178
  // WriteBarrier: install Component helpers once (no import needed in emitted code).
package/dist/dom-ssr.js CHANGED
@@ -578,6 +578,9 @@ const serializeApi = {
578
578
  adoptLeave() {
579
579
  /* resume scope only */
580
580
  },
581
+ onComponentEvent() {
582
+ /* component events attach only on the client */
583
+ },
581
584
  bindText(inst, bindingId, deps, get, textNode) {
582
585
  let raw = '';
583
586
  try {
@@ -3,6 +3,12 @@
3
3
  * Href authority is RouteId × LocaleId realization rows — not path parsing on existing href.
4
4
  */
5
5
  export declare const LOCALE_LINK_PLAN_SCHEMA = "vmz.static.locale_link_plan.v0";
6
+ export type LinkRouteAlias = {
7
+ /** Author Link `to` / `data-vmz-route` (class RouteId, e.g. IndexPage). */
8
+ linkRouteId: string;
9
+ /** Locale realization `routeId` (often chunk path, e.g. pages/index). */
10
+ realizationRouteId: string;
11
+ };
6
12
  export type LocaleHrefArtifact = {
7
13
  locales?: Array<{
8
14
  id: string;
@@ -18,6 +24,11 @@ export type LocaleHrefArtifact = {
18
24
  localeId: string;
19
25
  path: string;
20
26
  }>;
27
+ /**
28
+ * Bridge author Link RouteId ↔ realization routeId.
29
+ * Realization artifacts historically key by chunk path; Links emit class names.
30
+ */
31
+ linkRouteAliases?: LinkRouteAlias[];
21
32
  };
22
33
  export type LocaleLinkPlanRow = {
23
34
  routeId: string;
@@ -28,6 +39,12 @@ export type LocaleLinkPlan = {
28
39
  schema: typeof LOCALE_LINK_PLAN_SCHEMA;
29
40
  rows: LocaleLinkPlanRow[];
30
41
  };
42
+ /** Build Link RouteId aliases from deployment page units (`routeId` + `chunkId`). */
43
+ export declare function linkRouteAliasesFromUnits(units: Array<{
44
+ kind?: string;
45
+ routeId?: string;
46
+ chunkId?: string;
47
+ }> | null | undefined): LinkRouteAlias[];
31
48
  /** Build RouteId × LocaleId → href rows from locale route realization artifact. */
32
49
  export declare function buildLocaleLinkPlan(artifact: LocaleHrefArtifact | null | undefined): LocaleLinkPlan;
33
50
  /** Apply plan rows to `<a data-vmz-route="RouteId">` markers for one LocaleId. */
@@ -3,13 +3,41 @@
3
3
  * Href authority is RouteId × LocaleId realization rows — not path parsing on existing href.
4
4
  */
5
5
  export const LOCALE_LINK_PLAN_SCHEMA = 'vmz.static.locale_link_plan.v0';
6
+ /** Build Link RouteId aliases from deployment page units (`routeId` + `chunkId`). */
7
+ export function linkRouteAliasesFromUnits(units) {
8
+ const out = [];
9
+ const seen = new Set();
10
+ for (const unit of units || []) {
11
+ if (unit?.kind && unit.kind !== 'page')
12
+ continue;
13
+ const linkRouteId = typeof unit?.routeId === 'string' ? unit.routeId.trim() : '';
14
+ const realizationRouteId = typeof unit?.chunkId === 'string' ? unit.chunkId.replace(/\\/g, '/').trim() : '';
15
+ if (!linkRouteId || !realizationRouteId || linkRouteId === realizationRouteId)
16
+ continue;
17
+ const key = `${linkRouteId}\0${realizationRouteId}`;
18
+ if (seen.has(key))
19
+ continue;
20
+ seen.add(key);
21
+ out.push({ linkRouteId, realizationRouteId });
22
+ }
23
+ return out;
24
+ }
6
25
  /** Build RouteId × LocaleId → href rows from locale route realization artifact. */
7
26
  export function buildLocaleLinkPlan(artifact) {
8
27
  const rows = [];
28
+ const aliases = artifact?.linkRouteAliases || [];
9
29
  for (const r of artifact?.realizations || []) {
10
30
  if (!r?.routeId || !r?.localeId || !r?.path)
11
31
  continue;
12
- rows.push({ routeId: String(r.routeId), localeId: String(r.localeId), href: String(r.path) });
32
+ const realizationRouteId = String(r.routeId);
33
+ const localeId = String(r.localeId);
34
+ const href = String(r.path);
35
+ rows.push({ routeId: realizationRouteId, localeId, href });
36
+ for (const alias of aliases) {
37
+ if (alias.realizationRouteId === realizationRouteId && alias.linkRouteId) {
38
+ rows.push({ routeId: String(alias.linkRouteId), localeId, href });
39
+ }
40
+ }
13
41
  }
14
42
  rows.sort((a, b) => (a.routeId === b.routeId ? a.localeId.localeCompare(b.localeId) : a.routeId.localeCompare(b.routeId)));
15
43
  return { schema: LOCALE_LINK_PLAN_SCHEMA, rows };
@@ -32,6 +60,10 @@ export function applyLocaleLinkPlan(html, localeId, plan, escapeAttr = (s) => St
32
60
  const href = byRoute.get(rm[1]);
33
61
  if (!href)
34
62
  return full;
63
+ // Realization rows for dynamic routes keep unresolved patterns (`[id]` / `:id`).
64
+ // Do not clobber compile-time realized hrefs like `/products/sku-1`.
65
+ if (/\[[^\]]+\]/.test(href) || /\/:[^/]+/.test(href))
66
+ return full;
35
67
  const hm = attrs.match(/\bhref\s*=\s*"([^"]*)"/i);
36
68
  if (hm && hm[1] === href)
37
69
  return full;
@@ -22,10 +22,10 @@ import http from 'node:http';
22
22
  import { createRequire, registerHooks } from 'node:module';
23
23
  import path from 'node:path';
24
24
  import { fileURLToPath, pathToFileURL } from 'node:url';
25
- import { localizeBodyLinks } from './localize-body-links.js';
26
- import { createRenderHost } from './render-host.js';
27
25
  import { listClientComponents } from './list-client-components.js';
26
+ import { linkRouteAliasesFromUnits, localizeBodyLinks } from './localize-body-links.js';
28
27
  import { loadNativeAddon } from './native-addon.js';
28
+ import { createRenderHost } from './render-host.js';
29
29
  import { resolveRouteLayoutChain } from './route-layout-chain.js';
30
30
  import { handleNodeRequest, setRoutes, setServerModuleResolver } from './vmz-runtime.js';
31
31
  const require = createRequire(import.meta.url);
@@ -756,6 +756,26 @@ process.on('SIGTERM', () => {
756
756
  process.on('SIGINT', () => {
757
757
  void gracefulShutdown('SIGINT');
758
758
  });
759
+ /**
760
+ * Attach author Link RouteId aliases so localizeBodyLinks can match `data-vmz-route="IndexPage"`
761
+ * against realization rows keyed by chunk path (`pages/index`).
762
+ * @param {any} artifact
763
+ * @param {string} dir
764
+ */
765
+ function attachLinkRouteAliases(artifact, dir) {
766
+ if (!artifact || typeof artifact !== 'object')
767
+ return artifact;
768
+ try {
769
+ const dep = JSON.parse(readFileSync(path.join(dir, 'vmz-deployment.json'), 'utf8'));
770
+ const aliases = linkRouteAliasesFromUnits(Array.isArray(dep.units) ? dep.units : []);
771
+ if (!aliases.length)
772
+ return artifact;
773
+ return { ...artifact, linkRouteAliases: aliases };
774
+ }
775
+ catch {
776
+ return artifact;
777
+ }
778
+ }
759
779
  /**
760
780
  * Re-import routes / pages / components with a new cache-bust token.
761
781
  * Keeps the HTTP server process alive (no Node restart).
@@ -791,6 +811,7 @@ async function softReload(opts = {}) {
791
811
  }
792
812
  try {
793
813
  localeArtifact = JSON.parse(await readFile(path.join(distDir, '_vmz', 'locale-route-realization.json'), 'utf8'));
814
+ localeArtifact = attachLinkRouteAliases(localeArtifact, distDir);
794
815
  }
795
816
  catch {
796
817
  localeArtifact = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vmz/core",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "type": "module",
5
5
  "description": "VMZ production runtime core — DOM / SSR / HTTP / WriteBarrier (no compiler)",
6
6
  "exports": {
@@ -66,12 +66,12 @@
66
66
  "linkedom": "^0.18.13"
67
67
  },
68
68
  "optionalDependencies": {
69
- "@vmz/vmz-win32-x64": "0.1.25",
70
- "@vmz/vmz-win32-arm64": "0.1.25",
71
- "@vmz/vmz-darwin-x64": "0.1.25",
72
- "@vmz/vmz-darwin-arm64": "0.1.25",
73
- "@vmz/vmz-linux-x64": "0.1.25",
74
- "@vmz/vmz-linux-arm64": "0.1.25"
69
+ "@vmz/vmz-win32-x64": "0.1.27",
70
+ "@vmz/vmz-win32-arm64": "0.1.27",
71
+ "@vmz/vmz-darwin-x64": "0.1.27",
72
+ "@vmz/vmz-darwin-arm64": "0.1.27",
73
+ "@vmz/vmz-linux-x64": "0.1.27",
74
+ "@vmz/vmz-linux-arm64": "0.1.27"
75
75
  },
76
76
  "publishConfig": {
77
77
  "access": "public"