edges-svelte 2.2.1 → 3.0.1
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 +28 -50
- package/dist/client/NavigationStateObserver.svelte +9 -0
- package/dist/client/NavigationStateObserver.svelte.d.ts +3 -0
- package/dist/client/NavigationSync.svelte.d.ts +6 -0
- package/dist/client/NavigationSync.svelte.js +76 -140
- package/dist/context/Context.d.ts +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plugin/EdgesAutoHandlePlugin.d.ts +2 -4
- package/dist/plugin/EdgesAutoHandlePlugin.js +187 -19
- package/dist/provider/Provider.js +79 -3
- package/dist/server/AutoWrapHandle.d.ts +1 -7
- package/dist/server/AutoWrapHandle.js +3 -4
- package/dist/server/EdgesHandle.d.ts +1 -5
- package/dist/server/EdgesHandle.js +5 -56
- package/dist/server/EdgesHandleSimplified.d.ts +1 -5
- package/dist/server/ServerSync.d.ts +3 -0
- package/dist/server/ServerSync.js +125 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/store/State.svelte.d.ts +1 -4
- package/dist/store/State.svelte.js +72 -31
- package/dist/types.d.ts +0 -6
- package/dist/utils/batch.d.ts +1 -0
- package/dist/utils/batch.js +34 -14
- package/dist/utils/dev.js +1 -2
- package/dist/utils/environment.d.ts +1 -0
- package/dist/utils/environment.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
### A blazing-fast, extremely lightweight and SSR-friendly store for SvelteKit.
|
|
4
4
|
|
|
5
|
-
**EdgeS** brings seamless, per-request state management to Svelte apps
|
|
5
|
+
**EdgeS** brings seamless, per-request state management to Svelte apps.
|
|
6
6
|
|
|
7
|
-
No context boilerplate. No hydration headaches.
|
|
7
|
+
No context boilerplate. No hydration headaches.
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
9
|
+
- Persistent per-request memory via `AsyncLocalStorage`
|
|
10
|
+
- Tiny API
|
|
11
|
+
- Instant serialization without magic
|
|
12
|
+
- Dependency injection, zero runtime overhead
|
|
13
|
+
|
|
14
|
+
EdgeS is built to prevent state leaks. Its primary goal is to keep server-side state safely isolated per request while providing a clean developer experience with presenters, stores, and automatic client updates when fresh state arrives from the server. It is intentionally one-way sync from server to client and does not aim to provide full two-way state synchronization between client and server.
|
|
14
15
|
|
|
15
16
|
> Designed for **SvelteKit**.
|
|
16
17
|
|
|
@@ -77,8 +78,8 @@ const myStore = createStore('MyUniqueStoreName', ({ createState, createDerivedSt
|
|
|
77
78
|
<!-- Will update the state -->
|
|
78
79
|
```
|
|
79
80
|
|
|
80
|
-
-
|
|
81
|
-
-
|
|
81
|
+
- All stores created inside `createStore` use unique keys automatically and are request-scoped
|
|
82
|
+
- Fully SSR-safe — stores are isolated per request and serialized automatically
|
|
82
83
|
|
|
83
84
|
---
|
|
84
85
|
|
|
@@ -158,6 +159,22 @@ const useUserStore = withDeps(({ user, createState }) => {
|
|
|
158
159
|
});
|
|
159
160
|
```
|
|
160
161
|
|
|
162
|
+
For provider-to-provider dependencies, inject provider functions lazily:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const useAuth = createPresenter('AuthPresenter', () => ({ isLoggedIn: true }));
|
|
166
|
+
|
|
167
|
+
const useHeader = createPresenter(
|
|
168
|
+
'HeaderPresenter',
|
|
169
|
+
({ useAuth }) => ({
|
|
170
|
+
canShowProfile: () => useAuth().isLoggedIn
|
|
171
|
+
}),
|
|
172
|
+
{ useAuth }
|
|
173
|
+
);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Avoid injecting resolved provider instances. In development, edges throws a fail-fast error for eager provider injection and for circular dependency chains like `A -> B -> A`.
|
|
177
|
+
|
|
161
178
|
---
|
|
162
179
|
|
|
163
180
|
## createPresenter
|
|
@@ -279,48 +296,9 @@ You can skip specifying a unique key and pass the factory as the first argument
|
|
|
279
296
|
|
|
280
297
|
---
|
|
281
298
|
|
|
282
|
-
## State
|
|
283
|
-
|
|
284
|
-
### Method 1: Via Plugin (Recommended) ✨
|
|
299
|
+
## State Serialization
|
|
285
300
|
|
|
286
|
-
|
|
287
|
-
// vite.config.ts - Zero config compression!
|
|
288
|
-
import { sveltekit } from '@sveltejs/kit/vite';
|
|
289
|
-
import { defineConfig } from 'vite';
|
|
290
|
-
import { edgesPlugin } from 'edges-svelte/plugin';
|
|
291
|
-
|
|
292
|
-
export default defineConfig({
|
|
293
|
-
plugins: [
|
|
294
|
-
sveltekit(),
|
|
295
|
-
edgesPlugin({
|
|
296
|
-
compression: {
|
|
297
|
-
enabled: true, // Enable compression
|
|
298
|
-
threshold: 2048 // Compress states > 2KB
|
|
299
|
-
},
|
|
300
|
-
silentChromeDevtools: true // Optional: silence devtools requests
|
|
301
|
-
})
|
|
302
|
-
]
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
// That's it! No need to touch hooks.server.ts
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
### Method 2: Manual Setup (For advanced use cases)
|
|
309
|
-
|
|
310
|
-
```typescript
|
|
311
|
-
// hooks.server.ts - If you need custom control
|
|
312
|
-
import { edgesHandle } from 'edges-svelte/server';
|
|
313
|
-
|
|
314
|
-
export const handle = edgesHandle(({ serialize, edgesEvent, resolve }) => {
|
|
315
|
-
return resolve(edgesEvent, {
|
|
316
|
-
transformPageChunk: ({ html }) =>
|
|
317
|
-
serialize(html, {
|
|
318
|
-
compress: true, // Enable compression
|
|
319
|
-
compressionThreshold: 2048 // Compress states > 2KB
|
|
320
|
-
})
|
|
321
|
-
});
|
|
322
|
-
});
|
|
323
|
-
```
|
|
301
|
+
EdgeS serializes SSR state as plain script payloads. Transport-level compression should be handled by your HTTP stack (gzip/brotli) instead of application-level compression in this package.
|
|
324
302
|
|
|
325
303
|
---
|
|
326
304
|
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
export declare function registerStateUpdate(key: string, callback: (value: unknown) => void): void;
|
|
2
2
|
export declare function unregisterStateUpdate(key: string): void;
|
|
3
3
|
export declare function processEdgesState(edgesState: Record<string, unknown>): void;
|
|
4
|
+
export declare function applyEdgesFromPayload(payload: unknown): void;
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
__EDGES_NAVIGATION_SYNC_MOUNTED__?: boolean;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { browser } from '../utils/environment.js';
|
|
2
|
+
import { batch } from '../utils/batch.js';
|
|
2
3
|
const stateUpdateCallbacks = new Map();
|
|
3
4
|
const UNDEFINED_MARKER = '__EDGES_UNDEFINED__';
|
|
4
5
|
const NULL_MARKER = '__EDGES_NULL__';
|
|
5
|
-
const
|
|
6
|
+
const BIGINT_MARKER = '__EDGES_BIGINT__';
|
|
7
|
+
const EDGES_STATE_FIELD = '__edges_state__';
|
|
8
|
+
const EDGES_REV_FIELD = '__edges_rev__';
|
|
9
|
+
let lastAppliedRevision = 0;
|
|
10
|
+
const decodeEdgesValue = (value) => {
|
|
6
11
|
if (value && typeof value === 'object') {
|
|
7
|
-
if (UNDEFINED_MARKER in value)
|
|
12
|
+
if (UNDEFINED_MARKER in value)
|
|
8
13
|
return undefined;
|
|
9
|
-
|
|
10
|
-
if (NULL_MARKER in value) {
|
|
14
|
+
if (NULL_MARKER in value)
|
|
11
15
|
return null;
|
|
16
|
+
if (BIGINT_MARKER in value)
|
|
17
|
+
return BigInt(String(value[BIGINT_MARKER]));
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
return value.map((item) => decodeEdgesValue(item));
|
|
20
|
+
}
|
|
21
|
+
const decoded = {};
|
|
22
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
23
|
+
decoded[key] = decodeEdgesValue(nested);
|
|
12
24
|
}
|
|
25
|
+
return decoded;
|
|
13
26
|
}
|
|
14
27
|
return value;
|
|
15
28
|
};
|
|
@@ -22,142 +35,62 @@ export function unregisterStateUpdate(key) {
|
|
|
22
35
|
stateUpdateCallbacks.delete(key);
|
|
23
36
|
}
|
|
24
37
|
export function processEdgesState(edgesState) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
const store = window.__SAFE_SSR_STATE__ ?? new Map();
|
|
39
|
+
window.__SAFE_SSR_STATE__ = store;
|
|
40
|
+
batch(() => {
|
|
41
|
+
for (const [key, value] of Object.entries(edgesState)) {
|
|
42
|
+
let processedValue = decodeEdgesValue(value);
|
|
43
|
+
if (typeof value === 'string') {
|
|
44
|
+
try {
|
|
45
|
+
processedValue = decodeEdgesValue(JSON.parse(value));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* do nothing */
|
|
49
|
+
}
|
|
33
50
|
}
|
|
34
|
-
|
|
35
|
-
|
|
51
|
+
store.set(key, processedValue);
|
|
52
|
+
const callback = stateUpdateCallbacks.get(key);
|
|
53
|
+
if (callback) {
|
|
54
|
+
callback(processedValue);
|
|
36
55
|
}
|
|
37
56
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function applyEdgesFromPayload(payload) {
|
|
60
|
+
if (!payload || typeof payload !== 'object')
|
|
61
|
+
return;
|
|
62
|
+
const data = payload;
|
|
63
|
+
const rawState = data[EDGES_STATE_FIELD];
|
|
64
|
+
if (!rawState || typeof rawState !== 'object')
|
|
65
|
+
return;
|
|
66
|
+
const revision = Number(data[EDGES_REV_FIELD] ?? 0);
|
|
67
|
+
if (Number.isFinite(revision) && revision > 0) {
|
|
68
|
+
if (revision <= lastAppliedRevision)
|
|
69
|
+
return;
|
|
70
|
+
lastAppliedRevision = revision;
|
|
43
71
|
}
|
|
72
|
+
processEdgesState(rawState);
|
|
44
73
|
}
|
|
45
74
|
if (browser) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const [input, init] = args;
|
|
66
|
-
let reqInfo = { url: '', headers: {} };
|
|
67
|
-
if (typeof input === 'string') {
|
|
68
|
-
reqInfo = { url: input, headers: parseHeaders(init?.headers) };
|
|
69
|
-
}
|
|
70
|
-
else if (input instanceof Request) {
|
|
71
|
-
reqInfo = { url: input.url, headers: parseHeaders(input.headers) };
|
|
72
|
-
}
|
|
73
|
-
else if (input instanceof URL) {
|
|
74
|
-
reqInfo = { url: input.href, headers: parseHeaders(init?.headers) };
|
|
75
|
-
}
|
|
76
|
-
const isSvelteKitRequest = init.__sveltekit_fetch__ || reqInfo.headers['x-sveltekit-action'] || reqInfo.url.includes('__data.json');
|
|
77
|
-
const response = await originalFetch.apply(this, args);
|
|
78
|
-
if (!isSvelteKitRequest) {
|
|
79
|
-
return response;
|
|
80
|
-
}
|
|
81
|
-
if (!response.headers.get('content-type')?.includes('application/json')) {
|
|
82
|
-
return response;
|
|
83
|
-
}
|
|
84
|
-
const interceptEdgesStateFromResponse = (response) => {
|
|
85
|
-
if (!response.body)
|
|
86
|
-
return response;
|
|
87
|
-
if (init?.method === 'POST') {
|
|
88
|
-
const originalText = response.text.bind(response);
|
|
89
|
-
response.text = async function () {
|
|
90
|
-
const text = await originalText();
|
|
91
|
-
if (text.includes('__edges_state__')) {
|
|
92
|
-
try {
|
|
93
|
-
const parsed = JSON.parse(text);
|
|
94
|
-
if (parsed.__edges_state__) {
|
|
95
|
-
processEdgesState(parsed.__edges_state__);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
// ignore parsing errors
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return text;
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
const originalGetReader = response.body.getReader.bind(response.body);
|
|
106
|
-
response.body.getReader = function (opts) {
|
|
107
|
-
const reader = originalGetReader(opts);
|
|
108
|
-
if (!('read' in reader))
|
|
109
|
-
return reader;
|
|
110
|
-
const originalRead = reader.read.bind(reader);
|
|
111
|
-
const decoder = new TextDecoder();
|
|
112
|
-
let buffer = '';
|
|
113
|
-
let found = false;
|
|
114
|
-
let depth = 0;
|
|
115
|
-
let capture = '';
|
|
116
|
-
reader.read = async function () {
|
|
117
|
-
const result = await originalRead();
|
|
118
|
-
if (result.done || found)
|
|
119
|
-
return result;
|
|
120
|
-
buffer += decoder.decode(result.value, { stream: true });
|
|
121
|
-
const idx = buffer.indexOf('"__edges_state__"');
|
|
122
|
-
if (idx !== -1) {
|
|
123
|
-
const braceStart = buffer.indexOf('{', idx);
|
|
124
|
-
if (braceStart !== -1) {
|
|
125
|
-
for (let i = braceStart; i < buffer.length; i++) {
|
|
126
|
-
const ch = buffer[i];
|
|
127
|
-
if (ch === '{') {
|
|
128
|
-
if (depth++ === 0)
|
|
129
|
-
capture = '';
|
|
130
|
-
}
|
|
131
|
-
if (depth > 0)
|
|
132
|
-
capture += ch;
|
|
133
|
-
if (ch === '}') {
|
|
134
|
-
depth--;
|
|
135
|
-
if (depth === 0) {
|
|
136
|
-
try {
|
|
137
|
-
const parsed = JSON.parse(capture);
|
|
138
|
-
processEdgesState(parsed);
|
|
139
|
-
found = true;
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
// waiting for next iteration
|
|
143
|
-
}
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
const MAX_BUFFER = Math.max(8192, capture.length * 2);
|
|
150
|
-
if (buffer.length > MAX_BUFFER)
|
|
151
|
-
buffer = buffer.slice(-MAX_BUFFER / 2);
|
|
152
|
-
}
|
|
153
|
-
return result;
|
|
154
|
-
};
|
|
155
|
-
return reader;
|
|
156
|
-
};
|
|
157
|
-
return response;
|
|
158
|
-
};
|
|
159
|
-
return interceptEdgesStateFromResponse(response);
|
|
160
|
-
};
|
|
75
|
+
if (!window.__EDGES_NAVIGATION_SYNC_MOUNTED__) {
|
|
76
|
+
window.__EDGES_NAVIGATION_SYNC_MOUNTED__ = true;
|
|
77
|
+
void Promise.all([import('svelte'), import('./NavigationStateObserver.svelte')])
|
|
78
|
+
.then(([svelte, module]) => {
|
|
79
|
+
const target = document.body || document.documentElement;
|
|
80
|
+
const host = document.createElement('div');
|
|
81
|
+
host.setAttribute('data-edges-navigation-sync', '1');
|
|
82
|
+
host.style.display = 'none';
|
|
83
|
+
target.appendChild(host);
|
|
84
|
+
svelte.mount(module.default, { target: host });
|
|
85
|
+
window.addEventListener('beforeunload', () => {
|
|
86
|
+
host.remove();
|
|
87
|
+
window.__EDGES_NAVIGATION_SYNC_MOUNTED__ = false;
|
|
88
|
+
}, { once: true });
|
|
89
|
+
})
|
|
90
|
+
.catch(() => {
|
|
91
|
+
window.__EDGES_NAVIGATION_SYNC_MOUNTED__ = false;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
161
94
|
if (typeof MutationObserver !== 'undefined') {
|
|
162
95
|
const observer = new MutationObserver((mutations) => {
|
|
163
96
|
for (const mutation of mutations) {
|
|
@@ -167,13 +100,16 @@ if (browser) {
|
|
|
167
100
|
const text = node.textContent || '';
|
|
168
101
|
if (text.includes('__SAFE_SSR_STATE__') && text.includes('__EDGES_REVIVER__')) {
|
|
169
102
|
queueMicrotask(() => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
callback(
|
|
103
|
+
const store = window.__SAFE_SSR_STATE__;
|
|
104
|
+
if (store) {
|
|
105
|
+
batch(() => {
|
|
106
|
+
for (const [key, value] of store) {
|
|
107
|
+
const callback = stateUpdateCallbacks.get(key);
|
|
108
|
+
if (callback) {
|
|
109
|
+
callback(value);
|
|
110
|
+
}
|
|
175
111
|
}
|
|
176
|
-
}
|
|
112
|
+
});
|
|
177
113
|
}
|
|
178
114
|
});
|
|
179
115
|
}
|
|
@@ -7,6 +7,9 @@ export interface ContextData {
|
|
|
7
7
|
providers?: Map<string, unknown>;
|
|
8
8
|
providersAutoKeyCache?: WeakMap<(...args: unknown[]) => unknown, string>;
|
|
9
9
|
providersAutoKeyCounters?: Map<string, number>;
|
|
10
|
+
providersConstructionStack?: string[];
|
|
11
|
+
edgesDirtyKeys?: Set<string>;
|
|
12
|
+
edgesRevision?: number;
|
|
10
13
|
} & App.ContextDataExtended;
|
|
11
14
|
}
|
|
12
15
|
declare class RequestContextManager {
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
2
|
export interface EdgesPluginOptions {
|
|
3
|
-
compression?: {
|
|
4
|
-
enabled?: boolean;
|
|
5
|
-
threshold?: number;
|
|
6
|
-
};
|
|
7
3
|
silentChromeDevtools?: boolean;
|
|
4
|
+
syncFromServer?: boolean;
|
|
5
|
+
syncTransformMode?: 'ast' | 'regex' | 'hybrid';
|
|
8
6
|
}
|
|
9
7
|
/**
|
|
10
8
|
* Creates a factory for the edges plugin with a custom package name and server path.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
1
2
|
/**
|
|
2
3
|
* Creates a factory for the edges plugin with a custom package name and server path.
|
|
3
4
|
*
|
|
@@ -16,16 +17,197 @@
|
|
|
16
17
|
* ```
|
|
17
18
|
*/
|
|
18
19
|
export function createEdgesPluginFactory(packageName, serverPath) {
|
|
19
|
-
// Compile regex patterns once per factory (performance optimization)
|
|
20
20
|
const MANUAL_IMPORT_PATTERN = new RegExp(`from\\s+['"](?:${packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/server|\\$lib/server)['"]`, 'g');
|
|
21
|
-
// Match "export const handle" with optional type annotation (e.g., ": Handle")
|
|
22
21
|
const HANDLE_EXPORT_PATTERN = /export\s+const\s+handle\s*(?::\s*\w+\s*)?=/;
|
|
22
|
+
const LOAD_EXPORT_PATTERN = /export\s+const\s+load\s*(?::\s*[^=]+)?=/;
|
|
23
|
+
const ACTIONS_EXPORT_PATTERN = /export\s+const\s+actions\s*(?::\s*[^=]+)?=/;
|
|
24
|
+
const SERVER_ROUTE_PATTERN = /[\\/]\+((page|layout)\.server)\.(t|j)s$/;
|
|
25
|
+
const UNIVERSAL_ROUTE_PATTERN = /[\\/]\+((page|layout))\.(t|j)s$/;
|
|
26
|
+
const SYNC_MARKER = "void '__EDGES_SYNC_WRAPPED__';";
|
|
27
|
+
const AST_SERVER_LOAD_ALIAS = '__edgesWrappedServerLoad';
|
|
28
|
+
const AST_ACTIONS_ALIAS = '__edgesWrappedActions';
|
|
29
|
+
const AST_UNIVERSAL_LOAD_ALIAS = '__edgesWrappedUniversalLoad';
|
|
30
|
+
const applyEdits = (sourceCode, edits) => {
|
|
31
|
+
if (edits.length === 0)
|
|
32
|
+
return sourceCode;
|
|
33
|
+
const sorted = edits.sort((a, b) => b.start - a.start);
|
|
34
|
+
let result = sourceCode;
|
|
35
|
+
for (const edit of sorted) {
|
|
36
|
+
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
23
40
|
return function edgesPlugin(options) {
|
|
24
|
-
const {
|
|
41
|
+
const { silentChromeDevtools = true, syncFromServer = true, syncTransformMode = 'hybrid' } = options || {};
|
|
42
|
+
const findImportInsertPosition = (sourceCode) => {
|
|
43
|
+
const importRegex = /(?:^|\n)((?:import|export)\s+(?:type\s+)?(?:\{[^}]*\}|\*|\w+)(?:\s+from)?\s+['"][^'"]+['"];?)/gm;
|
|
44
|
+
let lastMatch = null;
|
|
45
|
+
let match;
|
|
46
|
+
while ((match = importRegex.exec(sourceCode)) !== null) {
|
|
47
|
+
lastMatch = match;
|
|
48
|
+
}
|
|
49
|
+
if (!lastMatch) {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
return lastMatch.index + lastMatch[0].length;
|
|
53
|
+
};
|
|
54
|
+
const ensureSyncImport = (sourceCode, importLine) => {
|
|
55
|
+
if (sourceCode.includes('__EDGES_SYNC_WRAPPED__'))
|
|
56
|
+
return sourceCode;
|
|
57
|
+
const insertPos = findImportInsertPosition(sourceCode);
|
|
58
|
+
const beforeImports = sourceCode.slice(0, insertPos);
|
|
59
|
+
const afterImports = sourceCode.slice(insertPos);
|
|
60
|
+
return `${beforeImports}\n${SYNC_MARKER}\n${importLine}\n${afterImports}`;
|
|
61
|
+
};
|
|
62
|
+
const ensureAstServerImport = (sourceCode) => ensureSyncImport(sourceCode, `import { __withEdgesServerLoad as ${AST_SERVER_LOAD_ALIAS}, __withEdgesActions as ${AST_ACTIONS_ALIAS} } from '${serverPath}';`);
|
|
63
|
+
const ensureAstUniversalImport = (sourceCode) => ensureSyncImport(sourceCode, `import { __withEdgesUniversalLoad as ${AST_UNIVERSAL_LOAD_ALIAS} } from '${packageName}';`);
|
|
64
|
+
const ensureRegexServerImport = (sourceCode) => ensureSyncImport(sourceCode, `import { __withEdgesServerLoad, __withEdgesActions } from '${serverPath}';`);
|
|
65
|
+
const ensureRegexUniversalImport = (sourceCode) => ensureSyncImport(sourceCode, `import { __withEdgesUniversalLoad } from '${packageName}';`);
|
|
66
|
+
const findExportedLocal = (sourceFile, code, exportedName) => {
|
|
67
|
+
const edits = [];
|
|
68
|
+
let localName = null;
|
|
69
|
+
let found = false;
|
|
70
|
+
for (const stmt of sourceFile.statements) {
|
|
71
|
+
if (ts.isVariableStatement(stmt)) {
|
|
72
|
+
const exportModifier = stmt.modifiers?.find((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
73
|
+
if (!exportModifier)
|
|
74
|
+
continue;
|
|
75
|
+
for (const declaration of stmt.declarationList.declarations) {
|
|
76
|
+
if (!ts.isIdentifier(declaration.name))
|
|
77
|
+
continue;
|
|
78
|
+
if (declaration.name.text !== exportedName)
|
|
79
|
+
continue;
|
|
80
|
+
localName = declaration.name.text;
|
|
81
|
+
found = true;
|
|
82
|
+
const modifierStart = exportModifier.getStart(sourceFile);
|
|
83
|
+
let modifierEnd = exportModifier.end;
|
|
84
|
+
while (modifierEnd < code.length && /\s/.test(code[modifierEnd]))
|
|
85
|
+
modifierEnd += 1;
|
|
86
|
+
edits.push({ start: modifierStart, end: modifierEnd, text: '' });
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!ts.isExportDeclaration(stmt) || !stmt.exportClause || !ts.isNamedExports(stmt.exportClause) || stmt.moduleSpecifier)
|
|
91
|
+
continue;
|
|
92
|
+
const named = stmt.exportClause;
|
|
93
|
+
const keepSpecs = [];
|
|
94
|
+
let statementHasTarget = false;
|
|
95
|
+
for (const el of named.elements) {
|
|
96
|
+
const exportName = el.name.text;
|
|
97
|
+
const sourceName = el.propertyName?.text ?? el.name.text;
|
|
98
|
+
if (exportName === exportedName) {
|
|
99
|
+
statementHasTarget = true;
|
|
100
|
+
found = true;
|
|
101
|
+
localName = sourceName;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
keepSpecs.push(code.slice(el.getStart(sourceFile), el.end).trim());
|
|
105
|
+
}
|
|
106
|
+
if (!statementHasTarget)
|
|
107
|
+
continue;
|
|
108
|
+
if (keepSpecs.length === 0) {
|
|
109
|
+
edits.push({ start: stmt.getStart(sourceFile), end: stmt.end, text: '' });
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
edits.push({ start: stmt.getStart(sourceFile), end: stmt.end, text: `export { ${keepSpecs.join(', ')} };` });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { localName, edits, found };
|
|
116
|
+
};
|
|
117
|
+
const wrapServerRouteModuleRegex = (sourceCode) => {
|
|
118
|
+
if (!LOAD_EXPORT_PATTERN.test(sourceCode) && !ACTIONS_EXPORT_PATTERN.test(sourceCode)) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
let wrapped = ensureRegexServerImport(sourceCode);
|
|
122
|
+
if (LOAD_EXPORT_PATTERN.test(wrapped)) {
|
|
123
|
+
wrapped = wrapped
|
|
124
|
+
.replace(LOAD_EXPORT_PATTERN, (match) => match.replace('export const load', 'const __userLoad'))
|
|
125
|
+
.concat('\n\nexport const load = __withEdgesServerLoad(__userLoad);');
|
|
126
|
+
}
|
|
127
|
+
if (ACTIONS_EXPORT_PATTERN.test(wrapped)) {
|
|
128
|
+
wrapped = wrapped
|
|
129
|
+
.replace(ACTIONS_EXPORT_PATTERN, (match) => match.replace('export const actions', 'const __userActions'))
|
|
130
|
+
.concat('\n\nexport const actions = __withEdgesActions(__userActions);');
|
|
131
|
+
}
|
|
132
|
+
return wrapped;
|
|
133
|
+
};
|
|
134
|
+
const wrapUniversalRouteModuleRegex = (sourceCode) => {
|
|
135
|
+
if (!LOAD_EXPORT_PATTERN.test(sourceCode))
|
|
136
|
+
return null;
|
|
137
|
+
let wrapped = ensureRegexUniversalImport(sourceCode);
|
|
138
|
+
wrapped = wrapped
|
|
139
|
+
.replace(LOAD_EXPORT_PATTERN, (match) => match.replace('export const load', 'const __userUniversalLoad'))
|
|
140
|
+
.concat('\n\nexport const load = __withEdgesUniversalLoad(__userUniversalLoad);');
|
|
141
|
+
return wrapped;
|
|
142
|
+
};
|
|
143
|
+
const wrapServerRouteModuleAst = (sourceCode) => {
|
|
144
|
+
if (sourceCode.includes('__EDGES_SYNC_WRAPPED__'))
|
|
145
|
+
return null;
|
|
146
|
+
let sourceFile;
|
|
147
|
+
try {
|
|
148
|
+
sourceFile = ts.createSourceFile('route.ts', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const loadInfo = findExportedLocal(sourceFile, sourceCode, 'load');
|
|
154
|
+
const actionsInfo = findExportedLocal(sourceFile, sourceCode, 'actions');
|
|
155
|
+
if (!loadInfo.found && !actionsInfo.found)
|
|
156
|
+
return null;
|
|
157
|
+
if ((loadInfo.found && !loadInfo.localName) || (actionsInfo.found && !actionsInfo.localName))
|
|
158
|
+
return null;
|
|
159
|
+
let nextCode = applyEdits(sourceCode, [...loadInfo.edits, ...actionsInfo.edits]);
|
|
160
|
+
nextCode = ensureAstServerImport(nextCode);
|
|
161
|
+
const append = [];
|
|
162
|
+
if (loadInfo.localName) {
|
|
163
|
+
append.push(`const __edgesServerLoad = ${AST_SERVER_LOAD_ALIAS}(${loadInfo.localName});`);
|
|
164
|
+
append.push(`export { __edgesServerLoad as load };`);
|
|
165
|
+
}
|
|
166
|
+
if (actionsInfo.localName) {
|
|
167
|
+
append.push(`const __edgesServerActions = ${AST_ACTIONS_ALIAS}(${actionsInfo.localName});`);
|
|
168
|
+
append.push(`export { __edgesServerActions as actions };`);
|
|
169
|
+
}
|
|
170
|
+
return `${nextCode}\n\n${append.join('\n')}`;
|
|
171
|
+
};
|
|
172
|
+
const wrapUniversalRouteModuleAst = (sourceCode) => {
|
|
173
|
+
if (sourceCode.includes('__EDGES_SYNC_WRAPPED__'))
|
|
174
|
+
return null;
|
|
175
|
+
let sourceFile;
|
|
176
|
+
try {
|
|
177
|
+
sourceFile = ts.createSourceFile('route.ts', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const loadInfo = findExportedLocal(sourceFile, sourceCode, 'load');
|
|
183
|
+
if (!loadInfo.found || !loadInfo.localName)
|
|
184
|
+
return null;
|
|
185
|
+
let nextCode = applyEdits(sourceCode, [...loadInfo.edits]);
|
|
186
|
+
nextCode = ensureAstUniversalImport(nextCode);
|
|
187
|
+
return `${nextCode}\n\nconst __edgesUniversalLoad = ${AST_UNIVERSAL_LOAD_ALIAS}(${loadInfo.localName});\nexport { __edgesUniversalLoad as load };`;
|
|
188
|
+
};
|
|
25
189
|
return {
|
|
26
190
|
name: `${packageName}-auto-handle`,
|
|
27
191
|
enforce: 'pre',
|
|
28
192
|
transform(code, id) {
|
|
193
|
+
if (syncFromServer && SERVER_ROUTE_PATTERN.test(id) && !id.includes('hooks.server')) {
|
|
194
|
+
const wrapped = syncTransformMode === 'regex'
|
|
195
|
+
? wrapServerRouteModuleRegex(code)
|
|
196
|
+
: syncTransformMode === 'ast'
|
|
197
|
+
? wrapServerRouteModuleAst(code)
|
|
198
|
+
: (wrapServerRouteModuleAst(code) ?? wrapServerRouteModuleRegex(code));
|
|
199
|
+
if (wrapped)
|
|
200
|
+
return { code: wrapped, map: null };
|
|
201
|
+
}
|
|
202
|
+
if (syncFromServer && UNIVERSAL_ROUTE_PATTERN.test(id) && !id.includes('.server.')) {
|
|
203
|
+
const wrapped = syncTransformMode === 'regex'
|
|
204
|
+
? wrapUniversalRouteModuleRegex(code)
|
|
205
|
+
: syncTransformMode === 'ast'
|
|
206
|
+
? wrapUniversalRouteModuleAst(code)
|
|
207
|
+
: (wrapUniversalRouteModuleAst(code) ?? wrapUniversalRouteModuleRegex(code));
|
|
208
|
+
if (wrapped)
|
|
209
|
+
return { code: wrapped, map: null };
|
|
210
|
+
}
|
|
29
211
|
if (!id.includes('hooks.server.ts'))
|
|
30
212
|
return null;
|
|
31
213
|
if (code.includes('__EDGES_AUTO_WRAPPED__'))
|
|
@@ -34,20 +216,7 @@ export function createEdgesPluginFactory(packageName, serverPath) {
|
|
|
34
216
|
return null;
|
|
35
217
|
}
|
|
36
218
|
const hasHandleExport = HANDLE_EXPORT_PATTERN.test(code);
|
|
37
|
-
const compressionOptions = compression.enabled ? `, { compress: true, compressionThreshold: ${compression.threshold || 1024} }` : '';
|
|
38
219
|
const silentOption = silentChromeDevtools ? '' : `, false`;
|
|
39
|
-
const findImportInsertPosition = (sourceCode) => {
|
|
40
|
-
const importRegex = /(?:^|\n)((?:import|export)\s+(?:type\s+)?(?:\{[^}]*\}|\*|\w+)(?:\s+from)?\s+['"][^'"]+['"];?)/gm;
|
|
41
|
-
let lastMatch = null;
|
|
42
|
-
let match;
|
|
43
|
-
while ((match = importRegex.exec(sourceCode)) !== null) {
|
|
44
|
-
lastMatch = match;
|
|
45
|
-
}
|
|
46
|
-
if (!lastMatch) {
|
|
47
|
-
return 0;
|
|
48
|
-
}
|
|
49
|
-
return lastMatch.index + lastMatch[0].length;
|
|
50
|
-
};
|
|
51
220
|
if (!hasHandleExport) {
|
|
52
221
|
const insertPos = findImportInsertPosition(code);
|
|
53
222
|
const beforeImports = code.slice(0, insertPos);
|
|
@@ -59,7 +228,7 @@ export function createEdgesPluginFactory(packageName, serverPath) {
|
|
|
59
228
|
afterImports +
|
|
60
229
|
`\n\n` +
|
|
61
230
|
`export const handle = edgesHandle(({ serialize, edgesEvent, resolve }) => ` +
|
|
62
|
-
`resolve(edgesEvent, { transformPageChunk: ({ html }) => serialize(html
|
|
231
|
+
`resolve(edgesEvent, { transformPageChunk: ({ html }) => serialize(html) })${silentOption});`,
|
|
63
232
|
map: null
|
|
64
233
|
};
|
|
65
234
|
}
|
|
@@ -71,9 +240,8 @@ export function createEdgesPluginFactory(packageName, serverPath) {
|
|
|
71
240
|
`import { __autoWrapHandle } from '${serverPath}';\n\n` +
|
|
72
241
|
afterImports.replace(/export\s+const\s+handle\s*(?::\s*\w+\s*)?=/, 'const __userHandle =') +
|
|
73
242
|
`\n\n` +
|
|
74
|
-
`const __compressionOptions = ${JSON.stringify({ compress: compression.enabled, compressionThreshold: compression.threshold })};\n` +
|
|
75
243
|
`const __silentChromeDevtools = ${silentChromeDevtools};\n` +
|
|
76
|
-
`export const handle = __autoWrapHandle(__userHandle,
|
|
244
|
+
`export const handle = __autoWrapHandle(__userHandle, __silentChromeDevtools);`;
|
|
77
245
|
return {
|
|
78
246
|
code: wrappedCode,
|
|
79
247
|
map: null
|