evolit 0.1.0-alpha.26 → 0.1.0-alpha.28
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 +12 -0
- package/package.json +6 -5
- package/src/client-assets.js +24 -8
- package/src/compiler.js +63 -3
- package/src/deployment-runtime.js +39 -2
- package/src/development-events.js +5 -0
- package/src/development-hot-client.js +65 -0
- package/src/navigation-client.js +96 -17
- package/src/server.js +104 -17
package/README.md
CHANGED
|
@@ -131,6 +131,18 @@ Both hooks update after client navigation. `useParams()` returns a read-only sna
|
|
|
131
131
|
`URLSearchParams` from `useSearchParams()` is a local snapshot, so build a new href and navigate to
|
|
132
132
|
it to update the URL.
|
|
133
133
|
|
|
134
|
+
### Development refresh
|
|
135
|
+
|
|
136
|
+
In development, each browser subscribes its active URL over the Evolit WebSocket. After source
|
|
137
|
+
invalidation, the server renders the fresh SSR representation once per request context and pushes
|
|
138
|
+
the resulting delta directly to its subscribers. No follow-up browser request is needed. Server-only
|
|
139
|
+
module and static-asset changes update only the affected route segment while preserving the document,
|
|
140
|
+
scroll position, and persistent layouts.
|
|
141
|
+
Changes to a hydrated client boundary rebuild its browser artifact and update the stable Evolit
|
|
142
|
+
development proxy registered for that tag. Existing instances receive the new implementation
|
|
143
|
+
without redefining the Custom Element or replacing the document. Invalid deltas and failed hot
|
|
144
|
+
updates still fall back to a document reload.
|
|
145
|
+
|
|
134
146
|
### Progressive links and forms
|
|
135
147
|
|
|
136
148
|
Links work without JavaScript. With JavaScript, Evolit intercepts ordinary same-origin left-clicks.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evolit",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.28",
|
|
4
4
|
"description": "A convention-driven application framework for LitSX and web components.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "yarn@4.10.3",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"./server": {
|
|
30
30
|
"browser": "./src/request-context-browser.js",
|
|
31
31
|
"default": "./src/server-api.js"
|
|
32
|
-
}
|
|
32
|
+
},
|
|
33
|
+
"./internal/development-hot": "./src/development-hot-client.js"
|
|
33
34
|
},
|
|
34
35
|
"scripts": {
|
|
35
36
|
"dev": "node ./src/cli.js dev",
|
|
@@ -54,9 +55,9 @@
|
|
|
54
55
|
},
|
|
55
56
|
"dependencies": {
|
|
56
57
|
"@jridgewell/remapping": "^2.3.5",
|
|
57
|
-
"@litsx/compiler": "0.10.0-canary-feat-ssr-
|
|
58
|
-
"@litsx/core": "0.17.0-canary-feat-ssr-
|
|
59
|
-
"@litsx/ssr": "0.2.0-canary-feat-ssr-
|
|
58
|
+
"@litsx/compiler": "0.10.0-canary-feat-ssr-20260731234459",
|
|
59
|
+
"@litsx/core": "0.17.0-canary-feat-ssr-20260731234459",
|
|
60
|
+
"@litsx/ssr": "0.2.0-canary-feat-ssr-20260731234459",
|
|
60
61
|
"@litsx/typescript": "^0.9.0",
|
|
61
62
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
62
63
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
package/src/client-assets.js
CHANGED
|
@@ -27,6 +27,7 @@ const MODULE_SPECIFIER_PATTERN =
|
|
|
27
27
|
|
|
28
28
|
const requireFromHere = createRequire(import.meta.url);
|
|
29
29
|
const EVOLIT_NAVIGATION_BROWSER_ENTRY = fileURLToPath(new URL("./navigation-client.js", import.meta.url));
|
|
30
|
+
const EVOLIT_DEVELOPMENT_HOT_ENTRY = fileURLToPath(new URL("./development-hot-client.js", import.meta.url));
|
|
30
31
|
const SHARED_VENDOR_SPECIFIERS = [
|
|
31
32
|
"@litsx/core",
|
|
32
33
|
"@litsx/core/elements",
|
|
@@ -34,6 +35,9 @@ const SHARED_VENDOR_SPECIFIERS = [
|
|
|
34
35
|
"evolit/navigation",
|
|
35
36
|
"lit",
|
|
36
37
|
];
|
|
38
|
+
const DEVELOPMENT_SHARED_VENDOR_SPECIFIERS = [
|
|
39
|
+
"evolit/internal/development-hot",
|
|
40
|
+
];
|
|
37
41
|
const browserSpecifierFilePathCache = new Map();
|
|
38
42
|
const packageRootCache = new Map();
|
|
39
43
|
const sharedRuntimeBuildCache = new Map();
|
|
@@ -303,6 +307,9 @@ export async function collectSharedVendorSpecifiers(
|
|
|
303
307
|
) {
|
|
304
308
|
return [...new Set([
|
|
305
309
|
...(options.includeBase === false ? [] : SHARED_VENDOR_SPECIFIERS),
|
|
310
|
+
...(options.includeBase === false || options.mode !== "development"
|
|
311
|
+
? []
|
|
312
|
+
: DEVELOPMENT_SHARED_VENDOR_SPECIFIERS),
|
|
306
313
|
...additionalEntrySpecifiers.filter((specifier) => isBareSpecifier(specifier)),
|
|
307
314
|
])].sort();
|
|
308
315
|
}
|
|
@@ -322,7 +329,9 @@ async function createSharedRuntimeInputEntries(projectRoot, entriesRoot, specifi
|
|
|
322
329
|
}
|
|
323
330
|
inputEntries[entryName] = specifier === "evolit/navigation"
|
|
324
331
|
? EVOLIT_NAVIGATION_BROWSER_ENTRY
|
|
325
|
-
:
|
|
332
|
+
: specifier === "evolit/internal/development-hot"
|
|
333
|
+
? EVOLIT_DEVELOPMENT_HOT_ENTRY
|
|
334
|
+
: await resolveBrowserSpecifierFilePath(specifier, { projectRoot });
|
|
326
335
|
}
|
|
327
336
|
|
|
328
337
|
return inputEntries;
|
|
@@ -512,7 +521,10 @@ export async function buildSharedVendorRuntime(
|
|
|
512
521
|
});
|
|
513
522
|
const additionalRuntimes = await Promise.all(
|
|
514
523
|
additionalEntrySpecifiers
|
|
515
|
-
.filter((specifier) => !
|
|
524
|
+
.filter((specifier) => ![
|
|
525
|
+
...SHARED_VENDOR_SPECIFIERS,
|
|
526
|
+
...DEVELOPMENT_SHARED_VENDOR_SPECIFIERS,
|
|
527
|
+
].includes(specifier))
|
|
516
528
|
.map((specifier) => buildSharedVendorRuntime(projectRoot, mode, {
|
|
517
529
|
...options,
|
|
518
530
|
additionalEntrySpecifiers: [specifier],
|
|
@@ -548,6 +560,7 @@ export async function buildSharedVendorRuntime(
|
|
|
548
560
|
);
|
|
549
561
|
const specifiers = await collectSharedVendorSpecifiers(additionalEntrySpecifiers, {
|
|
550
562
|
includeBase: options.includeBase !== false,
|
|
563
|
+
mode,
|
|
551
564
|
});
|
|
552
565
|
const inputEntries = await createSharedRuntimeInputEntries(projectRoot, entriesRoot, specifiers);
|
|
553
566
|
|
|
@@ -1161,7 +1174,8 @@ export function createHydrationBootstrap({
|
|
|
1161
1174
|
: []).filter((moduleUrl) => typeof moduleUrl === "string" && moduleUrl.length > 0),
|
|
1162
1175
|
)];
|
|
1163
1176
|
|
|
1164
|
-
|
|
1177
|
+
const hasHydrationWork = rootModuleUrls.length > 0 || clientImports.length > 0;
|
|
1178
|
+
if (!hasHydrationWork && !navigationModuleUrl) {
|
|
1165
1179
|
return "";
|
|
1166
1180
|
}
|
|
1167
1181
|
|
|
@@ -1169,9 +1183,8 @@ export function createHydrationBootstrap({
|
|
|
1169
1183
|
.map((moduleUrl) => ` () => import(${JSON.stringify(moduleUrl)})`)
|
|
1170
1184
|
.join(",\n");
|
|
1171
1185
|
|
|
1172
|
-
const
|
|
1186
|
+
const hydrationSource = hasHydrationWork ? `
|
|
1173
1187
|
import { hydratePage, registerHydrationModules } from ${JSON.stringify(hydrationModuleUrl)};
|
|
1174
|
-
${navigationModuleUrl ? `import { getNavigation } from ${JSON.stringify(navigationModuleUrl)};` : ""}
|
|
1175
1188
|
|
|
1176
1189
|
await hydratePage({
|
|
1177
1190
|
clientImports: ${JSON.stringify(clientImports)},
|
|
@@ -1179,10 +1192,13 @@ await hydratePage({
|
|
|
1179
1192
|
${moduleLoaders}
|
|
1180
1193
|
])
|
|
1181
1194
|
});
|
|
1182
|
-
|
|
1183
|
-
|
|
1195
|
+
` : "";
|
|
1196
|
+
const navigationSource = navigationModuleUrl ? `
|
|
1197
|
+
import { getNavigation } from ${JSON.stringify(navigationModuleUrl)};
|
|
1198
|
+
getNavigation();
|
|
1199
|
+
` : "";
|
|
1184
1200
|
|
|
1185
|
-
return escapeInlineScriptText(
|
|
1201
|
+
return escapeInlineScriptText(`${hydrationSource}${navigationSource}`);
|
|
1186
1202
|
}
|
|
1187
1203
|
|
|
1188
1204
|
export function normalizeHydrationDataForClient(
|
package/src/compiler.js
CHANGED
|
@@ -802,6 +802,56 @@ function getDevelopmentGraphCacheKey(entryPath, options) {
|
|
|
802
802
|
].join("::");
|
|
803
803
|
}
|
|
804
804
|
|
|
805
|
+
function wrapDevelopmentHydratableExports(code, moduleId, inputSourceMap = null) {
|
|
806
|
+
const sourcePath = moduleId;
|
|
807
|
+
const sourceFile = ts.createSourceFile(sourcePath, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
|
|
808
|
+
const magicSource = new MagicString(code);
|
|
809
|
+
const exports = [];
|
|
810
|
+
let index = 0;
|
|
811
|
+
for (const statement of sourceFile.statements) {
|
|
812
|
+
if (!ts.isClassDeclaration(statement) || !statement.name) continue;
|
|
813
|
+
const isHydratable = statement.members.some((member) =>
|
|
814
|
+
member.name?.getText(sourceFile).includes('Symbol.for("litsx.hydratableTag")')
|
|
815
|
+
);
|
|
816
|
+
if (!isHydratable) continue;
|
|
817
|
+
const modifiers = statement.modifiers ?? [];
|
|
818
|
+
const isExported = modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
819
|
+
if (!isExported) continue;
|
|
820
|
+
const isDefault = modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword);
|
|
821
|
+
for (const modifier of modifiers) {
|
|
822
|
+
if (modifier.kind === ts.SyntaxKind.ExportKeyword || modifier.kind === ts.SyntaxKind.DefaultKeyword) {
|
|
823
|
+
magicSource.remove(modifier.getStart(sourceFile), modifier.end);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const className = statement.name.text;
|
|
827
|
+
const proxyName = `__evolit_hot_${className}_${index++}`;
|
|
828
|
+
exports.push(
|
|
829
|
+
`const ${proxyName} = __evolitHotComponent(${JSON.stringify(moduleId)}, ${className});`,
|
|
830
|
+
isDefault ? `export default ${proxyName};` : `export { ${proxyName} as ${className} };`,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
if (exports.length === 0) return { code, map: inputSourceMap };
|
|
834
|
+
magicSource.append(
|
|
835
|
+
'\nimport { hotComponent as __evolitHotComponent } from "evolit/internal/development-hot";\n'
|
|
836
|
+
+ `${exports.join("\n")}\n`,
|
|
837
|
+
);
|
|
838
|
+
const hotTransformSource = `${moduleId}#evolit-hot`;
|
|
839
|
+
const hotTransformMap = magicSource.generateMap({
|
|
840
|
+
hires: true,
|
|
841
|
+
includeContent: false,
|
|
842
|
+
source: hotTransformSource,
|
|
843
|
+
file: moduleId,
|
|
844
|
+
});
|
|
845
|
+
return {
|
|
846
|
+
code: magicSource.toString(),
|
|
847
|
+
map: inputSourceMap
|
|
848
|
+
? remapping(hotTransformMap.toString(), (source) => (
|
|
849
|
+
source.endsWith("#evolit-hot") ? inputSourceMap : null
|
|
850
|
+
))
|
|
851
|
+
: null,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
805
855
|
export function invalidateDevelopmentCompilationCache(projectRoot, changedPaths = null) {
|
|
806
856
|
const resolvedProjectRoot = path.resolve(projectRoot);
|
|
807
857
|
const prefix = `${resolvedProjectRoot}::`;
|
|
@@ -904,14 +954,24 @@ async function compileModuleGraphUncached(entryPath, options = {}) {
|
|
|
904
954
|
clientImportParents,
|
|
905
955
|
);
|
|
906
956
|
}
|
|
907
|
-
const
|
|
957
|
+
const projectedCode = target === "client" && isMixedComponentModule
|
|
908
958
|
? createMixedModuleClientProjection(transformed.code, sourcePath)
|
|
909
959
|
: transformed.code;
|
|
960
|
+
const targetTransform = target === "client" && mode === "development"
|
|
961
|
+
? wrapDevelopmentHydratableExports(
|
|
962
|
+
projectedCode,
|
|
963
|
+
path.relative(projectRoot, sourcePath).split(path.sep).join("/"),
|
|
964
|
+
projectedCode === transformed.code ? transformed.map ?? null : null,
|
|
965
|
+
)
|
|
966
|
+
: {
|
|
967
|
+
code: projectedCode,
|
|
968
|
+
map: projectedCode === transformed.code ? transformed.map ?? null : null,
|
|
969
|
+
};
|
|
910
970
|
const rewritten = await rewriteRelativeSpecifiers({
|
|
911
971
|
projectRoot,
|
|
912
972
|
outputRoot,
|
|
913
973
|
sourcePath,
|
|
914
|
-
code:
|
|
974
|
+
code: targetTransform.code,
|
|
915
975
|
compileModule: (resolvedImportPath) => compileModule(resolvedImportPath, sourcePath),
|
|
916
976
|
moduleMetadata,
|
|
917
977
|
mode,
|
|
@@ -920,7 +980,7 @@ async function compileModuleGraphUncached(entryPath, options = {}) {
|
|
|
920
980
|
serverImportQuery,
|
|
921
981
|
sourceMaps,
|
|
922
982
|
outputPath,
|
|
923
|
-
inputSourceMap:
|
|
983
|
+
inputSourceMap: targetTransform.map,
|
|
924
984
|
onDevelopmentEvent,
|
|
925
985
|
staticAssetFiles,
|
|
926
986
|
managedSourceRoots,
|
|
@@ -250,6 +250,7 @@ export async function createRequestRenderer({
|
|
|
250
250
|
const devBundledEntries = new Set();
|
|
251
251
|
const devPreparedClientModules = new Set();
|
|
252
252
|
const devClientModuleDependencies = new Map();
|
|
253
|
+
const devKnownClientBoundarySources = new Set();
|
|
253
254
|
const devInventoryByEntry = new Map();
|
|
254
255
|
const devServerAssetImportsByEntry = {};
|
|
255
256
|
const devClientBoundariesByEntry = {};
|
|
@@ -412,8 +413,9 @@ export async function createRequestRenderer({
|
|
|
412
413
|
].filter((moduleId) => typeof moduleId === "string" && moduleId.length > 0))];
|
|
413
414
|
const unresolvedModules = [];
|
|
414
415
|
for (const moduleId of renderedModules) {
|
|
415
|
-
if (hasClientArtifact(moduleId)) continue;
|
|
416
416
|
const sourcePath = await resolveRenderedClientSource(moduleId, routeResult);
|
|
417
|
+
if (sourcePath) devKnownClientBoundarySources.add(path.resolve(sourcePath));
|
|
418
|
+
if (hasClientArtifact(moduleId)) continue;
|
|
417
419
|
const publicUrl = sourcePath ? currentAssetResolver(sourcePath) : null;
|
|
418
420
|
if (typeof publicUrl === "string") {
|
|
419
421
|
for (const root of result.hydrationData?.roots ?? []) {
|
|
@@ -453,6 +455,7 @@ export async function createRequestRenderer({
|
|
|
453
455
|
);
|
|
454
456
|
}
|
|
455
457
|
await compileDevelopmentClientBoundary(sourcePath);
|
|
458
|
+
devKnownClientBoundarySources.add(path.resolve(sourcePath));
|
|
456
459
|
compiledModules.push(getCompiledClientModule(projectRoot, sourcePath));
|
|
457
460
|
}
|
|
458
461
|
|
|
@@ -588,12 +591,17 @@ export async function createRequestRenderer({
|
|
|
588
591
|
/^(?:tsconfig|jsconfig)(?:\..+)?\.json$/.test(path.basename(changedPath))
|
|
589
592
|
|| path.basename(changedPath) === "package.json"
|
|
590
593
|
);
|
|
594
|
+
const affectedRouteEntries = new Set();
|
|
591
595
|
if (normalizedChangedPaths == null || moduleResolutionChanged) {
|
|
596
|
+
for (const entryPath of devInventoryByEntry.keys()) {
|
|
597
|
+
affectedRouteEntries.add(path.resolve(entryPath));
|
|
598
|
+
}
|
|
592
599
|
devInventoryByEntry.clear();
|
|
593
600
|
} else {
|
|
594
601
|
for (const [entryPath, pendingInventory] of devInventoryByEntry) {
|
|
595
602
|
const inventory = await pendingInventory;
|
|
596
603
|
if (inventory.sourceFiles.some((sourcePath) => normalizedChangedPaths.has(path.resolve(sourcePath)))) {
|
|
604
|
+
affectedRouteEntries.add(path.resolve(entryPath));
|
|
597
605
|
devInventoryByEntry.delete(entryPath);
|
|
598
606
|
}
|
|
599
607
|
}
|
|
@@ -607,6 +615,22 @@ export async function createRequestRenderer({
|
|
|
607
615
|
)
|
|
608
616
|
.map(([clientModule]) => clientModule),
|
|
609
617
|
);
|
|
618
|
+
if (normalizedChangedPaths) {
|
|
619
|
+
for (const clientModule of new Set([
|
|
620
|
+
...devPreparedClientModules,
|
|
621
|
+
...devKnownClientBoundarySources,
|
|
622
|
+
])) {
|
|
623
|
+
if (normalizedChangedPaths.has(path.resolve(clientModule))) {
|
|
624
|
+
affectedClientModules.add(clientModule);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const affectedExecutableClientEntryCount = [...affectedClientModules]
|
|
629
|
+
.filter((clientModule) =>
|
|
630
|
+
devPreparedClientModules.has(clientModule)
|
|
631
|
+
|| devKnownClientBoundarySources.has(path.resolve(clientModule))
|
|
632
|
+
)
|
|
633
|
+
.length;
|
|
610
634
|
|
|
611
635
|
if (affectedClientModules.size > 0 || normalizedChangedPaths == null) {
|
|
612
636
|
developmentMetrics.selectivelyInvalidatedClientEntries += affectedClientModules.size;
|
|
@@ -637,6 +661,8 @@ export async function createRequestRenderer({
|
|
|
637
661
|
type: "invalidated",
|
|
638
662
|
changedPathCount: normalizedChangedPaths?.size ?? null,
|
|
639
663
|
affectedClientEntryCount: affectedClientModules.size,
|
|
664
|
+
affectedExecutableClientEntryCount,
|
|
665
|
+
knownClientBoundaryCount: devKnownClientBoundarySources.size,
|
|
640
666
|
});
|
|
641
667
|
|
|
642
668
|
if (usesFrameworkRouteResolver) {
|
|
@@ -644,6 +670,13 @@ export async function createRequestRenderer({
|
|
|
644
670
|
reportDevelopmentEvent({ type: "segment-cache-invalidated", entryCount: segmentEntryCount });
|
|
645
671
|
effectiveRouteResolver = await createFrameworkRouteResolver();
|
|
646
672
|
}
|
|
673
|
+
|
|
674
|
+
return {
|
|
675
|
+
affectedClientEntryCount: affectedExecutableClientEntryCount,
|
|
676
|
+
affectedRouteEntryPaths: [...affectedRouteEntries]
|
|
677
|
+
.map((entryPath) => path.relative(projectRoot, entryPath).split(path.sep).join("/"))
|
|
678
|
+
.sort(),
|
|
679
|
+
};
|
|
647
680
|
});
|
|
648
681
|
},
|
|
649
682
|
async prepareRouteClientArtifacts(routeResult) {
|
|
@@ -663,6 +696,9 @@ export async function createRequestRenderer({
|
|
|
663
696
|
sourceFiles: [...new Set([...inventoriesByEntry.values()].flatMap((entry) => entry.sourceFiles))].sort(),
|
|
664
697
|
};
|
|
665
698
|
const clientBoundaries = inventory.clientBoundaries;
|
|
699
|
+
for (const clientBoundary of clientBoundaries) {
|
|
700
|
+
devKnownClientBoundarySources.add(path.resolve(clientBoundary));
|
|
701
|
+
}
|
|
666
702
|
const toProjectRelative = (filePath) => path.relative(projectRoot, filePath).split(path.sep).join("/");
|
|
667
703
|
const serverAssetImports = {
|
|
668
704
|
styles: inventory.styles.map((filePath) => getClientStaticAssetModule(projectRoot, filePath)),
|
|
@@ -836,11 +872,12 @@ export async function createDeploymentRuntime({
|
|
|
836
872
|
return;
|
|
837
873
|
}
|
|
838
874
|
|
|
839
|
-
await renderer.invalidateDevelopmentState(changedPaths);
|
|
875
|
+
const invalidationResult = await renderer.invalidateDevelopmentState(changedPaths);
|
|
840
876
|
if (typeof effectiveResponseCacheRuntime.store.clear === "function") {
|
|
841
877
|
await effectiveResponseCacheRuntime.store.clear();
|
|
842
878
|
}
|
|
843
879
|
runtimeState.assetManifest = null;
|
|
880
|
+
return invalidationResult;
|
|
844
881
|
},
|
|
845
882
|
async handle(request) {
|
|
846
883
|
const revalidationTasks = this.revalidationTasks ??= new Map();
|
|
@@ -51,6 +51,11 @@ export function createDevelopmentEventReporter() {
|
|
|
51
51
|
`${terminal.cyan("[evolit]")} ${terminal.yellow("△")} Source change detected${event.changedPathCount == null ? "" : ` (${event.changedPathCount} files)`}; invalidated ${event.affectedClientEntryCount} client entries.`,
|
|
52
52
|
);
|
|
53
53
|
break;
|
|
54
|
+
case "live-update":
|
|
55
|
+
console.log(
|
|
56
|
+
`${terminal.cyan("[evolit]")} ${event.strategy === "delta" ? terminal.green("↻") : terminal.magenta("↻")} ${event.strategy === "delta" ? "Pushed incremental route refresh." : "Pushed hot client refresh."}`,
|
|
57
|
+
);
|
|
58
|
+
break;
|
|
54
59
|
case "unmanaged-import":
|
|
55
60
|
console.warn(
|
|
56
61
|
`${terminal.yellow("[evolit]")} ${terminal.yellow("⚠")} Unmanaged import ${terminal.cyan(JSON.stringify(event.resolvedImportPath))} from ${terminal.cyan(JSON.stringify(event.sourcePath))} is outside the project root. Evolit does not watch it; changes may not trigger a rebuild, and it may be unavailable in another checkout or deployment. It will be emitted under ${terminal.cyan("/_evolit/static/__unmanaged__/")}.`,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { adoptStyles } from "lit";
|
|
2
|
+
|
|
3
|
+
const HOT_COMPONENTS = Symbol.for("evolit.hotComponents");
|
|
4
|
+
|
|
5
|
+
function state() {
|
|
6
|
+
return globalThis[HOT_COMPONENTS] ??= new Map();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function refreshInstances(tagName, proxy) {
|
|
10
|
+
const roots = [globalThis.document].filter(Boolean);
|
|
11
|
+
while (roots.length > 0) {
|
|
12
|
+
const root = roots.shift();
|
|
13
|
+
for (const element of root.querySelectorAll?.(tagName) ?? []) {
|
|
14
|
+
if (!(element instanceof proxy)) continue;
|
|
15
|
+
if (
|
|
16
|
+
element.shadowRoot
|
|
17
|
+
&& "adoptedStyleSheets" in element.shadowRoot
|
|
18
|
+
&& Array.isArray(proxy.elementStyles)
|
|
19
|
+
) {
|
|
20
|
+
adoptStyles(element.shadowRoot, proxy.elementStyles);
|
|
21
|
+
}
|
|
22
|
+
element.requestUpdate?.();
|
|
23
|
+
}
|
|
24
|
+
for (const element of root.querySelectorAll?.("*") ?? []) {
|
|
25
|
+
if (element.shadowRoot) roots.push(element.shadowRoot);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function replaceImplementation(record, implementation) {
|
|
31
|
+
const previousObservedAttributes = record.proxy.observedAttributes ?? [];
|
|
32
|
+
record.implementation = implementation;
|
|
33
|
+
Object.setPrototypeOf(record.proxy.prototype, implementation.prototype);
|
|
34
|
+
Object.setPrototypeOf(record.proxy, implementation);
|
|
35
|
+
delete record.proxy.finalized;
|
|
36
|
+
delete record.proxy.elementProperties;
|
|
37
|
+
delete record.proxy.elementStyles;
|
|
38
|
+
record.proxy.finalize?.();
|
|
39
|
+
const nextObservedAttributes = record.proxy.observedAttributes ?? [];
|
|
40
|
+
if (
|
|
41
|
+
previousObservedAttributes.length !== nextObservedAttributes.length
|
|
42
|
+
|| previousObservedAttributes.some((attribute, index) => attribute !== nextObservedAttributes[index])
|
|
43
|
+
) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Cannot hot-update <${record.tagName}> because its observed attributes changed.`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
refreshInstances(record.tagName, record.proxy);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function hotComponent(moduleId, implementation) {
|
|
52
|
+
const tagName = implementation?.[Symbol.for("litsx.hydratableTag")];
|
|
53
|
+
if (typeof tagName !== "string" || tagName.length === 0) return implementation;
|
|
54
|
+
const key = `${moduleId}::${tagName}`;
|
|
55
|
+
const existing = state().get(key);
|
|
56
|
+
if (existing) {
|
|
57
|
+
replaceImplementation(existing, implementation);
|
|
58
|
+
return existing.proxy;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class EvolitHotComponentProxy extends implementation {}
|
|
62
|
+
const record = { tagName, proxy: EvolitHotComponentProxy, implementation };
|
|
63
|
+
state().set(key, record);
|
|
64
|
+
return record.proxy;
|
|
65
|
+
}
|
package/src/navigation-client.js
CHANGED
|
@@ -10,6 +10,7 @@ export { createHref } from "./navigation-url.js";
|
|
|
10
10
|
|
|
11
11
|
let browserNavigation = null;
|
|
12
12
|
let nextHistoryEntryId = 0;
|
|
13
|
+
const DEVELOPMENT_REFRESH_EVENT = "evolit:development-refresh";
|
|
13
14
|
const historySessionId = globalThis.crypto?.randomUUID?.()
|
|
14
15
|
?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
15
16
|
|
|
@@ -174,7 +175,16 @@ function syncDocumentState(nextState, documentRef) {
|
|
|
174
175
|
if (script) script.textContent = JSON.stringify(nextState);
|
|
175
176
|
}
|
|
176
177
|
|
|
177
|
-
|
|
178
|
+
function versionClientImport(specifier, version) {
|
|
179
|
+
if (!version || typeof specifier !== "string") return specifier;
|
|
180
|
+
const url = new URL(specifier, globalThis.location?.href ?? "http://evolit.local/");
|
|
181
|
+
url.searchParams.set("__evolit_hot", String(version));
|
|
182
|
+
return url.origin === globalThis.location?.origin
|
|
183
|
+
? `${url.pathname}${url.search}${url.hash}`
|
|
184
|
+
: url.href;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function applyRouteDelta(delta, documentRef = document, signal, options = {}) {
|
|
178
188
|
throwIfAborted(signal);
|
|
179
189
|
const current = readRoute(documentRef);
|
|
180
190
|
const nextSegments = delta.route?.segments ?? [];
|
|
@@ -185,18 +195,44 @@ async function applyRouteDelta(delta, documentRef = document, signal) {
|
|
|
185
195
|
(segment, offset) => segment.inputKey !== currentSegments[offset]?.inputKey,
|
|
186
196
|
);
|
|
187
197
|
}
|
|
198
|
+
if (index < 0) {
|
|
199
|
+
index = nextSegments.findIndex(
|
|
200
|
+
(segment, offset) => segment.revision !== currentSegments[offset]?.revision,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (index < 0 && options.requireSegmentChange === true) return true;
|
|
188
204
|
if (index < 0) index = Math.max(0, nextSegments.length - 1);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
205
|
+
let next;
|
|
206
|
+
let previous;
|
|
207
|
+
let targets;
|
|
208
|
+
// A leaf rendered as plain text can have no physical segment markers: Lit
|
|
209
|
+
// escapes those markers inside its parent template. Walk outward to the
|
|
210
|
+
// nearest replaceable ancestor rather than treating a zero/zero match as a
|
|
211
|
+
// successful update. This also recovers safely from stale child cardinality.
|
|
212
|
+
for (let candidate = index; candidate >= 0; candidate -= 1) {
|
|
213
|
+
const candidateNext = nextSegments[candidate];
|
|
214
|
+
const candidatePrevious = currentSegments[candidate] ?? currentSegments.at(-1);
|
|
215
|
+
if (!candidateNext || !candidatePrevious) continue;
|
|
216
|
+
const candidateTargets = findMarkers(documentRef, candidatePrevious.id);
|
|
217
|
+
if (
|
|
218
|
+
candidateTargets.length > 0
|
|
219
|
+
&& candidateTargets.length === candidateNext.projections.length
|
|
220
|
+
) {
|
|
221
|
+
next = candidateNext;
|
|
222
|
+
previous = candidatePrevious;
|
|
223
|
+
targets = candidateTargets;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (!next || !previous || !targets) return false;
|
|
198
228
|
await syncRouteHeadAssets(delta.headAssets, documentRef);
|
|
199
229
|
throwIfAborted(signal);
|
|
230
|
+
const clientImports = (delta.hydrationData?.clientImports ?? [])
|
|
231
|
+
.map((specifier) => versionClientImport(specifier, options.moduleVersion));
|
|
232
|
+
await registerHydrationModules(
|
|
233
|
+
await Promise.all(clientImports.map((specifier) => import(specifier))),
|
|
234
|
+
);
|
|
235
|
+
throwIfAborted(signal);
|
|
200
236
|
syncRouteHeadMarkup(delta.head, documentRef);
|
|
201
237
|
syncDocumentState(delta.document, documentRef);
|
|
202
238
|
const insertedRoots = [];
|
|
@@ -232,16 +268,12 @@ async function applyRouteDelta(delta, documentRef = document, signal) {
|
|
|
232
268
|
const script = documentRef.getElementById("__EVOLIT_ROUTE__");
|
|
233
269
|
if (script) script.textContent = JSON.stringify(delta.route);
|
|
234
270
|
if (delta.title) documentRef.title = delta.title;
|
|
235
|
-
throwIfAborted(signal);
|
|
236
|
-
await registerHydrationModules(
|
|
237
|
-
await Promise.all((delta.hydrationData?.clientImports ?? []).map((specifier) => import(specifier))),
|
|
238
|
-
);
|
|
239
271
|
for (const { root, element } of insertedRoots) {
|
|
240
272
|
throwIfAborted(signal);
|
|
241
273
|
await hydrateRoot(element, {
|
|
242
274
|
rootId: root.id,
|
|
243
275
|
hydrationData: delta.hydrationData,
|
|
244
|
-
clientImports
|
|
276
|
+
clientImports,
|
|
245
277
|
});
|
|
246
278
|
}
|
|
247
279
|
return true;
|
|
@@ -354,8 +386,19 @@ export function createBrowserNavigation(options = {}) {
|
|
|
354
386
|
let navigationSequence = 0;
|
|
355
387
|
let state = { status: "idle", url: windowRef.location.href, pendingUrl: null, error: null };
|
|
356
388
|
const emit = () => listeners.forEach((listener) => listener(state));
|
|
389
|
+
const notifyLocationChange = () => {
|
|
390
|
+
const EventConstructor = windowRef.CustomEvent ?? globalThis.CustomEvent;
|
|
391
|
+
if (typeof windowRef.dispatchEvent === "function" && typeof EventConstructor === "function") {
|
|
392
|
+
windowRef.dispatchEvent(new EventConstructor("evolit:navigation", { detail: state }));
|
|
393
|
+
}
|
|
394
|
+
};
|
|
357
395
|
const applyDelta = options.applyDelta
|
|
358
|
-
?? ((delta,
|
|
396
|
+
?? ((delta, context = {}) => applyRouteDelta(
|
|
397
|
+
delta,
|
|
398
|
+
windowRef.document,
|
|
399
|
+
context.signal,
|
|
400
|
+
context,
|
|
401
|
+
));
|
|
359
402
|
|
|
360
403
|
function navigateDocument(href, mode) {
|
|
361
404
|
// This is deliberately a fallback, not the normal navigation path. It
|
|
@@ -482,7 +525,11 @@ export function createBrowserNavigation(options = {}) {
|
|
|
482
525
|
})();
|
|
483
526
|
if (!delta || !isCurrent()) return null;
|
|
484
527
|
if (delta.type === "redirect") return navigate(delta.location, "replace", false);
|
|
485
|
-
const applied = await applyDelta(delta, {
|
|
528
|
+
const applied = await applyDelta(delta, {
|
|
529
|
+
signal: navigationController.signal,
|
|
530
|
+
moduleVersion: options.moduleVersion,
|
|
531
|
+
hot: options.hot,
|
|
532
|
+
});
|
|
486
533
|
if (applied === false) {
|
|
487
534
|
navigateDocument(href, mode);
|
|
488
535
|
return null;
|
|
@@ -509,6 +556,7 @@ export function createBrowserNavigation(options = {}) {
|
|
|
509
556
|
);
|
|
510
557
|
state = { status: "idle", url: canonicalHref, pendingUrl: null, error: null };
|
|
511
558
|
emit();
|
|
559
|
+
notifyLocationChange();
|
|
512
560
|
return delta;
|
|
513
561
|
} catch (error) {
|
|
514
562
|
if (error?.name === "AbortError" || !isCurrent()) return null;
|
|
@@ -524,6 +572,37 @@ export function createBrowserNavigation(options = {}) {
|
|
|
524
572
|
scrollPosition: event.state?.__evolitScroll ?? null,
|
|
525
573
|
});
|
|
526
574
|
});
|
|
575
|
+
windowRef.addEventListener(DEVELOPMENT_REFRESH_EVENT, (event) => {
|
|
576
|
+
const detail = event.detail;
|
|
577
|
+
if (!detail || typeof detail !== "object") return;
|
|
578
|
+
detail.handled = true;
|
|
579
|
+
if (detail.update?.type !== "update" || detail.update?.strategy === "reload") {
|
|
580
|
+
detail.result = Promise.resolve(false);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
controller?.abort();
|
|
584
|
+
const refreshController = new AbortController();
|
|
585
|
+
controller = refreshController;
|
|
586
|
+
state = { ...state, status: "pending", pendingUrl: windowRef.location.href, error: null };
|
|
587
|
+
emit();
|
|
588
|
+
detail.result = applyDelta(detail.update.delta, {
|
|
589
|
+
signal: refreshController.signal,
|
|
590
|
+
hot: detail.update.strategy === "hot",
|
|
591
|
+
moduleVersion: detail.update.version,
|
|
592
|
+
requireSegmentChange: true,
|
|
593
|
+
}).then((applied) => {
|
|
594
|
+
if (applied === false || refreshController.signal.aborted) return false;
|
|
595
|
+
state = { status: "idle", url: windowRef.location.href, pendingUrl: null, error: null };
|
|
596
|
+
emit();
|
|
597
|
+
return true;
|
|
598
|
+
}).catch((error) => {
|
|
599
|
+
if (error?.name !== "AbortError") {
|
|
600
|
+
state = { ...state, status: "error", pendingUrl: null, error };
|
|
601
|
+
emit();
|
|
602
|
+
}
|
|
603
|
+
throw error;
|
|
604
|
+
});
|
|
605
|
+
});
|
|
527
606
|
windowRef.document.addEventListener("click", (event) => {
|
|
528
607
|
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
529
608
|
const anchor = event.target?.closest?.("a[href]");
|
package/src/server.js
CHANGED
|
@@ -65,7 +65,30 @@ function injectLiveReloadSnippet(response) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
function createLiveReloadSnippet() {
|
|
68
|
-
return `<script data-evolit-live-reload>const
|
|
68
|
+
return `<script data-evolit-live-reload>(()=>{const protocol=location.protocol==="https:"?"wss:":"ws:";let socket;let connected=false;const subscribe=()=>{if(socket?.readyState===WebSocket.OPEN)socket.send(JSON.stringify({type:"subscribe",url:location.href}))};const connect=()=>{socket=new WebSocket(protocol+"//"+location.host+${JSON.stringify(LIVE_RELOAD_PATHNAME)});socket.addEventListener("open",()=>{if(connected){location.reload();return}connected=true;subscribe()});socket.addEventListener("message",({data})=>{let update;try{update=JSON.parse(data)}catch{location.reload();return}if(update?.type==="reload"){location.reload();return}if(update?.type!=="update"||update.url!==location.href){return}const detail={update,handled:false,result:null};dispatchEvent(new CustomEvent("evolit:development-refresh",{detail}));if(!detail.handled){location.reload();return}Promise.resolve(detail.result).then((applied)=>{if(!applied)location.reload()}).catch(()=>location.reload())});socket.addEventListener("close",()=>setTimeout(connect,250))};addEventListener("popstate",subscribe);addEventListener("evolit:navigation",subscribe);connect()})();</script>`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createDevelopmentNavigationRequest(subscription, origin) {
|
|
72
|
+
const headers = new Headers(subscription.headers);
|
|
73
|
+
headers.set("accept", "application/vnd.evolit.navigation+json");
|
|
74
|
+
for (const name of ["connection", "upgrade", "sec-websocket-key", "sec-websocket-version", "sec-websocket-extensions"]) {
|
|
75
|
+
headers.delete(name);
|
|
76
|
+
}
|
|
77
|
+
return new Request(new URL(subscription.url, origin), { headers });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function annotateDevelopmentSegmentRevisions(delta, revisions) {
|
|
81
|
+
if (delta?.type !== "route" || !Array.isArray(delta.route?.segments)) return delta;
|
|
82
|
+
return {
|
|
83
|
+
...delta,
|
|
84
|
+
route: {
|
|
85
|
+
...delta.route,
|
|
86
|
+
segments: delta.route.segments.map((segment) => {
|
|
87
|
+
const revision = revisions.get(segment.modulePath);
|
|
88
|
+
return revision == null ? segment : { ...segment, revision };
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
69
92
|
}
|
|
70
93
|
|
|
71
94
|
async function createRecursiveDirectoryWatcher(rootDirectory, onChange, options = {}) {
|
|
@@ -194,9 +217,22 @@ async function createServer(projectRoot, mode, explicitPort, options = {}) {
|
|
|
194
217
|
});
|
|
195
218
|
const port = getPort(explicitPort);
|
|
196
219
|
const liveReloadServer = mode === "development" ? new WebSocketServer({ noServer: true }) : null;
|
|
197
|
-
const liveReloadSockets = new
|
|
198
|
-
liveReloadServer?.on("connection", (socket) => {
|
|
199
|
-
|
|
220
|
+
const liveReloadSockets = new Map();
|
|
221
|
+
liveReloadServer?.on("connection", (socket, request) => {
|
|
222
|
+
const subscription = { url: null, headers: request.headers };
|
|
223
|
+
liveReloadSockets.set(socket, subscription);
|
|
224
|
+
socket.on("message", (message) => {
|
|
225
|
+
try {
|
|
226
|
+
const payload = JSON.parse(String(message));
|
|
227
|
+
if (payload?.type !== "subscribe" || typeof payload.url !== "string") return;
|
|
228
|
+
const origin = `http://${request.headers.host ?? `localhost:${port}`}`;
|
|
229
|
+
const url = new URL(payload.url, origin);
|
|
230
|
+
if (url.origin !== new URL(origin).origin) return;
|
|
231
|
+
subscription.url = url.href;
|
|
232
|
+
} catch {
|
|
233
|
+
// Ignore malformed development protocol messages.
|
|
234
|
+
}
|
|
235
|
+
});
|
|
200
236
|
socket.on("close", () => {
|
|
201
237
|
liveReloadSockets.delete(socket);
|
|
202
238
|
});
|
|
@@ -206,6 +242,63 @@ async function createServer(projectRoot, mode, explicitPort, options = {}) {
|
|
|
206
242
|
let resolvePendingInvalidation = null;
|
|
207
243
|
let invalidationPromise = Promise.resolve();
|
|
208
244
|
const pendingChangedPaths = new Set();
|
|
245
|
+
let developmentVersion = 0;
|
|
246
|
+
const segmentRevisions = new Map();
|
|
247
|
+
|
|
248
|
+
async function pushDevelopmentUpdates(changedPaths, invalidationResult) {
|
|
249
|
+
developmentVersion += 1;
|
|
250
|
+
for (const modulePath of invalidationResult?.affectedRouteEntryPaths ?? []) {
|
|
251
|
+
segmentRevisions.set(modulePath, developmentVersion);
|
|
252
|
+
}
|
|
253
|
+
const strategy = (invalidationResult?.affectedClientEntryCount ?? 0) > 0 ? "hot" : "delta";
|
|
254
|
+
const renders = new Map();
|
|
255
|
+
await Promise.all([...liveReloadSockets].map(async ([socket, subscription]) => {
|
|
256
|
+
if (socket.readyState !== WebSocket.OPEN || !subscription.url) return;
|
|
257
|
+
const cacheKey = JSON.stringify([
|
|
258
|
+
subscription.url,
|
|
259
|
+
subscription.headers.cookie ?? "",
|
|
260
|
+
subscription.headers.authorization ?? "",
|
|
261
|
+
subscription.headers["accept-language"] ?? "",
|
|
262
|
+
]);
|
|
263
|
+
let pending = renders.get(cacheKey);
|
|
264
|
+
if (!pending) {
|
|
265
|
+
pending = (async () => {
|
|
266
|
+
const origin = `http://${subscription.headers.host ?? `localhost:${port}`}`;
|
|
267
|
+
const response = await deploymentRuntime.handle(
|
|
268
|
+
createDevelopmentNavigationRequest(subscription, origin),
|
|
269
|
+
);
|
|
270
|
+
const navigationResponse = createNavigationResponseFromDocument(response);
|
|
271
|
+
if (!getResponseHeader(navigationResponse.headers, "content-type")
|
|
272
|
+
?.includes("application/vnd.evolit.navigation+json")) return null;
|
|
273
|
+
const delta = JSON.parse(navigationResponse.body);
|
|
274
|
+
return annotateDevelopmentSegmentRevisions(delta, segmentRevisions);
|
|
275
|
+
})();
|
|
276
|
+
renders.set(cacheKey, pending);
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const delta = await pending;
|
|
280
|
+
if (!delta) {
|
|
281
|
+
socket.send(JSON.stringify({ type: "reload", version: developmentVersion }));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
socket.send(JSON.stringify({
|
|
285
|
+
type: "update",
|
|
286
|
+
strategy,
|
|
287
|
+
version: developmentVersion,
|
|
288
|
+
url: subscription.url,
|
|
289
|
+
delta,
|
|
290
|
+
}));
|
|
291
|
+
} catch {
|
|
292
|
+
socket.send(JSON.stringify({ type: "reload", version: developmentVersion }));
|
|
293
|
+
}
|
|
294
|
+
}));
|
|
295
|
+
reportDevelopmentEvent?.({
|
|
296
|
+
type: "live-update",
|
|
297
|
+
strategy,
|
|
298
|
+
affectedClientEntryCount: invalidationResult?.affectedClientEntryCount ?? null,
|
|
299
|
+
subscriberCount: liveReloadSockets.size,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
209
302
|
|
|
210
303
|
function scheduleDevelopmentInvalidation(changedPath = null) {
|
|
211
304
|
if (changedPath) {
|
|
@@ -232,18 +325,12 @@ async function createServer(projectRoot, mode, explicitPort, options = {}) {
|
|
|
232
325
|
resolvePendingInvalidation = null;
|
|
233
326
|
invalidationPromise = invalidationPromise
|
|
234
327
|
.catch(() => {})
|
|
235
|
-
.then(() =>
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
resolve();
|
|
244
|
-
},
|
|
245
|
-
resolve,
|
|
246
|
-
);
|
|
328
|
+
.then(async () => {
|
|
329
|
+
const invalidationResult = await deploymentRuntime.invalidateDevelopmentState(changedPaths);
|
|
330
|
+
await pushDevelopmentUpdates(changedPaths, invalidationResult);
|
|
331
|
+
return invalidationResult;
|
|
332
|
+
});
|
|
333
|
+
invalidationPromise.then(resolve, resolve);
|
|
247
334
|
}, 40);
|
|
248
335
|
}
|
|
249
336
|
|
|
@@ -329,7 +416,7 @@ async function createServer(projectRoot, mode, explicitPort, options = {}) {
|
|
|
329
416
|
clearTimeout(invalidationTimer);
|
|
330
417
|
}
|
|
331
418
|
for (const watcher of watchers) watcher.close();
|
|
332
|
-
for (const socket of liveReloadSockets) {
|
|
419
|
+
for (const socket of liveReloadSockets.keys()) {
|
|
333
420
|
socket.close();
|
|
334
421
|
}
|
|
335
422
|
if (liveReloadServer) {
|