@vertz/ui-server 0.2.0 → 0.2.3
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/README.md +298 -310
- package/dist/bun-dev-server.d.ts +115 -0
- package/dist/bun-dev-server.js +2032 -0
- package/dist/bun-plugin/fast-refresh-dom-state.d.ts +51 -0
- package/dist/bun-plugin/fast-refresh-dom-state.js +10 -0
- package/dist/bun-plugin/fast-refresh-runtime.d.ts +43 -0
- package/dist/bun-plugin/fast-refresh-runtime.js +150 -0
- package/dist/bun-plugin/index.d.ts +44 -0
- package/dist/bun-plugin/index.js +197 -0
- package/dist/dom-shim/index.d.ts +37 -6
- package/dist/dom-shim/index.js +12 -324
- package/dist/index.d.ts +331 -64
- package/dist/index.js +285 -292
- package/dist/jsx-runtime/index.js +15 -2
- package/dist/shared/chunk-2qsqp9xj.js +150 -0
- package/dist/shared/chunk-32688jav.js +564 -0
- package/dist/shared/chunk-4t0ekdyv.js +513 -0
- package/dist/shared/chunk-eb80r8e8.js +4 -0
- package/dist/ssr/index.d.ts +86 -0
- package/dist/ssr/index.js +11 -0
- package/package.json +35 -18
|
@@ -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,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,44 @@
|
|
|
1
|
+
import { CSSExtractionResult } from "@vertz/ui-compiler";
|
|
2
|
+
import { BunPlugin as BunPlugin_seob6 } from "bun";
|
|
3
|
+
interface VertzBunPluginOptions {
|
|
4
|
+
/** Regex filter for files to transform. Defaults to .tsx files. */
|
|
5
|
+
filter?: RegExp;
|
|
6
|
+
/** Compilation target. 'dom' (default) or 'tui'. */
|
|
7
|
+
target?: "dom" | "tui";
|
|
8
|
+
/**
|
|
9
|
+
* Directory for CSS sidecar files.
|
|
10
|
+
* Defaults to `.vertz/css` relative to the project root.
|
|
11
|
+
*/
|
|
12
|
+
cssOutDir?: string;
|
|
13
|
+
/** Enable HMR support (import.meta.hot.accept). Defaults to true. */
|
|
14
|
+
hmr?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Enable Fast Refresh (component-level HMR).
|
|
17
|
+
* When true, components are wrapped with tracking code and re-mounted
|
|
18
|
+
* on file change instead of doing a full page reload.
|
|
19
|
+
* Defaults to true when hmr is true.
|
|
20
|
+
*/
|
|
21
|
+
fastRefresh?: boolean;
|
|
22
|
+
/** Project root for computing relative paths. */
|
|
23
|
+
projectRoot?: string;
|
|
24
|
+
}
|
|
25
|
+
/** CSS extractions tracked across all transformed files (for dead CSS elimination). */
|
|
26
|
+
type FileExtractionsMap = Map<string, CSSExtractionResult>;
|
|
27
|
+
/** Map of source file path to CSS sidecar file path (for debugging). */
|
|
28
|
+
type CSSSidecarMap = Map<string, string>;
|
|
29
|
+
interface VertzBunPluginResult {
|
|
30
|
+
/** The Bun plugin to pass to Bun.build or bunfig.toml. */
|
|
31
|
+
plugin: BunPlugin_seob6;
|
|
32
|
+
/** CSS extractions for all transformed files (for production dead CSS elimination). */
|
|
33
|
+
fileExtractions: FileExtractionsMap;
|
|
34
|
+
/** Map of source file to CSS sidecar file path (for debugging). */
|
|
35
|
+
cssSidecarMap: CSSSidecarMap;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create a Vertz Bun plugin with CSS sidecar support and optional Fast Refresh.
|
|
39
|
+
*
|
|
40
|
+
* Returns the plugin along with maps for CSS extractions and sidecar paths,
|
|
41
|
+
* which build scripts need for dead CSS elimination.
|
|
42
|
+
*/
|
|
43
|
+
declare function createVertzBunPlugin(options?: VertzBunPluginOptions): VertzBunPluginResult;
|
|
44
|
+
export { createVertzBunPlugin, VertzBunPluginResult, VertzBunPluginOptions, FileExtractionsMap, CSSSidecarMap };
|
|
@@ -0,0 +1,197 @@
|
|
|
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 fileExtractions = new Map;
|
|
104
|
+
const cssSidecarMap = new Map;
|
|
105
|
+
mkdirSync(cssOutDir, { recursive: true });
|
|
106
|
+
const plugin = {
|
|
107
|
+
name: "vertz-bun-plugin",
|
|
108
|
+
setup(build) {
|
|
109
|
+
build.onLoad({ filter }, async (args) => {
|
|
110
|
+
try {
|
|
111
|
+
const source = await Bun.file(args.path).text();
|
|
112
|
+
const hydrationS = new MagicString(source);
|
|
113
|
+
const hydrationProject = new Project({
|
|
114
|
+
useInMemoryFileSystem: true,
|
|
115
|
+
compilerOptions: {
|
|
116
|
+
jsx: ts2.JsxEmit.Preserve,
|
|
117
|
+
strict: true
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
const hydrationSourceFile = hydrationProject.createSourceFile(args.path, source);
|
|
121
|
+
const hydrationTransformer = new HydrationTransformer;
|
|
122
|
+
hydrationTransformer.transform(hydrationS, hydrationSourceFile);
|
|
123
|
+
if (fastRefresh) {
|
|
124
|
+
const relFilePath = relative(projectRoot, args.path);
|
|
125
|
+
injectContextStableIds(hydrationS, hydrationSourceFile, relFilePath);
|
|
126
|
+
}
|
|
127
|
+
const hydratedCode = hydrationS.toString();
|
|
128
|
+
const hydrationMap = hydrationS.generateMap({
|
|
129
|
+
source: args.path,
|
|
130
|
+
includeContent: true
|
|
131
|
+
});
|
|
132
|
+
const compileResult = compile(hydratedCode, {
|
|
133
|
+
filename: args.path,
|
|
134
|
+
target: options?.target
|
|
135
|
+
});
|
|
136
|
+
const remapped = remapping([compileResult.map, hydrationMap], () => null);
|
|
137
|
+
const extraction = cssExtractor.extract(source, args.path);
|
|
138
|
+
let cssImportLine = "";
|
|
139
|
+
if (extraction.css.length > 0) {
|
|
140
|
+
fileExtractions.set(args.path, extraction);
|
|
141
|
+
if (hmr) {
|
|
142
|
+
const hash = filePathHash(args.path);
|
|
143
|
+
const cssFileName = `${hash}.css`;
|
|
144
|
+
const cssFilePath = resolve(cssOutDir, cssFileName);
|
|
145
|
+
writeFileSync(cssFilePath, extraction.css);
|
|
146
|
+
cssSidecarMap.set(args.path, cssFilePath);
|
|
147
|
+
const relPath = relative(dirname(args.path), cssFilePath);
|
|
148
|
+
const importPath = relPath.startsWith(".") ? relPath : `./${relPath}`;
|
|
149
|
+
cssImportLine = `import '${importPath}';
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
let refreshPreamble = "";
|
|
154
|
+
let refreshEpilogue = "";
|
|
155
|
+
if (fastRefresh) {
|
|
156
|
+
const components = componentAnalyzer.analyze(hydrationSourceFile);
|
|
157
|
+
const refreshCode = generateRefreshCode(args.path, components);
|
|
158
|
+
if (refreshCode) {
|
|
159
|
+
refreshPreamble = refreshCode.preamble;
|
|
160
|
+
refreshEpilogue = refreshCode.epilogue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const mapBase64 = Buffer.from(remapped.toString()).toString("base64");
|
|
164
|
+
const sourceMapComment = `
|
|
165
|
+
//# sourceMappingURL=data:application/json;base64,${mapBase64}`;
|
|
166
|
+
let contents = "";
|
|
167
|
+
if (cssImportLine) {
|
|
168
|
+
contents += cssImportLine;
|
|
169
|
+
}
|
|
170
|
+
if (refreshPreamble) {
|
|
171
|
+
contents += refreshPreamble;
|
|
172
|
+
}
|
|
173
|
+
contents += compileResult.code;
|
|
174
|
+
if (refreshEpilogue) {
|
|
175
|
+
contents += refreshEpilogue;
|
|
176
|
+
}
|
|
177
|
+
if (hmr) {
|
|
178
|
+
contents += `
|
|
179
|
+
import.meta.hot.accept();
|
|
180
|
+
`;
|
|
181
|
+
}
|
|
182
|
+
contents += sourceMapComment;
|
|
183
|
+
return { contents, loader: "tsx" };
|
|
184
|
+
} catch (err) {
|
|
185
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
186
|
+
const relPath = relative(projectRoot, args.path);
|
|
187
|
+
console.error(`[vertz-bun-plugin] Failed to process ${relPath}:`, message);
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
return { plugin, fileExtractions, cssSidecarMap };
|
|
194
|
+
}
|
|
195
|
+
export {
|
|
196
|
+
createVertzBunPlugin
|
|
197
|
+
};
|
package/dist/dom-shim/index.d.ts
CHANGED
|
@@ -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:
|
|
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 };
|