@vertz/ui-server 0.2.0 → 0.2.4

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,51 @@
1
+ /**
2
+ * DOM state snapshot/restore utilities for fast refresh.
3
+ *
4
+ * Captures transient DOM state (form values, focus, scroll positions) before
5
+ * a component is replaced, and restores it to the new DOM tree after replacement.
6
+ *
7
+ * Separated from fast-refresh-runtime.ts so it can be tested without
8
+ * import.meta.hot (which only exists in Bun's dev server environment).
9
+ */
10
+ /** Captured form field value keyed by name attribute. */
11
+ interface FormFieldSnapshot {
12
+ value: string;
13
+ checked: boolean;
14
+ selectedIndex: number;
15
+ type: string;
16
+ }
17
+ /** Captured focus state. */
18
+ interface FocusSnapshot {
19
+ /** The name or id used to locate the element in the new tree. */
20
+ matchKey: string;
21
+ /** Whether matchKey is a name or id attribute. */
22
+ matchBy: "name" | "id";
23
+ /** Selection start for input/textarea. -1 if not applicable. */
24
+ selectionStart: number;
25
+ /** Selection end for input/textarea. -1 if not applicable. */
26
+ selectionEnd: number;
27
+ }
28
+ /** Captured scroll position for a single element. */
29
+ interface ScrollSnapshot {
30
+ /** Key to locate the element: id value or tagName.className */
31
+ matchKey: string;
32
+ matchBy: "id" | "selector";
33
+ scrollTop: number;
34
+ scrollLeft: number;
35
+ }
36
+ /** Complete DOM state snapshot for a component tree. */
37
+ interface DOMStateSnapshot {
38
+ formFields: Map<string, FormFieldSnapshot>;
39
+ focus: FocusSnapshot | null;
40
+ scrollPositions: ScrollSnapshot[];
41
+ }
42
+ /**
43
+ * Capture transient DOM state from a component's element tree.
44
+ * Returns a snapshot that can be applied to a new tree via restoreDOMState.
45
+ */
46
+ declare function captureDOMState(element: Element): DOMStateSnapshot;
47
+ /**
48
+ * Restore previously captured DOM state to a new component tree.
49
+ */
50
+ declare function restoreDOMState(newElement: Element, snapshot: DOMStateSnapshot): void;
51
+ export { restoreDOMState, captureDOMState, ScrollSnapshot, FormFieldSnapshot, FocusSnapshot, DOMStateSnapshot };
@@ -0,0 +1,10 @@
1
+ // @bun
2
+ import {
3
+ captureDOMState,
4
+ restoreDOMState
5
+ } from "../shared/chunk-2qsqp9xj.js";
6
+ import"../shared/chunk-eb80r8e8.js";
7
+ export {
8
+ restoreDOMState,
9
+ captureDOMState
10
+ };
@@ -0,0 +1,43 @@
1
+ import { getContextScope } from "@vertz/ui/internals";
2
+ /** Disposal cleanup function. */
3
+ type DisposeFn = () => void;
4
+ /** Derive ContextScope from the actual return type of getContextScope. */
5
+ type ContextScope = NonNullable<ReturnType<typeof getContextScope>>;
6
+ /** Signal ref used for state preservation. Has peek() and settable value. */
7
+ interface SignalRef {
8
+ peek(): unknown;
9
+ value: unknown;
10
+ }
11
+ /**
12
+ * Register or update a component factory in the registry.
13
+ *
14
+ * Called at module top-level after each component definition.
15
+ * On first load: creates a new record. On HMR re-evaluation: updates
16
+ * the factory reference (instances are preserved for __$refreshPerform).
17
+ */
18
+ declare function __$refreshReg(moduleId: string, name: string, factory: (...args: unknown[]) => HTMLElement): void;
19
+ /**
20
+ * Track a live component instance for HMR replacement.
21
+ *
22
+ * Called by the wrapper injected around each component. Captures the DOM
23
+ * element, disposal cleanups, and context scope. Returns the element
24
+ * unchanged (transparent to callers).
25
+ *
26
+ * Prunes stale instances (elements no longer in the DOM) on each call
27
+ * to prevent memory leaks from navigated-away pages.
28
+ */
29
+ declare function __$refreshTrack(moduleId: string, name: string, element: HTMLElement, args: unknown[], cleanups: DisposeFn[], contextScope: ContextScope | null, signals?: SignalRef[]): HTMLElement;
30
+ /**
31
+ * Perform hot replacement for all components in a module.
32
+ *
33
+ * Called from the HMR accept handler after module re-evaluation.
34
+ * For each component with tracked instances:
35
+ * 1. Skip instances whose elements are no longer in the DOM
36
+ * 2. Run old cleanups (LIFO order via runCleanups)
37
+ * 3. Create a new disposal scope + restore context
38
+ * 4. Re-execute the factory to get a new DOM element
39
+ * 5. Replace the old element in the DOM
40
+ * 6. Update the instance record
41
+ */
42
+ declare function __$refreshPerform(moduleId: string): void;
43
+ export { __$refreshTrack, __$refreshReg, __$refreshPerform };
@@ -0,0 +1,150 @@
1
+ // @bun
2
+ import {
3
+ captureDOMState,
4
+ restoreDOMState
5
+ } from "../shared/chunk-2qsqp9xj.js";
6
+ import"../shared/chunk-eb80r8e8.js";
7
+
8
+ // src/bun-plugin/fast-refresh-runtime.ts
9
+ import {
10
+ _tryOnCleanup,
11
+ getContextScope,
12
+ popScope,
13
+ pushScope,
14
+ runCleanups,
15
+ setContextScope,
16
+ startSignalCollection,
17
+ stopSignalCollection
18
+ } from "@vertz/ui/internals";
19
+ if (undefined)
20
+ ;
21
+ var REGISTRY_KEY = Symbol.for("vertz:fast-refresh:registry");
22
+ var DIRTY_KEY = Symbol.for("vertz:fast-refresh:dirty");
23
+ var registry = globalThis[REGISTRY_KEY] ??= new Map;
24
+ var dirtyModules = globalThis[DIRTY_KEY] ??= new Set;
25
+ var performingRefresh = false;
26
+ function getModule(moduleId) {
27
+ let mod = registry.get(moduleId);
28
+ if (!mod) {
29
+ mod = new Map;
30
+ registry.set(moduleId, mod);
31
+ }
32
+ return mod;
33
+ }
34
+ function __$refreshReg(moduleId, name, factory) {
35
+ const mod = getModule(moduleId);
36
+ const existing = mod.get(name);
37
+ if (existing) {
38
+ existing.factory = factory;
39
+ dirtyModules.add(moduleId);
40
+ } else {
41
+ mod.set(name, { factory, instances: [] });
42
+ }
43
+ }
44
+ function __$refreshTrack(moduleId, name, element, args, cleanups, contextScope, signals = []) {
45
+ if (performingRefresh)
46
+ return element;
47
+ const mod = registry.get(moduleId);
48
+ if (!mod)
49
+ return element;
50
+ const record = mod.get(name);
51
+ if (!record)
52
+ return element;
53
+ record.instances.push({ element, args, cleanups, contextScope, signals });
54
+ return element;
55
+ }
56
+ function __$refreshPerform(moduleId) {
57
+ if (!dirtyModules.has(moduleId))
58
+ return;
59
+ dirtyModules.delete(moduleId);
60
+ const mod = registry.get(moduleId);
61
+ if (!mod)
62
+ return;
63
+ performingRefresh = true;
64
+ for (const [name, record] of mod) {
65
+ const { factory, instances } = record;
66
+ const updatedInstances = [];
67
+ for (const instance of instances) {
68
+ const { element, args, cleanups, contextScope, signals: oldSignals } = instance;
69
+ const parent = element.parentNode;
70
+ if (!parent)
71
+ continue;
72
+ const savedValues = oldSignals.map((s) => s.peek());
73
+ const newCleanups = pushScope();
74
+ const prevScope = setContextScope(contextScope);
75
+ let newElement;
76
+ let newSignals;
77
+ let newContextScope;
78
+ try {
79
+ startSignalCollection();
80
+ newElement = factory(...args);
81
+ newSignals = stopSignalCollection();
82
+ newContextScope = getContextScope();
83
+ } catch (err) {
84
+ stopSignalCollection();
85
+ runCleanups(newCleanups);
86
+ popScope();
87
+ setContextScope(prevScope);
88
+ console.error(`[vertz-hmr] Error re-mounting ${name}:`, err);
89
+ updatedInstances.push(instance);
90
+ continue;
91
+ }
92
+ if (savedValues.length > 0 && newSignals.length === savedValues.length) {
93
+ for (let i = 0;i < newSignals.length; i++) {
94
+ const sig = newSignals[i];
95
+ if (sig)
96
+ sig.value = savedValues[i];
97
+ }
98
+ } else if (savedValues.length > 0 && newSignals.length !== savedValues.length) {
99
+ console.warn(`[vertz-hmr] Signal count changed in ${name} ` + `(${savedValues.length} \u2192 ${newSignals.length}). State reset.`);
100
+ }
101
+ popScope();
102
+ runCleanups(cleanups);
103
+ if (newCleanups.length > 0) {
104
+ _tryOnCleanup(() => runCleanups(newCleanups));
105
+ }
106
+ setContextScope(prevScope);
107
+ let domSnapshot = null;
108
+ try {
109
+ domSnapshot = captureDOMState(element);
110
+ } catch (_) {}
111
+ parent.replaceChild(newElement, element);
112
+ if (domSnapshot) {
113
+ try {
114
+ restoreDOMState(newElement, domSnapshot);
115
+ } catch (_) {
116
+ console.warn("[vertz-hmr] Failed to restore DOM state");
117
+ }
118
+ }
119
+ updatedInstances.push({
120
+ element: newElement,
121
+ args,
122
+ cleanups: newCleanups,
123
+ contextScope: newContextScope,
124
+ signals: newSignals
125
+ });
126
+ }
127
+ record.instances = updatedInstances;
128
+ }
129
+ performingRefresh = false;
130
+ console.log(`[vertz-hmr] Hot updated: ${moduleId}`);
131
+ }
132
+ var FR_KEY = Symbol.for("vertz:fast-refresh");
133
+ globalThis[FR_KEY] = {
134
+ __$refreshReg,
135
+ __$refreshTrack,
136
+ __$refreshPerform,
137
+ pushScope,
138
+ popScope,
139
+ _tryOnCleanup,
140
+ runCleanups,
141
+ getContextScope,
142
+ setContextScope,
143
+ startSignalCollection,
144
+ stopSignalCollection
145
+ };
146
+ export {
147
+ __$refreshTrack,
148
+ __$refreshReg,
149
+ __$refreshPerform
150
+ };
@@ -0,0 +1,120 @@
1
+ type ErrorCategory = "build" | "resolve" | "runtime" | "ssr";
2
+ import { CSSExtractionResult } from "@vertz/ui-compiler";
3
+ type DebugCategory = "plugin" | "ssr" | "watcher" | "ws";
4
+ interface DebugLogger {
5
+ log(category: DebugCategory, message: string, data?: Record<string, unknown>): void;
6
+ isEnabled(category: DebugCategory): boolean;
7
+ }
8
+ interface DiagnosticsSnapshot {
9
+ status: "ok";
10
+ uptime: number;
11
+ plugin: {
12
+ filter: string;
13
+ hmr: boolean;
14
+ fastRefresh: boolean;
15
+ processedFiles: string[];
16
+ processedCount: number;
17
+ };
18
+ ssr: {
19
+ moduleStatus: "pending" | "loaded" | "error";
20
+ lastReloadTime: string | null;
21
+ lastReloadDurationMs: number | null;
22
+ lastReloadError: string | null;
23
+ reloadCount: number;
24
+ failedReloadCount: number;
25
+ };
26
+ hmr: {
27
+ bundledScriptUrl: string | null;
28
+ bootstrapDiscovered: boolean;
29
+ };
30
+ errors: {
31
+ current: ErrorCategory | null;
32
+ lastCategory: ErrorCategory | null;
33
+ lastMessage: string | null;
34
+ };
35
+ websocket: {
36
+ connectedClients: number;
37
+ };
38
+ watcher: {
39
+ lastChangedFile: string | null;
40
+ lastChangeTime: string | null;
41
+ };
42
+ }
43
+ declare class DiagnosticsCollector {
44
+ private startTime;
45
+ private pluginFilter;
46
+ private pluginHmr;
47
+ private pluginFastRefresh;
48
+ private processedFilesSet;
49
+ private processedCount;
50
+ private ssrModuleStatus;
51
+ private ssrLastReloadTime;
52
+ private ssrLastReloadDurationMs;
53
+ private ssrLastReloadError;
54
+ private ssrReloadCount;
55
+ private ssrFailedReloadCount;
56
+ private hmrBundledScriptUrl;
57
+ private hmrBootstrapDiscovered;
58
+ private errorCurrent;
59
+ private errorLastCategory;
60
+ private errorLastMessage;
61
+ private wsConnectedClients;
62
+ private watcherLastChangedFile;
63
+ private watcherLastChangeTime;
64
+ recordPluginConfig(filter: string, hmr: boolean, fastRefresh: boolean): void;
65
+ recordPluginProcess(file: string): void;
66
+ recordSSRReload(success: boolean, durationMs: number, error?: string): void;
67
+ recordHMRAssets(bundledScriptUrl: string | null, bootstrapDiscovered: boolean): void;
68
+ recordError(category: ErrorCategory, message: string): void;
69
+ recordErrorClear(): void;
70
+ recordWebSocketChange(count: number): void;
71
+ recordFileChange(file: string): void;
72
+ getSnapshot(): DiagnosticsSnapshot;
73
+ }
74
+ import { BunPlugin as BunPlugin_seob6 } from "bun";
75
+ interface VertzBunPluginOptions {
76
+ /** Regex filter for files to transform. Defaults to .tsx files. */
77
+ filter?: RegExp;
78
+ /** Compilation target. 'dom' (default) or 'tui'. */
79
+ target?: "dom" | "tui";
80
+ /**
81
+ * Directory for CSS sidecar files.
82
+ * Defaults to `.vertz/css` relative to the project root.
83
+ */
84
+ cssOutDir?: string;
85
+ /** Enable HMR support (import.meta.hot.accept). Defaults to true. */
86
+ hmr?: boolean;
87
+ /**
88
+ * Enable Fast Refresh (component-level HMR).
89
+ * When true, components are wrapped with tracking code and re-mounted
90
+ * on file change instead of doing a full page reload.
91
+ * Defaults to true when hmr is true.
92
+ */
93
+ fastRefresh?: boolean;
94
+ /** Project root for computing relative paths. */
95
+ projectRoot?: string;
96
+ /** Debug logger for opt-in diagnostic logging. */
97
+ logger?: DebugLogger;
98
+ /** Diagnostics collector for the health check endpoint. */
99
+ diagnostics?: DiagnosticsCollector;
100
+ }
101
+ /** CSS extractions tracked across all transformed files (for dead CSS elimination). */
102
+ type FileExtractionsMap = Map<string, CSSExtractionResult>;
103
+ /** Map of source file path to CSS sidecar file path (for debugging). */
104
+ type CSSSidecarMap = Map<string, string>;
105
+ interface VertzBunPluginResult {
106
+ /** The Bun plugin to pass to Bun.build or bunfig.toml. */
107
+ plugin: BunPlugin_seob6;
108
+ /** CSS extractions for all transformed files (for production dead CSS elimination). */
109
+ fileExtractions: FileExtractionsMap;
110
+ /** Map of source file to CSS sidecar file path (for debugging). */
111
+ cssSidecarMap: CSSSidecarMap;
112
+ }
113
+ /**
114
+ * Create a Vertz Bun plugin with CSS sidecar support and optional Fast Refresh.
115
+ *
116
+ * Returns the plugin along with maps for CSS extractions and sidecar paths,
117
+ * which build scripts need for dead CSS elimination.
118
+ */
119
+ declare function createVertzBunPlugin(options?: VertzBunPluginOptions): VertzBunPluginResult;
120
+ export { createVertzBunPlugin, VertzBunPluginResult, VertzBunPluginOptions, FileExtractionsMap, CSSSidecarMap };
@@ -0,0 +1,216 @@
1
+ // @bun
2
+ import"../shared/chunk-eb80r8e8.js";
3
+
4
+ // src/bun-plugin/plugin.ts
5
+ import { mkdirSync, writeFileSync } from "fs";
6
+ import { dirname, relative, resolve } from "path";
7
+ import remapping from "@ampproject/remapping";
8
+ import { ComponentAnalyzer, CSSExtractor, compile, HydrationTransformer } from "@vertz/ui-compiler";
9
+ import MagicString from "magic-string";
10
+ import { Project, ts as ts2 } from "ts-morph";
11
+
12
+ // src/bun-plugin/context-stable-ids.ts
13
+ import { ts } from "ts-morph";
14
+ function injectContextStableIds(source, sourceFile, relFilePath) {
15
+ for (const stmt of sourceFile.getStatements()) {
16
+ if (!ts.isVariableStatement(stmt.compilerNode))
17
+ continue;
18
+ for (const decl of stmt.compilerNode.declarationList.declarations) {
19
+ if (!decl.initializer || !ts.isCallExpression(decl.initializer))
20
+ continue;
21
+ const callText = decl.initializer.expression.getText(sourceFile.compilerNode);
22
+ if (callText !== "createContext")
23
+ continue;
24
+ if (!ts.isIdentifier(decl.name))
25
+ continue;
26
+ const varName = decl.name.text;
27
+ const escapedPath = relFilePath.replace(/['\\]/g, "\\$&");
28
+ const stableId = `${escapedPath}::${varName}`;
29
+ const callExpr = decl.initializer;
30
+ const argsArr = callExpr.arguments;
31
+ if (argsArr.length === 0) {
32
+ const closeParenPos = callExpr.end - 1;
33
+ source.appendLeft(closeParenPos, `undefined, '${stableId}'`);
34
+ } else {
35
+ const lastArg = argsArr[argsArr.length - 1];
36
+ if (lastArg)
37
+ source.appendLeft(lastArg.end, `, '${stableId}'`);
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ // src/bun-plugin/fast-refresh-codegen.ts
44
+ function generateRefreshPreamble(moduleId) {
45
+ const escapedId = moduleId.replace(/['\\]/g, "\\$&");
46
+ return `const __$fr = globalThis[Symbol.for('vertz:fast-refresh')];
47
+ ` + `const { __$refreshReg, __$refreshTrack, __$refreshPerform, ` + `pushScope: __$pushScope, popScope: __$popScope, ` + `_tryOnCleanup: __$tryCleanup, runCleanups: __$runCleanups, ` + `getContextScope: __$getCtx, setContextScope: __$setCtx, ` + `startSignalCollection: __$startSigCol, stopSignalCollection: __$stopSigCol } = __$fr;
48
+ ` + `const __$moduleId = '${escapedId}';
49
+ `;
50
+ }
51
+ function generateRefreshWrapper(componentName) {
52
+ return `
53
+ const __$orig_${componentName} = ${componentName};
54
+ ` + `${componentName} = function(...__$args) {
55
+ ` + ` const __$scope = __$pushScope();
56
+ ` + ` const __$ctx = __$getCtx();
57
+ ` + ` __$startSigCol();
58
+ ` + ` const __$ret = __$orig_${componentName}.apply(this, __$args);
59
+ ` + ` const __$sigs = __$stopSigCol();
60
+ ` + ` __$popScope();
61
+ ` + ` if (__$scope.length > 0) {
62
+ ` + ` __$tryCleanup(() => __$runCleanups(__$scope));
63
+ ` + ` }
64
+ ` + ` return __$refreshTrack(__$moduleId, '${componentName}', __$ret, __$args, __$scope, __$ctx, __$sigs);
65
+ ` + `};
66
+ ` + `__$refreshReg(__$moduleId, '${componentName}', ${componentName});
67
+ `;
68
+ }
69
+ function generateRefreshPerform() {
70
+ return `__$refreshPerform(__$moduleId);
71
+ `;
72
+ }
73
+ function generateRefreshCode(moduleId, components) {
74
+ if (components.length === 0)
75
+ return null;
76
+ const preamble = generateRefreshPreamble(moduleId);
77
+ let epilogue = "";
78
+ for (const comp of components) {
79
+ epilogue += generateRefreshWrapper(comp.name);
80
+ }
81
+ epilogue += generateRefreshPerform();
82
+ return { preamble, epilogue };
83
+ }
84
+
85
+ // src/bun-plugin/file-path-hash.ts
86
+ function filePathHash(filePath) {
87
+ let hash = 5381;
88
+ for (let i = 0;i < filePath.length; i++) {
89
+ hash = (hash << 5) + hash + filePath.charCodeAt(i) >>> 0;
90
+ }
91
+ return hash.toString(36);
92
+ }
93
+
94
+ // src/bun-plugin/plugin.ts
95
+ function createVertzBunPlugin(options) {
96
+ const filter = options?.filter ?? /\.tsx$/;
97
+ const hmr = options?.hmr ?? true;
98
+ const fastRefresh = options?.fastRefresh ?? hmr;
99
+ const projectRoot = options?.projectRoot ?? process.cwd();
100
+ const cssOutDir = options?.cssOutDir ?? resolve(projectRoot, ".vertz", "css");
101
+ const cssExtractor = new CSSExtractor;
102
+ const componentAnalyzer = new ComponentAnalyzer;
103
+ const logger = options?.logger;
104
+ const diagnostics = options?.diagnostics;
105
+ const fileExtractions = new Map;
106
+ const cssSidecarMap = new Map;
107
+ mkdirSync(cssOutDir, { recursive: true });
108
+ const plugin = {
109
+ name: "vertz-bun-plugin",
110
+ setup(build) {
111
+ build.onLoad({ filter }, async (args) => {
112
+ try {
113
+ const startMs = logger?.isEnabled("plugin") ? performance.now() : 0;
114
+ const source = await Bun.file(args.path).text();
115
+ const relPath = relative(projectRoot, args.path);
116
+ logger?.log("plugin", "onLoad", { file: relPath, bytes: source.length });
117
+ const hydrationS = new MagicString(source);
118
+ const hydrationProject = new Project({
119
+ useInMemoryFileSystem: true,
120
+ compilerOptions: {
121
+ jsx: ts2.JsxEmit.Preserve,
122
+ strict: true
123
+ }
124
+ });
125
+ const hydrationSourceFile = hydrationProject.createSourceFile(args.path, source);
126
+ const hydrationTransformer = new HydrationTransformer;
127
+ hydrationTransformer.transform(hydrationS, hydrationSourceFile);
128
+ if (fastRefresh) {
129
+ const relFilePath = relative(projectRoot, args.path);
130
+ injectContextStableIds(hydrationS, hydrationSourceFile, relFilePath);
131
+ }
132
+ const hydratedCode = hydrationS.toString();
133
+ const hydrationMap = hydrationS.generateMap({
134
+ source: args.path,
135
+ includeContent: true
136
+ });
137
+ const compileResult = compile(hydratedCode, {
138
+ filename: args.path,
139
+ target: options?.target
140
+ });
141
+ const remapped = remapping([compileResult.map, hydrationMap], () => null);
142
+ const extraction = cssExtractor.extract(source, args.path);
143
+ let cssImportLine = "";
144
+ if (extraction.css.length > 0) {
145
+ fileExtractions.set(args.path, extraction);
146
+ if (hmr) {
147
+ const hash = filePathHash(args.path);
148
+ const cssFileName = `${hash}.css`;
149
+ const cssFilePath = resolve(cssOutDir, cssFileName);
150
+ writeFileSync(cssFilePath, extraction.css);
151
+ cssSidecarMap.set(args.path, cssFilePath);
152
+ const relPath2 = relative(dirname(args.path), cssFilePath);
153
+ const importPath = relPath2.startsWith(".") ? relPath2 : `./${relPath2}`;
154
+ cssImportLine = `import '${importPath}';
155
+ `;
156
+ }
157
+ }
158
+ let refreshPreamble = "";
159
+ let refreshEpilogue = "";
160
+ if (fastRefresh) {
161
+ const components = componentAnalyzer.analyze(hydrationSourceFile);
162
+ const refreshCode = generateRefreshCode(args.path, components);
163
+ if (refreshCode) {
164
+ refreshPreamble = refreshCode.preamble;
165
+ refreshEpilogue = refreshCode.epilogue;
166
+ }
167
+ }
168
+ const mapBase64 = Buffer.from(remapped.toString()).toString("base64");
169
+ const sourceMapComment = `
170
+ //# sourceMappingURL=data:application/json;base64,${mapBase64}`;
171
+ let contents = "";
172
+ if (cssImportLine) {
173
+ contents += cssImportLine;
174
+ }
175
+ if (refreshPreamble) {
176
+ contents += refreshPreamble;
177
+ }
178
+ contents += compileResult.code;
179
+ if (refreshEpilogue) {
180
+ contents += refreshEpilogue;
181
+ }
182
+ if (hmr) {
183
+ contents += `
184
+ import.meta.hot.accept();
185
+ `;
186
+ }
187
+ contents += sourceMapComment;
188
+ if (logger?.isEnabled("plugin")) {
189
+ const durationMs = Math.round(performance.now() - startMs);
190
+ const stages = [
191
+ "hydration",
192
+ fastRefresh ? "stableIds" : null,
193
+ "compile",
194
+ "sourceMap",
195
+ extraction.css.length > 0 ? "css" : null,
196
+ fastRefresh && refreshPreamble ? "fastRefresh" : null,
197
+ hmr ? "hmr" : null
198
+ ].filter(Boolean).join(",");
199
+ logger.log("plugin", "done", { file: relPath, durationMs, stages });
200
+ }
201
+ diagnostics?.recordPluginProcess(relPath);
202
+ return { contents, loader: "tsx" };
203
+ } catch (err) {
204
+ const message = err instanceof Error ? err.message : String(err);
205
+ const relPath = relative(projectRoot, args.path);
206
+ console.error(`[vertz-bun-plugin] Failed to process ${relPath}:`, message);
207
+ throw err;
208
+ }
209
+ });
210
+ }
211
+ };
212
+ return { plugin, fileExtractions, cssSidecarMap };
213
+ }
214
+ export {
215
+ createVertzBunPlugin
216
+ };
@@ -22,6 +22,18 @@ declare class SSRNode {
22
22
  replaceChild(newNode: SSRNode, oldNode: SSRNode): SSRNode;
23
23
  }
24
24
  /**
25
+ * SSR comment node — serialized as `<!-- text -->` in HTML output.
26
+ *
27
+ * Used by __conditional to emit anchor comment nodes that the client-side
28
+ * hydration cursor can claim during mount.
29
+ */
30
+ declare class SSRComment extends SSRNode {
31
+ text: string;
32
+ constructor(text: string);
33
+ get data(): string;
34
+ set data(value: string);
35
+ }
36
+ /**
25
37
  * SSR text node
26
38
  */
27
39
  declare class SSRTextNode extends SSRNode {
@@ -43,16 +55,23 @@ declare class SSRDocumentFragment extends SSRNode {
43
55
  declare class SSRElement extends SSRNode {
44
56
  tag: string;
45
57
  attrs: Record<string, string>;
46
- children: (SSRElement | string)[];
58
+ children: (SSRElement | SSRComment | string)[];
47
59
  _classList: Set<string>;
48
60
  _textContent: string | null;
49
61
  _innerHTML: string | null;
50
- style: Record<string, any>;
62
+ style: {
63
+ display: string;
64
+ [key: string]: any;
65
+ };
51
66
  constructor(tag: string);
52
67
  setAttribute(name: string, value: string): void;
53
68
  getAttribute(name: string): string | null;
54
69
  removeAttribute(name: string): void;
55
- appendChild(child: SSRElement | SSRTextNode | SSRDocumentFragment): void;
70
+ appendChild(child: SSRElement | SSRTextNode | SSRComment | SSRDocumentFragment): void;
71
+ insertBefore(newNode: SSRNode, referenceNode: SSRNode | null): SSRNode;
72
+ replaceChild(newNode: SSRNode, oldNode: SSRNode): SSRNode;
73
+ /** Find a node's index in the children array via childNodes identity lookup. */
74
+ private _findChildIndex;
56
75
  removeChild(child: SSRNode): SSRNode;
57
76
  get classList(): {
58
77
  add: (cls: string) => void;
@@ -70,15 +89,27 @@ declare class SSRElement extends SSRNode {
70
89
  toVNode(): VNode;
71
90
  }
72
91
  /**
73
- * Create and install the DOM shim
92
+ * Create and install the DOM shim.
93
+ *
94
+ * @deprecated Use `setAdapter(createSSRAdapter())` instead.
95
+ * This function is kept for backward compatibility — it installs the
96
+ * SSR adapter and the global DOM shim. New code should use the adapter
97
+ * directly via `setAdapter()`.
74
98
  */
75
99
  declare function installDomShim(): void;
76
100
  /**
77
- * Remove the DOM shim
101
+ * Remove the DOM shim.
102
+ *
103
+ * @deprecated Use `setAdapter(null)` instead.
104
+ * This function is kept for backward compatibility.
105
+ *
106
+ * If globals existed before installDomShim() (e.g., happydom in a test runner),
107
+ * they are restored instead of deleted. This prevents contamination in
108
+ * single-process test runners like `bun test`.
78
109
  */
79
110
  declare function removeDomShim(): void;
80
111
  /**
81
112
  * Convert an SSRElement to a VNode
82
113
  */
83
114
  declare function toVNode2(element: any): VNode;
84
- export { toVNode2 as toVNode, removeDomShim, installDomShim, SSRTextNode, SSRNode, SSRElement, SSRDocumentFragment };
115
+ export { toVNode2 as toVNode, removeDomShim, installDomShim, SSRTextNode, SSRNode, SSRElement, SSRDocumentFragment, SSRComment };