@emberkit/core 0.1.2-alpha.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/LICENSE +199 -0
- package/dist/boundaries/error-boundary.js +70 -0
- package/dist/boundaries/errors.js +72 -0
- package/dist/boundaries/index.js +3 -0
- package/dist/boundaries/loading-boundary.js +106 -0
- package/dist/cache/index.js +213 -0
- package/dist/compiler/compiler.js +44 -0
- package/dist/compiler/helpers/attributes.js +35 -0
- package/dist/compiler/helpers/utils.js +31 -0
- package/dist/compiler/index.js +4 -0
- package/dist/compiler/types.js +3 -0
- package/dist/context/index.js +51 -0
- package/dist/context/types.js +1 -0
- package/dist/dev-server/index.js +121 -0
- package/dist/forms/index.js +164 -0
- package/dist/forms/mutations.js +258 -0
- package/dist/hmr/client.js +84 -0
- package/dist/hmr/index.js +2 -0
- package/dist/hmr/types.js +133 -0
- package/dist/hydration/helpers/analyzer.js +94 -0
- package/dist/hydration/helpers/hydration.js +129 -0
- package/dist/hydration/index.js +3 -0
- package/dist/hydration/types.js +18 -0
- package/dist/image/index.js +34 -0
- package/dist/image/processor.js +143 -0
- package/dist/index.js +16 -0
- package/dist/jsx-dev-runtime.js +7 -0
- package/dist/jsx-runtime.js +7 -0
- package/dist/loader/helpers/loader.js +61 -0
- package/dist/loader/index.js +2 -0
- package/dist/loader/types.js +14 -0
- package/dist/markdown/index.js +365 -0
- package/dist/mdx/index.js +156 -0
- package/dist/mdx/loader.js +6 -0
- package/dist/meta/head-registry.js +15 -0
- package/dist/meta/head.js +100 -0
- package/dist/meta/index.js +210 -0
- package/dist/navigation/helpers/navigation.js +53 -0
- package/dist/navigation/helpers/useNavigate.js +10 -0
- package/dist/navigation/index.js +3 -0
- package/dist/navigation/types.js +2 -0
- package/dist/plugin/index.js +74 -0
- package/dist/router/helpers/path.js +74 -0
- package/dist/router/helpers/route.js +109 -0
- package/dist/router/index.js +110 -0
- package/dist/router/types.js +5 -0
- package/dist/runtime/helpers/element.js +52 -0
- package/dist/runtime/helpers/render.js +121 -0
- package/dist/runtime/index.js +132 -0
- package/dist/runtime/types.js +1 -0
- package/dist/signals/helpers/core.js +96 -0
- package/dist/signals/helpers/utils.js +22 -0
- package/dist/signals/index.js +3 -0
- package/dist/signals/types.js +1 -0
- package/dist/ssg/index.js +119 -0
- package/dist/ssr/helpers/render-html.js +55 -0
- package/dist/ssr/helpers/ssr.js +90 -0
- package/dist/ssr/index.js +3 -0
- package/dist/ssr/types.js +24 -0
- package/dist/vite-plugin/index.js +650 -0
- package/dist/vite-plugin/types.js +13 -0
- package/package.json +66 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
export function createAction(handler, options) {
|
|
2
|
+
return async (variables, context) => {
|
|
3
|
+
let optimisticData;
|
|
4
|
+
try {
|
|
5
|
+
if (options?.onMutate) {
|
|
6
|
+
optimisticData = await options.onMutate(variables);
|
|
7
|
+
}
|
|
8
|
+
const data = await handler(variables, {
|
|
9
|
+
request: context?.request ?? new Request('http://localhost'),
|
|
10
|
+
params: context?.params ?? {},
|
|
11
|
+
query: context?.query ?? new URLSearchParams(),
|
|
12
|
+
});
|
|
13
|
+
options?.onSuccess?.(data, variables);
|
|
14
|
+
return {
|
|
15
|
+
data,
|
|
16
|
+
error: null,
|
|
17
|
+
status: 200,
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
23
|
+
options?.onError?.(error, variables);
|
|
24
|
+
return {
|
|
25
|
+
data: null,
|
|
26
|
+
error: error.message,
|
|
27
|
+
status: 500,
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
options?.onSettled?.(optimisticData);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function createMutation(handler, options) {
|
|
37
|
+
let state = {
|
|
38
|
+
data: null,
|
|
39
|
+
error: null,
|
|
40
|
+
status: 'idle',
|
|
41
|
+
isPending: false,
|
|
42
|
+
isSuccess: false,
|
|
43
|
+
isError: false,
|
|
44
|
+
};
|
|
45
|
+
const listeners = new Set();
|
|
46
|
+
const notify = () => {
|
|
47
|
+
listeners.forEach((fn) => fn(state));
|
|
48
|
+
};
|
|
49
|
+
const mutate = async (variables) => {
|
|
50
|
+
state = {
|
|
51
|
+
data: null,
|
|
52
|
+
error: null,
|
|
53
|
+
status: 'pending',
|
|
54
|
+
isPending: true,
|
|
55
|
+
isSuccess: false,
|
|
56
|
+
isError: false,
|
|
57
|
+
};
|
|
58
|
+
notify();
|
|
59
|
+
try {
|
|
60
|
+
let optimisticData;
|
|
61
|
+
if (options?.onMutate) {
|
|
62
|
+
optimisticData = await options.onMutate(variables);
|
|
63
|
+
}
|
|
64
|
+
const data = await handler(variables, {
|
|
65
|
+
request: new Request('http://localhost'),
|
|
66
|
+
params: {},
|
|
67
|
+
query: new URLSearchParams(),
|
|
68
|
+
});
|
|
69
|
+
state = {
|
|
70
|
+
data,
|
|
71
|
+
error: null,
|
|
72
|
+
status: 'success',
|
|
73
|
+
isPending: false,
|
|
74
|
+
isSuccess: true,
|
|
75
|
+
isError: false,
|
|
76
|
+
};
|
|
77
|
+
notify();
|
|
78
|
+
options?.onSuccess?.(data, variables);
|
|
79
|
+
options?.onSettled?.(data);
|
|
80
|
+
return { data, error: null, status: 200 };
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
84
|
+
state = {
|
|
85
|
+
data: null,
|
|
86
|
+
error,
|
|
87
|
+
status: 'error',
|
|
88
|
+
isPending: false,
|
|
89
|
+
isSuccess: false,
|
|
90
|
+
isError: true,
|
|
91
|
+
};
|
|
92
|
+
notify();
|
|
93
|
+
options?.onError?.(error, variables);
|
|
94
|
+
options?.onSettled?.(undefined, error);
|
|
95
|
+
return { data: null, error: error.message, status: 500 };
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const subscribe = (listener) => {
|
|
99
|
+
listeners.add(listener);
|
|
100
|
+
return () => listeners.delete(listener);
|
|
101
|
+
};
|
|
102
|
+
const getState = () => state;
|
|
103
|
+
const reset = () => {
|
|
104
|
+
state = {
|
|
105
|
+
data: null,
|
|
106
|
+
error: null,
|
|
107
|
+
status: 'idle',
|
|
108
|
+
isPending: false,
|
|
109
|
+
isSuccess: false,
|
|
110
|
+
isError: false,
|
|
111
|
+
};
|
|
112
|
+
notify();
|
|
113
|
+
};
|
|
114
|
+
return { mutate, subscribe, getState, reset };
|
|
115
|
+
}
|
|
116
|
+
export async function handleAction(handler, request) {
|
|
117
|
+
let variables = {};
|
|
118
|
+
if (request.method === 'POST' || request.method === 'PUT' || request.method === 'PATCH') {
|
|
119
|
+
const contentType = request.headers.get('Content-Type') ?? '';
|
|
120
|
+
if (contentType.includes('application/json')) {
|
|
121
|
+
variables = await request.json();
|
|
122
|
+
}
|
|
123
|
+
else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
124
|
+
const formData = await request.formData();
|
|
125
|
+
for (const [key, value] of formData.entries()) {
|
|
126
|
+
variables[key] = value;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
variables = await request.text().then((t) => JSON.parse(t)).catch(() => ({}));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (request.method === 'GET') {
|
|
134
|
+
const url = new URL(request.url);
|
|
135
|
+
for (const [key, value] of url.searchParams.entries()) {
|
|
136
|
+
variables[key] = value;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const url = new URL(request.url);
|
|
140
|
+
const params = {};
|
|
141
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
142
|
+
for (let i = 0; i < segments.length; i++) {
|
|
143
|
+
const segment = segments[i];
|
|
144
|
+
if (segment.startsWith(':') || segment.startsWith('[')) {
|
|
145
|
+
const paramName = segment.replace(/^[:[\]]+/g, '');
|
|
146
|
+
params[paramName] = segments[i + 1] ?? '';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const context = {
|
|
150
|
+
request,
|
|
151
|
+
params,
|
|
152
|
+
query: url.searchParams,
|
|
153
|
+
};
|
|
154
|
+
try {
|
|
155
|
+
const data = await handler(variables, context);
|
|
156
|
+
return Response.json({ data }, {
|
|
157
|
+
status: 200,
|
|
158
|
+
headers: { 'X-Action': 'success' },
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
const message = err instanceof Error ? err.message : 'Action failed';
|
|
163
|
+
return Response.json({ error: message }, {
|
|
164
|
+
status: 500,
|
|
165
|
+
headers: { 'X-Action': 'error' },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export function createActionHandler(handler) {
|
|
170
|
+
return async (request) => {
|
|
171
|
+
return handleAction(handler, request);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
export const mutationCache = new Map();
|
|
175
|
+
export function getCachedMutation(key) {
|
|
176
|
+
return mutationCache.get(key);
|
|
177
|
+
}
|
|
178
|
+
export function setCachedMutation(key, result) {
|
|
179
|
+
mutationCache.set(key, result);
|
|
180
|
+
}
|
|
181
|
+
export function invalidateMutation(key) {
|
|
182
|
+
mutationCache.delete(key);
|
|
183
|
+
}
|
|
184
|
+
export function clearMutationCache() {
|
|
185
|
+
mutationCache.clear();
|
|
186
|
+
}
|
|
187
|
+
export function useMutation(action, options) {
|
|
188
|
+
let state = {
|
|
189
|
+
data: null,
|
|
190
|
+
error: null,
|
|
191
|
+
status: 'idle',
|
|
192
|
+
isPending: false,
|
|
193
|
+
isSuccess: false,
|
|
194
|
+
isError: false,
|
|
195
|
+
};
|
|
196
|
+
const listeners = new Set();
|
|
197
|
+
const notify = () => listeners.forEach((fn) => fn(state));
|
|
198
|
+
const mutate = async (variables) => {
|
|
199
|
+
state = {
|
|
200
|
+
data: null,
|
|
201
|
+
error: null,
|
|
202
|
+
status: 'pending',
|
|
203
|
+
isPending: true,
|
|
204
|
+
isSuccess: false,
|
|
205
|
+
isError: false,
|
|
206
|
+
};
|
|
207
|
+
notify();
|
|
208
|
+
try {
|
|
209
|
+
if (options?.onMutate) {
|
|
210
|
+
await options.onMutate(variables);
|
|
211
|
+
}
|
|
212
|
+
const data = await action(variables, {
|
|
213
|
+
request: new Request('http://localhost'),
|
|
214
|
+
params: {},
|
|
215
|
+
query: new URLSearchParams(),
|
|
216
|
+
});
|
|
217
|
+
state = {
|
|
218
|
+
data,
|
|
219
|
+
error: null,
|
|
220
|
+
status: 'success',
|
|
221
|
+
isPending: false,
|
|
222
|
+
isSuccess: true,
|
|
223
|
+
isError: false,
|
|
224
|
+
};
|
|
225
|
+
notify();
|
|
226
|
+
options?.onSuccess?.(data, variables);
|
|
227
|
+
options?.onSettled?.(data);
|
|
228
|
+
return { data, error: null, status: 200 };
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
232
|
+
state = {
|
|
233
|
+
data: null,
|
|
234
|
+
error,
|
|
235
|
+
status: 'error',
|
|
236
|
+
isPending: false,
|
|
237
|
+
isSuccess: false,
|
|
238
|
+
isError: true,
|
|
239
|
+
};
|
|
240
|
+
notify();
|
|
241
|
+
options?.onError?.(error, variables);
|
|
242
|
+
options?.onSettled?.(undefined, error);
|
|
243
|
+
return { data: null, error: error.message, status: 500 };
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
const reset = () => {
|
|
247
|
+
state = {
|
|
248
|
+
data: null,
|
|
249
|
+
error: null,
|
|
250
|
+
status: 'idle',
|
|
251
|
+
isPending: false,
|
|
252
|
+
isSuccess: false,
|
|
253
|
+
isError: false,
|
|
254
|
+
};
|
|
255
|
+
notify();
|
|
256
|
+
};
|
|
257
|
+
return { mutate, state, reset };
|
|
258
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { registerHotModule, getHotModule, } from './types.js';
|
|
2
|
+
export function createHotAPI(context, moduleId, moduleUrl) {
|
|
3
|
+
const hotModule = {
|
|
4
|
+
id: moduleId,
|
|
5
|
+
url: moduleUrl,
|
|
6
|
+
needsAccept: false,
|
|
7
|
+
acceptCallbacks: [],
|
|
8
|
+
disposeCallbacks: [],
|
|
9
|
+
};
|
|
10
|
+
registerHotModule(context, hotModule);
|
|
11
|
+
return {
|
|
12
|
+
accept(deps, callback) {
|
|
13
|
+
if (typeof deps === 'function') {
|
|
14
|
+
callback = deps;
|
|
15
|
+
deps = undefined;
|
|
16
|
+
}
|
|
17
|
+
hotModule.needsAccept = true;
|
|
18
|
+
if (callback) {
|
|
19
|
+
hotModule.acceptCallbacks.push(callback);
|
|
20
|
+
}
|
|
21
|
+
if (deps && typeof deps === 'string') {
|
|
22
|
+
import(deps);
|
|
23
|
+
}
|
|
24
|
+
else if (Array.isArray(deps)) {
|
|
25
|
+
Promise.all(deps.map((d) => import(d)));
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
decline() {
|
|
29
|
+
import.meta.hot = undefined;
|
|
30
|
+
},
|
|
31
|
+
dispose(callback) {
|
|
32
|
+
hotModule.disposeCallbacks.push(callback);
|
|
33
|
+
},
|
|
34
|
+
data: {},
|
|
35
|
+
on(event, callback) {
|
|
36
|
+
if (event === 'vite:beforeUpdate') {
|
|
37
|
+
const module = getHotModule(context, moduleId);
|
|
38
|
+
if (module) {
|
|
39
|
+
module.acceptCallbacks.push(() => callback({ type: 'update' }));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
send(event, data) {
|
|
44
|
+
console.log('[HMR] Send:', event, data);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function setupHMRClient(context) {
|
|
49
|
+
if (typeof window === 'undefined')
|
|
50
|
+
return;
|
|
51
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
52
|
+
const wsUrl = `${protocol}//${location.host}/__emberkit_hmr`;
|
|
53
|
+
const ws = new WebSocket(wsUrl);
|
|
54
|
+
ws.onopen = () => {
|
|
55
|
+
console.log('[HMR Client] Connected');
|
|
56
|
+
};
|
|
57
|
+
ws.onmessage = async (event) => {
|
|
58
|
+
try {
|
|
59
|
+
const data = JSON.parse(event.data);
|
|
60
|
+
if (data.type === 'update') {
|
|
61
|
+
await import(/* @vite-ignore */ data.url + '?t=' + Date.now());
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error('[HMR Client] Error:', error);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
ws.onclose = () => {
|
|
69
|
+
console.log('[HMR Client] Disconnected, reconnecting...');
|
|
70
|
+
setTimeout(() => setupHMRClient(context), 1000);
|
|
71
|
+
};
|
|
72
|
+
ws.onerror = (error) => {
|
|
73
|
+
console.error('[HMR Client] Error:', error);
|
|
74
|
+
};
|
|
75
|
+
context.connections.set('main', ws);
|
|
76
|
+
}
|
|
77
|
+
export function cleanupHMRClient() {
|
|
78
|
+
if (typeof window === 'undefined')
|
|
79
|
+
return;
|
|
80
|
+
const ws = window.__emberkit_hmr_ws;
|
|
81
|
+
if (ws) {
|
|
82
|
+
ws.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
export class HMRConnection {
|
|
2
|
+
ws = null;
|
|
3
|
+
url;
|
|
4
|
+
constructor(url) {
|
|
5
|
+
this.url = url;
|
|
6
|
+
}
|
|
7
|
+
connect(onMessage) {
|
|
8
|
+
try {
|
|
9
|
+
this.ws = new WebSocket(this.url);
|
|
10
|
+
this.ws.onopen = () => {
|
|
11
|
+
console.log('[HMR] Connected to dev server');
|
|
12
|
+
};
|
|
13
|
+
this.ws.onmessage = (event) => {
|
|
14
|
+
try {
|
|
15
|
+
const data = JSON.parse(event.data);
|
|
16
|
+
onMessage(data);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
console.error('[HMR] Failed to parse message');
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
this.ws.onerror = (error) => {
|
|
23
|
+
console.error('[HMR] WebSocket error:', error);
|
|
24
|
+
};
|
|
25
|
+
this.ws.onclose = () => {
|
|
26
|
+
console.log('[HMR] Disconnected');
|
|
27
|
+
setTimeout(() => this.reconnect(onMessage), 1000);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error('[HMR] Failed to connect:', error);
|
|
32
|
+
setTimeout(() => this.reconnect(onMessage), 1000);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
reconnect(onMessage) {
|
|
36
|
+
console.log('[HMR] Attempting to reconnect...');
|
|
37
|
+
this.connect(onMessage);
|
|
38
|
+
}
|
|
39
|
+
send(data) {
|
|
40
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
41
|
+
this.ws.send(JSON.stringify(data));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
close() {
|
|
45
|
+
this.ws?.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function createHMRContext() {
|
|
49
|
+
return {
|
|
50
|
+
connections: new Map(),
|
|
51
|
+
modules: new Map(),
|
|
52
|
+
listeners: [],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function subscribeToHMR(context, callback) {
|
|
56
|
+
context.listeners.push(callback);
|
|
57
|
+
return () => {
|
|
58
|
+
const index = context.listeners.indexOf(callback);
|
|
59
|
+
if (index > -1) {
|
|
60
|
+
context.listeners.splice(index, 1);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function emitHMREvent(context, event) {
|
|
65
|
+
for (const listener of context.listeners) {
|
|
66
|
+
listener(event);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function handleHMRMessage(context, data) {
|
|
70
|
+
const type = data.type;
|
|
71
|
+
switch (type) {
|
|
72
|
+
case 'hot':
|
|
73
|
+
await handleHotUpdate(context, data);
|
|
74
|
+
break;
|
|
75
|
+
case 'close':
|
|
76
|
+
handleClose(context);
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
console.warn('[HMR] Unknown message type:', type);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function handleHotUpdate(context, data) {
|
|
83
|
+
const moduleId = data.moduleId;
|
|
84
|
+
const hotModule = context.modules.get(moduleId);
|
|
85
|
+
if (!hotModule) {
|
|
86
|
+
console.warn('[HMR] Module not found:', moduleId);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
for (const callback of hotModule.acceptCallbacks) {
|
|
90
|
+
try {
|
|
91
|
+
callback();
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.error('[HMR] Error in accept callback:', error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
emitHMREvent(context, {
|
|
98
|
+
type: 'update',
|
|
99
|
+
moduleId,
|
|
100
|
+
timestamp: Date.now(),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function handleClose(context) {
|
|
104
|
+
for (const connection of context.connections.values()) {
|
|
105
|
+
connection.close();
|
|
106
|
+
}
|
|
107
|
+
context.connections.clear();
|
|
108
|
+
context.modules.clear();
|
|
109
|
+
emitHMREvent(context, {
|
|
110
|
+
type: 'disconnected',
|
|
111
|
+
timestamp: Date.now(),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export function getHotModule(context, id) {
|
|
115
|
+
return context.modules.get(id);
|
|
116
|
+
}
|
|
117
|
+
export function registerHotModule(context, module) {
|
|
118
|
+
context.modules.set(module.id, module);
|
|
119
|
+
}
|
|
120
|
+
export function disposeHotModule(context, id) {
|
|
121
|
+
const module = context.modules.get(id);
|
|
122
|
+
if (module) {
|
|
123
|
+
for (const callback of module.disposeCallbacks) {
|
|
124
|
+
try {
|
|
125
|
+
callback();
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.error('[HMR] Error in dispose callback:', error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
context.modules.delete(id);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { hasEventHandlers, isInteractiveTag, INTERACTIVE_ATTRIBUTES, } from '../types.js';
|
|
2
|
+
export function analyzeElement(element) {
|
|
3
|
+
const { type, props } = element;
|
|
4
|
+
if (typeof type !== 'string') {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const needsHydration = isInteractiveTag(type) || hasEventHandlers(props ?? {});
|
|
8
|
+
const eventHandlers = extractEventHandlers(props ?? {});
|
|
9
|
+
const strategy = determineHydrationStrategy(type, props ?? {});
|
|
10
|
+
return {
|
|
11
|
+
selector: buildSelector(type, props ?? {}),
|
|
12
|
+
eventHandlers,
|
|
13
|
+
needsHydration,
|
|
14
|
+
strategy,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function extractEventHandlers(props) {
|
|
18
|
+
const handlers = new Set();
|
|
19
|
+
for (const key of Object.keys(props)) {
|
|
20
|
+
if (INTERACTIVE_ATTRIBUTES.has(key)) {
|
|
21
|
+
handlers.add(key);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return handlers;
|
|
25
|
+
}
|
|
26
|
+
export function buildSelector(tagName, props) {
|
|
27
|
+
let selector = tagName.toLowerCase();
|
|
28
|
+
if (props.id && typeof props.id === 'string') {
|
|
29
|
+
selector = `#${props.id}`;
|
|
30
|
+
}
|
|
31
|
+
else if (props.class && typeof props.class === 'string') {
|
|
32
|
+
const classes = props.class.split(' ').filter(Boolean).join('.');
|
|
33
|
+
if (classes) {
|
|
34
|
+
selector = `${tagName.toLowerCase()}.${classes}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return selector;
|
|
38
|
+
}
|
|
39
|
+
export function determineHydrationStrategy(tagName, props) {
|
|
40
|
+
if (props['data-hydrate'] === 'false') {
|
|
41
|
+
return { type: 'none' };
|
|
42
|
+
}
|
|
43
|
+
if (props['data-hydrate'] === 'lazy') {
|
|
44
|
+
return { type: 'lazy', priority: 'low' };
|
|
45
|
+
}
|
|
46
|
+
if (props['data-hydrate'] === 'deferred') {
|
|
47
|
+
return { type: 'deferred', priority: 'low', timeout: 2000 };
|
|
48
|
+
}
|
|
49
|
+
const hasClickHandler = Object.keys(props).some((key) => key === 'onClick');
|
|
50
|
+
if (hasClickHandler) {
|
|
51
|
+
return { type: 'eager', priority: 'high' };
|
|
52
|
+
}
|
|
53
|
+
if (isInteractiveTag(tagName)) {
|
|
54
|
+
return { type: 'eager', priority: 'medium' };
|
|
55
|
+
}
|
|
56
|
+
return { type: 'none' };
|
|
57
|
+
}
|
|
58
|
+
export function analyzeTree(element) {
|
|
59
|
+
const elements = [];
|
|
60
|
+
let hydrationRequired = 0;
|
|
61
|
+
let hydrationSkipped = 0;
|
|
62
|
+
function traverse(node) {
|
|
63
|
+
if (!node)
|
|
64
|
+
return;
|
|
65
|
+
const analyzed = analyzeElement(node);
|
|
66
|
+
if (analyzed) {
|
|
67
|
+
elements.push(analyzed);
|
|
68
|
+
if (analyzed.needsHydration) {
|
|
69
|
+
hydrationRequired++;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
hydrationSkipped++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const children = (node.props?.children ?? []);
|
|
76
|
+
if (Array.isArray(children)) {
|
|
77
|
+
for (const child of children) {
|
|
78
|
+
if (typeof child === 'object' && child !== null && 'type' in child) {
|
|
79
|
+
traverse(child);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
traverse(element);
|
|
85
|
+
return {
|
|
86
|
+
elements,
|
|
87
|
+
totalElements: elements.length,
|
|
88
|
+
hydrationRequired,
|
|
89
|
+
hydrationSkipped,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function getHydrationCandidates(manifest, strategy) {
|
|
93
|
+
return manifest.elements.filter((el) => el.strategy.type === strategy);
|
|
94
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createElement } from '../../runtime/index.js';
|
|
2
|
+
import { analyzeTree, getHydrationCandidates } from './analyzer.js';
|
|
3
|
+
const hydrationCache = new Map();
|
|
4
|
+
export async function hydrateSelective(container, element, options = {}) {
|
|
5
|
+
const root = typeof container === 'string' ? document.querySelector(container) : container;
|
|
6
|
+
if (!root || !element)
|
|
7
|
+
return;
|
|
8
|
+
const { hydrateInteractive = true, onHydrated, onError, } = options;
|
|
9
|
+
const manifest = analyzeTree(element);
|
|
10
|
+
if (!hydrateInteractive) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const candidates = getHydrationCandidates(manifest, 'eager');
|
|
14
|
+
await Promise.allSettled(candidates.map((candidate) => hydrateElement(root, candidate, element, onHydrated, onError)));
|
|
15
|
+
}
|
|
16
|
+
async function hydrateElement(root, candidate, element, onHydrated, onError) {
|
|
17
|
+
const cacheKey = candidate.selector;
|
|
18
|
+
if (hydrationCache.has(cacheKey)) {
|
|
19
|
+
await hydrationCache.get(cacheKey);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const promise = new Promise((resolve) => {
|
|
23
|
+
const targetElement = root.querySelector(candidate.selector);
|
|
24
|
+
if (!targetElement) {
|
|
25
|
+
resolve();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
attachEventHandlers(targetElement, candidate.eventHandlers);
|
|
30
|
+
if (onHydrated) {
|
|
31
|
+
onHydrated(candidate);
|
|
32
|
+
}
|
|
33
|
+
resolve();
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (onError && err instanceof Error) {
|
|
37
|
+
onError(candidate, err);
|
|
38
|
+
}
|
|
39
|
+
resolve();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
hydrationCache.set(cacheKey, promise);
|
|
43
|
+
await promise;
|
|
44
|
+
}
|
|
45
|
+
export function attachEventHandlers(element, handlers) {
|
|
46
|
+
for (const handler of handlers) {
|
|
47
|
+
const attribute = handler.toLowerCase();
|
|
48
|
+
const attrValue = element.getAttribute(attribute);
|
|
49
|
+
if (attrValue) {
|
|
50
|
+
try {
|
|
51
|
+
const fn = new Function('event', attrValue);
|
|
52
|
+
element.addEventListener(mapHandlerToEvent(handler), fn);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Invalid handler, skip
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function mapHandlerToEvent(handler) {
|
|
61
|
+
const mapping = {
|
|
62
|
+
onClick: 'click',
|
|
63
|
+
onMouseDown: 'mousedown',
|
|
64
|
+
onMouseUp: 'mouseup',
|
|
65
|
+
onMouseEnter: 'mouseenter',
|
|
66
|
+
onMouseLeave: 'mouseleave',
|
|
67
|
+
onFocus: 'focus',
|
|
68
|
+
onBlur: 'blur',
|
|
69
|
+
onChange: 'change',
|
|
70
|
+
onInput: 'input',
|
|
71
|
+
onSubmit: 'submit',
|
|
72
|
+
onKeyDown: 'keydown',
|
|
73
|
+
onKeyUp: 'keyup',
|
|
74
|
+
onKeyPress: 'keypress',
|
|
75
|
+
onScroll: 'scroll',
|
|
76
|
+
onTouchStart: 'touchstart',
|
|
77
|
+
onTouchEnd: 'touchend',
|
|
78
|
+
onTouchMove: 'touchmove',
|
|
79
|
+
onDragStart: 'dragstart',
|
|
80
|
+
onDrag: 'drag',
|
|
81
|
+
onDragEnd: 'dragend',
|
|
82
|
+
onWheel: 'wheel',
|
|
83
|
+
onAnimationStart: 'animationstart',
|
|
84
|
+
onAnimationEnd: 'animationend',
|
|
85
|
+
};
|
|
86
|
+
return mapping[handler] ?? handler.toLowerCase().replace('on', '');
|
|
87
|
+
}
|
|
88
|
+
export function createLazyHydration(loader, options = {}) {
|
|
89
|
+
const { fallback = createElement('div', { 'data-loading': '' }, 'Loading...'), timeout, onLoaded, onError, } = options;
|
|
90
|
+
const container = createElement('div', {
|
|
91
|
+
'data-lazy': '',
|
|
92
|
+
'data-loader': loader.toString(),
|
|
93
|
+
}, fallback);
|
|
94
|
+
if (typeof IntersectionObserver !== 'undefined') {
|
|
95
|
+
requestAnimationFrame(() => {
|
|
96
|
+
observeAndHydrate(container, loader, timeout, onLoaded, onError);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
loadImmediately(loader, onLoaded, onError);
|
|
101
|
+
}
|
|
102
|
+
return container;
|
|
103
|
+
}
|
|
104
|
+
function observeAndHydrate(container, loader, timeout, onLoaded, onError) {
|
|
105
|
+
const observer = new IntersectionObserver((entries) => {
|
|
106
|
+
if (entries[0]?.isIntersecting) {
|
|
107
|
+
observer.disconnect();
|
|
108
|
+
loadImmediately(loader, onLoaded, onError);
|
|
109
|
+
}
|
|
110
|
+
}, { rootMargin: '100px' });
|
|
111
|
+
const root = document.querySelector(`[data-lazy]`);
|
|
112
|
+
if (root) {
|
|
113
|
+
observer.observe(root);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function loadImmediately(loader, onLoaded, onError) {
|
|
117
|
+
try {
|
|
118
|
+
const result = await loader();
|
|
119
|
+
onLoaded?.(result);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
if (err instanceof Error && onError) {
|
|
123
|
+
onError(err);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export function clearHydrationCache() {
|
|
128
|
+
hydrationCache.clear();
|
|
129
|
+
}
|