@nisli/core 0.49.2 → 0.51.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.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * hmr/registry.ts — Transport-agnostic HMR core for Nisli (ADR 0021, ADR 0110).
3
+ *
4
+ * The tag→setup registry, the stable indirection thunk, and the in-place
5
+ * re-mount through the custom-element lifecycle. This is the part that is
6
+ * IDENTICAL regardless of how change notifications arrive — change
7
+ * (`../esbuild-hmr/*`) or Vite `import.meta.hot` (`../vite-hmr/*`) both drive
8
+ * the same registry. Keeping it here means one implementation, one set of
9
+ * correctness guarantees (ADR 0008.1 disposal), and no duplication.
10
+ *
11
+ * DEV-ONLY: reachable only via the dev subpaths, never from the `@nisli/core`
12
+ * `.` entry, so it contributes zero production bytes.
13
+ */
14
+ /** The author's setup function, kept opaque here (props proxy, host). */
15
+ export type HmrSetup = (props: unknown, host: HTMLElement) => unknown;
16
+ /**
17
+ * Begin a caller-owned drain window: suppress auto-scheduling AND cancel any
18
+ * pending microtask drain, so the caller can re-evaluate a whole bundle and
19
+ * register ALL changed tags before a single explicit `drainRemounts()`. Used by
20
+ * the Vite path drains per-module.
21
+ */
22
+ export declare function beginManualDrain(): void;
23
+ /** End a caller-owned drain window (re-enable auto-scheduling). */
24
+ export declare function endManualDrain(): void;
25
+ /**
26
+ * Register (or update) the setup for a tag and return a STABLE indirection
27
+ * thunk that always reads the *current* setup from the registry.
28
+ *
29
+ * First registration for a tag (initial load) schedules no re-mount. A
30
+ * subsequent registration with a *different* setup reference (a rebuild
31
+ * re-evaluating the module) queues the tag for re-mount.
32
+ *
33
+ * The returned thunk is what `component()` hands to the real
34
+ * `FrameworkComponent`; because it reads `registry.get(tag)` lazily, the live
35
+ * element picks up new setup with no class swap or tag re-definition.
36
+ */
37
+ export declare function __register(tag: string, setup: HmrSetup): HmrSetup;
38
+ /** Number of tags awaiting a re-mount (for orchestration/tests). */
39
+ export declare function pendingRemountCount(): number;
40
+ /** Drain and re-mount every pending tag. Returns the tags that were remounted. */
41
+ export declare function drainRemounts(): string[];
42
+ /**
43
+ * Re-mount every live element of `tag` in place, on the SAME instance:
44
+ *
45
+ * disconnectedCallback() — resets `_mounted`, disposes effects + template
46
+ * (the ADR 0008.1 disposal contract)
47
+ * replaceChildren() — clears old DOM, firing nested custom elements'
48
+ * disconnectedCallback so their disposers run too
49
+ * connectedCallback() — re-runs the NEW setup (via the registry thunk)
50
+ * inside the same `untrack(...)` mount path
51
+ *
52
+ * Props survive because `_propsProxy` is constructor-owned (component.ts), and
53
+ * the element node identity is preserved — siblings, scroll, route, and global
54
+ * stores are untouched.
55
+ */
56
+ export declare function remount(tag: string): void;
57
+ /** Test-only: clear the registry and pending state between cases. */
58
+ export declare function __resetRegistry(): void;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * hmr/registry.ts — Transport-agnostic HMR core for Nisli (ADR 0021, ADR 0110).
3
+ *
4
+ * The tag→setup registry, the stable indirection thunk, and the in-place
5
+ * re-mount through the custom-element lifecycle. This is the part that is
6
+ * IDENTICAL regardless of how change notifications arrive — change
7
+ * (`../esbuild-hmr/*`) or Vite `import.meta.hot` (`../vite-hmr/*`) both drive
8
+ * the same registry. Keeping it here means one implementation, one set of
9
+ * correctness guarantees (ADR 0008.1 disposal), and no duplication.
10
+ *
11
+ * DEV-ONLY: reachable only via the dev subpaths, never from the `@nisli/core`
12
+ * `.` entry, so it contributes zero production bytes.
13
+ */
14
+ // ── Tag → setup registry (Ruling 2) ─────────────────────────────────
15
+ const registry = new Map();
16
+ /** Tags whose setup changed since the last drain — pending a re-mount. */
17
+ const pendingRemounts = new Set();
18
+ let remountScheduled = false;
19
+ /**
20
+ * When true, `__register` accumulates pending tags but does NOT auto-schedule a
21
+ * microtask drain — the caller owns the drain so it can wait for a full module
22
+ * re-eval to register ALL changed tags first.
23
+ * The Vite path leaves this false and drains explicitly from its accept hook.
24
+ */
25
+ let suppressAutoDrain = false;
26
+ /**
27
+ * Begin a caller-owned drain window: suppress auto-scheduling AND cancel any
28
+ * pending microtask drain, so the caller can re-evaluate a whole bundle and
29
+ * register ALL changed tags before a single explicit `drainRemounts()`. Used by
30
+ * the Vite path drains per-module.
31
+ */
32
+ export function beginManualDrain() {
33
+ suppressAutoDrain = true;
34
+ remountScheduled = false;
35
+ }
36
+ /** End a caller-owned drain window (re-enable auto-scheduling). */
37
+ export function endManualDrain() {
38
+ suppressAutoDrain = false;
39
+ }
40
+ /**
41
+ * Register (or update) the setup for a tag and return a STABLE indirection
42
+ * thunk that always reads the *current* setup from the registry.
43
+ *
44
+ * First registration for a tag (initial load) schedules no re-mount. A
45
+ * subsequent registration with a *different* setup reference (a rebuild
46
+ * re-evaluating the module) queues the tag for re-mount.
47
+ *
48
+ * The returned thunk is what `component()` hands to the real
49
+ * `FrameworkComponent`; because it reads `registry.get(tag)` lazily, the live
50
+ * element picks up new setup with no class swap or tag re-definition.
51
+ */
52
+ export function __register(tag, setup) {
53
+ const prev = registry.get(tag);
54
+ registry.set(tag, setup);
55
+ if (prev !== undefined && prev !== setup) {
56
+ pendingRemounts.add(tag);
57
+ if (!suppressAutoDrain)
58
+ scheduleRemount();
59
+ }
60
+ return (props, host) => {
61
+ const current = registry.get(tag);
62
+ if (!current)
63
+ throw new Error(`[nisli-hmr] no setup registered for <${tag}>`);
64
+ return current(props, host);
65
+ };
66
+ }
67
+ /** Number of tags awaiting a re-mount (for orchestration/tests). */
68
+ export function pendingRemountCount() {
69
+ return pendingRemounts.size;
70
+ }
71
+ /** Drain and re-mount every pending tag. Returns the tags that were remounted. */
72
+ export function drainRemounts() {
73
+ const tags = [...pendingRemounts];
74
+ pendingRemounts.clear();
75
+ remountScheduled = false;
76
+ for (const tag of tags)
77
+ remount(tag);
78
+ return tags;
79
+ }
80
+ function scheduleRemount() {
81
+ if (remountScheduled)
82
+ return;
83
+ remountScheduled = true;
84
+ // Defer so a full module re-eval can register *all* changed tags first.
85
+ queueMicrotask(() => {
86
+ if (remountScheduled)
87
+ drainRemounts();
88
+ });
89
+ }
90
+ // ── Re-mount through the existing lifecycle (Ruling 3) ──────────────
91
+ /**
92
+ * Re-mount every live element of `tag` in place, on the SAME instance:
93
+ *
94
+ * disconnectedCallback() — resets `_mounted`, disposes effects + template
95
+ * (the ADR 0008.1 disposal contract)
96
+ * replaceChildren() — clears old DOM, firing nested custom elements'
97
+ * disconnectedCallback so their disposers run too
98
+ * connectedCallback() — re-runs the NEW setup (via the registry thunk)
99
+ * inside the same `untrack(...)` mount path
100
+ *
101
+ * Props survive because `_propsProxy` is constructor-owned (component.ts), and
102
+ * the element node identity is preserved — siblings, scroll, route, and global
103
+ * stores are untouched.
104
+ */
105
+ export function remount(tag) {
106
+ const elements = document.querySelectorAll(tag);
107
+ elements.forEach((node) => {
108
+ const el = node;
109
+ // Dispose first: must run BEFORE clearing DOM or effects/subscriptions
110
+ // leak across reloads (ADR 0008.1 hazard).
111
+ el.disconnectedCallback?.();
112
+ // Clear old DOM — removal fires nested children's disconnectedCallback.
113
+ el.replaceChildren();
114
+ // Re-run setup + re-mount on the same instance.
115
+ el.connectedCallback?.();
116
+ });
117
+ }
118
+ /** Test-only: clear the registry and pending state between cases. */
119
+ export function __resetRegistry() {
120
+ registry.clear();
121
+ pendingRemounts.clear();
122
+ remountScheduled = false;
123
+ suppressAutoDrain = false;
124
+ }
125
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/hmr/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAYH,uEAAuE;AAEvE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAoB,CAAC;AAE7C,0EAA0E;AAC1E,MAAM,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;AAC1C,IAAI,gBAAgB,GAAG,KAAK,CAAC;AAE7B;;;;;GAKG;AACH,IAAI,iBAAiB,GAAG,KAAK,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB;IAC9B,iBAAiB,GAAG,IAAI,CAAC;IACzB,gBAAgB,GAAG,KAAK,CAAC;AAC3B,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,cAAc;IAC5B,iBAAiB,GAAG,KAAK,CAAC;AAC5B,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,KAAe;IACrD,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC/B,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACzB,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACzC,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,iBAAiB;YAAE,eAAe,EAAE,CAAC;IAC5C,CAAC;IACD,OAAO,CAAC,KAAc,EAAE,IAAiB,EAAE,EAAE;QAC3C,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,wCAAwC,GAAG,GAAG,CAAC,CAAC;QAC9E,OAAO,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAC9B,CAAC,CAAC;AACJ,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,mBAAmB;IACjC,OAAO,eAAe,CAAC,IAAI,CAAC;AAC9B,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,aAAa;IAC3B,MAAM,IAAI,GAAG,CAAC,GAAG,eAAe,CAAC,CAAC;IAClC,eAAe,CAAC,KAAK,EAAE,CAAC;IACxB,gBAAgB,GAAG,KAAK,CAAC;IACzB,KAAK,MAAM,GAAG,IAAI,IAAI;QAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IACrC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,eAAe;IACtB,IAAI,gBAAgB;QAAE,OAAO;IAC7B,gBAAgB,GAAG,IAAI,CAAC;IACxB,wEAAwE;IACxE,cAAc,CAAC,GAAG,EAAE;QAClB,IAAI,gBAAgB;YAAE,aAAa,EAAE,CAAC;IACxC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,uEAAuE;AAEvE;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,OAAO,CAAC,GAAW;IACjC,MAAM,QAAQ,GAAG,QAAQ,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAChD,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QACxB,MAAM,EAAE,GAAG,IAAyB,CAAC;QACrC,uEAAuE;QACvE,2CAA2C;QAC3C,EAAE,CAAC,oBAAoB,EAAE,EAAE,CAAC;QAC5B,wEAAwE;QACxE,EAAE,CAAC,eAAe,EAAE,CAAC;QACrB,gDAAgD;QAChD,EAAE,CAAC,iBAAiB,EAAE,EAAE,CAAC;IAC3B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,eAAe;IAC7B,QAAQ,CAAC,KAAK,EAAE,CAAC;IACjB,eAAe,CAAC,KAAK,EAAE,CAAC;IACxB,gBAAgB,GAAG,KAAK,CAAC;IACzB,iBAAiB,GAAG,KAAK,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * hmr/transform.ts — Shared source rewrite for Nisli HMR (ADR 0021, ADR 0110).
3
+ *
4
+ * Wraps the `component` a module imports from `@nisli/core` with the registry
5
+ * indirection so a rebuilt module's `component()` calls re-register their setup
6
+ * and trigger an in-place re-mount. Transport-agnostic and pure — used by both
7
+ * the Vite `transform` hook.
8
+ */
9
+ /**
10
+ * Rewrite a module's source so the `component` it imports from `coreSpecifier`
11
+ * is wrapped by the registry indirection from `runtimeSpecifier`.
12
+ *
13
+ * Strategy: alias the real import, then shadow `component` with a wrapper that
14
+ * registers the setup and passes a STABLE thunk to the real `component`. Cost:
15
+ * one Map write + one thunk allocation per `component()` call.
16
+ *
17
+ * Returns the original source unchanged if it does not import `component` from
18
+ * the core specifier (nothing to wrap) — the caller uses identity to decide
19
+ * whether the module is HMR-relevant.
20
+ */
21
+ export declare function transformSource(code: string, coreSpecifier: string, runtimeSpecifier: string): string;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * hmr/transform.ts — Shared source rewrite for Nisli HMR (ADR 0021, ADR 0110).
3
+ *
4
+ * Wraps the `component` a module imports from `@nisli/core` with the registry
5
+ * indirection so a rebuilt module's `component()` calls re-register their setup
6
+ * and trigger an in-place re-mount. Transport-agnostic and pure — used by both
7
+ * the Vite `transform` hook.
8
+ */
9
+ function escapeRegExp(s) {
10
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
11
+ }
12
+ /**
13
+ * Rewrite a module's source so the `component` it imports from `coreSpecifier`
14
+ * is wrapped by the registry indirection from `runtimeSpecifier`.
15
+ *
16
+ * Strategy: alias the real import, then shadow `component` with a wrapper that
17
+ * registers the setup and passes a STABLE thunk to the real `component`. Cost:
18
+ * one Map write + one thunk allocation per `component()` call.
19
+ *
20
+ * Returns the original source unchanged if it does not import `component` from
21
+ * the core specifier (nothing to wrap) — the caller uses identity to decide
22
+ * whether the module is HMR-relevant.
23
+ */
24
+ export function transformSource(code, coreSpecifier, runtimeSpecifier) {
25
+ const spec = escapeRegExp(coreSpecifier);
26
+ // Match a named import of `component` (alone or among others) from core.
27
+ const importRe = new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*['"]${spec}['"]`, 'g');
28
+ let importsComponent = false;
29
+ const rewritten = code.replace(importRe, (_full, names) => {
30
+ const parts = names.split(',').map((s) => s.trim()).filter(Boolean);
31
+ const next = parts.map((p) => {
32
+ // Handle `component` and `component as X` — we only wrap the bare name.
33
+ if (p === 'component') {
34
+ importsComponent = true;
35
+ return 'component as __nisliRealComponent';
36
+ }
37
+ return p;
38
+ });
39
+ return `import { ${next.join(', ')} } from '${coreSpecifier}'`;
40
+ });
41
+ if (!importsComponent)
42
+ return code;
43
+ // Prepend the registry-import + wrapper. `__register` returns the STABLE
44
+ // thunk the real component captures; the wrapper preserves the factory's
45
+ // return value and signature.
46
+ const preamble = `import { __register as __nisliRegister } from '${runtimeSpecifier}';\n` +
47
+ `function component(tag, setup, opts) {\n` +
48
+ ` return __nisliRealComponent(tag, __nisliRegister(tag, setup), opts);\n` +
49
+ `}\n`;
50
+ return preamble + rewritten;
51
+ }
52
+ //# sourceMappingURL=transform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform.js","sourceRoot":"","sources":["../../src/hmr/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,SAAS,YAAY,CAAC,CAAS;IAC7B,OAAO,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AAClD,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,aAAqB,EAAE,gBAAwB;IAC3F,MAAM,IAAI,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;IACzC,yEAAyE;IACzE,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,0CAA0C,IAAI,MAAM,EAAE,GAAG,CAAC,CAAC;IAEvF,IAAI,gBAAgB,GAAG,KAAK,CAAC;IAC7B,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAa,EAAE,KAAa,EAAE,EAAE;QACxE,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACpE,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YAC3B,wEAAwE;YACxE,IAAI,CAAC,KAAK,WAAW,EAAE,CAAC;gBACtB,gBAAgB,GAAG,IAAI,CAAC;gBACxB,OAAO,mCAAmC,CAAC;YAC7C,CAAC;YACD,OAAO,CAAC,CAAC;QACX,CAAC,CAAC,CAAC;QACH,OAAO,YAAY,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,aAAa,GAAG,CAAC;IACjE,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,gBAAgB;QAAE,OAAO,IAAI,CAAC;IAEnC,yEAAyE;IACzE,yEAAyE;IACzE,8BAA8B;IAC9B,MAAM,QAAQ,GACZ,kDAAkD,gBAAgB,MAAM;QACxE,0CAA0C;QAC1C,0EAA0E;QAC1E,KAAK,CAAC;IAER,OAAO,QAAQ,GAAG,SAAS,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Minimal structural subset of Vite's `Plugin` we implement. Declared
3
+ * structurally (not imported from `vite`) so `@nisli/core` keeps its
4
+ * zero-dependency footprint — `vite` is the consumer's dependency, not ours.
5
+ */
6
+ export interface NisliVitePlugin {
7
+ name: string;
8
+ enforce?: 'pre' | 'post';
9
+ apply?: 'serve' | 'build';
10
+ transform(code: string, id: string): {
11
+ code: string;
12
+ map: null;
13
+ } | undefined;
14
+ }
15
+ export interface NisliViteHmrOptions {
16
+ /** The specifier modules import `component` from. Default `@nisli/core`. */
17
+ coreSpecifier?: string;
18
+ /** The runtime subpath providing `__register`/`__drain`. Default `@nisli/core/vite-hmr/runtime`. */
19
+ runtimeSpecifier?: string;
20
+ /** Which module ids to transform. Default: TS/JS sources. */
21
+ filter?: RegExp;
22
+ }
23
+ /**
24
+ * Create the dev-only Nisli Vite HMR plugin. Add it to `plugins` in
25
+ * `vite.config.ts`; `apply: 'serve'` keeps it off the production build path.
26
+ */
27
+ export declare function nisliHmr(options?: NisliViteHmrOptions): NisliVitePlugin;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * vite-hmr/plugin.ts — Dev-only Vite plugin for Nisli HMR (ADR 0110).
3
+ *
4
+ * Vite plugin that wraps component() calls (shared `transformSource`),
5
+ * but drives re-mount through Vite's native `import.meta.hot` instead of an SSE
6
+ * hub + whole-bundle re-import. For each source module that imports `component`
7
+ * from `@nisli/core`, it:
8
+ * 1. wraps `component(tag, setup)` → `component(tag, __register(tag, setup))`
9
+ * so a re-evaluated module swaps its setup in the shared registry, and
10
+ * 2. appends `import.meta.hot.accept(() => __drain())` so Vite replaces JUST
11
+ * this module (granular HMR) and the queued re-mounts are applied.
12
+ *
13
+ * Because Vite re-evaluates only the changed module against a STABLE framework
14
+ * module, there is exactly one `@nisli/core` instance — the dual-instance
15
+ * "split-brain" of whole-bundle re-import cannot occur.
16
+ *
17
+ * `apply: 'serve'` makes this dev-only: the production `vite build` never runs
18
+ * the transform, so no wrapper, no registry import, and no `import.meta.hot`
19
+ * reach the prod bundle (zero prod HMR bytes).
20
+ */
21
+ import { transformSource } from '../hmr/transform.js';
22
+ const DEFAULT_FILTER = /\.[mc]?[jt]sx?$/;
23
+ /**
24
+ * Create the dev-only Nisli Vite HMR plugin. Add it to `plugins` in
25
+ * `vite.config.ts`; `apply: 'serve'` keeps it off the production build path.
26
+ */
27
+ export function nisliHmr(options = {}) {
28
+ const coreSpecifier = options.coreSpecifier ?? '@nisli/core';
29
+ const runtimeSpecifier = options.runtimeSpecifier ?? '@nisli/core/vite-hmr/runtime';
30
+ const filter = options.filter ?? DEFAULT_FILTER;
31
+ return {
32
+ name: 'nisli-vite-hmr',
33
+ // Run on the raw TS source before Vite's own transforms rewrite imports.
34
+ enforce: 'pre',
35
+ // Dev server only — never transform the production build.
36
+ apply: 'serve',
37
+ transform(code, id) {
38
+ const clean = id.split('?')[0] ?? id;
39
+ if (clean.includes('node_modules'))
40
+ return undefined;
41
+ if (!filter.test(clean))
42
+ return undefined;
43
+ const wrapped = transformSource(code, coreSpecifier, runtimeSpecifier);
44
+ // Identity ⇒ the module does not import `component` — not HMR-relevant.
45
+ // Leaving it un-accepted lets Vite propagate to a full reload (correct for
46
+ // services/utils edits).
47
+ if (wrapped === code)
48
+ return undefined;
49
+ const out = `import { __drain as __nisliViteDrain } from ${JSON.stringify(runtimeSpecifier)};\n` +
50
+ wrapped +
51
+ `\nif (import.meta.hot) {\n` +
52
+ ` import.meta.hot.accept(() => { __nisliViteDrain(); });\n` +
53
+ `}\n`;
54
+ // map: null — the injection is an additive prepend/append on a handful of
55
+ // component modules in DEV only; a precise map would require a sourcemap
56
+ // dependency (magic-string), which `@nisli/core` deliberately avoids to
57
+ // stay zero-dependency. Original statements keep their column offsets.
58
+ return { code: out, map: null };
59
+ },
60
+ };
61
+ }
62
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/vite-hmr/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAuBtD,MAAM,cAAc,GAAG,iBAAiB,CAAC;AAEzC;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,UAA+B,EAAE;IACxD,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,aAAa,CAAC;IAC7D,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,8BAA8B,CAAC;IACpF,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,cAAc,CAAC;IAEhD,OAAO;QACL,IAAI,EAAE,gBAAgB;QACtB,yEAAyE;QACzE,OAAO,EAAE,KAAK;QACd,0DAA0D;QAC1D,KAAK,EAAE,OAAO;QACd,SAAS,CAAC,IAAY,EAAE,EAAU;YAChC,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACrC,IAAI,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC;gBAAE,OAAO,SAAS,CAAC;YACrD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;gBAAE,OAAO,SAAS,CAAC;YAE1C,MAAM,OAAO,GAAG,eAAe,CAAC,IAAI,EAAE,aAAa,EAAE,gBAAgB,CAAC,CAAC;YACvE,wEAAwE;YACxE,2EAA2E;YAC3E,yBAAyB;YACzB,IAAI,OAAO,KAAK,IAAI;gBAAE,OAAO,SAAS,CAAC;YAEvC,MAAM,GAAG,GACP,+CAA+C,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,KAAK;gBACpF,OAAO;gBACP,4BAA4B;gBAC5B,4DAA4D;gBAC5D,KAAK,CAAC;YAER,0EAA0E;YAC1E,yEAAyE;YACzE,wEAAwE;YACxE,uEAAuE;YACvE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;QAClC,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * vite-hmr/runtime.ts — Browser runtime for Nisli HMR under Vite (ADR 0110).
3
+ *
4
+ * DEV-ONLY. The Vite plugin (`./plugin.ts`) injects calls to these into each
5
+ * component module:
6
+ * - `__register(tag, setup)` wraps `component()` so a re-evaluated module swaps
7
+ * its setup in the shared registry and queues an in-place re-mount.
8
+ * - `__drain()` is called from the module's `import.meta.hot.accept` hook to
9
+ * apply the queued re-mounts.
10
+ *
11
+ * Vite re-evaluates ONLY the changed module against a
12
+ * STABLE framework module — so there is exactly one `@nisli/core` instance and
13
+ * no lifecycle/reactive split-brain. This module just re-exports the
14
+ * transport-agnostic registry core.
15
+ */
16
+ export { __register, drainRemounts as __drain, pendingRemountCount, remount } from '../hmr/registry.js';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * vite-hmr/runtime.ts — Browser runtime for Nisli HMR under Vite (ADR 0110).
3
+ *
4
+ * DEV-ONLY. The Vite plugin (`./plugin.ts`) injects calls to these into each
5
+ * component module:
6
+ * - `__register(tag, setup)` wraps `component()` so a re-evaluated module swaps
7
+ * its setup in the shared registry and queues an in-place re-mount.
8
+ * - `__drain()` is called from the module's `import.meta.hot.accept` hook to
9
+ * apply the queued re-mounts.
10
+ *
11
+ * Vite re-evaluates ONLY the changed module against a
12
+ * STABLE framework module — so there is exactly one `@nisli/core` instance and
13
+ * no lifecycle/reactive split-brain. This module just re-exports the
14
+ * transport-agnostic registry core.
15
+ */
16
+ export { __register, drainRemounts as __drain, pendingRemountCount, remount } from '../hmr/registry.js';
17
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/vite-hmr/runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,UAAU,EAAE,aAAa,IAAI,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nisli/core",
3
- "version": "0.49.2",
3
+ "version": "0.51.0",
4
4
  "description": "A reactive web component framework. Signals, templates, dependency injection — no build step, no virtual DOM, no dependencies.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,30 +22,17 @@
22
22
  "types": "./dist/index.d.ts",
23
23
  "default": "./dist/index.js"
24
24
  },
25
- "./esbuild-hmr": {
26
- "types": "./dist/esbuild-hmr/plugin.d.ts",
27
- "default": "./dist/esbuild-hmr/plugin.js"
25
+ "./vite-hmr": {
26
+ "types": "./dist/vite-hmr/plugin.d.ts",
27
+ "default": "./dist/vite-hmr/plugin.js"
28
28
  },
29
- "./esbuild-hmr/runtime": {
30
- "types": "./dist/esbuild-hmr/runtime.d.ts",
31
- "default": "./dist/esbuild-hmr/runtime.js"
32
- },
33
- "./esbuild-hmr/server": {
34
- "types": "./dist/esbuild-hmr/server.d.ts",
35
- "default": "./dist/esbuild-hmr/server.js"
36
- }
37
- },
38
- "peerDependencies": {
39
- "esbuild": "*"
40
- },
41
- "peerDependenciesMeta": {
42
- "esbuild": {
43
- "optional": true
29
+ "./vite-hmr/runtime": {
30
+ "types": "./dist/vite-hmr/runtime.d.ts",
31
+ "default": "./dist/vite-hmr/runtime.js"
44
32
  }
45
33
  },
46
34
  "devDependencies": {
47
- "@types/node": "^22.0.0",
48
- "esbuild": "^0.28.1"
35
+ "@types/node": "^22.0.0"
49
36
  },
50
37
  "files": [
51
38
  "dist",
@@ -1,161 +0,0 @@
1
- /**
2
- * esbuild-hmr/plugin.ts — Dev-only esbuild plugin for Nisli component HMR.
3
- *
4
- * DEV-ONLY, build-time (Node). Reachable only via `@nisli/core/esbuild-hmr`,
5
- * never from the runtime `.` entry (ADR 0021, Ruling 1). It is added to the
6
- * esbuild plugin list ONLY on the `--watch` path; `pnpm build` (no watch) never
7
- * sees it, so prod is byte-identical by construction (Ruling 5).
8
- *
9
- * `esbuild` is an OPTIONAL peerDependency. To keep `@nisli/core` dependency-free
10
- * we do NOT `import` esbuild here — we type the plugin against a minimal local
11
- * structural surface. The host build script supplies the real esbuild.
12
- *
13
- * What it does:
14
- * 1. onLoad (Ruling 2): for author modules that import `component` from
15
- * `@nisli/core`, rewrite the import so `component(tag, setup, opts)` routes
16
- * `setup` through the runtime tag-keyed registry's STABLE indirection thunk.
17
- * The real `FrameworkComponent` thus captures a thunk that always reads the
18
- * current setup — no class swap, no tag re-definition.
19
- * 2. onEnd (Ruling 4 transport): diff the metafile outputs against the previous
20
- * build and broadcast a `change` payload ({added, removed, updated}) over
21
- * SSE so the browser client can escalate (css/js/reload).
22
- * 3. Inject the browser dev client (`@nisli/core/esbuild-hmr/runtime` +
23
- * `connect({ path })`) via esbuild's `inject` option pointed at a VIRTUAL
24
- * shim module, so the runtime is RESOLVED + BUNDLED into the output (no
25
- * author HTML/JS change needed). We deliberately do NOT use `banner`: a
26
- * banner is raw passthrough text that esbuild neither resolves nor bundles,
27
- * so a bare `import … from "@nisli/core/esbuild-hmr/runtime"` would leak
28
- * verbatim into the output and the browser would throw
29
- * `Failed to resolve module specifier` (no import map). `inject` runs the
30
- * shim through onResolve/onLoad so the runtime is inlined at build time, and
31
- * esbuild applies `inject` ONLY to JS modules — never to file/copy-loaded
32
- * entries (e.g. the viewer's `logo.svg`). Verified empirically against
33
- * esbuild 0.28.1.
34
- */
35
- import type { ChangeBroadcaster } from './server.js';
36
- /** A subset of esbuild's `OnLoadArgs` we rely on. */
37
- interface OnLoadArgs {
38
- path: string;
39
- }
40
- /** A subset of esbuild's `OnLoadResult`. */
41
- interface OnLoadResult {
42
- contents: string;
43
- loader?: 'ts' | 'tsx' | 'js' | 'jsx';
44
- /** Directory used to resolve imports from a virtual module (the client shim). */
45
- resolveDir?: string;
46
- }
47
- /** A subset of esbuild's `OnResolveArgs`. */
48
- interface OnResolveArgs {
49
- path: string;
50
- }
51
- /** A subset of esbuild's `OnResolveResult`. */
52
- interface OnResolveResult {
53
- path: string;
54
- namespace?: string;
55
- }
56
- /** A subset of esbuild's metafile. */
57
- interface Metafile {
58
- outputs: Record<string, {
59
- bytes: number;
60
- hash?: string;
61
- }>;
62
- }
63
- /** A subset of esbuild's `BuildResult`. */
64
- interface BuildResult {
65
- metafile?: Metafile;
66
- }
67
- /** The hooks surface of esbuild's `PluginBuild` we use. */
68
- interface PluginBuild {
69
- initialOptions: {
70
- banner?: {
71
- js?: string;
72
- } & Record<string, string | undefined>;
73
- /** Files esbuild injects into every JS module (never file/copy entries). */
74
- inject?: string[];
75
- /** esbuild's working directory; anchors virtual-module resolution. */
76
- absWorkingDir?: string;
77
- [key: string]: unknown;
78
- };
79
- onResolve(options: {
80
- filter: RegExp;
81
- namespace?: string;
82
- }, callback: (args: OnResolveArgs) => OnResolveResult | undefined): void;
83
- onLoad(options: {
84
- filter: RegExp;
85
- namespace?: string;
86
- }, callback: (args: OnLoadArgs) => OnLoadResult | undefined): void;
87
- onEnd(callback: (result: BuildResult) => void): void;
88
- }
89
- /** esbuild's `Plugin` shape (structural). */
90
- export interface EsbuildPlugin {
91
- name: string;
92
- setup(build: PluginBuild): void;
93
- }
94
- export interface NisliHmrPluginOptions {
95
- /**
96
- * Broadcaster used to push `change` events to connected clients. Supply the
97
- * one returned by `createHmrServer()` (server.ts), or any object with a
98
- * `broadcast(payload)` method (e.g. an esbuild `serve` adapter).
99
- */
100
- broadcaster: ChangeBroadcaster;
101
- /**
102
- * The module specifier author code imports `component` from.
103
- * Defaults to `@nisli/core`.
104
- */
105
- coreSpecifier?: string;
106
- /**
107
- * The specifier of the browser dev runtime, inlined into the bundle via the
108
- * injected client shim. Defaults to `@nisli/core/esbuild-hmr/runtime`.
109
- */
110
- runtimeSpecifier?: string;
111
- /**
112
- * The SSE endpoint the browser dev client connects to. Baked into the injected
113
- * shim as `connect({ path: clientUrl })`. Defaults to `/esbuild` (esbuild's
114
- * client convention, same-origin).
115
- *
116
- * Set this to an ABSOLUTE URL when the SSE hub (server.ts) runs on a different
117
- * origin than the page server — e.g. `http://localhost:3031/esbuild` when the
118
- * hub listens on :3031 and the page is served by Hono on :3040. The hub already
119
- * sends `Access-Control-Allow-Origin: '*'`, so cross-origin SSE works.
120
- */
121
- clientUrl?: string;
122
- /** Files to transform. Defaults to TS/JS sources (excludes node_modules). */
123
- filter?: RegExp;
124
- }
125
- /**
126
- * Rewrite a module's source so the `component` it imports from `@nisli/core` is
127
- * wrapped by the registry indirection. Pure function — exported for testing.
128
- *
129
- * Strategy: alias the real import, then shadow `component` with a wrapper that
130
- * registers the setup and passes a STABLE thunk to the real `component`. The
131
- * wrapper is a no-op-shaped passthrough at runtime cost of one Map write +
132
- * one thunk allocation per `component()` call.
133
- *
134
- * Returns the original source unchanged if it does not import `component` from
135
- * the core specifier (nothing to wrap).
136
- */
137
- export declare function transformSource(code: string, coreSpecifier: string, runtimeSpecifier: string): string;
138
- /**
139
- * Source of the virtual client shim that esbuild's `inject` pulls into every JS
140
- * module. Because it is `inject`ed (not a `banner`), esbuild RESOLVES the
141
- * `runtimeSpecifier` import and BUNDLES `connect` inline — no bare specifier
142
- * survives in the output (the 0.49.0 bug). `clientUrl` is baked in so the
143
- * client targets the right SSE origin. Pure function — exported for testing.
144
- */
145
- export declare function clientShimSource(runtimeSpecifier: string, clientUrl: string): string;
146
- /**
147
- * Create the dev-only Nisli HMR esbuild plugin. Add it to `plugins` ONLY when
148
- * building with `--watch`; never on the prod build path.
149
- */
150
- export declare function nisliHmrPlugin(options: NisliHmrPluginOptions): EsbuildPlugin;
151
- /**
152
- * Diff two output-hash maps into an esbuild-shaped change payload.
153
- * Returns `null` for the very first build (no previous baseline).
154
- * Exported for testing.
155
- */
156
- export declare function diffOutputs(prev: Map<string, string> | null, next: Map<string, string>): {
157
- added: string[];
158
- removed: string[];
159
- updated: string[];
160
- } | null;
161
- export {};