@relicloops/cathode 2.0.0 → 2.1.0-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.
- package/esbuild-plugin.js +75 -0
- package/hmr.js +71 -0
- package/lib/hmr-runtime.js +415 -0
- package/lib/lazy.js +3 -3
- package/lib/runtime.js +34 -11
- package/package.json +11 -1
- package/types/esbuild-plugin.d.ts +50 -0
- package/types/hmr.d.ts +21 -0
- package/types/lib/hmr-runtime.d.ts +82 -0
- package/types/lib/suspense.d.ts +1 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* esbuild plugin that automatically wraps exported JSX components with
|
|
4
|
+
* cathode's HMR proxy at build time.
|
|
5
|
+
*
|
|
6
|
+
* Only activate this plugin in watch / dev mode:
|
|
7
|
+
*
|
|
8
|
+
* plugins: [
|
|
9
|
+
* ...(watchMode ? [cathodeHMRPlugin()] : []),
|
|
10
|
+
* deployPlugin,
|
|
11
|
+
* ]
|
|
12
|
+
*
|
|
13
|
+
* The plugin detects top-level exports whose name starts with an uppercase
|
|
14
|
+
* letter (PascalCase convention for components) and rewrites:
|
|
15
|
+
*
|
|
16
|
+
* export const Foo = ( props ) => { ... };
|
|
17
|
+
* export function Bar( props ) { ... }
|
|
18
|
+
*
|
|
19
|
+
* into:
|
|
20
|
+
*
|
|
21
|
+
* import { hmr as __cathode_hmr } from '@relicloops/cathode/hmr';
|
|
22
|
+
* const _Foo = ( props ) => { ... };
|
|
23
|
+
* function _Bar( props ) { ... }
|
|
24
|
+
* export const Foo = __cathode_hmr.wrap( _Foo, import.meta.url, 'Foo' );
|
|
25
|
+
* export const Bar = __cathode_hmr.wrap( _Bar, import.meta.url, 'Bar' );
|
|
26
|
+
*
|
|
27
|
+
* Files that already contain '__cathode_hmr' or 'hmr.wrap' are left
|
|
28
|
+
* untouched so that explicit wrapping is never doubled.
|
|
29
|
+
*/
|
|
30
|
+
export function cathodeHMRPlugin(options = {}) {
|
|
31
|
+
const filter = options.filter ?? /\.tsx$/;
|
|
32
|
+
return {
|
|
33
|
+
name: 'cathode-hmr',
|
|
34
|
+
setup(build) {
|
|
35
|
+
build.onLoad({ filter }, ({ path }) => {
|
|
36
|
+
const source = readFileSync(path, 'utf8');
|
|
37
|
+
// Skip files that already opt in to explicit HMR wrapping.
|
|
38
|
+
if (source.includes('__cathode_hmr') || source.includes('hmr.wrap')) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const names = [];
|
|
42
|
+
// Match top-level: export const PascalName
|
|
43
|
+
const constRe = /^export\s+const\s+([A-Z]\w*)\b/gm;
|
|
44
|
+
// Match top-level: export function PascalName
|
|
45
|
+
const fnRe = /^export\s+function\s+([A-Z]\w*)\b/gm;
|
|
46
|
+
let m;
|
|
47
|
+
while ((m = constRe.exec(source)) !== null) {
|
|
48
|
+
names.push(m[1]);
|
|
49
|
+
}
|
|
50
|
+
while ((m = fnRe.exec(source)) !== null) {
|
|
51
|
+
names.push(m[1]);
|
|
52
|
+
}
|
|
53
|
+
if (names.length === 0) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
// Rename each exported symbol to a private name by stripping
|
|
57
|
+
// the 'export' keyword and prefixing the identifier with '_'.
|
|
58
|
+
let transformed = source;
|
|
59
|
+
for (const name of names) {
|
|
60
|
+
transformed = transformed
|
|
61
|
+
.replace(new RegExp(`^export\\s+const\\s+${name}\\b`, 'gm'), `const _${name}`)
|
|
62
|
+
.replace(new RegExp(`^export\\s+function\\s+${name}\\b`, 'gm'), `function _${name}`);
|
|
63
|
+
}
|
|
64
|
+
const hmrImport = `import { hmr as __cathode_hmr } from '@relicloops/cathode/hmr';\n`;
|
|
65
|
+
const wrapLines = names
|
|
66
|
+
.map(name => `export const ${name} = __cathode_hmr.wrap( _${name}, import.meta.url, '${name}' );`)
|
|
67
|
+
.join('\n');
|
|
68
|
+
return {
|
|
69
|
+
contents: `${hmrImport}${transformed}\n${wrapLines}\n`,
|
|
70
|
+
loader: 'tsx',
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
package/hmr.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { applyUpdate, enableDevMode, isDevMode, refreshModule, registerRoot, useCallback, useEffect, useMemo, useRef, useState, wrapComponent, } from './lib/hmr-runtime.js';
|
|
2
|
+
export const hmr = {
|
|
3
|
+
wrap: wrapComponent,
|
|
4
|
+
registerRoot,
|
|
5
|
+
applyUpdate,
|
|
6
|
+
refreshModule,
|
|
7
|
+
};
|
|
8
|
+
export { applyUpdate, enableDevMode, refreshModule, registerRoot, useCallback, useEffect, useMemo, useRef, useState, wrapComponent as wrap, };
|
|
9
|
+
export function connectHMR(options = {}) {
|
|
10
|
+
const isDev = import.meta?.env?.DEV;
|
|
11
|
+
if (!isDev && !isDevMode()) {
|
|
12
|
+
console.warn('[hmr] disabled outside dev mode');
|
|
13
|
+
return () => { };
|
|
14
|
+
}
|
|
15
|
+
const endpoint = options.endpoint ?? '/__neon/hmr';
|
|
16
|
+
const source = new EventSource(endpoint);
|
|
17
|
+
console.trace('[hmr] connecting', endpoint);
|
|
18
|
+
source.onopen = () => {
|
|
19
|
+
console.trace('[hmr] connected', endpoint);
|
|
20
|
+
};
|
|
21
|
+
source.onerror = (err) => {
|
|
22
|
+
console.trace('[hmr] error', err);
|
|
23
|
+
};
|
|
24
|
+
source.onmessage = (msg) => {
|
|
25
|
+
console.trace('[hmr] message', msg.data);
|
|
26
|
+
let data;
|
|
27
|
+
try {
|
|
28
|
+
data = JSON.parse(msg.data);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
console.trace('[hmr] parse error', msg.data);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (data.type !== 'hmr') {
|
|
35
|
+
console.trace('[hmr] ignored event type', data.type);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
console.trace('[hmr] update received', data);
|
|
39
|
+
options.onBeforeReload?.(data);
|
|
40
|
+
void handleHotUpdate(data, options);
|
|
41
|
+
};
|
|
42
|
+
return () => {
|
|
43
|
+
console.trace('[hmr] closing', endpoint);
|
|
44
|
+
source.close();
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function handleHotUpdate(data, options) {
|
|
48
|
+
const moduleUrl = normalizeModuleUrl(data.path, options.pathTransform);
|
|
49
|
+
// Skip directories and non-JS paths (CSS, HTML, media, empty string, etc.).
|
|
50
|
+
// NeonSignal fires a directory-level event in addition to file-level events;
|
|
51
|
+
// importing a directory URL returns HTML, not a JS module.
|
|
52
|
+
if (!moduleUrl.endsWith('.js')) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const mod = await import(`${moduleUrl}?t=${Date.now()}`);
|
|
57
|
+
options.onHotUpdate?.(data, moduleUrl, mod);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
options.onHotError?.(data, error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function normalizeModuleUrl(path, transform) {
|
|
64
|
+
const transformed = transform ? transform(path) : path;
|
|
65
|
+
try {
|
|
66
|
+
return new URL(transformed, window.location.origin).pathname;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return transformed;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
const rootMap = new WeakMap();
|
|
2
|
+
const moduleRegistry = new Map();
|
|
3
|
+
const scheduledRoots = new Set();
|
|
4
|
+
let devMode = false;
|
|
5
|
+
let currentInstance = null;
|
|
6
|
+
let renderNode = null;
|
|
7
|
+
let renderRoot = null;
|
|
8
|
+
export function enableDevMode() {
|
|
9
|
+
devMode = true;
|
|
10
|
+
}
|
|
11
|
+
export function isDevMode() {
|
|
12
|
+
return devMode;
|
|
13
|
+
}
|
|
14
|
+
export function setDevRenderers(nodeRenderer, rootRenderer) {
|
|
15
|
+
renderNode = nodeRenderer;
|
|
16
|
+
renderRoot = rootRenderer;
|
|
17
|
+
}
|
|
18
|
+
export function prepareRootContext(parent, node) {
|
|
19
|
+
const existing = rootMap.get(parent);
|
|
20
|
+
const root = existing ?? {
|
|
21
|
+
parent,
|
|
22
|
+
instances: [],
|
|
23
|
+
cursor: 0,
|
|
24
|
+
effects: [],
|
|
25
|
+
lastNode: node,
|
|
26
|
+
};
|
|
27
|
+
root.cursor = 0;
|
|
28
|
+
root.effects = [];
|
|
29
|
+
root.lastNode = node;
|
|
30
|
+
rootMap.set(parent, root);
|
|
31
|
+
return root;
|
|
32
|
+
}
|
|
33
|
+
export function finishRootRender(root) {
|
|
34
|
+
trimInstances(root.instances, root.cursor);
|
|
35
|
+
flushEffects(root);
|
|
36
|
+
}
|
|
37
|
+
export function createOrReuseInstance(root, parentInstance, component, props) {
|
|
38
|
+
const list = parentInstance ? parentInstance.childInstances : root.instances;
|
|
39
|
+
const index = parentInstance ? parentInstance.childCursor : root.cursor;
|
|
40
|
+
let instance = list[index];
|
|
41
|
+
if (!instance) {
|
|
42
|
+
instance = {
|
|
43
|
+
component,
|
|
44
|
+
props,
|
|
45
|
+
hooks: [],
|
|
46
|
+
hookIndex: 0,
|
|
47
|
+
childInstances: [],
|
|
48
|
+
childCursor: 0,
|
|
49
|
+
root,
|
|
50
|
+
domStart: null,
|
|
51
|
+
domEnd: null,
|
|
52
|
+
};
|
|
53
|
+
list[index] = instance;
|
|
54
|
+
}
|
|
55
|
+
else if (instance.component !== component) {
|
|
56
|
+
disposeInstance(instance);
|
|
57
|
+
instance.component = component;
|
|
58
|
+
instance.props = props;
|
|
59
|
+
instance.hooks = [];
|
|
60
|
+
instance.childInstances = [];
|
|
61
|
+
instance.childCursor = 0;
|
|
62
|
+
instance.root = root;
|
|
63
|
+
}
|
|
64
|
+
instance.component = component;
|
|
65
|
+
instance.props = props;
|
|
66
|
+
instance.hookIndex = 0;
|
|
67
|
+
instance.childCursor = 0;
|
|
68
|
+
instance.root = root;
|
|
69
|
+
if (parentInstance) {
|
|
70
|
+
parentInstance.childCursor++;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
root.cursor++;
|
|
74
|
+
}
|
|
75
|
+
registerInstanceIfNeeded(instance, component);
|
|
76
|
+
return instance;
|
|
77
|
+
}
|
|
78
|
+
export function beginInstanceRender(instance) {
|
|
79
|
+
currentInstance = instance;
|
|
80
|
+
instance.hookIndex = 0;
|
|
81
|
+
}
|
|
82
|
+
export function endInstanceRender() {
|
|
83
|
+
currentInstance = null;
|
|
84
|
+
}
|
|
85
|
+
export function finalizeInstance(instance) {
|
|
86
|
+
trimInstances(instance.childInstances, instance.childCursor);
|
|
87
|
+
}
|
|
88
|
+
export function makeInstanceFragment(instance, content) {
|
|
89
|
+
const fragment = document.createDocumentFragment();
|
|
90
|
+
const start = document.createComment('cathode:hmr:start');
|
|
91
|
+
const end = document.createComment('cathode:hmr:end');
|
|
92
|
+
instance.domStart = start;
|
|
93
|
+
instance.domEnd = end;
|
|
94
|
+
fragment.appendChild(start);
|
|
95
|
+
fragment.appendChild(content);
|
|
96
|
+
fragment.appendChild(end);
|
|
97
|
+
return fragment;
|
|
98
|
+
}
|
|
99
|
+
export function useState(initial) {
|
|
100
|
+
const instance = requireInstance();
|
|
101
|
+
const index = instance.hookIndex++;
|
|
102
|
+
let hook = instance.hooks[index];
|
|
103
|
+
if (!hook) {
|
|
104
|
+
const state = typeof initial === 'function' ? initial() : initial;
|
|
105
|
+
hook = {
|
|
106
|
+
kind: 'state',
|
|
107
|
+
state,
|
|
108
|
+
setState: (value) => {
|
|
109
|
+
const next = typeof value === 'function'
|
|
110
|
+
? value(hook.state)
|
|
111
|
+
: value;
|
|
112
|
+
if (Object.is(next, hook.state)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
hook.state = next;
|
|
116
|
+
scheduleRootRender(instance.root);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
instance.hooks[index] = hook;
|
|
120
|
+
}
|
|
121
|
+
return [hook.state, hook.setState];
|
|
122
|
+
}
|
|
123
|
+
export function useEffect(create, deps) {
|
|
124
|
+
const instance = requireInstance();
|
|
125
|
+
const index = instance.hookIndex++;
|
|
126
|
+
let hook = instance.hooks[index];
|
|
127
|
+
if (!hook) {
|
|
128
|
+
hook = {
|
|
129
|
+
kind: 'effect',
|
|
130
|
+
deps,
|
|
131
|
+
};
|
|
132
|
+
instance.hooks[index] = hook;
|
|
133
|
+
instance.root.effects.push({ instance, index, create, deps });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!depsEqual(hook.deps, deps)) {
|
|
137
|
+
hook.deps = deps;
|
|
138
|
+
instance.root.effects.push({ instance, index, create, deps });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function useMemo(factory, deps) {
|
|
142
|
+
const instance = requireInstance();
|
|
143
|
+
const index = instance.hookIndex++;
|
|
144
|
+
let hook = instance.hooks[index];
|
|
145
|
+
if (!hook) {
|
|
146
|
+
const value = factory();
|
|
147
|
+
hook = {
|
|
148
|
+
kind: 'memo',
|
|
149
|
+
deps,
|
|
150
|
+
value,
|
|
151
|
+
};
|
|
152
|
+
instance.hooks[index] = hook;
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
if (depsEqual(hook.deps, deps)) {
|
|
156
|
+
return hook.value;
|
|
157
|
+
}
|
|
158
|
+
const value = factory();
|
|
159
|
+
hook.deps = deps;
|
|
160
|
+
hook.value = value;
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
export function useRef(initial) {
|
|
164
|
+
const instance = requireInstance();
|
|
165
|
+
const index = instance.hookIndex++;
|
|
166
|
+
let hook = instance.hooks[index];
|
|
167
|
+
if (!hook) {
|
|
168
|
+
hook = {
|
|
169
|
+
kind: 'ref',
|
|
170
|
+
ref: { current: initial },
|
|
171
|
+
};
|
|
172
|
+
instance.hooks[index] = hook;
|
|
173
|
+
}
|
|
174
|
+
return hook.ref;
|
|
175
|
+
}
|
|
176
|
+
export function useCallback(fn, deps) {
|
|
177
|
+
const instance = requireInstance();
|
|
178
|
+
const index = instance.hookIndex++;
|
|
179
|
+
let hook = instance.hooks[index];
|
|
180
|
+
if (!hook) {
|
|
181
|
+
hook = {
|
|
182
|
+
kind: 'callback',
|
|
183
|
+
deps,
|
|
184
|
+
value: fn,
|
|
185
|
+
};
|
|
186
|
+
instance.hooks[index] = hook;
|
|
187
|
+
return fn;
|
|
188
|
+
}
|
|
189
|
+
if (depsEqual(hook.deps, deps)) {
|
|
190
|
+
return hook.value;
|
|
191
|
+
}
|
|
192
|
+
hook.deps = deps;
|
|
193
|
+
hook.value = fn;
|
|
194
|
+
return fn;
|
|
195
|
+
}
|
|
196
|
+
// Strip the esbuild content hash from chunk filenames so that registry keys
|
|
197
|
+
// remain stable across rebuilds. Works for both full URLs and path strings.
|
|
198
|
+
// 'https://host/Index-DFFMCZIQ.js' → '/Index.js'
|
|
199
|
+
// '/Index-DFFMCZIQ.js?t=1234' → '/Index.js'
|
|
200
|
+
// '/app.js' → '/app.js' (no hash — unchanged)
|
|
201
|
+
function normalizeRegistryKey(url) {
|
|
202
|
+
let pathname;
|
|
203
|
+
try {
|
|
204
|
+
pathname = new URL(url, 'http://x').pathname;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
pathname = url.split('?')[0];
|
|
208
|
+
}
|
|
209
|
+
// esbuild hashes are exactly 8 uppercase-alphanumeric chars before .js
|
|
210
|
+
return pathname.replace(/-[A-Za-z0-9]{8}(?=\.js$)/, '');
|
|
211
|
+
}
|
|
212
|
+
export function wrapComponent(component, moduleUrl, exportName) {
|
|
213
|
+
const key = normalizeRegistryKey(moduleUrl);
|
|
214
|
+
const meta = { moduleUrl: key, exportName };
|
|
215
|
+
// Reuse the existing proxy across HMR reloads so that instances holding a
|
|
216
|
+
// reference to the proxy are never disposed due to a reference identity change.
|
|
217
|
+
// The proxy delegates to __current, which we update in place on each reload.
|
|
218
|
+
const record = getModuleRecord(key);
|
|
219
|
+
const existing = record.exports.get(exportName);
|
|
220
|
+
if (existing) {
|
|
221
|
+
console.trace('[hmr:registry] wrapComponent:update', { key, exportName });
|
|
222
|
+
existing.__current = component;
|
|
223
|
+
return existing;
|
|
224
|
+
}
|
|
225
|
+
console.trace('[hmr:registry] wrapComponent:create', { key, exportName });
|
|
226
|
+
const proxy = ((props) => proxy.__current(props));
|
|
227
|
+
proxy.__current = component;
|
|
228
|
+
proxy.__cathode_hmr = meta;
|
|
229
|
+
registerExport(meta, proxy);
|
|
230
|
+
return proxy;
|
|
231
|
+
}
|
|
232
|
+
export function registerRoot(moduleUrl, parent, getNode) {
|
|
233
|
+
const key = normalizeRegistryKey(moduleUrl);
|
|
234
|
+
console.trace('[hmr:registry] registerRoot', { raw: moduleUrl, key });
|
|
235
|
+
const record = getModuleRecord(key);
|
|
236
|
+
record.roots.add({ parent, getNode });
|
|
237
|
+
}
|
|
238
|
+
export function applyUpdate(moduleUrl, mod) {
|
|
239
|
+
const key = normalizeRegistryKey(moduleUrl);
|
|
240
|
+
const record = moduleRegistry.get(key);
|
|
241
|
+
console.trace('[hmr:registry] applyUpdate', {
|
|
242
|
+
raw: moduleUrl,
|
|
243
|
+
key,
|
|
244
|
+
found: !!record,
|
|
245
|
+
exports: record ? [...record.exports.keys()] : [],
|
|
246
|
+
instances: record ? record.instances.size : 0,
|
|
247
|
+
incoming: Object.keys(mod),
|
|
248
|
+
});
|
|
249
|
+
// Proxy implementations are already updated by wrapComponent during module
|
|
250
|
+
// re-import, so there is nothing left to do here.
|
|
251
|
+
}
|
|
252
|
+
export function refreshModule(moduleUrl) {
|
|
253
|
+
const key = normalizeRegistryKey(moduleUrl);
|
|
254
|
+
const record = moduleRegistry.get(key);
|
|
255
|
+
console.trace('[hmr:registry] refreshModule', {
|
|
256
|
+
raw: moduleUrl,
|
|
257
|
+
key,
|
|
258
|
+
found: !!record,
|
|
259
|
+
instances: record ? record.instances.size : 0,
|
|
260
|
+
roots: record ? record.roots.size : 0,
|
|
261
|
+
});
|
|
262
|
+
if (!record) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (record.instances.size > 0) {
|
|
266
|
+
record.instances.forEach((instance) => {
|
|
267
|
+
rerenderInstance(instance);
|
|
268
|
+
});
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
record.roots.forEach((root) => {
|
|
272
|
+
rerenderRoot(root);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
export function registerInstanceIfNeeded(instance, component) {
|
|
276
|
+
const meta = component.__cathode_hmr;
|
|
277
|
+
if (!meta) {
|
|
278
|
+
if (instance.meta) {
|
|
279
|
+
unregisterInstance(instance, instance.meta);
|
|
280
|
+
instance.meta = undefined;
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (instance.meta?.moduleUrl !== meta.moduleUrl || instance.meta?.exportName !== meta.exportName) {
|
|
285
|
+
if (instance.meta) {
|
|
286
|
+
unregisterInstance(instance, instance.meta);
|
|
287
|
+
}
|
|
288
|
+
instance.meta = meta;
|
|
289
|
+
}
|
|
290
|
+
const record = getModuleRecord(meta.moduleUrl);
|
|
291
|
+
record.instances.add(instance);
|
|
292
|
+
}
|
|
293
|
+
function requireInstance() {
|
|
294
|
+
if (!currentInstance) {
|
|
295
|
+
throw new Error('Cathode hooks can only be used inside components.');
|
|
296
|
+
}
|
|
297
|
+
return currentInstance;
|
|
298
|
+
}
|
|
299
|
+
function depsEqual(prev, next) {
|
|
300
|
+
if (prev === next) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
if (!prev || !next) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
if (prev.length !== next.length) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
for (let i = 0; i < prev.length; i++) {
|
|
310
|
+
if (!Object.is(prev[i], next[i])) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
function flushEffects(root) {
|
|
317
|
+
root.effects.forEach((record) => {
|
|
318
|
+
const hook = record.instance.hooks[record.index];
|
|
319
|
+
if (hook?.cleanup) {
|
|
320
|
+
hook.cleanup();
|
|
321
|
+
}
|
|
322
|
+
const cleanup = record.create();
|
|
323
|
+
if (hook) {
|
|
324
|
+
hook.cleanup = typeof cleanup === 'function' ? cleanup : undefined;
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
root.effects = [];
|
|
328
|
+
}
|
|
329
|
+
function scheduleRootRender(root) {
|
|
330
|
+
if (!renderRoot) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (scheduledRoots.has(root)) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
scheduledRoots.add(root);
|
|
337
|
+
queueMicrotask(() => {
|
|
338
|
+
scheduledRoots.delete(root);
|
|
339
|
+
renderRoot?.(root);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
function rerenderInstance(instance) {
|
|
343
|
+
if (!renderNode || !instance.domStart || !instance.domEnd) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
beginInstanceRender(instance);
|
|
347
|
+
instance.childCursor = 0;
|
|
348
|
+
const nextNode = instance.component(instance.props);
|
|
349
|
+
endInstanceRender();
|
|
350
|
+
const content = renderNode(nextNode, instance.root, instance);
|
|
351
|
+
finalizeInstance(instance);
|
|
352
|
+
replaceBetween(instance.domStart, instance.domEnd, content);
|
|
353
|
+
flushEffects(instance.root);
|
|
354
|
+
}
|
|
355
|
+
function rerenderRoot(rootRef) {
|
|
356
|
+
if (!renderRoot) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const root = prepareRootContext(rootRef.parent, rootRef.getNode());
|
|
360
|
+
renderRoot(root);
|
|
361
|
+
}
|
|
362
|
+
function replaceBetween(start, end, content) {
|
|
363
|
+
const parent = start.parentNode;
|
|
364
|
+
if (!parent) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
let cursor = start.nextSibling;
|
|
368
|
+
while (cursor && cursor !== end) {
|
|
369
|
+
const next = cursor.nextSibling;
|
|
370
|
+
parent.removeChild(cursor);
|
|
371
|
+
cursor = next;
|
|
372
|
+
}
|
|
373
|
+
parent.insertBefore(content, end);
|
|
374
|
+
}
|
|
375
|
+
function registerExport(meta, component) {
|
|
376
|
+
const record = getModuleRecord(meta.moduleUrl);
|
|
377
|
+
record.exports.set(meta.exportName, component);
|
|
378
|
+
}
|
|
379
|
+
function getModuleRecord(moduleUrl) {
|
|
380
|
+
let record = moduleRegistry.get(moduleUrl);
|
|
381
|
+
if (!record) {
|
|
382
|
+
record = {
|
|
383
|
+
exports: new Map(),
|
|
384
|
+
instances: new Set(),
|
|
385
|
+
roots: new Set(),
|
|
386
|
+
};
|
|
387
|
+
moduleRegistry.set(moduleUrl, record);
|
|
388
|
+
}
|
|
389
|
+
return record;
|
|
390
|
+
}
|
|
391
|
+
function unregisterInstance(instance, meta) {
|
|
392
|
+
const record = moduleRegistry.get(meta.moduleUrl);
|
|
393
|
+
if (!record) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
record.instances.delete(instance);
|
|
397
|
+
}
|
|
398
|
+
function disposeInstance(instance) {
|
|
399
|
+
if (instance.meta) {
|
|
400
|
+
unregisterInstance(instance, instance.meta);
|
|
401
|
+
}
|
|
402
|
+
instance.hooks.forEach((hook) => {
|
|
403
|
+
if (hook.kind === 'effect' && hook.cleanup) {
|
|
404
|
+
hook.cleanup();
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
instance.childInstances.forEach(disposeInstance);
|
|
408
|
+
}
|
|
409
|
+
function trimInstances(list, cursor) {
|
|
410
|
+
if (list.length <= cursor) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const removed = list.splice(cursor);
|
|
414
|
+
removed.forEach(disposeInstance);
|
|
415
|
+
}
|
package/lib/lazy.js
CHANGED
|
@@ -51,7 +51,7 @@ function createLazyComponent(loader, autoLoad) {
|
|
|
51
51
|
}
|
|
52
52
|
// console.trace( '[LazyWrapper.awake] Starting loader()' );
|
|
53
53
|
LazyWrapper.__promise = loader()
|
|
54
|
-
.then(mod => {
|
|
54
|
+
.then((mod) => {
|
|
55
55
|
let component = null;
|
|
56
56
|
if (mod && typeof mod === 'object' && 'default' in mod && mod.default) {
|
|
57
57
|
component = mod.default;
|
|
@@ -60,7 +60,7 @@ function createLazyComponent(loader, autoLoad) {
|
|
|
60
60
|
component = mod;
|
|
61
61
|
}
|
|
62
62
|
else if (mod && typeof mod === 'object') {
|
|
63
|
-
const candidates = Object.values(mod).filter(
|
|
63
|
+
const candidates = Object.values(mod).filter(value => typeof value === 'function');
|
|
64
64
|
if (candidates.length === 1) {
|
|
65
65
|
component = candidates[0];
|
|
66
66
|
}
|
|
@@ -72,7 +72,7 @@ function createLazyComponent(loader, autoLoad) {
|
|
|
72
72
|
LazyWrapper.__component = component;
|
|
73
73
|
LazyWrapper.__status = 'resolved';
|
|
74
74
|
})
|
|
75
|
-
.catch(err => {
|
|
75
|
+
.catch((err) => {
|
|
76
76
|
LazyWrapper.__error = err;
|
|
77
77
|
LazyWrapper.__status = 'rejected';
|
|
78
78
|
});
|
package/lib/runtime.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
+
import { beginInstanceRender, createOrReuseInstance, endInstanceRender, finalizeInstance, finishRootRender, isDevMode, makeInstanceFragment, prepareRootContext, setDevRenderers, } from './hmr-runtime.js';
|
|
1
2
|
import { findLazyPending } from './suspense.js';
|
|
2
3
|
export function h(type, props, ...children) {
|
|
3
4
|
return { type, props: props || {}, children };
|
|
4
5
|
}
|
|
5
6
|
export const Fragment = (props) => Array.isArray(props.children) ? props.children : [props.children];
|
|
6
7
|
export function render(node, parent) {
|
|
8
|
+
if (isDevMode()) {
|
|
9
|
+
const root = prepareRootContext(parent, node);
|
|
10
|
+
parent.innerHTML = '';
|
|
11
|
+
parent.appendChild(toDOM(node, root, null));
|
|
12
|
+
finishRootRender(root);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
7
15
|
parent.innerHTML = '';
|
|
8
16
|
parent.appendChild(toDOM(node));
|
|
9
17
|
}
|
|
10
|
-
function toDOM(node) {
|
|
18
|
+
function toDOM(node, root, parentInstance) {
|
|
11
19
|
if (node === null || node === undefined) {
|
|
12
20
|
return document.createTextNode('');
|
|
13
21
|
}
|
|
@@ -16,7 +24,7 @@ function toDOM(node) {
|
|
|
16
24
|
}
|
|
17
25
|
if (Array.isArray(node)) {
|
|
18
26
|
const frag = document.createDocumentFragment();
|
|
19
|
-
node.forEach(child => frag.appendChild(toDOM(child)));
|
|
27
|
+
node.forEach(child => frag.appendChild(toDOM(child, root, parentInstance ?? null)));
|
|
20
28
|
return frag;
|
|
21
29
|
}
|
|
22
30
|
// Debug: log VNode type
|
|
@@ -28,13 +36,13 @@ function toDOM(node) {
|
|
|
28
36
|
const { fallback, children } = node.props;
|
|
29
37
|
const pending = findLazyPending(children);
|
|
30
38
|
if (pending.length === 0) {
|
|
31
|
-
return toDOM(children);
|
|
39
|
+
return toDOM(children, root, parentInstance ?? null);
|
|
32
40
|
}
|
|
33
41
|
const container = document.createElement('div');
|
|
34
42
|
container.setAttribute('data-cathode-suspense', 'true');
|
|
35
43
|
container.appendChild(toDOM(fallback));
|
|
36
44
|
Promise.all(pending.map(lc => lc.awake())).then(() => {
|
|
37
|
-
const content = toDOM(children);
|
|
45
|
+
const content = toDOM(children, root, parentInstance ?? null);
|
|
38
46
|
container.innerHTML = '';
|
|
39
47
|
container.appendChild(content);
|
|
40
48
|
});
|
|
@@ -47,7 +55,7 @@ function toDOM(node) {
|
|
|
47
55
|
placeholder.setAttribute('data-cathode-lazy', 'pending');
|
|
48
56
|
wrapper.awake().then(() => {
|
|
49
57
|
if (wrapper.__status === 'resolved') {
|
|
50
|
-
const resolved = toDOM(h(wrapper.__component, componentProps));
|
|
58
|
+
const resolved = toDOM(h(wrapper.__component, componentProps), root, parentInstance ?? null);
|
|
51
59
|
placeholder.replaceWith(resolved);
|
|
52
60
|
}
|
|
53
61
|
});
|
|
@@ -87,7 +95,7 @@ function toDOM(node) {
|
|
|
87
95
|
wrapper.__promise.then(() => {
|
|
88
96
|
// console.trace( '[lazy ondemand] Component loaded:', wrapper.__component?.name, 'status:', wrapper.__status, 'connected:', placeholder.isConnected );
|
|
89
97
|
if (wrapper.__status === 'resolved' && placeholder.isConnected) {
|
|
90
|
-
const resolved = toDOM(h(wrapper.__component, componentProps));
|
|
98
|
+
const resolved = toDOM(h(wrapper.__component, componentProps), root, parentInstance ?? null);
|
|
91
99
|
// console.trace( '[lazy ondemand] Replacing placeholder with resolved component' );
|
|
92
100
|
placeholder.replaceWith(resolved);
|
|
93
101
|
}
|
|
@@ -119,7 +127,7 @@ function toDOM(node) {
|
|
|
119
127
|
if (entry.isIntersecting) {
|
|
120
128
|
component.awake().then(() => {
|
|
121
129
|
if (component.__status === 'resolved') {
|
|
122
|
-
const resolved = toDOM(h(component, componentProps));
|
|
130
|
+
const resolved = toDOM(h(component, componentProps), root, parentInstance ?? null);
|
|
123
131
|
container.innerHTML = '';
|
|
124
132
|
container.appendChild(resolved);
|
|
125
133
|
}
|
|
@@ -147,16 +155,26 @@ function toDOM(node) {
|
|
|
147
155
|
if (node.type === '__error_boundary__') {
|
|
148
156
|
const { fallback, children } = node.props;
|
|
149
157
|
try {
|
|
150
|
-
return toDOM(children);
|
|
158
|
+
return toDOM(children, root, parentInstance ?? null);
|
|
151
159
|
}
|
|
152
160
|
catch (error) {
|
|
153
161
|
if (typeof fallback === 'function') {
|
|
154
|
-
return toDOM(fallback(error));
|
|
162
|
+
return toDOM(fallback(error), root, parentInstance ?? null);
|
|
155
163
|
}
|
|
156
|
-
return toDOM(fallback);
|
|
164
|
+
return toDOM(fallback, root, parentInstance ?? null);
|
|
157
165
|
}
|
|
158
166
|
}
|
|
159
167
|
if (typeof node.type === 'function') {
|
|
168
|
+
if (isDevMode() && root) {
|
|
169
|
+
const props = { ...node.props, children: node.children };
|
|
170
|
+
const instance = createOrReuseInstance(root, parentInstance ?? null, node.type, props);
|
|
171
|
+
beginInstanceRender(instance);
|
|
172
|
+
const nextNode = instance.component(props);
|
|
173
|
+
endInstanceRender();
|
|
174
|
+
const content = toDOM(nextNode, root, instance);
|
|
175
|
+
finalizeInstance(instance);
|
|
176
|
+
return makeInstanceFragment(instance, content);
|
|
177
|
+
}
|
|
160
178
|
return toDOM(node.type({ ...node.props, children: node.children }));
|
|
161
179
|
}
|
|
162
180
|
const el = document.createElement(node.type);
|
|
@@ -176,7 +194,7 @@ function toDOM(node) {
|
|
|
176
194
|
});
|
|
177
195
|
if (node.children) {
|
|
178
196
|
node.children.forEach((child) => {
|
|
179
|
-
el.appendChild(toDOM(child));
|
|
197
|
+
el.appendChild(toDOM(child, root, parentInstance ?? null));
|
|
180
198
|
});
|
|
181
199
|
}
|
|
182
200
|
return el;
|
|
@@ -186,3 +204,8 @@ if (typeof globalThis !== 'undefined') {
|
|
|
186
204
|
globalThis.h = h;
|
|
187
205
|
globalThis.Fragment = Fragment;
|
|
188
206
|
}
|
|
207
|
+
setDevRenderers((node, root, parentInstance) => toDOM(node, root, parentInstance), (root) => {
|
|
208
|
+
root.parent.innerHTML = '';
|
|
209
|
+
root.parent.appendChild(toDOM(root.lastNode, root, null));
|
|
210
|
+
finishRootRender(root);
|
|
211
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relicloops/cathode",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0-000",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Lightweight JSX runtime.",
|
|
6
6
|
"type": "module",
|
|
@@ -18,12 +18,22 @@
|
|
|
18
18
|
"./jsx-dev-runtime": {
|
|
19
19
|
"types": "./types/jsx-dev-runtime.d.ts",
|
|
20
20
|
"default": "./jsx-dev-runtime.js"
|
|
21
|
+
},
|
|
22
|
+
"./hmr": {
|
|
23
|
+
"types": "./types/hmr.d.ts",
|
|
24
|
+
"default": "./hmr.js"
|
|
25
|
+
},
|
|
26
|
+
"./esbuild-plugin": {
|
|
27
|
+
"types": "./types/esbuild-plugin.d.ts",
|
|
28
|
+
"default": "./esbuild-plugin.js"
|
|
21
29
|
}
|
|
22
30
|
},
|
|
23
31
|
"files": [
|
|
24
32
|
"index.js",
|
|
25
33
|
"jsx-runtime.js",
|
|
26
34
|
"jsx-dev-runtime.js",
|
|
35
|
+
"hmr.js",
|
|
36
|
+
"esbuild-plugin.js",
|
|
27
37
|
"lib/**/*.js",
|
|
28
38
|
"types/**/*.d.ts",
|
|
29
39
|
"README.md",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
type Build = {
|
|
2
|
+
onLoad: (options: {
|
|
3
|
+
filter: RegExp;
|
|
4
|
+
}, callback: (args: {
|
|
5
|
+
path: string;
|
|
6
|
+
}) => {
|
|
7
|
+
contents: string;
|
|
8
|
+
loader: string;
|
|
9
|
+
} | undefined) => void;
|
|
10
|
+
};
|
|
11
|
+
export type CathodeHMRPluginOptions = {
|
|
12
|
+
/**
|
|
13
|
+
* Regex that controls which files are transformed.
|
|
14
|
+
* Default: /\.tsx$/ — all TSX files in the bundle.
|
|
15
|
+
*/
|
|
16
|
+
filter?: RegExp;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* esbuild plugin that automatically wraps exported JSX components with
|
|
20
|
+
* cathode's HMR proxy at build time.
|
|
21
|
+
*
|
|
22
|
+
* Only activate this plugin in watch / dev mode:
|
|
23
|
+
*
|
|
24
|
+
* plugins: [
|
|
25
|
+
* ...(watchMode ? [cathodeHMRPlugin()] : []),
|
|
26
|
+
* deployPlugin,
|
|
27
|
+
* ]
|
|
28
|
+
*
|
|
29
|
+
* The plugin detects top-level exports whose name starts with an uppercase
|
|
30
|
+
* letter (PascalCase convention for components) and rewrites:
|
|
31
|
+
*
|
|
32
|
+
* export const Foo = ( props ) => { ... };
|
|
33
|
+
* export function Bar( props ) { ... }
|
|
34
|
+
*
|
|
35
|
+
* into:
|
|
36
|
+
*
|
|
37
|
+
* import { hmr as __cathode_hmr } from '@relicloops/cathode/hmr';
|
|
38
|
+
* const _Foo = ( props ) => { ... };
|
|
39
|
+
* function _Bar( props ) { ... }
|
|
40
|
+
* export const Foo = __cathode_hmr.wrap( _Foo, import.meta.url, 'Foo' );
|
|
41
|
+
* export const Bar = __cathode_hmr.wrap( _Bar, import.meta.url, 'Bar' );
|
|
42
|
+
*
|
|
43
|
+
* Files that already contain '__cathode_hmr' or 'hmr.wrap' are left
|
|
44
|
+
* untouched so that explicit wrapping is never doubled.
|
|
45
|
+
*/
|
|
46
|
+
export declare function cathodeHMRPlugin(options?: CathodeHMRPluginOptions): {
|
|
47
|
+
name: string;
|
|
48
|
+
setup: (build: Build) => void;
|
|
49
|
+
};
|
|
50
|
+
export {};
|
package/types/hmr.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { applyUpdate, enableDevMode, refreshModule, registerRoot, useCallback, useEffect, useMemo, useRef, useState, wrapComponent } from './lib/hmr-runtime.js';
|
|
2
|
+
export type HMREvent = {
|
|
3
|
+
type: 'hmr';
|
|
4
|
+
path: string;
|
|
5
|
+
event: 'created' | 'modified' | 'deleted' | 'renamed';
|
|
6
|
+
};
|
|
7
|
+
export type HMROptions = {
|
|
8
|
+
endpoint?: string;
|
|
9
|
+
pathTransform?: (path: string) => string;
|
|
10
|
+
onBeforeReload?: (event: HMREvent) => void;
|
|
11
|
+
onHotUpdate?: (event: HMREvent, moduleUrl: string, mod: Record<string, any>) => void;
|
|
12
|
+
onHotError?: (event: HMREvent, error: unknown) => void;
|
|
13
|
+
};
|
|
14
|
+
export declare const hmr: {
|
|
15
|
+
wrap: typeof wrapComponent;
|
|
16
|
+
registerRoot: typeof registerRoot;
|
|
17
|
+
applyUpdate: typeof applyUpdate;
|
|
18
|
+
refreshModule: typeof refreshModule;
|
|
19
|
+
};
|
|
20
|
+
export { applyUpdate, enableDevMode, refreshModule, registerRoot, useCallback, useEffect, useMemo, useRef, useState, wrapComponent as wrap, };
|
|
21
|
+
export declare function connectHMR(options?: HMROptions): () => void;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type HMRMeta = {
|
|
2
|
+
moduleUrl: string;
|
|
3
|
+
exportName: string;
|
|
4
|
+
};
|
|
5
|
+
export type ComponentFn = (props: any) => any;
|
|
6
|
+
export type ComponentInstance = {
|
|
7
|
+
component: ComponentFn;
|
|
8
|
+
props: any;
|
|
9
|
+
hooks: HookEntry[];
|
|
10
|
+
hookIndex: number;
|
|
11
|
+
childInstances: ComponentInstance[];
|
|
12
|
+
childCursor: number;
|
|
13
|
+
root: RootContext;
|
|
14
|
+
domStart: Comment | null;
|
|
15
|
+
domEnd: Comment | null;
|
|
16
|
+
meta?: HMRMeta;
|
|
17
|
+
};
|
|
18
|
+
export type RootContext = {
|
|
19
|
+
parent: HTMLElement;
|
|
20
|
+
instances: ComponentInstance[];
|
|
21
|
+
cursor: number;
|
|
22
|
+
effects: EffectRecord[];
|
|
23
|
+
lastNode: any;
|
|
24
|
+
};
|
|
25
|
+
type EffectRecord = {
|
|
26
|
+
instance: ComponentInstance;
|
|
27
|
+
index: number;
|
|
28
|
+
create: () => void | (() => void);
|
|
29
|
+
deps?: any[];
|
|
30
|
+
};
|
|
31
|
+
type HookEntry = StateHook | EffectHook | MemoHook | RefHook | CallbackHook;
|
|
32
|
+
type StateHook = {
|
|
33
|
+
kind: 'state';
|
|
34
|
+
state: any;
|
|
35
|
+
setState: (value: any) => void;
|
|
36
|
+
};
|
|
37
|
+
type EffectHook = {
|
|
38
|
+
kind: 'effect';
|
|
39
|
+
deps?: any[];
|
|
40
|
+
cleanup?: () => void;
|
|
41
|
+
};
|
|
42
|
+
type MemoHook = {
|
|
43
|
+
kind: 'memo';
|
|
44
|
+
deps?: any[];
|
|
45
|
+
value: any;
|
|
46
|
+
};
|
|
47
|
+
type RefHook = {
|
|
48
|
+
kind: 'ref';
|
|
49
|
+
ref: {
|
|
50
|
+
current: any;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
type CallbackHook = {
|
|
54
|
+
kind: 'callback';
|
|
55
|
+
deps?: any[];
|
|
56
|
+
value: any;
|
|
57
|
+
};
|
|
58
|
+
type RootRender = (root: RootContext) => void;
|
|
59
|
+
type NodeRender = (node: any, root: RootContext, parentInstance: ComponentInstance | null) => Node;
|
|
60
|
+
export declare function enableDevMode(): void;
|
|
61
|
+
export declare function isDevMode(): boolean;
|
|
62
|
+
export declare function setDevRenderers(nodeRenderer: NodeRender, rootRenderer: RootRender): void;
|
|
63
|
+
export declare function prepareRootContext(parent: HTMLElement, node: any): RootContext;
|
|
64
|
+
export declare function finishRootRender(root: RootContext): void;
|
|
65
|
+
export declare function createOrReuseInstance(root: RootContext, parentInstance: ComponentInstance | null, component: ComponentFn, props: any): ComponentInstance;
|
|
66
|
+
export declare function beginInstanceRender(instance: ComponentInstance): void;
|
|
67
|
+
export declare function endInstanceRender(): void;
|
|
68
|
+
export declare function finalizeInstance(instance: ComponentInstance): void;
|
|
69
|
+
export declare function makeInstanceFragment(instance: ComponentInstance, content: Node): DocumentFragment;
|
|
70
|
+
export declare function useState<T>(initial: T | (() => T)): [T, (value: T | ((prev: T) => T)) => void];
|
|
71
|
+
export declare function useEffect(create: () => void | (() => void), deps?: any[]): void;
|
|
72
|
+
export declare function useMemo<T>(factory: () => T, deps?: any[]): T;
|
|
73
|
+
export declare function useRef<T>(initial: T): {
|
|
74
|
+
current: T;
|
|
75
|
+
};
|
|
76
|
+
export declare function useCallback<T extends (...args: any[]) => any>(fn: T, deps?: any[]): T;
|
|
77
|
+
export declare function wrapComponent<T extends ComponentFn>(component: T, moduleUrl: string, exportName: string): T;
|
|
78
|
+
export declare function registerRoot(moduleUrl: string, parent: HTMLElement, getNode: () => any): void;
|
|
79
|
+
export declare function applyUpdate(moduleUrl: string, mod: Record<string, any>): void;
|
|
80
|
+
export declare function refreshModule(moduleUrl: string): void;
|
|
81
|
+
export declare function registerInstanceIfNeeded(instance: ComponentInstance, component: ComponentFn): void;
|
|
82
|
+
export {};
|