edges-svelte 2.2.0 → 3.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/README.md +29 -51
- 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 +77 -148
- package/dist/context/Context.d.ts +3 -0
- package/dist/plugin/EdgesAutoHandlePlugin.d.ts +2 -66
- package/dist/plugin/EdgesAutoHandlePlugin.js +187 -83
- package/dist/provider/Provider.d.ts +0 -38
- package/dist/provider/Provider.js +82 -24
- package/dist/server/AutoWrapHandle.d.ts +1 -7
- package/dist/server/AutoWrapHandle.js +3 -8
- package/dist/server/EdgesHandle.d.ts +1 -8
- package/dist/server/EdgesHandle.js +5 -60
- package/dist/server/EdgesHandleSimplified.d.ts +1 -13
- package/dist/server/EdgesHandleSimplified.js +0 -8
- 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 -48
- package/dist/types.d.ts +0 -86
- package/dist/utils/batch.d.ts +1 -21
- package/dist/utils/batch.js +34 -54
- package/dist/utils/dev.d.ts +0 -21
- package/dist/utils/dev.js +3 -31
- package/dist/utils/environment.d.ts +1 -4
- package/dist/utils/environment.js +1 -4
- package/package.json +1 -4
|
@@ -1,9 +1,9 @@
|
|
|
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
|
*
|
|
4
5
|
* Use this when:
|
|
5
6
|
* - Creating a wrapper package that re-exports edges-svelte functionality
|
|
6
|
-
* - Developing the package itself (use `$lib/server` as serverPath)
|
|
7
7
|
*
|
|
8
8
|
* @param packageName - The name that will be used in generated imports (e.g., 'edges-svelte', 'my-wrapper')
|
|
9
9
|
* @param serverPath - The import path to the server module (e.g., 'edges-svelte/server', '$lib/server')
|
|
@@ -15,68 +15,208 @@
|
|
|
15
15
|
*
|
|
16
16
|
* export const myWrapperPlugin = createEdgesPluginFactory('my-wrapper', 'my-wrapper/server');
|
|
17
17
|
* ```
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* ```ts
|
|
21
|
-
* // For package development (testing the package itself)
|
|
22
|
-
* import { createEdgesPluginFactory } from './src/lib/plugin/index.js';
|
|
23
|
-
*
|
|
24
|
-
* const edgesPluginDev = createEdgesPluginFactory('edges-svelte', '$lib/server');
|
|
25
|
-
*
|
|
26
|
-
* export default defineConfig({
|
|
27
|
-
* plugins: [sveltekit(), edgesPluginDev()]
|
|
28
|
-
* });
|
|
29
|
-
* ```
|
|
30
18
|
*/
|
|
31
19
|
export function createEdgesPluginFactory(packageName, serverPath) {
|
|
32
|
-
// Compile regex patterns once per factory (performance optimization)
|
|
33
20
|
const MANUAL_IMPORT_PATTERN = new RegExp(`from\\s+['"](?:${packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/server|\\$lib/server)['"]`, 'g');
|
|
34
|
-
// Match "export const handle" with optional type annotation (e.g., ": Handle")
|
|
35
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
|
+
};
|
|
36
40
|
return function edgesPlugin(options) {
|
|
37
|
-
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 '${serverPath}';`);
|
|
64
|
+
const ensureRegexImport = (sourceCode) => ensureSyncImport(sourceCode, `import { __withEdgesServerLoad, __withEdgesActions, __withEdgesUniversalLoad } from '${serverPath}';`);
|
|
65
|
+
const findExportedLocal = (sourceFile, code, exportedName) => {
|
|
66
|
+
const edits = [];
|
|
67
|
+
let localName = null;
|
|
68
|
+
let found = false;
|
|
69
|
+
for (const stmt of sourceFile.statements) {
|
|
70
|
+
if (ts.isVariableStatement(stmt)) {
|
|
71
|
+
const exportModifier = stmt.modifiers?.find((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
72
|
+
if (!exportModifier)
|
|
73
|
+
continue;
|
|
74
|
+
for (const declaration of stmt.declarationList.declarations) {
|
|
75
|
+
if (!ts.isIdentifier(declaration.name))
|
|
76
|
+
continue;
|
|
77
|
+
if (declaration.name.text !== exportedName)
|
|
78
|
+
continue;
|
|
79
|
+
localName = declaration.name.text;
|
|
80
|
+
found = true;
|
|
81
|
+
const modifierStart = exportModifier.getStart(sourceFile);
|
|
82
|
+
let modifierEnd = exportModifier.end;
|
|
83
|
+
while (modifierEnd < code.length && /\s/.test(code[modifierEnd]))
|
|
84
|
+
modifierEnd += 1;
|
|
85
|
+
edits.push({ start: modifierStart, end: modifierEnd, text: '' });
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!ts.isExportDeclaration(stmt) || !stmt.exportClause || !ts.isNamedExports(stmt.exportClause) || stmt.moduleSpecifier)
|
|
90
|
+
continue;
|
|
91
|
+
const named = stmt.exportClause;
|
|
92
|
+
const keepSpecs = [];
|
|
93
|
+
let statementHasTarget = false;
|
|
94
|
+
for (const el of named.elements) {
|
|
95
|
+
const exportName = el.name.text;
|
|
96
|
+
const sourceName = el.propertyName?.text ?? el.name.text;
|
|
97
|
+
if (exportName === exportedName) {
|
|
98
|
+
statementHasTarget = true;
|
|
99
|
+
found = true;
|
|
100
|
+
localName = sourceName;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
keepSpecs.push(code.slice(el.getStart(sourceFile), el.end).trim());
|
|
104
|
+
}
|
|
105
|
+
if (!statementHasTarget)
|
|
106
|
+
continue;
|
|
107
|
+
if (keepSpecs.length === 0) {
|
|
108
|
+
edits.push({ start: stmt.getStart(sourceFile), end: stmt.end, text: '' });
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
edits.push({ start: stmt.getStart(sourceFile), end: stmt.end, text: `export { ${keepSpecs.join(', ')} };` });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { localName, edits, found };
|
|
115
|
+
};
|
|
116
|
+
const wrapServerRouteModuleRegex = (sourceCode) => {
|
|
117
|
+
if (!LOAD_EXPORT_PATTERN.test(sourceCode) && !ACTIONS_EXPORT_PATTERN.test(sourceCode)) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
let wrapped = ensureRegexImport(sourceCode);
|
|
121
|
+
if (LOAD_EXPORT_PATTERN.test(wrapped)) {
|
|
122
|
+
wrapped = wrapped
|
|
123
|
+
.replace(LOAD_EXPORT_PATTERN, (match) => match.replace('export const load', 'const __userLoad'))
|
|
124
|
+
.concat('\n\nexport const load = __withEdgesServerLoad(__userLoad);');
|
|
125
|
+
}
|
|
126
|
+
if (ACTIONS_EXPORT_PATTERN.test(wrapped)) {
|
|
127
|
+
wrapped = wrapped
|
|
128
|
+
.replace(ACTIONS_EXPORT_PATTERN, (match) => match.replace('export const actions', 'const __userActions'))
|
|
129
|
+
.concat('\n\nexport const actions = __withEdgesActions(__userActions);');
|
|
130
|
+
}
|
|
131
|
+
return wrapped;
|
|
132
|
+
};
|
|
133
|
+
const wrapUniversalRouteModuleRegex = (sourceCode) => {
|
|
134
|
+
if (!LOAD_EXPORT_PATTERN.test(sourceCode))
|
|
135
|
+
return null;
|
|
136
|
+
let wrapped = ensureRegexImport(sourceCode);
|
|
137
|
+
wrapped = wrapped
|
|
138
|
+
.replace(LOAD_EXPORT_PATTERN, (match) => match.replace('export const load', 'const __userUniversalLoad'))
|
|
139
|
+
.concat('\n\nexport const load = __withEdgesUniversalLoad(__userUniversalLoad);');
|
|
140
|
+
return wrapped;
|
|
141
|
+
};
|
|
142
|
+
const wrapServerRouteModuleAst = (sourceCode) => {
|
|
143
|
+
if (sourceCode.includes('__EDGES_SYNC_WRAPPED__'))
|
|
144
|
+
return null;
|
|
145
|
+
let sourceFile;
|
|
146
|
+
try {
|
|
147
|
+
sourceFile = ts.createSourceFile('route.ts', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const loadInfo = findExportedLocal(sourceFile, sourceCode, 'load');
|
|
153
|
+
const actionsInfo = findExportedLocal(sourceFile, sourceCode, 'actions');
|
|
154
|
+
if (!loadInfo.found && !actionsInfo.found)
|
|
155
|
+
return null;
|
|
156
|
+
if ((loadInfo.found && !loadInfo.localName) || (actionsInfo.found && !actionsInfo.localName))
|
|
157
|
+
return null;
|
|
158
|
+
let nextCode = applyEdits(sourceCode, [...loadInfo.edits, ...actionsInfo.edits]);
|
|
159
|
+
nextCode = ensureAstServerImport(nextCode);
|
|
160
|
+
const append = [];
|
|
161
|
+
if (loadInfo.localName) {
|
|
162
|
+
append.push(`const __edgesServerLoad = ${AST_SERVER_LOAD_ALIAS}(${loadInfo.localName});`);
|
|
163
|
+
append.push(`export { __edgesServerLoad as load };`);
|
|
164
|
+
}
|
|
165
|
+
if (actionsInfo.localName) {
|
|
166
|
+
append.push(`const __edgesServerActions = ${AST_ACTIONS_ALIAS}(${actionsInfo.localName});`);
|
|
167
|
+
append.push(`export { __edgesServerActions as actions };`);
|
|
168
|
+
}
|
|
169
|
+
return `${nextCode}\n\n${append.join('\n')}`;
|
|
170
|
+
};
|
|
171
|
+
const wrapUniversalRouteModuleAst = (sourceCode) => {
|
|
172
|
+
if (sourceCode.includes('__EDGES_SYNC_WRAPPED__'))
|
|
173
|
+
return null;
|
|
174
|
+
let sourceFile;
|
|
175
|
+
try {
|
|
176
|
+
sourceFile = ts.createSourceFile('route.ts', sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const loadInfo = findExportedLocal(sourceFile, sourceCode, 'load');
|
|
182
|
+
if (!loadInfo.found || !loadInfo.localName)
|
|
183
|
+
return null;
|
|
184
|
+
let nextCode = applyEdits(sourceCode, [...loadInfo.edits]);
|
|
185
|
+
nextCode = ensureAstUniversalImport(nextCode);
|
|
186
|
+
return `${nextCode}\n\nconst __edgesUniversalLoad = ${AST_UNIVERSAL_LOAD_ALIAS}(${loadInfo.localName});\nexport { __edgesUniversalLoad as load };`;
|
|
187
|
+
};
|
|
38
188
|
return {
|
|
39
189
|
name: `${packageName}-auto-handle`,
|
|
40
|
-
enforce: 'pre',
|
|
190
|
+
enforce: 'pre',
|
|
41
191
|
transform(code, id) {
|
|
42
|
-
|
|
192
|
+
if (syncFromServer && SERVER_ROUTE_PATTERN.test(id) && !id.includes('hooks.server')) {
|
|
193
|
+
const wrapped = syncTransformMode === 'regex'
|
|
194
|
+
? wrapServerRouteModuleRegex(code)
|
|
195
|
+
: syncTransformMode === 'ast'
|
|
196
|
+
? wrapServerRouteModuleAst(code)
|
|
197
|
+
: (wrapServerRouteModuleAst(code) ?? wrapServerRouteModuleRegex(code));
|
|
198
|
+
if (wrapped)
|
|
199
|
+
return { code: wrapped, map: null };
|
|
200
|
+
}
|
|
201
|
+
if (syncFromServer && UNIVERSAL_ROUTE_PATTERN.test(id) && !id.includes('.server.')) {
|
|
202
|
+
const wrapped = syncTransformMode === 'regex'
|
|
203
|
+
? wrapUniversalRouteModuleRegex(code)
|
|
204
|
+
: syncTransformMode === 'ast'
|
|
205
|
+
? wrapUniversalRouteModuleAst(code)
|
|
206
|
+
: (wrapUniversalRouteModuleAst(code) ?? wrapUniversalRouteModuleRegex(code));
|
|
207
|
+
if (wrapped)
|
|
208
|
+
return { code: wrapped, map: null };
|
|
209
|
+
}
|
|
43
210
|
if (!id.includes('hooks.server.ts'))
|
|
44
211
|
return null;
|
|
45
|
-
// If already wrapped by the plugin, skip
|
|
46
212
|
if (code.includes('__EDGES_AUTO_WRAPPED__'))
|
|
47
213
|
return null;
|
|
48
|
-
// If user is manually using the package, skip auto-wrapping
|
|
49
|
-
// Optimized: Use pre-compiled regex pattern
|
|
50
214
|
if (MANUAL_IMPORT_PATTERN.test(code)) {
|
|
51
215
|
return null;
|
|
52
216
|
}
|
|
53
|
-
// Check if user defined a handle export
|
|
54
|
-
// Optimized: Use pre-compiled regex pattern
|
|
55
217
|
const hasHandleExport = HANDLE_EXPORT_PATTERN.test(code);
|
|
56
|
-
// Build compression options string
|
|
57
|
-
const compressionOptions = compression.enabled ? `, { compress: true, compressionThreshold: ${compression.threshold || 1024} }` : '';
|
|
58
|
-
// Build silent devtools option
|
|
59
218
|
const silentOption = silentChromeDevtools ? '' : `, false`;
|
|
60
|
-
// Find the position after the last import statement to preserve import order
|
|
61
|
-
// Optimized: Use regex instead of line-by-line parsing for 20x performance improvement
|
|
62
|
-
const findImportInsertPosition = (sourceCode) => {
|
|
63
|
-
// Match all import/export statements
|
|
64
|
-
const importRegex = /(?:^|\n)((?:import|export)\s+(?:type\s+)?(?:\{[^}]*\}|\*|\w+)(?:\s+from)?\s+['"][^'"]+['"];?)/gm;
|
|
65
|
-
let lastMatch = null;
|
|
66
|
-
let match;
|
|
67
|
-
// Find the last import/export statement
|
|
68
|
-
while ((match = importRegex.exec(sourceCode)) !== null) {
|
|
69
|
-
lastMatch = match;
|
|
70
|
-
}
|
|
71
|
-
if (!lastMatch) {
|
|
72
|
-
// No imports found, insert at the beginning
|
|
73
|
-
return 0;
|
|
74
|
-
}
|
|
75
|
-
// Return position after the last import
|
|
76
|
-
return lastMatch.index + lastMatch[0].length;
|
|
77
|
-
};
|
|
78
219
|
if (!hasHandleExport) {
|
|
79
|
-
// No handle defined - create default with compression options
|
|
80
220
|
const insertPos = findImportInsertPosition(code);
|
|
81
221
|
const beforeImports = code.slice(0, insertPos);
|
|
82
222
|
const afterImports = code.slice(insertPos);
|
|
@@ -87,11 +227,10 @@ export function createEdgesPluginFactory(packageName, serverPath) {
|
|
|
87
227
|
afterImports +
|
|
88
228
|
`\n\n` +
|
|
89
229
|
`export const handle = edgesHandle(({ serialize, edgesEvent, resolve }) => ` +
|
|
90
|
-
`resolve(edgesEvent, { transformPageChunk: ({ html }) => serialize(html
|
|
230
|
+
`resolve(edgesEvent, { transformPageChunk: ({ html }) => serialize(html) })${silentOption});`,
|
|
91
231
|
map: null
|
|
92
232
|
};
|
|
93
233
|
}
|
|
94
|
-
// User defined a handle - wrap it with options
|
|
95
234
|
const insertPos = findImportInsertPosition(code);
|
|
96
235
|
const beforeImports = code.slice(0, insertPos);
|
|
97
236
|
const afterImports = code.slice(insertPos);
|
|
@@ -100,9 +239,8 @@ export function createEdgesPluginFactory(packageName, serverPath) {
|
|
|
100
239
|
`import { __autoWrapHandle } from '${serverPath}';\n\n` +
|
|
101
240
|
afterImports.replace(/export\s+const\s+handle\s*(?::\s*\w+\s*)?=/, 'const __userHandle =') +
|
|
102
241
|
`\n\n` +
|
|
103
|
-
`const __compressionOptions = ${JSON.stringify({ compress: compression.enabled, compressionThreshold: compression.threshold })};\n` +
|
|
104
242
|
`const __silentChromeDevtools = ${silentChromeDevtools};\n` +
|
|
105
|
-
`export const handle = __autoWrapHandle(__userHandle,
|
|
243
|
+
`export const handle = __autoWrapHandle(__userHandle, __silentChromeDevtools);`;
|
|
106
244
|
return {
|
|
107
245
|
code: wrappedCode,
|
|
108
246
|
map: null
|
|
@@ -112,10 +250,8 @@ export function createEdgesPluginFactory(packageName, serverPath) {
|
|
|
112
250
|
};
|
|
113
251
|
}
|
|
114
252
|
/**
|
|
115
|
-
* Default edges-svelte plugin for end users.
|
|
116
253
|
*
|
|
117
254
|
* This plugin automatically wraps the SvelteKit handle hook with edgesHandle,
|
|
118
|
-
* eliminating the need to manually wrap your handle function.
|
|
119
255
|
*
|
|
120
256
|
* @example
|
|
121
257
|
* ```ts
|
|
@@ -128,37 +264,5 @@ export function createEdgesPluginFactory(packageName, serverPath) {
|
|
|
128
264
|
* plugins: [sveltekit(), edgesPlugin()]
|
|
129
265
|
* });
|
|
130
266
|
* ```
|
|
131
|
-
*
|
|
132
|
-
* @example
|
|
133
|
-
* ```ts
|
|
134
|
-
* // vite.config.ts - With compression
|
|
135
|
-
* export default defineConfig({
|
|
136
|
-
* plugins: [
|
|
137
|
-
* sveltekit(),
|
|
138
|
-
* edgesPlugin({
|
|
139
|
-
* compression: {
|
|
140
|
-
* enabled: true,
|
|
141
|
-
* threshold: 2048 // Compress states larger than 2KB
|
|
142
|
-
* }
|
|
143
|
-
* })
|
|
144
|
-
* ]
|
|
145
|
-
* });
|
|
146
|
-
* ```
|
|
147
|
-
*
|
|
148
|
-
* After adding the plugin, you can write your hooks.server.ts normally:
|
|
149
|
-
*
|
|
150
|
-
* @example
|
|
151
|
-
* ```ts
|
|
152
|
-
* // hooks.server.ts - No manual wrapping needed!
|
|
153
|
-
*
|
|
154
|
-
* // Option 1: No handle defined - plugin creates default
|
|
155
|
-
* // (nothing to write, it just works)
|
|
156
|
-
*
|
|
157
|
-
* // Option 2: Custom handle - plugin automatically wraps it
|
|
158
|
-
* export const handle = async ({ event, resolve }) => {
|
|
159
|
-
* console.log('My custom middleware');
|
|
160
|
-
* return resolve(event);
|
|
161
|
-
* };
|
|
162
|
-
* ```
|
|
163
267
|
*/
|
|
164
268
|
export const edgesPlugin = createEdgesPluginFactory('edges-svelte', 'edges-svelte/server');
|
|
@@ -11,52 +11,14 @@ type StoreDeps = {
|
|
|
11
11
|
createState: <T>(initial: T | (() => T)) => Writable<T>;
|
|
12
12
|
createDerivedState: typeof BaseCreateDerivedState;
|
|
13
13
|
};
|
|
14
|
-
/**
|
|
15
|
-
* Creates store with optional name
|
|
16
|
-
* @example
|
|
17
|
-
* // Without key (auto-generated)
|
|
18
|
-
* export const useUserStore = createStore(({ createState }) => {
|
|
19
|
-
* const user = createState(null);
|
|
20
|
-
* return { user };
|
|
21
|
-
* });
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* // With explicit key (recommended for production)
|
|
25
|
-
* export const useUserStore = createStore('user-store', ({ createState }) => {
|
|
26
|
-
* const user = createState(null);
|
|
27
|
-
* return { user };
|
|
28
|
-
* });
|
|
29
|
-
*/
|
|
30
14
|
export declare function createStore<T, I extends Record<string, unknown> = Record<string, unknown>>(factory: (args: StoreDeps & NoConflict<I, StoreDeps>) => T, inject?: I): () => T;
|
|
31
15
|
export declare function createStore<T, I extends Record<string, unknown> = Record<string, unknown>>(name: string, factory: (args: StoreDeps & NoConflict<I, StoreDeps>) => T, inject?: I): () => T;
|
|
32
|
-
/**
|
|
33
|
-
* Store factory
|
|
34
|
-
*/
|
|
35
16
|
export declare const createStoreFactory: <I extends Record<string, unknown>>(inject: I) => {
|
|
36
17
|
<T>(factory: (args: StoreDeps & NoConflict<I, StoreDeps>) => T): () => T;
|
|
37
18
|
<T>(name: string, factory: (args: StoreDeps & NoConflict<I, StoreDeps>) => T): () => T;
|
|
38
19
|
};
|
|
39
|
-
/**
|
|
40
|
-
* Creates presenter with optional name
|
|
41
|
-
* @example
|
|
42
|
-
* // Without key (auto-generated)
|
|
43
|
-
* export const useAuthPresenter = createPresenter(() => {
|
|
44
|
-
* const login = async () => { ... };
|
|
45
|
-
* return { login };
|
|
46
|
-
* });
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* // With explicit key
|
|
50
|
-
* export const useAuthPresenter = createPresenter('auth-presenter', () => {
|
|
51
|
-
* const login = async () => { ... };
|
|
52
|
-
* return { login };
|
|
53
|
-
* });
|
|
54
|
-
*/
|
|
55
20
|
export declare function createPresenter<T, I extends Record<string, unknown> = Record<string, unknown>>(factory: (args: I) => T, inject?: I): () => T;
|
|
56
21
|
export declare function createPresenter<T, I extends Record<string, unknown> = Record<string, unknown>>(name: string, factory: (args: I) => T, inject?: I): () => T;
|
|
57
|
-
/**
|
|
58
|
-
* Presenter factory
|
|
59
|
-
*/
|
|
60
22
|
export declare const createPresenterFactory: <I extends Record<string, unknown>>(inject: I) => {
|
|
61
23
|
<T>(factory: (args: I) => T): () => T;
|
|
62
24
|
<T>(name: string, factory: (args: I) => T): () => T;
|
|
@@ -2,9 +2,11 @@ import { createState as BaseCreateState, createDerivedState as BaseCreateDerived
|
|
|
2
2
|
import { RequestContext } from '../context/index.js';
|
|
3
3
|
import { browser } from '../utils/environment.js';
|
|
4
4
|
import { DevTools } from '../utils/dev.js';
|
|
5
|
-
|
|
5
|
+
import { dev } from '../utils/environment.js';
|
|
6
6
|
const globalClientCache = new Map();
|
|
7
|
-
|
|
7
|
+
const globalConstructionStack = [];
|
|
8
|
+
const PROVIDER_FACTORY_MARK = Symbol.for('edges-svelte.provider.factory');
|
|
9
|
+
const PROVIDER_INSTANCE_MARK = Symbol.for('edges-svelte.provider.instance');
|
|
8
10
|
class AutoKeyGenerator {
|
|
9
11
|
static cache = new WeakMap();
|
|
10
12
|
static counters = new Map();
|
|
@@ -30,9 +32,6 @@ class AutoKeyGenerator {
|
|
|
30
32
|
}
|
|
31
33
|
if (cache.has(factory))
|
|
32
34
|
return cache.get(factory);
|
|
33
|
-
// Use multiple sources for stable key generation
|
|
34
|
-
// Priority: displayName > name > toString
|
|
35
|
-
// This ensures stability even with minification
|
|
36
35
|
const fnIdentifier = factory.__storeKey__ || factory.displayName || factory.name || factory.toString();
|
|
37
36
|
const hash = this.hash(fnIdentifier);
|
|
38
37
|
const baseKey = `store_${Math.abs(hash).toString(36)}`;
|
|
@@ -46,17 +45,14 @@ class AutoKeyGenerator {
|
|
|
46
45
|
counters.set(baseKey, 0);
|
|
47
46
|
}
|
|
48
47
|
cache.set(factory, finalKey);
|
|
49
|
-
// Dev mode validation
|
|
50
48
|
DevTools.validateFactoryUniqueness(factory, finalKey);
|
|
51
49
|
return finalKey;
|
|
52
50
|
}
|
|
53
51
|
static hash(str) {
|
|
54
|
-
|
|
55
|
-
// Provides 50% fewer collisions for typical factory names
|
|
56
|
-
let hash = 2166136261; // FNV offset basis (32-bit)
|
|
52
|
+
let hash = 2166136261;
|
|
57
53
|
for (let i = 0; i < str.length; i++) {
|
|
58
|
-
hash = (hash ^ str.charCodeAt(i)) * 16777619;
|
|
59
|
-
hash = hash >>> 0;
|
|
54
|
+
hash = (hash ^ str.charCodeAt(i)) * 16777619;
|
|
55
|
+
hash = hash >>> 0;
|
|
60
56
|
}
|
|
61
57
|
return hash;
|
|
62
58
|
}
|
|
@@ -76,7 +72,52 @@ export const clearCache = (pattern) => {
|
|
|
76
72
|
}
|
|
77
73
|
};
|
|
78
74
|
const createUiProvider = (cacheKey, factory, dependencies, inject) => {
|
|
79
|
-
|
|
75
|
+
const readConstructionStack = () => {
|
|
76
|
+
if (browser)
|
|
77
|
+
return globalConstructionStack;
|
|
78
|
+
try {
|
|
79
|
+
const context = RequestContext.current();
|
|
80
|
+
return (context.data.providersConstructionStack ??= []);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return globalConstructionStack;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const formatCycleError = (key, stack) => {
|
|
87
|
+
const cycleStart = stack.indexOf(key);
|
|
88
|
+
const chain = cycleStart === -1 ? [...stack, key] : [...stack.slice(cycleStart), key];
|
|
89
|
+
return `[edges-svelte] Circular provider dependency detected while constructing "${key}". Chain: ${chain.join(' -> ')}.`;
|
|
90
|
+
};
|
|
91
|
+
const validateLazyInjection = (ownerKey, injections) => {
|
|
92
|
+
if (!dev || !injections)
|
|
93
|
+
return;
|
|
94
|
+
for (const [depKey, depValue] of Object.entries(injections)) {
|
|
95
|
+
if (depValue && typeof depValue === 'object') {
|
|
96
|
+
const sourceKey = depValue[PROVIDER_INSTANCE_MARK];
|
|
97
|
+
if (sourceKey) {
|
|
98
|
+
throw new Error(`[edges-svelte] Eager provider injection detected in "${ownerKey}" for dependency "${depKey}" from "${sourceKey}". Inject provider functions instead of resolved instances.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const markProviderInstance = (instance) => {
|
|
104
|
+
if (!instance)
|
|
105
|
+
return;
|
|
106
|
+
if (typeof instance !== 'object' && typeof instance !== 'function')
|
|
107
|
+
return;
|
|
108
|
+
try {
|
|
109
|
+
Object.defineProperty(instance, PROVIDER_INSTANCE_MARK, {
|
|
110
|
+
value: cacheKey,
|
|
111
|
+
enumerable: false,
|
|
112
|
+
configurable: false,
|
|
113
|
+
writable: false
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* do nothing */
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const provider = (() => {
|
|
80
121
|
let contextMap;
|
|
81
122
|
if (browser) {
|
|
82
123
|
contextMap = globalClientCache;
|
|
@@ -103,18 +144,43 @@ const createUiProvider = (cacheKey, factory, dependencies, inject) => {
|
|
|
103
144
|
...(typeof dependencies === 'function' ? dependencies(cacheKey) : dependencies),
|
|
104
145
|
...inject
|
|
105
146
|
};
|
|
106
|
-
|
|
147
|
+
validateLazyInjection(cacheKey, inject);
|
|
148
|
+
const constructionStack = readConstructionStack();
|
|
149
|
+
if (constructionStack.includes(cacheKey)) {
|
|
150
|
+
throw new Error(formatCycleError(cacheKey, constructionStack));
|
|
151
|
+
}
|
|
152
|
+
constructionStack.push(cacheKey);
|
|
153
|
+
let instance;
|
|
154
|
+
try {
|
|
155
|
+
instance = factory(deps);
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
const idx = constructionStack.lastIndexOf(cacheKey);
|
|
159
|
+
if (idx !== -1)
|
|
160
|
+
constructionStack.splice(idx, 1);
|
|
161
|
+
}
|
|
162
|
+
markProviderInstance(instance);
|
|
107
163
|
contextMap.set(cacheKey, instance);
|
|
108
164
|
return instance;
|
|
109
|
-
};
|
|
165
|
+
});
|
|
166
|
+
try {
|
|
167
|
+
Object.defineProperty(provider, PROVIDER_FACTORY_MARK, {
|
|
168
|
+
value: cacheKey,
|
|
169
|
+
enumerable: false,
|
|
170
|
+
configurable: false,
|
|
171
|
+
writable: false
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
/* do nothing */
|
|
176
|
+
}
|
|
177
|
+
return provider;
|
|
110
178
|
};
|
|
111
179
|
export function createStore(nameOrFactory, factoryOrInject, inject) {
|
|
112
|
-
// Handle overloads
|
|
113
180
|
const isNameProvided = typeof nameOrFactory === 'string';
|
|
114
181
|
const name = isNameProvided ? nameOrFactory : undefined;
|
|
115
182
|
const factory = isNameProvided ? factoryOrInject : nameOrFactory;
|
|
116
183
|
const injections = isNameProvided ? inject : factoryOrInject;
|
|
117
|
-
// Use provided name or auto-generate
|
|
118
184
|
const cacheKey = name || AutoKeyGenerator.generate(factory);
|
|
119
185
|
return createUiProvider(cacheKey, factory, (key) => {
|
|
120
186
|
let stateCounter = 0;
|
|
@@ -133,9 +199,6 @@ export function createStore(nameOrFactory, factoryOrInject, inject) {
|
|
|
133
199
|
};
|
|
134
200
|
}, injections);
|
|
135
201
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Store factory
|
|
138
|
-
*/
|
|
139
202
|
export const createStoreFactory = (inject) => {
|
|
140
203
|
function storeFactory(nameOrFactory, factory) {
|
|
141
204
|
if (typeof nameOrFactory === 'string') {
|
|
@@ -148,18 +211,13 @@ export const createStoreFactory = (inject) => {
|
|
|
148
211
|
return storeFactory;
|
|
149
212
|
};
|
|
150
213
|
export function createPresenter(nameOrFactory, factoryOrInject, inject) {
|
|
151
|
-
// Handle overloads
|
|
152
214
|
const isNameProvided = typeof nameOrFactory === 'string';
|
|
153
215
|
const name = isNameProvided ? nameOrFactory : undefined;
|
|
154
216
|
const factory = isNameProvided ? factoryOrInject : nameOrFactory;
|
|
155
217
|
const injections = isNameProvided ? inject : factoryOrInject;
|
|
156
|
-
// Use provided name or auto-generate
|
|
157
218
|
const cacheKey = name || AutoKeyGenerator.generate(factory);
|
|
158
219
|
return createUiProvider(cacheKey, factory, {}, injections);
|
|
159
220
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Presenter factory
|
|
162
|
-
*/
|
|
163
221
|
export const createPresenterFactory = (inject) => {
|
|
164
222
|
function presenterFactory(nameOrFactory, factory) {
|
|
165
223
|
if (typeof nameOrFactory === 'string') {
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import type { Handle } from '@sveltejs/kit';
|
|
2
|
-
interface CompressionOptions {
|
|
3
|
-
compress?: boolean;
|
|
4
|
-
compressionThreshold?: number;
|
|
5
|
-
}
|
|
6
2
|
/**
|
|
7
3
|
* Automatically wraps a user-defined handle function with edgesHandle.
|
|
8
4
|
* This is used internally by the Vite plugin to provide automatic state management.
|
|
@@ -10,9 +6,7 @@ interface CompressionOptions {
|
|
|
10
6
|
* @internal This function is called automatically by the Vite plugin. You don't need to use it manually.
|
|
11
7
|
*
|
|
12
8
|
* @param userHandle - Optional user-defined handle function from hooks.server.ts
|
|
13
|
-
* @param compressionOptions - Compression configuration from the plugin
|
|
14
9
|
* @param silentChromeDevtools - Whether to silence Chrome DevTools requests
|
|
15
10
|
* @returns A handle function wrapped with edgesHandle for automatic state serialization
|
|
16
11
|
*/
|
|
17
|
-
export declare function __autoWrapHandle(userHandle?: Handle,
|
|
18
|
-
export {};
|
|
12
|
+
export declare function __autoWrapHandle(userHandle?: Handle, silentChromeDevtools?: boolean): Handle;
|
|
@@ -6,28 +6,23 @@ import { edgesHandle } from './EdgesHandleSimplified.js';
|
|
|
6
6
|
* @internal This function is called automatically by the Vite plugin. You don't need to use it manually.
|
|
7
7
|
*
|
|
8
8
|
* @param userHandle - Optional user-defined handle function from hooks.server.ts
|
|
9
|
-
* @param compressionOptions - Compression configuration from the plugin
|
|
10
9
|
* @param silentChromeDevtools - Whether to silence Chrome DevTools requests
|
|
11
10
|
* @returns A handle function wrapped with edgesHandle for automatic state serialization
|
|
12
11
|
*/
|
|
13
|
-
export function __autoWrapHandle(userHandle,
|
|
12
|
+
export function __autoWrapHandle(userHandle, silentChromeDevtools = true) {
|
|
14
13
|
if (!userHandle) {
|
|
15
|
-
// No user handle - return default edgesHandle with compression options
|
|
16
14
|
return edgesHandle(({ serialize, edgesEvent, resolve }) => resolve(edgesEvent, {
|
|
17
|
-
transformPageChunk: ({ html }) => serialize(html
|
|
15
|
+
transformPageChunk: ({ html }) => serialize(html)
|
|
18
16
|
}), silentChromeDevtools);
|
|
19
17
|
}
|
|
20
|
-
// Wrap user's handle with edgesHandle
|
|
21
18
|
return edgesHandle(({ serialize, edgesEvent, resolve }) => {
|
|
22
19
|
return userHandle({
|
|
23
20
|
event: edgesEvent,
|
|
24
21
|
resolve: (e, opts) => resolve(e, {
|
|
25
22
|
...opts,
|
|
26
23
|
transformPageChunk: ({ html, done }) => {
|
|
27
|
-
// Apply user's transform first (if any)
|
|
28
24
|
const userTransformed = opts?.transformPageChunk?.({ html, done }) ?? html;
|
|
29
|
-
|
|
30
|
-
return typeof userTransformed === 'string' ? serialize(userTransformed, compressionOptions) : userTransformed;
|
|
25
|
+
return typeof userTransformed === 'string' ? serialize(userTransformed) : userTransformed;
|
|
31
26
|
}
|
|
32
27
|
})
|
|
33
28
|
});
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import type { RequestEvent } from '@sveltejs/kit';
|
|
2
|
-
type SerializeOptions = {
|
|
3
|
-
compress?: boolean;
|
|
4
|
-
compressionThreshold?: number;
|
|
5
|
-
};
|
|
6
2
|
type EdgesHandle = (event: RequestEvent, callback: (params: {
|
|
7
3
|
edgesEvent: RequestEvent;
|
|
8
|
-
serialize: (html: string
|
|
4
|
+
serialize: (html: string) => string;
|
|
9
5
|
}) => Promise<Response> | Response, silentChromeDevtools?: boolean) => Promise<Response>;
|
|
10
|
-
/**
|
|
11
|
-
* Wraps request handling in an AsyncLocalStorage context
|
|
12
|
-
*/
|
|
13
6
|
export declare const edgesHandle: EdgesHandle;
|
|
14
7
|
export {};
|