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 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.0",
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-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
- "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 = 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}`);
@@ -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 (!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