evolit 0.2.0 → 0.3.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 +50 -3
- package/package.json +2 -2
- package/src/build.js +27 -5
- package/src/compiler.js +248 -23
- package/src/deployment-runtime.js +19 -4
- package/src/navigation-client.js +99 -10
- package/src/render.js +118 -37
- package/src/request-context.js +4 -1
- package/src/server-setup.js +64 -0
- package/src/server.js +1 -0
- package/src/urql-ssr.js +67 -2
package/README.md
CHANGED
|
@@ -46,6 +46,26 @@ Generated applications declare `"@/*": ["./*"]` in `jsconfig.json`, so editors a
|
|
|
46
46
|
use the same convention. An explicit `@/*` mapping in `jsconfig.json` or `tsconfig.json` takes
|
|
47
47
|
priority when an application needs a different source root.
|
|
48
48
|
|
|
49
|
+
### Interpolated dynamic imports
|
|
50
|
+
|
|
51
|
+
Relative dynamic imports may use a template literal when every possible module can be discovered
|
|
52
|
+
from the literal path at build time:
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
const module = await import(`./templates/${locale}/${name}.tsx`);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Evolit treats each interpolation as one wildcard that matches a single path segment. The example
|
|
59
|
+
above discovers `./templates/*/*.tsx`, compiles every matching module, and emits a finite runtime
|
|
60
|
+
dispatcher whose cases preserve the original source specifiers. Interpolations never cross a `/`,
|
|
61
|
+
and there is no framework-imposed candidate limit.
|
|
62
|
+
|
|
63
|
+
The template must be relative, end in a supported authored module extension, and match at least one
|
|
64
|
+
module during compilation. A runtime value that was not among the discovered candidates rejects the
|
|
65
|
+
import. Development watches the matched directory tree, so adding a new candidate invalidates the
|
|
66
|
+
affected graph. Arbitrary computed specifiers that do not use this template-literal form remain
|
|
67
|
+
outside the statically discoverable graph.
|
|
68
|
+
|
|
49
69
|
### Package CSS and static assets
|
|
50
70
|
|
|
51
71
|
Applications can import stylesheets exposed through package `exports` in the same way as local
|
|
@@ -162,9 +182,10 @@ from a connected client component.
|
|
|
162
182
|
|
|
163
183
|
Navigation context is intended for compact UI state that must survive back/forward navigation but
|
|
164
184
|
does not belong in the public URL. It must be a JSON-safe object and is limited to 8 KiB after UTF-8
|
|
165
|
-
serialization. Evolit includes it in browser
|
|
166
|
-
|
|
167
|
-
|
|
185
|
+
serialization. Evolit includes it in browser and response-cache identity. Segment caching and delta
|
|
186
|
+
identity track only the top-level context keys each layout or page reads, so unrelated context
|
|
187
|
+
changes preserve mounted layouts while consumers still update. Invalid context is rejected at the
|
|
188
|
+
client boundary; malformed context received by the server is ignored.
|
|
168
189
|
|
|
169
190
|
Client components can also read the active route state with browser-only hooks:
|
|
170
191
|
|
|
@@ -300,6 +321,32 @@ server components and helpers that do not receive route props directly. It retur
|
|
|
300
321
|
Reading `getRouteState().url` has the same dynamic-rendering semantics as `requestUrl()`; reading
|
|
301
322
|
`params` and `searchParams` participates in the normal segment-cache key tracking.
|
|
302
323
|
|
|
324
|
+
## Server Setup
|
|
325
|
+
|
|
326
|
+
Server integrations that need one-time application setup can be registered with
|
|
327
|
+
`server.setup`. A module specifier is compiled as part of the server graph, so the
|
|
328
|
+
setup may be authored in JavaScript or TypeScript:
|
|
329
|
+
|
|
330
|
+
```js
|
|
331
|
+
// evolit.config.js
|
|
332
|
+
export default {
|
|
333
|
+
server: {
|
|
334
|
+
setup: "./src/server/setup.ts",
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
The module exports `setup` (or a default function). Evolit invokes it once per
|
|
340
|
+
runtime and once before build-time prerendering with `{ projectRoot, mode }`.
|
|
341
|
+
It may return a cleanup function or `{ dispose() }`; runtime cleanup runs from
|
|
342
|
+
`runtime.close()`.
|
|
343
|
+
|
|
344
|
+
When `@litsx/urql` is installed, Evolit opens its SSR resource around the whole
|
|
345
|
+
route render and supplies `{ request, responseHeaders }`. Request-derived URQL
|
|
346
|
+
configuration therefore stays isolated per render, extracted SSR data is read
|
|
347
|
+
before the scope closes, and integration response headers are included before
|
|
348
|
+
the response is cached.
|
|
349
|
+
|
|
303
350
|
## Extensions
|
|
304
351
|
|
|
305
352
|
Optional integrations are configured explicitly in `evolit.config.js`. Core only coordinates their
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evolit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A convention-driven application framework for LitSX and web components.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "yarn@4.10.3",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"ws": "^8.18.3"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
|
-
"@litsx/urql": "^0.
|
|
74
|
+
"@litsx/urql": "^0.4.0"
|
|
75
75
|
},
|
|
76
76
|
"peerDependenciesMeta": {
|
|
77
77
|
"@litsx/urql": {
|
package/src/build.js
CHANGED
|
@@ -51,7 +51,13 @@ import {
|
|
|
51
51
|
import { serializeRouteCachePolicy } from "./route-config.js";
|
|
52
52
|
import { createSsrAdapter, renderRouteTreeWithAdapter } from "./ssr-adapter.js";
|
|
53
53
|
import { ensureDirectory, writeJson } from "./fs-utils.js";
|
|
54
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
appendSsrUrqlData,
|
|
56
|
+
applySsrUrqlRequestContext,
|
|
57
|
+
createSsrUrqlRequestContext,
|
|
58
|
+
runWithOptionalSsrUrqlScope,
|
|
59
|
+
} from "./urql-ssr.js";
|
|
60
|
+
import { runServerSetup } from "./server-setup.js";
|
|
55
61
|
|
|
56
62
|
const CONTENT_TYPE_BY_EXTENSION = new Map([
|
|
57
63
|
[".css", "text/css; charset=utf-8"],
|
|
@@ -157,6 +163,11 @@ async function writeDeploymentRuntimeEntry(buildRoot) {
|
|
|
157
163
|
|
|
158
164
|
export async function buildProject(projectRoot, options = {}) {
|
|
159
165
|
const evolitConfig = await loadEvolitConfig(projectRoot);
|
|
166
|
+
const serverSetupCleanup = await runServerSetup({
|
|
167
|
+
projectRoot,
|
|
168
|
+
mode: "production",
|
|
169
|
+
evolitConfig,
|
|
170
|
+
});
|
|
160
171
|
const extensions = resolveEvolitExtensions(evolitConfig);
|
|
161
172
|
const extensionClientDescriptors = getExtensionClientDescriptors(extensions);
|
|
162
173
|
const packageClientSpecifiers = new Set(
|
|
@@ -541,23 +552,32 @@ export async function buildProject(projectRoot, options = {}) {
|
|
|
541
552
|
seenPrerenderTargets.add(targetPathname);
|
|
542
553
|
|
|
543
554
|
const targetRequest = new Request(`http://evolit.local${targetPathname}`);
|
|
544
|
-
const
|
|
555
|
+
const urqlRequestContext = createSsrUrqlRequestContext(targetRequest);
|
|
556
|
+
const { routeResult, response } = await runWithOptionalSsrUrqlScope(urqlRequestContext, async (urqlAdapter) => {
|
|
545
557
|
const resolvedRouteResult = await routeResolver.resolveRequest(targetRequest);
|
|
546
558
|
if (resolvedRouteResult.type !== "route" || resolvedRouteResult.cachePolicy.mode === "dynamic") {
|
|
547
559
|
return { routeResult: resolvedRouteResult, response: null };
|
|
548
560
|
}
|
|
549
561
|
|
|
550
562
|
const renderedResponse = await renderRouteTreeWithAdapter(resolvedRouteResult, ssrAdapter);
|
|
563
|
+
const responseWithData = urqlAdapter
|
|
564
|
+
? appendSsrUrqlData(renderedResponse, await urqlAdapter.getUrqlSsrData())
|
|
565
|
+
: renderedResponse;
|
|
551
566
|
return {
|
|
552
567
|
routeResult: resolvedRouteResult,
|
|
553
|
-
response:
|
|
554
|
-
|
|
555
|
-
|
|
568
|
+
response: applySsrUrqlRequestContext(
|
|
569
|
+
resolvedRouteResult,
|
|
570
|
+
responseWithData,
|
|
571
|
+
urqlRequestContext,
|
|
572
|
+
),
|
|
556
573
|
};
|
|
557
574
|
});
|
|
558
575
|
if (!response) {
|
|
559
576
|
continue;
|
|
560
577
|
}
|
|
578
|
+
if (routeResult.cachePolicy.mode === "dynamic") {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
561
581
|
if (response.status !== 200) {
|
|
562
582
|
continue;
|
|
563
583
|
}
|
|
@@ -656,5 +676,7 @@ export async function buildProject(projectRoot, options = {}) {
|
|
|
656
676
|
await writeJson(path.join(buildRoot, DEPLOY_ASSETS_MANIFEST_FILENAME), deployAssets);
|
|
657
677
|
await writeJson(path.join(buildRoot, DEPLOY_SERVER_MANIFEST_FILENAME), deployServer);
|
|
658
678
|
|
|
679
|
+
await serverSetupCleanup?.();
|
|
680
|
+
|
|
659
681
|
return manifestPath;
|
|
660
682
|
}
|
package/src/compiler.js
CHANGED
|
@@ -20,6 +20,7 @@ import { ensureDirectory } from "./fs-utils.js";
|
|
|
20
20
|
const MODULE_SPECIFIER_PATTERN =
|
|
21
21
|
/\b(?:import|export)\s+(?:[^"']*?\s+from\s+)?["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
22
22
|
const CSS_DEPENDENCY_PATTERN = /(?:@import\s+(?:url\(\s*)?["']([^"']+)["']\s*\)?|url\(\s*["']?([^"')]+)["']?\s*\))/g;
|
|
23
|
+
const DYNAMIC_IMPORT_WILDCARD = "\u0000EVOLIT_DYNAMIC_IMPORT_WILDCARD\u0000";
|
|
23
24
|
const RESOLVABLE_IMPORT_EXTENSIONS = [
|
|
24
25
|
...MODULE_EXTENSIONS,
|
|
25
26
|
...STATIC_ASSET_EXTENSIONS,
|
|
@@ -388,17 +389,30 @@ function getCompiledServerComponentExports(code, sourcePath) {
|
|
|
388
389
|
return exports;
|
|
389
390
|
}
|
|
390
391
|
|
|
391
|
-
function
|
|
392
|
-
const sourceFile = ts.createSourceFile(sourcePath, code, ts.ScriptTarget.Latest, true,
|
|
392
|
+
function analyzeModuleReferences(code, sourcePath, scriptKind = ts.ScriptKind.JS) {
|
|
393
|
+
const sourceFile = ts.createSourceFile(sourcePath, code, ts.ScriptTarget.Latest, true, scriptKind);
|
|
393
394
|
const bySpecifier = new Map();
|
|
395
|
+
const staticReferences = [];
|
|
396
|
+
const dynamicReferences = [];
|
|
394
397
|
const add = (specifier, name) => {
|
|
395
398
|
const names = bySpecifier.get(specifier) ?? new Set();
|
|
396
399
|
names.add(name);
|
|
397
400
|
bySpecifier.set(specifier, names);
|
|
398
401
|
};
|
|
402
|
+
const addStaticReference = (node, importedName = null) => {
|
|
403
|
+
const specifier = node.text;
|
|
404
|
+
staticReferences.push({
|
|
405
|
+
specifier,
|
|
406
|
+
start: node.getStart(sourceFile) + 1,
|
|
407
|
+
end: node.end - 1,
|
|
408
|
+
});
|
|
409
|
+
if (importedName) add(specifier, importedName);
|
|
410
|
+
};
|
|
411
|
+
|
|
399
412
|
for (const statement of sourceFile.statements) {
|
|
400
|
-
if (ts.isImportDeclaration(statement) && ts.
|
|
413
|
+
if (ts.isImportDeclaration(statement) && ts.isStringLiteralLike(statement.moduleSpecifier)) {
|
|
401
414
|
const specifier = statement.moduleSpecifier.text;
|
|
415
|
+
addStaticReference(statement.moduleSpecifier);
|
|
402
416
|
if (statement.importClause?.name) add(specifier, "default");
|
|
403
417
|
if (statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings)) {
|
|
404
418
|
for (const element of statement.importClause.namedBindings.elements) {
|
|
@@ -408,9 +422,10 @@ function collectImportedNamesBySpecifier(code, sourcePath) {
|
|
|
408
422
|
} else if (
|
|
409
423
|
ts.isExportDeclaration(statement)
|
|
410
424
|
&& statement.moduleSpecifier
|
|
411
|
-
&& ts.
|
|
425
|
+
&& ts.isStringLiteralLike(statement.moduleSpecifier)
|
|
412
426
|
) {
|
|
413
427
|
const specifier = statement.moduleSpecifier.text;
|
|
428
|
+
addStaticReference(statement.moduleSpecifier);
|
|
414
429
|
if (!statement.exportClause) add(specifier, "*");
|
|
415
430
|
else if (ts.isNamedExports(statement.exportClause)) {
|
|
416
431
|
for (const element of statement.exportClause.elements) add(specifier, element.propertyName?.text ?? element.name.text);
|
|
@@ -421,13 +436,119 @@ function collectImportedNamesBySpecifier(code, sourcePath) {
|
|
|
421
436
|
if (
|
|
422
437
|
ts.isCallExpression(node)
|
|
423
438
|
&& node.expression.kind === ts.SyntaxKind.ImportKeyword
|
|
424
|
-
&& node.arguments.length
|
|
425
|
-
|
|
426
|
-
|
|
439
|
+
&& node.arguments.length >= 1
|
|
440
|
+
) {
|
|
441
|
+
const argument = node.arguments[0];
|
|
442
|
+
if (ts.isStringLiteralLike(argument)) {
|
|
443
|
+
addStaticReference(argument, "*");
|
|
444
|
+
} else if (ts.isTemplateExpression(argument)) {
|
|
445
|
+
const staticParts = [
|
|
446
|
+
argument.head.text,
|
|
447
|
+
...argument.templateSpans.map((span) => span.literal.text),
|
|
448
|
+
];
|
|
449
|
+
dynamicReferences.push({
|
|
450
|
+
pattern: staticParts.join(DYNAMIC_IMPORT_WILDCARD),
|
|
451
|
+
displayPattern: staticParts.join("*"),
|
|
452
|
+
expressionText: code.slice(argument.getStart(sourceFile), argument.end),
|
|
453
|
+
optionsText: node.arguments.length > 1
|
|
454
|
+
? code.slice(node.arguments[1].getStart(sourceFile), node.arguments.at(-1).end)
|
|
455
|
+
: null,
|
|
456
|
+
start: node.getStart(sourceFile),
|
|
457
|
+
end: node.end,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
427
461
|
ts.forEachChild(node, visit);
|
|
428
462
|
};
|
|
429
463
|
visit(sourceFile);
|
|
430
|
-
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
importedNamesBySpecifier: bySpecifier,
|
|
467
|
+
staticReferences,
|
|
468
|
+
dynamicReferences,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function escapeRegExp(value) {
|
|
473
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function createDynamicImportSegmentPattern(segment) {
|
|
477
|
+
return new RegExp(
|
|
478
|
+
`^${segment.split(DYNAMIC_IMPORT_WILDCARD).map(escapeRegExp).join("[^/]+")}$`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function discoverDynamicImportCandidates({
|
|
483
|
+
projectRoot,
|
|
484
|
+
sourcePath,
|
|
485
|
+
pattern,
|
|
486
|
+
displayPattern,
|
|
487
|
+
managedSourceRoots,
|
|
488
|
+
}) {
|
|
489
|
+
const normalizedPattern = pattern.replaceAll("\\", "/");
|
|
490
|
+
if (!isRelativeSpecifier(normalizedPattern)) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
`Dynamic import pattern ${JSON.stringify(displayPattern)} in ${sourcePath} must be relative.`,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
if (!MODULE_EXTENSIONS.some((extension) => normalizedPattern.endsWith(extension))) {
|
|
496
|
+
throw new Error(
|
|
497
|
+
`Dynamic import pattern ${JSON.stringify(displayPattern)} in ${sourcePath} must end in a supported module extension.`,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const segments = normalizedPattern.split("/");
|
|
502
|
+
const firstDynamicSegment = segments.findIndex((segment) => segment.includes(DYNAMIC_IMPORT_WILDCARD));
|
|
503
|
+
if (firstDynamicSegment < 0) return { candidates: [], watchedDirectories: [] };
|
|
504
|
+
|
|
505
|
+
const scanRoot = path.resolve(path.dirname(sourcePath), ...segments.slice(0, firstDynamicSegment));
|
|
506
|
+
if (isPathOutsideManagedRoots(projectRoot, managedSourceRoots, scanRoot)) {
|
|
507
|
+
throw new Error(
|
|
508
|
+
`Dynamic import pattern ${JSON.stringify(displayPattern)} in ${sourcePath} resolves outside the managed source roots.`,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const segmentPatterns = segments
|
|
513
|
+
.slice(firstDynamicSegment)
|
|
514
|
+
.map(createDynamicImportSegmentPattern);
|
|
515
|
+
const candidates = [];
|
|
516
|
+
const watchedDirectories = new Set();
|
|
517
|
+
|
|
518
|
+
async function visitDirectory(directory, segmentIndex) {
|
|
519
|
+
watchedDirectories.add(directory);
|
|
520
|
+
let entries;
|
|
521
|
+
try {
|
|
522
|
+
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
523
|
+
} catch (error) {
|
|
524
|
+
if (error?.code === "ENOENT" || error?.code === "ENOTDIR") return;
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const matcher = segmentPatterns[segmentIndex];
|
|
529
|
+
const isFinalSegment = segmentIndex === segmentPatterns.length - 1;
|
|
530
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
531
|
+
if (!matcher.test(entry.name)) continue;
|
|
532
|
+
const candidatePath = path.join(directory, entry.name);
|
|
533
|
+
if (isFinalSegment) {
|
|
534
|
+
if (entry.isFile()) candidates.push(candidatePath);
|
|
535
|
+
} else if (entry.isDirectory()) {
|
|
536
|
+
await visitDirectory(candidatePath, segmentIndex + 1);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
await visitDirectory(scanRoot, 0);
|
|
542
|
+
if (candidates.length === 0) {
|
|
543
|
+
throw new Error(
|
|
544
|
+
`Dynamic import pattern ${JSON.stringify(displayPattern)} in ${sourcePath} did not match any modules.`,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
candidates,
|
|
550
|
+
watchedDirectories: [...watchedDirectories],
|
|
551
|
+
};
|
|
431
552
|
}
|
|
432
553
|
|
|
433
554
|
function createClientServerComponentImportError(entryPath, sourcePath, parents) {
|
|
@@ -841,18 +962,17 @@ async function rewriteRelativeSpecifiers({
|
|
|
841
962
|
managedSourceRoots,
|
|
842
963
|
serverExportsByModule,
|
|
843
964
|
packageImports,
|
|
965
|
+
dynamicImportDirectories,
|
|
844
966
|
}) {
|
|
845
967
|
const magicSource = new MagicString(code);
|
|
846
968
|
let didRewrite = false;
|
|
969
|
+
const moduleReferences = analyzeModuleReferences(code, sourcePath);
|
|
847
970
|
const importedNamesBySpecifier = target === "client"
|
|
848
|
-
?
|
|
971
|
+
? moduleReferences.importedNamesBySpecifier
|
|
849
972
|
: new Map();
|
|
850
973
|
|
|
851
|
-
for (const
|
|
852
|
-
const specifier =
|
|
853
|
-
if (!specifier) {
|
|
854
|
-
continue;
|
|
855
|
-
}
|
|
974
|
+
for (const reference of moduleReferences.staticReferences) {
|
|
975
|
+
const { specifier } = reference;
|
|
856
976
|
|
|
857
977
|
const aliasedImportPath = isBareSpecifier(specifier)
|
|
858
978
|
? await resolveProjectMappedImport(projectRoot, sourcePath, specifier)
|
|
@@ -996,18 +1116,99 @@ async function rewriteRelativeSpecifiers({
|
|
|
996
1116
|
) {
|
|
997
1117
|
normalizedReplacement = `${normalizedReplacement}?${serverImportQuery}`;
|
|
998
1118
|
}
|
|
999
|
-
const quotedSpecifierIndex = match.index + match[0].indexOf(specifier);
|
|
1000
1119
|
const replacementValue = normalizedReplacement.split(path.sep).join("/");
|
|
1001
1120
|
if (replacementValue !== specifier) {
|
|
1002
1121
|
didRewrite = true;
|
|
1003
1122
|
magicSource.update(
|
|
1004
|
-
|
|
1005
|
-
|
|
1123
|
+
reference.start,
|
|
1124
|
+
reference.end,
|
|
1006
1125
|
replacementValue,
|
|
1007
1126
|
);
|
|
1008
1127
|
}
|
|
1009
1128
|
}
|
|
1010
1129
|
|
|
1130
|
+
const dynamicImportHelpers = [];
|
|
1131
|
+
for (const [index, reference] of moduleReferences.dynamicReferences.entries()) {
|
|
1132
|
+
const discovery = await discoverDynamicImportCandidates({
|
|
1133
|
+
projectRoot,
|
|
1134
|
+
sourcePath,
|
|
1135
|
+
pattern: reference.pattern,
|
|
1136
|
+
displayPattern: reference.displayPattern,
|
|
1137
|
+
managedSourceRoots,
|
|
1138
|
+
});
|
|
1139
|
+
discovery.watchedDirectories.forEach((directory) => dynamicImportDirectories.add(directory));
|
|
1140
|
+
|
|
1141
|
+
const cases = [];
|
|
1142
|
+
for (const resolvedImportPath of discovery.candidates) {
|
|
1143
|
+
const compiledImportPath = await compileModule(resolvedImportPath);
|
|
1144
|
+
const serverExports = serverExportsByModule?.get(resolvedImportPath) ?? new Set();
|
|
1145
|
+
if (target === "client" && serverExports.size > 0) {
|
|
1146
|
+
throw new Error(
|
|
1147
|
+
`Client module ${sourcePath} dynamically imports Server Component export(s) from ${resolvedImportPath}: `
|
|
1148
|
+
+ `${[...serverExports].join(", ")}. Split the server exports into a separate module.`,
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const importerOutputPath = toOutputPath(projectRoot, outputRoot, sourcePath);
|
|
1153
|
+
let replacementPath = path.relative(path.dirname(importerOutputPath), compiledImportPath);
|
|
1154
|
+
if (!replacementPath.startsWith(".")) replacementPath = `./${replacementPath}`;
|
|
1155
|
+
replacementPath = replacementPath.split(path.sep).join("/");
|
|
1156
|
+
if (
|
|
1157
|
+
target === "server"
|
|
1158
|
+
&& mode === "development"
|
|
1159
|
+
&& typeof serverImportQuery === "string"
|
|
1160
|
+
&& serverImportQuery.length > 0
|
|
1161
|
+
) {
|
|
1162
|
+
replacementPath = `${replacementPath}?${serverImportQuery}`;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
let sourceSpecifier = path.relative(path.dirname(sourcePath), resolvedImportPath)
|
|
1166
|
+
.split(path.sep)
|
|
1167
|
+
.join("/");
|
|
1168
|
+
if (!sourceSpecifier.startsWith(".")) sourceSpecifier = `./${sourceSpecifier}`;
|
|
1169
|
+
cases.push(
|
|
1170
|
+
` case ${JSON.stringify(sourceSpecifier)}: return import(${JSON.stringify(replacementPath)}${reference.optionsText ? ", options" : ""});`,
|
|
1171
|
+
);
|
|
1172
|
+
|
|
1173
|
+
if (target === "client") {
|
|
1174
|
+
const sourceMetadata = moduleMetadata.get(sourcePath) ?? {
|
|
1175
|
+
moduleImports: new Set(),
|
|
1176
|
+
vendorImports: new Set(),
|
|
1177
|
+
styleImports: new Set(),
|
|
1178
|
+
assetImports: new Set(),
|
|
1179
|
+
};
|
|
1180
|
+
sourceMetadata.moduleImports.add(
|
|
1181
|
+
path.relative(outputRoot, compiledImportPath).split(path.sep).join("/"),
|
|
1182
|
+
);
|
|
1183
|
+
moduleMetadata.set(sourcePath, sourceMetadata);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
let helperName = `__evolit_dynamic_import_${index}`;
|
|
1188
|
+
while (code.includes(helperName)) helperName = `_${helperName}`;
|
|
1189
|
+
const sourceModuleId = path.relative(projectRoot, sourcePath).split(path.sep).join("/");
|
|
1190
|
+
const unknownImportPrefix = `Unknown dynamic import in ${sourceModuleId}: `;
|
|
1191
|
+
dynamicImportHelpers.push([
|
|
1192
|
+
`function ${helperName}(specifier${reference.optionsText ? ", options" : ""}) {`,
|
|
1193
|
+
" switch (specifier) {",
|
|
1194
|
+
...cases,
|
|
1195
|
+
" default:",
|
|
1196
|
+
` return Promise.reject(new Error(${JSON.stringify(unknownImportPrefix)} + specifier));`,
|
|
1197
|
+
" }",
|
|
1198
|
+
"}",
|
|
1199
|
+
].join("\n"));
|
|
1200
|
+
magicSource.update(
|
|
1201
|
+
reference.start,
|
|
1202
|
+
reference.end,
|
|
1203
|
+
`${helperName}(${reference.expressionText}${reference.optionsText ? `, ${reference.optionsText}` : ""})`,
|
|
1204
|
+
);
|
|
1205
|
+
didRewrite = true;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (dynamicImportHelpers.length > 0) {
|
|
1209
|
+
magicSource.append(`\n\n${dynamicImportHelpers.join("\n\n")}\n`);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1011
1212
|
const rewrittenCode = magicSource.toString();
|
|
1012
1213
|
if (!sourceMaps || !didRewrite) {
|
|
1013
1214
|
return {
|
|
@@ -1131,7 +1332,10 @@ export function invalidateDevelopmentCompilationCache(projectRoot, changedPaths
|
|
|
1131
1332
|
const shouldInvalidate =
|
|
1132
1333
|
normalizedChangedPaths == null ||
|
|
1133
1334
|
!dependencies ||
|
|
1134
|
-
[...dependencies].some((dependency) => normalizedChangedPaths.has(dependency))
|
|
1335
|
+
[...dependencies.files].some((dependency) => normalizedChangedPaths.has(dependency)) ||
|
|
1336
|
+
[...dependencies.directories].some((directory) =>
|
|
1337
|
+
[...normalizedChangedPaths].some((changedPath) => !isPathOutsideProject(directory, changedPath))
|
|
1338
|
+
);
|
|
1135
1339
|
if (shouldInvalidate) {
|
|
1136
1340
|
developmentGraphCache.delete(key);
|
|
1137
1341
|
developmentGraphDependencies.delete(key);
|
|
@@ -1163,6 +1367,7 @@ async function compileModuleGraphUncached(entryPath, options = {}) {
|
|
|
1163
1367
|
const moduleMetadata = new Map();
|
|
1164
1368
|
const serverExportsByModule = new Map();
|
|
1165
1369
|
const packageImports = new Set();
|
|
1370
|
+
const dynamicImportDirectories = new Set();
|
|
1166
1371
|
const serverImportQuery = target === "server" && mode === "development"
|
|
1167
1372
|
? `t=${Date.now()}`
|
|
1168
1373
|
: null;
|
|
@@ -1233,6 +1438,7 @@ async function compileModuleGraphUncached(entryPath, options = {}) {
|
|
|
1233
1438
|
managedSourceRoots,
|
|
1234
1439
|
serverExportsByModule,
|
|
1235
1440
|
packageImports,
|
|
1441
|
+
dynamicImportDirectories,
|
|
1236
1442
|
});
|
|
1237
1443
|
|
|
1238
1444
|
await fs.writeFile(outputPath, rewritten.code, "utf8");
|
|
@@ -1262,6 +1468,7 @@ async function compileModuleGraphUncached(entryPath, options = {}) {
|
|
|
1262
1468
|
entrypoint: await compileModule(entryPath),
|
|
1263
1469
|
outputRoot,
|
|
1264
1470
|
sourceFiles: [...new Set([...visited.keys(), ...staticAssetFiles])],
|
|
1471
|
+
watchedDirectories: [...dynamicImportDirectories],
|
|
1265
1472
|
packageImports: [...packageImports].sort(),
|
|
1266
1473
|
};
|
|
1267
1474
|
}
|
|
@@ -1286,7 +1493,10 @@ export async function compileModuleGraph(entryPath, options = {}) {
|
|
|
1286
1493
|
developmentGraphCache.set(cacheKey, pendingCompile);
|
|
1287
1494
|
pendingCompile.then((result) => {
|
|
1288
1495
|
if (developmentGraphCache.get(cacheKey) === pendingCompile) {
|
|
1289
|
-
developmentGraphDependencies.set(cacheKey,
|
|
1496
|
+
developmentGraphDependencies.set(cacheKey, {
|
|
1497
|
+
files: new Set(result.sourceFiles),
|
|
1498
|
+
directories: new Set(result.watchedDirectories ?? []),
|
|
1499
|
+
});
|
|
1290
1500
|
}
|
|
1291
1501
|
}, () => {});
|
|
1292
1502
|
}
|
|
@@ -1425,15 +1635,15 @@ export async function collectClientGraphInventory(entryPaths, options = {}) {
|
|
|
1425
1635
|
if (isComponent && isServer) boundaries.add(sourcePath);
|
|
1426
1636
|
if (!traverseNonBoundary) return;
|
|
1427
1637
|
const componentImportSpecifiers = collectComponentImportSpecifiers(source, sourcePath);
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1638
|
+
const moduleReferences = analyzeModuleReferences(source, sourcePath, ts.ScriptKind.TSX);
|
|
1639
|
+
|
|
1640
|
+
async function visitResolvedSpecifier(specifier) {
|
|
1431
1641
|
const aliasResolved = isBareSpecifier(specifier)
|
|
1432
1642
|
? await resolveProjectMappedImport(projectRoot, sourcePath, specifier)
|
|
1433
1643
|
: null;
|
|
1434
1644
|
const resolved = aliasResolved
|
|
1435
1645
|
?? await resolveProjectModuleSpecifier(projectRoot, sourcePath, specifier);
|
|
1436
|
-
if (!resolved)
|
|
1646
|
+
if (!resolved) return;
|
|
1437
1647
|
if (shouldCompileModule(resolved)) {
|
|
1438
1648
|
if (isBareSpecifier(specifier) && aliasResolved == null) {
|
|
1439
1649
|
if (componentImportSpecifiers.has(specifier)) {
|
|
@@ -1450,6 +1660,21 @@ export async function collectClientGraphInventory(entryPaths, options = {}) {
|
|
|
1450
1660
|
assets.add(resolved);
|
|
1451
1661
|
}
|
|
1452
1662
|
}
|
|
1663
|
+
|
|
1664
|
+
for (const reference of moduleReferences.staticReferences) {
|
|
1665
|
+
await visitResolvedSpecifier(reference.specifier);
|
|
1666
|
+
}
|
|
1667
|
+
for (const reference of moduleReferences.dynamicReferences) {
|
|
1668
|
+
const discovery = await discoverDynamicImportCandidates({
|
|
1669
|
+
projectRoot,
|
|
1670
|
+
sourcePath,
|
|
1671
|
+
pattern: reference.pattern,
|
|
1672
|
+
displayPattern: reference.displayPattern,
|
|
1673
|
+
});
|
|
1674
|
+
for (const candidate of discovery.candidates) {
|
|
1675
|
+
await visit(candidate, true);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1453
1678
|
}
|
|
1454
1679
|
|
|
1455
1680
|
for (const entryPath of entryPaths) await visit(entryPath);
|
|
@@ -41,12 +41,18 @@ import {
|
|
|
41
41
|
} from "./response-cache.js";
|
|
42
42
|
import { createSsrAdapter, renderRouteTreeWithAdapter } from "./ssr-adapter.js";
|
|
43
43
|
import { createNavigationResponseFromDocument } from "./route-segments.js";
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
appendSsrUrqlData,
|
|
46
|
+
applySsrUrqlRequestContext,
|
|
47
|
+
createSsrUrqlRequestContext,
|
|
48
|
+
runWithOptionalSsrUrqlScope,
|
|
49
|
+
} from "./urql-ssr.js";
|
|
45
50
|
import {
|
|
46
51
|
getExtensionClientDescriptors,
|
|
47
52
|
resolveEvolitExtensions,
|
|
48
53
|
runRequestExtensions,
|
|
49
54
|
} from "./extensions.js";
|
|
55
|
+
import { runServerSetup } from "./server-setup.js";
|
|
50
56
|
|
|
51
57
|
function isBarePackageModuleId(value) {
|
|
52
58
|
return typeof value === "string"
|
|
@@ -882,7 +888,8 @@ export async function createRequestRenderer({
|
|
|
882
888
|
return effectiveRouteResolver.resolveRoutePolicy(request, options);
|
|
883
889
|
},
|
|
884
890
|
async renderRoute(request, routePolicyResult = null, options = {}) {
|
|
885
|
-
|
|
891
|
+
const urqlRequestContext = createSsrUrqlRequestContext(request);
|
|
892
|
+
return runWithOptionalSsrUrqlScope(urqlRequestContext, async (urqlAdapter) => {
|
|
886
893
|
const shouldPrepareBeforeResolve = mode === "development" && !currentAssetManifest;
|
|
887
894
|
let resolvedRoutePolicyResult = routePolicyResult;
|
|
888
895
|
if (shouldPrepareBeforeResolve) {
|
|
@@ -895,9 +902,14 @@ export async function createRequestRenderer({
|
|
|
895
902
|
await this.prepareRouteClientArtifacts(routeResult);
|
|
896
903
|
}
|
|
897
904
|
const renderedResponse = await renderRouteTreeWithAdapter(routeResult, ssrAdapter);
|
|
898
|
-
const
|
|
905
|
+
const responseWithData = urqlAdapter
|
|
899
906
|
? appendSsrUrqlData(renderedResponse, await urqlAdapter.getUrqlSsrData())
|
|
900
907
|
: renderedResponse;
|
|
908
|
+
const response = applySsrUrqlRequestContext(
|
|
909
|
+
routeResult,
|
|
910
|
+
responseWithData,
|
|
911
|
+
urqlRequestContext,
|
|
912
|
+
);
|
|
901
913
|
return {
|
|
902
914
|
routeResult,
|
|
903
915
|
response,
|
|
@@ -921,8 +933,10 @@ export async function createDeploymentRuntime({
|
|
|
921
933
|
routeResolver,
|
|
922
934
|
onDevelopmentEvent,
|
|
923
935
|
managedSourceRoots,
|
|
936
|
+
evolitConfig: configuredEvolitConfig,
|
|
924
937
|
} = {}) {
|
|
925
|
-
const evolitConfig = await loadEvolitConfig(projectRoot);
|
|
938
|
+
const evolitConfig = configuredEvolitConfig ?? await loadEvolitConfig(projectRoot);
|
|
939
|
+
const serverSetupCleanup = await runServerSetup({ projectRoot, mode, evolitConfig });
|
|
926
940
|
const extensions = resolveEvolitExtensions(evolitConfig);
|
|
927
941
|
const extensionClientSpecifiers = getExtensionClientDescriptors(extensions).map((descriptor) => descriptor.module);
|
|
928
942
|
const effectiveManagedSourceRoots = managedSourceRoots
|
|
@@ -1072,6 +1086,7 @@ export async function createDeploymentRuntime({
|
|
|
1072
1086
|
if (revalidationTasks) {
|
|
1073
1087
|
await Promise.all(revalidationTasks.values());
|
|
1074
1088
|
}
|
|
1089
|
+
await serverSetupCleanup?.();
|
|
1075
1090
|
},
|
|
1076
1091
|
};
|
|
1077
1092
|
}
|
package/src/navigation-client.js
CHANGED
|
@@ -194,6 +194,79 @@ function syncDocumentState(nextState, documentRef) {
|
|
|
194
194
|
if (script) script.textContent = JSON.stringify(nextState);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
function readElementAttributes(element) {
|
|
198
|
+
return Object.fromEntries([...element.attributes].map((attribute) => [
|
|
199
|
+
attribute.name,
|
|
200
|
+
attribute.value,
|
|
201
|
+
]));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isRootBodyProjection(segment, projection) {
|
|
205
|
+
return segment?.kind === "layout"
|
|
206
|
+
&& segment?.depth === 0
|
|
207
|
+
&& typeof projection?.html === "string"
|
|
208
|
+
&& /<body(?:\s|>)/iu.test(projection.html);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parseRootBodyProjection(projectionHtml, documentRef) {
|
|
212
|
+
const DOMParserConstructor = documentRef.defaultView?.DOMParser ?? globalThis.DOMParser;
|
|
213
|
+
if (typeof DOMParserConstructor !== "function") return null;
|
|
214
|
+
return new DOMParserConstructor().parseFromString(projectionHtml, "text/html").body;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function collectRouteRuntimeScripts(root) {
|
|
218
|
+
const scripts = [...root.querySelectorAll("script")];
|
|
219
|
+
return scripts.filter((script) =>
|
|
220
|
+
script.id === "__EVOLIT_ROUTE__"
|
|
221
|
+
|| script.id === "__EVOLIT_DOCUMENT__"
|
|
222
|
+
|| script.id === "__LITSX_HYDRATION__"
|
|
223
|
+
|| script.hasAttribute("data-evolit-live-reload")
|
|
224
|
+
|| (script.type === "module" && (
|
|
225
|
+
script.textContent?.includes("getNavigation")
|
|
226
|
+
|| script.textContent?.includes("hydratePage")
|
|
227
|
+
)),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function replaceRootBodyProjection({
|
|
232
|
+
bodyProjection,
|
|
233
|
+
delta,
|
|
234
|
+
documentRef,
|
|
235
|
+
fragment,
|
|
236
|
+
target,
|
|
237
|
+
}) {
|
|
238
|
+
const body = documentRef.body;
|
|
239
|
+
if (!body || !bodyProjection) return false;
|
|
240
|
+
|
|
241
|
+
const authoredAttributes = readElementAttributes(bodyProjection);
|
|
242
|
+
const managedAttributes = normalizeManagedAttributes(delta.document?.bodyAttributes);
|
|
243
|
+
syncElementAttributes(
|
|
244
|
+
body,
|
|
245
|
+
readElementAttributes(body),
|
|
246
|
+
{ ...authoredAttributes, ...managedAttributes },
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const runtimeScripts = collectRouteRuntimeScripts(documentRef);
|
|
250
|
+
body.replaceChildren(target.start, fragment, target.end, ...runtimeScripts);
|
|
251
|
+
|
|
252
|
+
// HTML parsing can leave nodes from an authored root <body> beside the live
|
|
253
|
+
// body. Once the stable body owns the committed projection and runtime
|
|
254
|
+
// scripts, discard only those invalid element siblings.
|
|
255
|
+
for (const element of [...documentRef.documentElement.children]) {
|
|
256
|
+
if (element !== documentRef.head && element !== body) element.remove();
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function syncRouteStateScripts(delta, documentRef) {
|
|
262
|
+
const routeScript = documentRef.getElementById("__EVOLIT_ROUTE__");
|
|
263
|
+
if (routeScript) routeScript.textContent = JSON.stringify(delta.route);
|
|
264
|
+
const hydrationScript = documentRef.getElementById("__LITSX_HYDRATION__");
|
|
265
|
+
if (hydrationScript && delta.hydrationData) {
|
|
266
|
+
hydrationScript.textContent = JSON.stringify(delta.hydrationData);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
197
270
|
function versionClientImport(specifier, version) {
|
|
198
271
|
if (!version || typeof specifier !== "string") return specifier;
|
|
199
272
|
const url = new URL(specifier, globalThis.location?.href ?? "http://evolit.local/");
|
|
@@ -260,9 +333,18 @@ async function applyRouteDelta(delta, documentRef = document, signal, options =
|
|
|
260
333
|
throwIfAborted(signal);
|
|
261
334
|
const projection = next.projections[offset] ?? next.projections[0];
|
|
262
335
|
if (!projection) continue;
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
336
|
+
const bodyProjection = isRootBodyProjection(next, projection)
|
|
337
|
+
? parseRootBodyProjection(projection.html, documentRef)
|
|
338
|
+
: null;
|
|
339
|
+
const fragment = documentRef.createDocumentFragment();
|
|
340
|
+
if (bodyProjection) {
|
|
341
|
+
for (const script of collectRouteRuntimeScripts(bodyProjection)) script.remove();
|
|
342
|
+
fragment.append(...bodyProjection.childNodes);
|
|
343
|
+
} else {
|
|
344
|
+
const template = documentRef.createElement("template");
|
|
345
|
+
template.innerHTML = projection.html;
|
|
346
|
+
fragment.append(template.content);
|
|
347
|
+
}
|
|
266
348
|
materializeDeclarativeShadowDom(fragment);
|
|
267
349
|
// Restore opaque SSR forwarded refs while the fragment is detached, before
|
|
268
350
|
// its custom elements upgrade on insertion.
|
|
@@ -279,11 +361,19 @@ async function applyRouteDelta(delta, documentRef = document, signal, options =
|
|
|
279
361
|
roots.map(({ root, element }) => ({ ...root, element })),
|
|
280
362
|
delta.hydrationData,
|
|
281
363
|
);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
364
|
+
if (!replaceRootBodyProjection({
|
|
365
|
+
bodyProjection,
|
|
366
|
+
delta,
|
|
367
|
+
documentRef,
|
|
368
|
+
fragment,
|
|
369
|
+
target,
|
|
370
|
+
})) {
|
|
371
|
+
const range = documentRef.createRange();
|
|
372
|
+
range.setStartAfter(target.start);
|
|
373
|
+
range.setEndBefore(target.end);
|
|
374
|
+
range.deleteContents();
|
|
375
|
+
range.insertNode(fragment);
|
|
376
|
+
}
|
|
287
377
|
insertedRoots.push(...roots);
|
|
288
378
|
target.start.data = `evolit:segment:start:${next.id}`;
|
|
289
379
|
target.end.data = `evolit:segment:end:${next.id}`;
|
|
@@ -292,8 +382,7 @@ async function applyRouteDelta(delta, documentRef = document, signal, options =
|
|
|
292
382
|
// document to point persistent layout refs at the new target, or clear them
|
|
293
383
|
// when the replacement page no longer exposes one.
|
|
294
384
|
prepareForwardedRefs(documentRef);
|
|
295
|
-
|
|
296
|
-
if (script) script.textContent = JSON.stringify(delta.route);
|
|
385
|
+
syncRouteStateScripts(delta, documentRef);
|
|
297
386
|
if (delta.title) documentRef.title = delta.title;
|
|
298
387
|
for (const { root, element } of insertedRoots) {
|
|
299
388
|
throwIfAborted(signal);
|
package/src/render.js
CHANGED
|
@@ -295,6 +295,67 @@ function createTrackedRouteValues(values) {
|
|
|
295
295
|
};
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
function createTrackedNavigationContext(values) {
|
|
299
|
+
const tracked = createTrackedRouteValues(values ?? {});
|
|
300
|
+
let accessed = false;
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
value: values == null ? null : tracked.value,
|
|
304
|
+
read() {
|
|
305
|
+
accessed = true;
|
|
306
|
+
return values == null ? null : tracked.value;
|
|
307
|
+
},
|
|
308
|
+
profile() {
|
|
309
|
+
return { accessed, ...tracked.profile() };
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function createTrackedSegmentProps(
|
|
315
|
+
trackedParams,
|
|
316
|
+
trackedSearchParams,
|
|
317
|
+
trackedNavigationContext,
|
|
318
|
+
props,
|
|
319
|
+
) {
|
|
320
|
+
const result = {
|
|
321
|
+
params: trackedParams.value,
|
|
322
|
+
searchParams: trackedSearchParams.value,
|
|
323
|
+
...props,
|
|
324
|
+
};
|
|
325
|
+
Object.defineProperty(result, "navigationContext", {
|
|
326
|
+
enumerable: true,
|
|
327
|
+
get: () => trackedNavigationContext.read(),
|
|
328
|
+
});
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function createSegmentDependencyProfile(
|
|
333
|
+
trackedParams,
|
|
334
|
+
trackedSearchParams,
|
|
335
|
+
trackedNavigationContext,
|
|
336
|
+
) {
|
|
337
|
+
return {
|
|
338
|
+
params: trackedParams.profile(),
|
|
339
|
+
searchParams: trackedSearchParams.profile(),
|
|
340
|
+
navigationContext: trackedNavigationContext.profile(),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function selectTrackedValues(profile, values) {
|
|
345
|
+
const normalized = profile ?? { all: false, keys: [] };
|
|
346
|
+
return Object.fromEntries(
|
|
347
|
+
(normalized.all ? Object.keys(values ?? {}) : normalized.keys ?? [])
|
|
348
|
+
.sort()
|
|
349
|
+
.map((key) => [key, values?.[key]]),
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function selectTrackedNavigationContext(profile, navigationContext) {
|
|
354
|
+
if (!profile?.accessed) return undefined;
|
|
355
|
+
if (navigationContext == null) return null;
|
|
356
|
+
return selectTrackedValues(profile, navigationContext);
|
|
357
|
+
}
|
|
358
|
+
|
|
298
359
|
function createSegmentCacheKey(
|
|
299
360
|
segment,
|
|
300
361
|
idPrefix,
|
|
@@ -303,30 +364,26 @@ function createSegmentCacheKey(
|
|
|
303
364
|
searchParams,
|
|
304
365
|
navigationContext,
|
|
305
366
|
) {
|
|
306
|
-
const select = (values) => Object.fromEntries(
|
|
307
|
-
(profile.all ? Object.keys(values) : profile.keys)
|
|
308
|
-
.sort()
|
|
309
|
-
.map((key) => [key, values[key]]),
|
|
310
|
-
);
|
|
311
367
|
return JSON.stringify({
|
|
312
368
|
modulePath: segment.modulePath,
|
|
313
369
|
idPrefix,
|
|
314
|
-
params:
|
|
315
|
-
searchParams:
|
|
316
|
-
navigationContext:
|
|
370
|
+
params: selectTrackedValues(profile.params, params),
|
|
371
|
+
searchParams: selectTrackedValues(profile.searchParams, searchParams),
|
|
372
|
+
navigationContext: selectTrackedNavigationContext(
|
|
373
|
+
profile.navigationContext,
|
|
374
|
+
navigationContext,
|
|
375
|
+
),
|
|
317
376
|
});
|
|
318
377
|
}
|
|
319
378
|
|
|
320
379
|
function createSegmentInputKey(profile, params, searchParams, navigationContext) {
|
|
321
|
-
const select = (values) => Object.fromEntries(
|
|
322
|
-
(profile.all ? Object.keys(values) : profile.keys)
|
|
323
|
-
.sort()
|
|
324
|
-
.map((key) => [key, values[key]]),
|
|
325
|
-
);
|
|
326
380
|
return JSON.stringify({
|
|
327
|
-
params:
|
|
328
|
-
searchParams:
|
|
329
|
-
navigationContext:
|
|
381
|
+
params: selectTrackedValues(profile.params, params),
|
|
382
|
+
searchParams: selectTrackedValues(profile.searchParams, searchParams),
|
|
383
|
+
navigationContext: selectTrackedNavigationContext(
|
|
384
|
+
profile.navigationContext,
|
|
385
|
+
navigationContext,
|
|
386
|
+
),
|
|
330
387
|
});
|
|
331
388
|
}
|
|
332
389
|
|
|
@@ -457,18 +514,37 @@ async function renderSegmentedComponentTree(
|
|
|
457
514
|
const idPrefix = `${segment.id}-p${projectionPath.join("-") || "0"}`;
|
|
458
515
|
|
|
459
516
|
if (index === layoutComponents.length) {
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
517
|
+
const trackedParams = createTrackedRouteValues(requestContext.params);
|
|
518
|
+
const trackedSearchParams = createTrackedRouteValues(requestContext.searchParams);
|
|
519
|
+
const trackedNavigationContext = createTrackedNavigationContext(
|
|
520
|
+
requestContext.navigationContext,
|
|
521
|
+
);
|
|
522
|
+
const pageValue = await runWithRouteState(requestContext, {
|
|
523
|
+
params: trackedParams.value,
|
|
524
|
+
searchParams: trackedSearchParams.value,
|
|
525
|
+
navigationContext: trackedNavigationContext.value,
|
|
526
|
+
trackNavigationContext: () => trackedNavigationContext.read(),
|
|
527
|
+
}, () => component(createTrackedSegmentProps(
|
|
528
|
+
trackedParams,
|
|
529
|
+
trackedSearchParams,
|
|
530
|
+
trackedNavigationContext,
|
|
531
|
+
{ request, ...extraProps },
|
|
532
|
+
), incomingRef));
|
|
467
533
|
const pageResult = await renderToString(wrapRouteSegment(segment, pageValue), {
|
|
468
534
|
assetResolver: options.assetResolver,
|
|
469
535
|
context: { idPrefix },
|
|
470
536
|
});
|
|
471
537
|
pageResult.segmentModulePath = segment.modulePath;
|
|
538
|
+
segment.inputKey = createSegmentInputKey(
|
|
539
|
+
createSegmentDependencyProfile(
|
|
540
|
+
trackedParams,
|
|
541
|
+
trackedSearchParams,
|
|
542
|
+
trackedNavigationContext,
|
|
543
|
+
),
|
|
544
|
+
requestContext.params,
|
|
545
|
+
requestContext.searchParams,
|
|
546
|
+
requestContext.navigationContext,
|
|
547
|
+
);
|
|
472
548
|
results.push(pageResult);
|
|
473
549
|
return pageResult.html;
|
|
474
550
|
}
|
|
@@ -486,31 +562,36 @@ async function renderSegmentedComponentTree(
|
|
|
486
562
|
if (!layoutResult) {
|
|
487
563
|
const trackedParams = createTrackedRouteValues(requestContext.params);
|
|
488
564
|
const trackedSearchParams = createTrackedRouteValues(requestContext.searchParams);
|
|
565
|
+
const trackedNavigationContext = createTrackedNavigationContext(
|
|
566
|
+
requestContext.navigationContext,
|
|
567
|
+
);
|
|
489
568
|
const didUseDynamicRequestData = requestContext.didUseDynamicRequestData;
|
|
490
569
|
layoutResult = await runWithRouteState(requestContext, {
|
|
491
570
|
params: trackedParams.value,
|
|
492
571
|
searchParams: trackedSearchParams.value,
|
|
572
|
+
navigationContext: trackedNavigationContext.value,
|
|
573
|
+
trackNavigationContext: () => trackedNavigationContext.read(),
|
|
493
574
|
}, async () => {
|
|
494
|
-
const layoutValue = await layoutComponents[index](
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
575
|
+
const layoutValue = await layoutComponents[index](createTrackedSegmentProps(
|
|
576
|
+
trackedParams,
|
|
577
|
+
trackedSearchParams,
|
|
578
|
+
trackedNavigationContext,
|
|
579
|
+
{
|
|
580
|
+
request,
|
|
581
|
+
children: withForwardedChildRef(html`${unsafeHTML(childrenMarker)}`, childRef),
|
|
582
|
+
},
|
|
583
|
+
), incomingRef);
|
|
501
584
|
return renderToString(wrapRouteSegment(segment, layoutValue), {
|
|
502
585
|
assetResolver: options.assetResolver,
|
|
503
586
|
context: { idPrefix },
|
|
504
587
|
});
|
|
505
588
|
});
|
|
506
589
|
layoutResult.segmentModulePath = segment.modulePath;
|
|
507
|
-
profile =
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
])].sort(),
|
|
513
|
-
};
|
|
590
|
+
profile = createSegmentDependencyProfile(
|
|
591
|
+
trackedParams,
|
|
592
|
+
trackedSearchParams,
|
|
593
|
+
trackedNavigationContext,
|
|
594
|
+
);
|
|
514
595
|
if (!didUseDynamicRequestData && !requestContext.didUseDynamicRequestData) {
|
|
515
596
|
options.segmentCache?.set(
|
|
516
597
|
segment,
|
package/src/request-context.js
CHANGED
|
@@ -175,7 +175,10 @@ export function getRouteState() {
|
|
|
175
175
|
},
|
|
176
176
|
navigationContext: {
|
|
177
177
|
enumerable: true,
|
|
178
|
-
|
|
178
|
+
get() {
|
|
179
|
+
trackedState?.trackNavigationContext?.();
|
|
180
|
+
return routeState.navigationContext;
|
|
181
|
+
},
|
|
179
182
|
},
|
|
180
183
|
});
|
|
181
184
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { importCompiledModule, resolveProjectModuleSpecifier } from "./compiler.js";
|
|
3
|
+
|
|
4
|
+
function resolveSetupExport(moduleRecord, setupPath) {
|
|
5
|
+
const setup = moduleRecord.setup ?? moduleRecord.default;
|
|
6
|
+
if (typeof setup !== "function") {
|
|
7
|
+
throw new TypeError(
|
|
8
|
+
`Expected server setup module ${JSON.stringify(setupPath)} to export a setup function or a default function.`,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
return setup;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeCleanup(result) {
|
|
15
|
+
if (result == null) return null;
|
|
16
|
+
if (typeof result === "function") return result;
|
|
17
|
+
if (typeof result === "object" && typeof result.dispose === "function") {
|
|
18
|
+
return () => result.dispose();
|
|
19
|
+
}
|
|
20
|
+
throw new TypeError("Evolit server setup must return nothing, a cleanup function, or an object with dispose().");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Runs the application server bootstrap once for a build or runtime instance. */
|
|
24
|
+
export async function runServerSetup({ projectRoot, mode, evolitConfig = {} }) {
|
|
25
|
+
const serverConfig = evolitConfig.server;
|
|
26
|
+
if (serverConfig == null) return null;
|
|
27
|
+
if (!serverConfig || typeof serverConfig !== "object" || Array.isArray(serverConfig)) {
|
|
28
|
+
throw new TypeError("Expected server in evolit.config.js to be an object.");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const configuredSetup = serverConfig.setup;
|
|
32
|
+
if (configuredSetup == null) return null;
|
|
33
|
+
|
|
34
|
+
let setup = configuredSetup;
|
|
35
|
+
if (typeof configuredSetup === "string") {
|
|
36
|
+
if (configuredSetup.length === 0) {
|
|
37
|
+
throw new TypeError("Expected server.setup to be a non-empty module specifier.");
|
|
38
|
+
}
|
|
39
|
+
const setupPath = await resolveProjectModuleSpecifier(
|
|
40
|
+
projectRoot,
|
|
41
|
+
path.join(projectRoot, "evolit.config.js"),
|
|
42
|
+
configuredSetup,
|
|
43
|
+
);
|
|
44
|
+
if (!setupPath) {
|
|
45
|
+
throw new Error(`Unable to resolve configured server setup ${JSON.stringify(configuredSetup)}.`);
|
|
46
|
+
}
|
|
47
|
+
const moduleRecord = await importCompiledModule(setupPath, {
|
|
48
|
+
projectRoot,
|
|
49
|
+
mode,
|
|
50
|
+
sourceMaps: mode === "development",
|
|
51
|
+
target: "server",
|
|
52
|
+
});
|
|
53
|
+
setup = resolveSetupExport(moduleRecord, configuredSetup);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof setup !== "function") {
|
|
57
|
+
throw new TypeError("Expected server.setup in evolit.config.js to be a function or module specifier.");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return normalizeCleanup(await setup({
|
|
61
|
+
projectRoot,
|
|
62
|
+
mode,
|
|
63
|
+
}));
|
|
64
|
+
}
|
package/src/server.js
CHANGED
|
@@ -214,6 +214,7 @@ async function createServer(projectRoot, mode, explicitPort, options = {}) {
|
|
|
214
214
|
responseCacheRuntime,
|
|
215
215
|
onDevelopmentEvent: reportDevelopmentEvent,
|
|
216
216
|
managedSourceRoots,
|
|
217
|
+
evolitConfig,
|
|
217
218
|
});
|
|
218
219
|
const port = getPort(explicitPort);
|
|
219
220
|
const liveReloadServer = mode === "development" ? new WebSocketServer({ noServer: true }) : null;
|
package/src/urql-ssr.js
CHANGED
|
@@ -26,7 +26,18 @@ export async function getSsrUrqlAdapter() {
|
|
|
26
26
|
* Evolit owns only lifecycle here; client creation and URQL configuration stay
|
|
27
27
|
* entirely in the application and @litsx/urql.
|
|
28
28
|
*/
|
|
29
|
-
export async function runWithOptionalSsrUrqlScope(
|
|
29
|
+
export async function runWithOptionalSsrUrqlScope(
|
|
30
|
+
contextOrCallback,
|
|
31
|
+
callbackOrOptions = {},
|
|
32
|
+
maybeOptions = {},
|
|
33
|
+
) {
|
|
34
|
+
const hasRequestContext = typeof contextOrCallback !== "function";
|
|
35
|
+
const requestContext = hasRequestContext ? contextOrCallback : null;
|
|
36
|
+
const callback = hasRequestContext ? callbackOrOptions : contextOrCallback;
|
|
37
|
+
const options = hasRequestContext ? maybeOptions : callbackOrOptions;
|
|
38
|
+
if (typeof callback !== "function") {
|
|
39
|
+
throw new TypeError("runWithOptionalSsrUrqlScope() expects a render callback.");
|
|
40
|
+
}
|
|
30
41
|
const adapter = Object.hasOwn(options, "adapter")
|
|
31
42
|
? options.adapter
|
|
32
43
|
: await getSsrUrqlAdapter();
|
|
@@ -34,7 +45,61 @@ export async function runWithOptionalSsrUrqlScope(callback, options = {}) {
|
|
|
34
45
|
return callback(null);
|
|
35
46
|
}
|
|
36
47
|
|
|
37
|
-
return
|
|
48
|
+
return hasRequestContext
|
|
49
|
+
? adapter.runWithUrqlScope(requestContext, () => callback(adapter))
|
|
50
|
+
: adapter.runWithUrqlScope(() => callback(adapter));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Creates the HTTP context passed to a request-scoped URQL resource factory. */
|
|
54
|
+
export function createSsrUrqlRequestContext(request) {
|
|
55
|
+
let didUseRequestData = false;
|
|
56
|
+
const trackedRequest = new Proxy(request, {
|
|
57
|
+
get(target, property) {
|
|
58
|
+
if (property !== "constructor" && property !== Symbol.toStringTag) {
|
|
59
|
+
didUseRequestData = true;
|
|
60
|
+
}
|
|
61
|
+
const value = Reflect.get(target, property, target);
|
|
62
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const responseHeaders = new Headers();
|
|
66
|
+
return {
|
|
67
|
+
request: trackedRequest,
|
|
68
|
+
responseHeaders,
|
|
69
|
+
didUseDynamicRequestData() {
|
|
70
|
+
return didUseRequestData || !responseHeaders.entries().next().done;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function responseHeadersToObject(headers) {
|
|
76
|
+
const values = Object.fromEntries(headers.entries());
|
|
77
|
+
if (typeof headers.getSetCookie === "function") {
|
|
78
|
+
const cookies = headers.getSetCookie();
|
|
79
|
+
if (cookies.length > 0) values["set-cookie"] = cookies;
|
|
80
|
+
}
|
|
81
|
+
return values;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Applies request-resource response state before Evolit caches the render. */
|
|
85
|
+
export function applySsrUrqlRequestContext(routeResult, response, context) {
|
|
86
|
+
if (!context) return response;
|
|
87
|
+
const urqlHeaders = responseHeadersToObject(context.responseHeaders);
|
|
88
|
+
if (context.didUseDynamicRequestData()) {
|
|
89
|
+
routeResult.cachePolicy = { mode: "dynamic" };
|
|
90
|
+
}
|
|
91
|
+
if (Object.keys(urqlHeaders).length === 0) return response;
|
|
92
|
+
routeResult.responseHeaders = {
|
|
93
|
+
...(routeResult.responseHeaders ?? {}),
|
|
94
|
+
...urqlHeaders,
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
...response,
|
|
98
|
+
headers: {
|
|
99
|
+
...(response.headers ?? {}),
|
|
100
|
+
...urqlHeaders,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
38
103
|
}
|
|
39
104
|
|
|
40
105
|
function escapeJsonForHtml(value) {
|