evolit 0.1.0-alpha.1 → 0.1.0-alpha.11
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 +3 -2
- package/package.json +6 -5
- package/src/build.js +6 -2
- package/src/cli.js +3 -2
- package/src/client-assets.js +36 -6
- package/src/compiler.js +10 -2
- package/src/deployment-runtime.js +6 -2
- package/src/scaffold.js +4 -0
- package/src/server.js +78 -4
- package/templates/default/yarnrc.yml.template +1 -0
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ Supported authored module extensions:
|
|
|
38
38
|
|
|
39
39
|
```sh
|
|
40
40
|
yarn install
|
|
41
|
-
npx evolit@alpha my-site
|
|
41
|
+
npx -p evolit@alpha evolit my-site
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Additional commands:
|
|
@@ -61,7 +61,8 @@ yarn start
|
|
|
61
61
|
The framework ships a starter at `templates/default`.
|
|
62
62
|
|
|
63
63
|
`evolit <directory>` copies that template, writes a site `package.json`, and leaves the app ready
|
|
64
|
-
to run. `evolit init <directory>` is supported as an explicit equivalent.
|
|
64
|
+
to run. `evolit init <directory>` is supported as an explicit equivalent. The `npx -p … evolit`
|
|
65
|
+
form keeps the CLI arguments unambiguous across npm versions.
|
|
65
66
|
|
|
66
67
|
## Architecture
|
|
67
68
|
|
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.11",
|
|
4
4
|
"description": "A convention-driven application framework for LitSX and web components.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "yarn@4.10.3",
|
|
@@ -47,15 +47,16 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@jridgewell/remapping": "^2.3.5",
|
|
50
|
-
"@litsx/compiler": "0.9.5-canary-feat-ssr-
|
|
51
|
-
"@litsx/core": "0.17.0-canary-feat-ssr-
|
|
52
|
-
"@litsx/ssr": "0.2.0-canary-feat-ssr-
|
|
50
|
+
"@litsx/compiler": "0.9.5-canary-feat-ssr-20260726184312",
|
|
51
|
+
"@litsx/core": "0.17.0-canary-feat-ssr-20260726184312",
|
|
52
|
+
"@litsx/ssr": "0.2.0-canary-feat-ssr-20260726184312",
|
|
53
53
|
"@litsx/typescript": "^0.9.0",
|
|
54
54
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
55
55
|
"lit": "^3.3.3",
|
|
56
56
|
"magic-string": "^1.1.0",
|
|
57
57
|
"rollup": "^4.46.1",
|
|
58
|
-
"typescript": "^6.0.0"
|
|
58
|
+
"typescript": "^6.0.0",
|
|
59
|
+
"ws": "^8.18.3"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"vite": "^8.1.5"
|
package/src/build.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
emitBundledClientAssets,
|
|
16
16
|
emitHashedClientAssets,
|
|
17
17
|
normalizeHydrationDataForClient,
|
|
18
|
+
resolveRouteClientImports,
|
|
18
19
|
resolveSharedVendorModuleUrl,
|
|
19
20
|
rewriteHydrationDataScript,
|
|
20
21
|
rewriteServerAssetPlaceholders,
|
|
@@ -241,8 +242,11 @@ export async function buildProject(projectRoot) {
|
|
|
241
242
|
);
|
|
242
243
|
const ssrAdapter = createSsrAdapter({
|
|
243
244
|
assetResolver,
|
|
244
|
-
async resolveAdditionalHead({ result }) {
|
|
245
|
-
const clientImports =
|
|
245
|
+
async resolveAdditionalHead({ routeResult, result }) {
|
|
246
|
+
const clientImports = [
|
|
247
|
+
...(Array.isArray(result.clientImports) ? result.clientImports : []),
|
|
248
|
+
...resolveRouteClientImports(routeResult, projectRoot, clientAssets),
|
|
249
|
+
];
|
|
246
250
|
const urls = collectTransitiveAssetPreloads(clientImports, clientAssets);
|
|
247
251
|
const styleUrls = collectTransitiveStyleUrls(clientImports, clientAssets);
|
|
248
252
|
|
package/src/cli.js
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
|
-
import { buildProject } from "./build.js";
|
|
6
5
|
import { scaffoldSite } from "./scaffold.js";
|
|
7
|
-
import { createDevServer, createStartServer } from "./server.js";
|
|
8
6
|
|
|
9
7
|
function parseArguments(argv) {
|
|
10
8
|
const [command, ...rest] = argv;
|
|
@@ -55,12 +53,14 @@ async function run() {
|
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
if (command === "build") {
|
|
56
|
+
const { buildProject } = await import("./build.js");
|
|
58
57
|
const manifestPath = await buildProject(projectRoot);
|
|
59
58
|
console.log(`Built evolit app: ${path.relative(projectRoot, manifestPath)}`);
|
|
60
59
|
return;
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
if (command === "start") {
|
|
63
|
+
const { createStartServer } = await import("./server.js");
|
|
64
64
|
const server = await createStartServer(projectRoot, options);
|
|
65
65
|
await server.listen();
|
|
66
66
|
console.log(`evolit start listening on http://localhost:${server.port}`);
|
|
@@ -68,6 +68,7 @@ async function run() {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
if (command === "dev") {
|
|
71
|
+
const { createDevServer } = await import("./server.js");
|
|
71
72
|
const server = await createDevServer(projectRoot, options);
|
|
72
73
|
await server.listen();
|
|
73
74
|
console.log(`evolit dev listening on http://localhost:${server.port}`);
|
package/src/client-assets.js
CHANGED
|
@@ -57,6 +57,18 @@ function isRelativeSpecifier(specifier) {
|
|
|
57
57
|
);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function stripImportSuffix(specifier) {
|
|
61
|
+
return specifier.split("?")[0].split("#")[0];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveRelativeClientImportPath(fromClientModule, specifier) {
|
|
65
|
+
const normalizedSpecifier = stripImportSuffix(specifier);
|
|
66
|
+
return path
|
|
67
|
+
.normalize(path.join(path.dirname(fromClientModule), normalizedSpecifier))
|
|
68
|
+
.split(path.sep)
|
|
69
|
+
.join("/");
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
function parsePackageSpecifier(specifier) {
|
|
61
73
|
if (!isBareSpecifier(specifier)) {
|
|
62
74
|
return null;
|
|
@@ -1191,9 +1203,7 @@ function collectRelativeClientImportPaths(source, fileRelativePath, publicPathBy
|
|
|
1191
1203
|
continue;
|
|
1192
1204
|
}
|
|
1193
1205
|
|
|
1194
|
-
const resolvedRelativePath =
|
|
1195
|
-
path.join(path.dirname(fileRelativePath), specifier),
|
|
1196
|
-
);
|
|
1206
|
+
const resolvedRelativePath = resolveRelativeClientImportPath(fileRelativePath, specifier);
|
|
1197
1207
|
if (!publicPathByRelativePath.has(resolvedRelativePath)) {
|
|
1198
1208
|
continue;
|
|
1199
1209
|
}
|
|
@@ -1217,9 +1227,7 @@ function rewriteRelativeClientImports(source, fileRelativePath, publicPathByRela
|
|
|
1217
1227
|
continue;
|
|
1218
1228
|
}
|
|
1219
1229
|
|
|
1220
|
-
const resolvedRelativePath =
|
|
1221
|
-
path.join(path.dirname(fileRelativePath), specifier),
|
|
1222
|
-
);
|
|
1230
|
+
const resolvedRelativePath = resolveRelativeClientImportPath(fileRelativePath, specifier);
|
|
1223
1231
|
const rewrittenTarget = publicPathByRelativePath.get(resolvedRelativePath);
|
|
1224
1232
|
if (!rewrittenTarget) {
|
|
1225
1233
|
continue;
|
|
@@ -1776,6 +1784,28 @@ export function collectTransitiveAssetPreloads(publicUrls, assetManifest) {
|
|
|
1776
1784
|
return discovered;
|
|
1777
1785
|
}
|
|
1778
1786
|
|
|
1787
|
+
export function resolveRouteClientImports(routeResult, projectRoot, assetManifest) {
|
|
1788
|
+
const normalizedManifest = normalizeClientAssetManifest(assetManifest);
|
|
1789
|
+
if (!normalizedManifest) {
|
|
1790
|
+
return [];
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
const routeModules = [
|
|
1794
|
+
routeResult?.route?.page,
|
|
1795
|
+
...(routeResult?.route?.layouts ?? []),
|
|
1796
|
+
routeResult?.boundaryModule,
|
|
1797
|
+
].filter((modulePath) => typeof modulePath === "string");
|
|
1798
|
+
|
|
1799
|
+
return [...new Set(routeModules.map((modulePath) => {
|
|
1800
|
+
const relativePath = path.relative(projectRoot, modulePath).split(path.sep).join("/");
|
|
1801
|
+
const extension = path.extname(relativePath);
|
|
1802
|
+
const clientModule = extension
|
|
1803
|
+
? `${relativePath.slice(0, -extension.length)}.mjs`
|
|
1804
|
+
: `${relativePath}.mjs`;
|
|
1805
|
+
return normalizedManifest.byClientModule[clientModule] ?? null;
|
|
1806
|
+
}).filter((publicUrl) => typeof publicUrl === "string"))];
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1779
1809
|
export function collectTransitiveStyleUrls(publicUrls, assetManifest) {
|
|
1780
1810
|
const normalizedManifest = normalizeClientAssetManifest(assetManifest);
|
|
1781
1811
|
if (!normalizedManifest) {
|
package/src/compiler.js
CHANGED
|
@@ -17,6 +17,10 @@ import { ensureDirectory } from "./fs-utils.js";
|
|
|
17
17
|
|
|
18
18
|
const MODULE_SPECIFIER_PATTERN =
|
|
19
19
|
/\b(?:import|export)\s+(?:[^"']*?\s+from\s+)?["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
20
|
+
const RESOLVABLE_IMPORT_EXTENSIONS = [
|
|
21
|
+
...MODULE_EXTENSIONS,
|
|
22
|
+
...STATIC_ASSET_EXTENSIONS,
|
|
23
|
+
];
|
|
20
24
|
|
|
21
25
|
function isRelativeSpecifier(specifier) {
|
|
22
26
|
return specifier.startsWith("./") || specifier.startsWith("../");
|
|
@@ -44,6 +48,10 @@ function isStaticAssetPath(filePath) {
|
|
|
44
48
|
return STATIC_ASSET_EXTENSIONS.some((extension) => filePath.endsWith(extension));
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
function hasResolvableImportExtension(filePath) {
|
|
52
|
+
return RESOLVABLE_IMPORT_EXTENSIONS.some((extension) => filePath.endsWith(extension));
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
function isStyleAssetPath(filePath) {
|
|
48
56
|
return filePath.endsWith(".css");
|
|
49
57
|
}
|
|
@@ -65,8 +73,8 @@ async function resolveImportPath(importerPath, specifier) {
|
|
|
65
73
|
const basePath = path.resolve(path.dirname(importerPath), specifier);
|
|
66
74
|
const candidates = [basePath];
|
|
67
75
|
|
|
68
|
-
if (!
|
|
69
|
-
for (const extension of
|
|
76
|
+
if (!hasResolvableImportExtension(basePath)) {
|
|
77
|
+
for (const extension of RESOLVABLE_IMPORT_EXTENSIONS) {
|
|
70
78
|
candidates.push(`${basePath}${extension}`);
|
|
71
79
|
candidates.push(path.join(basePath, `index${extension}`));
|
|
72
80
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getSharedOutputRoot,
|
|
13
13
|
normalizeHydrationDataForClient,
|
|
14
14
|
normalizeClientAssetManifest,
|
|
15
|
+
resolveRouteClientImports,
|
|
15
16
|
resolveSharedVendorModuleUrl,
|
|
16
17
|
resolveBrowserPackageAssetFilePath,
|
|
17
18
|
rewriteHydrationDataScript,
|
|
@@ -233,8 +234,11 @@ export async function createRequestRenderer({ projectRoot, mode, assetManifest,
|
|
|
233
234
|
assetResolver(moduleId) {
|
|
234
235
|
return currentAssetResolver(moduleId);
|
|
235
236
|
},
|
|
236
|
-
async resolveAdditionalHead({ result }) {
|
|
237
|
-
const clientImports =
|
|
237
|
+
async resolveAdditionalHead({ routeResult, result }) {
|
|
238
|
+
const clientImports = [
|
|
239
|
+
...(Array.isArray(result.clientImports) ? result.clientImports : []),
|
|
240
|
+
...resolveRouteClientImports(routeResult, projectRoot, currentAssetManifest),
|
|
241
|
+
];
|
|
238
242
|
const urls = currentAssetManifest
|
|
239
243
|
? collectTransitiveAssetPreloads(clientImports, currentAssetManifest)
|
|
240
244
|
: [];
|
package/src/scaffold.js
CHANGED
|
@@ -49,6 +49,10 @@ export async function scaffoldSite(targetDirectory, options = {}) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
await copyDirectory(TEMPLATE_ROOT, absoluteTargetDirectory);
|
|
52
|
+
await fs.rename(
|
|
53
|
+
path.join(absoluteTargetDirectory, "yarnrc.yml.template"),
|
|
54
|
+
path.join(absoluteTargetDirectory, ".yarnrc.yml"),
|
|
55
|
+
);
|
|
52
56
|
await writeSitePackageJson(absoluteTargetDirectory, siteName);
|
|
53
57
|
await fs.writeFile(
|
|
54
58
|
path.join(absoluteTargetDirectory, "evolit.config.js"),
|
package/src/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import http from "node:http";
|
|
|
2
2
|
import { watch } from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
5
6
|
import { loadEvolitConfig } from "./config.js";
|
|
6
7
|
import { createDeploymentRuntime } from "./deployment-runtime.js";
|
|
7
8
|
import { APP_DIRECTORY, BUILD_DIRECTORY, INTERNAL_DIRECTORY, MANIFEST_FILENAME } from "./constants.js";
|
|
@@ -35,6 +36,36 @@ async function writeResponseBody(response, body) {
|
|
|
35
36
|
response.end();
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
const LIVE_RELOAD_PATHNAME = "/_evolit/live-reload";
|
|
40
|
+
|
|
41
|
+
function getResponseHeader(headers, name) {
|
|
42
|
+
if (headers?.get) {
|
|
43
|
+
return headers.get(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return headers?.[name] ?? headers?.[name.toLowerCase()] ?? null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function injectLiveReloadSnippet(response) {
|
|
50
|
+
if (
|
|
51
|
+
typeof response.body !== "string"
|
|
52
|
+
|| !getResponseHeader(response.headers, "content-type")?.includes("text/html")
|
|
53
|
+
) {
|
|
54
|
+
return response;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
...response,
|
|
59
|
+
body: response.body.includes("</body>")
|
|
60
|
+
? response.body.replace("</body>", `${createLiveReloadSnippet()}</body>`)
|
|
61
|
+
: `${response.body}${createLiveReloadSnippet()}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createLiveReloadSnippet() {
|
|
66
|
+
return `<script data-evolit-live-reload>const evolitProtocol=window.location.protocol==="https:"?"wss:":"ws:";const evolitSocket=new WebSocket(evolitProtocol+"//"+window.location.host+${JSON.stringify(LIVE_RELOAD_PATHNAME)});evolitSocket.addEventListener("message",({data})=>{if(data==="reload")window.location.reload()});</script>`;
|
|
67
|
+
}
|
|
68
|
+
|
|
38
69
|
async function createRecursiveDirectoryWatcher(rootDirectory, onChange) {
|
|
39
70
|
const watchers = new Map();
|
|
40
71
|
let closed = false;
|
|
@@ -122,6 +153,14 @@ async function createServer(projectRoot, mode, explicitPort, options = {}) {
|
|
|
122
153
|
responseCacheRuntime,
|
|
123
154
|
});
|
|
124
155
|
const port = getPort(explicitPort);
|
|
156
|
+
const liveReloadServer = mode === "development" ? new WebSocketServer({ noServer: true }) : null;
|
|
157
|
+
const liveReloadSockets = new Set();
|
|
158
|
+
liveReloadServer?.on("connection", (socket) => {
|
|
159
|
+
liveReloadSockets.add(socket);
|
|
160
|
+
socket.on("close", () => {
|
|
161
|
+
liveReloadSockets.delete(socket);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
125
164
|
let invalidationTimer = null;
|
|
126
165
|
let pendingInvalidation = null;
|
|
127
166
|
let resolvePendingInvalidation = null;
|
|
@@ -146,7 +185,17 @@ async function createServer(projectRoot, mode, explicitPort, options = {}) {
|
|
|
146
185
|
invalidationPromise = invalidationPromise
|
|
147
186
|
.catch(() => {})
|
|
148
187
|
.then(() => deploymentRuntime.invalidateDevelopmentState());
|
|
149
|
-
invalidationPromise.then(
|
|
188
|
+
invalidationPromise.then(
|
|
189
|
+
() => {
|
|
190
|
+
for (const socket of liveReloadSockets) {
|
|
191
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
192
|
+
socket.send("reload");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
resolve();
|
|
196
|
+
},
|
|
197
|
+
resolve,
|
|
198
|
+
);
|
|
150
199
|
}, 40);
|
|
151
200
|
}
|
|
152
201
|
|
|
@@ -159,9 +208,10 @@ async function createServer(projectRoot, mode, explicitPort, options = {}) {
|
|
|
159
208
|
|
|
160
209
|
const server = http.createServer(async (req, res) => {
|
|
161
210
|
try {
|
|
211
|
+
const origin = `http://${req.headers.host ?? `localhost:${port}`}`;
|
|
212
|
+
const pathname = new URL(req.url ?? "/", origin).pathname;
|
|
162
213
|
await pendingInvalidation;
|
|
163
214
|
await invalidationPromise;
|
|
164
|
-
const origin = `http://${req.headers.host ?? `localhost:${port}`}`;
|
|
165
215
|
const method = req.method ?? "GET";
|
|
166
216
|
const request = new Request(new URL(req.url ?? "/", origin), {
|
|
167
217
|
method,
|
|
@@ -169,15 +219,31 @@ async function createServer(projectRoot, mode, explicitPort, options = {}) {
|
|
|
169
219
|
...(method === "GET" || method === "HEAD" ? {} : { body: req, duplex: "half" }),
|
|
170
220
|
});
|
|
171
221
|
const response = await deploymentRuntime.handle(request);
|
|
222
|
+
const servedResponse = mode === "development"
|
|
223
|
+
? injectLiveReloadSnippet(response)
|
|
224
|
+
: response;
|
|
172
225
|
|
|
173
|
-
res.writeHead(
|
|
174
|
-
await writeResponseBody(res,
|
|
226
|
+
res.writeHead(servedResponse.status, servedResponse.headers);
|
|
227
|
+
await writeResponseBody(res, servedResponse.body);
|
|
175
228
|
} catch (error) {
|
|
176
229
|
res.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
|
|
177
230
|
res.end(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
178
231
|
}
|
|
179
232
|
});
|
|
180
233
|
|
|
234
|
+
server.on("upgrade", (request, socket, head) => {
|
|
235
|
+
const origin = `http://${request.headers.host ?? `localhost:${port}`}`;
|
|
236
|
+
const pathname = new URL(request.url ?? "/", origin).pathname;
|
|
237
|
+
if (!liveReloadServer || pathname !== LIVE_RELOAD_PATHNAME) {
|
|
238
|
+
socket.destroy();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
liveReloadServer.handleUpgrade(request, socket, head, (webSocket) => {
|
|
243
|
+
liveReloadServer.emit("connection", webSocket, request);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
181
247
|
return {
|
|
182
248
|
port,
|
|
183
249
|
deploymentRuntime,
|
|
@@ -192,6 +258,14 @@ async function createServer(projectRoot, mode, explicitPort, options = {}) {
|
|
|
192
258
|
clearTimeout(invalidationTimer);
|
|
193
259
|
}
|
|
194
260
|
watcher?.close();
|
|
261
|
+
for (const socket of liveReloadSockets) {
|
|
262
|
+
socket.close();
|
|
263
|
+
}
|
|
264
|
+
if (liveReloadServer) {
|
|
265
|
+
await new Promise((resolve) => {
|
|
266
|
+
liveReloadServer.close(resolve);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
195
269
|
await new Promise((resolve, reject) => {
|
|
196
270
|
server.close((error) => {
|
|
197
271
|
if (error) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nodeLinker: node-modules
|