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 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.1",
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-20260726130435",
51
- "@litsx/core": "0.17.0-canary-feat-ssr-20260726130435",
52
- "@litsx/ssr": "0.2.0-canary-feat-ssr-20260726130435",
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 = Array.isArray(result.clientImports) ? result.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}`);
@@ -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 = path.normalize(
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 = path.normalize(
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 (!path.extname(basePath)) {
69
- for (const extension of [...MODULE_EXTENSIONS, ...STATIC_ASSET_EXTENSIONS]) {
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 = Array.isArray(result.clientImports) ? result.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(resolve, resolve);
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(response.status, response.headers);
174
- await writeResponseBody(res, response.body);
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