evolit 0.1.0-alpha.0 → 0.1.0-alpha.10
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 +9 -8
- package/src/build.js +6 -2
- package/src/cli.js +3 -2
- package/src/client-assets.js +22 -0
- 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.10",
|
|
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,17 +47,18 @@
|
|
|
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
|
-
"lit": "^3.3.3",
|
|
55
|
-
"magic-string": "^1.1.0"
|
|
56
|
-
},
|
|
57
|
-
"devDependencies": {
|
|
58
54
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
55
|
+
"lit": "^3.3.3",
|
|
56
|
+
"magic-string": "^1.1.0",
|
|
59
57
|
"rollup": "^4.46.1",
|
|
60
58
|
"typescript": "^6.0.0",
|
|
59
|
+
"ws": "^8.18.3"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
61
62
|
"vite": "^8.1.5"
|
|
62
63
|
}
|
|
63
64
|
}
|
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
|
@@ -1776,6 +1776,28 @@ export function collectTransitiveAssetPreloads(publicUrls, assetManifest) {
|
|
|
1776
1776
|
return discovered;
|
|
1777
1777
|
}
|
|
1778
1778
|
|
|
1779
|
+
export function resolveRouteClientImports(routeResult, projectRoot, assetManifest) {
|
|
1780
|
+
const normalizedManifest = normalizeClientAssetManifest(assetManifest);
|
|
1781
|
+
if (!normalizedManifest) {
|
|
1782
|
+
return [];
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
const routeModules = [
|
|
1786
|
+
routeResult?.route?.page,
|
|
1787
|
+
...(routeResult?.route?.layouts ?? []),
|
|
1788
|
+
routeResult?.boundaryModule,
|
|
1789
|
+
].filter((modulePath) => typeof modulePath === "string");
|
|
1790
|
+
|
|
1791
|
+
return [...new Set(routeModules.map((modulePath) => {
|
|
1792
|
+
const relativePath = path.relative(projectRoot, modulePath).split(path.sep).join("/");
|
|
1793
|
+
const extension = path.extname(relativePath);
|
|
1794
|
+
const clientModule = extension
|
|
1795
|
+
? `${relativePath.slice(0, -extension.length)}.mjs`
|
|
1796
|
+
: `${relativePath}.mjs`;
|
|
1797
|
+
return normalizedManifest.byClientModule[clientModule] ?? null;
|
|
1798
|
+
}).filter((publicUrl) => typeof publicUrl === "string"))];
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1779
1801
|
export function collectTransitiveStyleUrls(publicUrls, assetManifest) {
|
|
1780
1802
|
const normalizedManifest = normalizeClientAssetManifest(assetManifest);
|
|
1781
1803
|
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
|