evolit 0.2.1 → 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 +46 -0
- 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/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
|
|
@@ -301,6 +321,32 @@ server components and helpers that do not receive route props directly. It retur
|
|
|
301
321
|
Reading `getRouteState().url` has the same dynamic-rendering semantics as `requestUrl()`; reading
|
|
302
322
|
`params` and `searchParams` participates in the normal segment-cache key tracking.
|
|
303
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
|
+
|
|
304
350
|
## Extensions
|
|
305
351
|
|
|
306
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
|
}
|
|
@@ -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) {
|