evolit 0.1.0-alpha.0

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/src/server.js ADDED
@@ -0,0 +1,230 @@
1
+ import http from "node:http";
2
+ import { watch } from "node:fs";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { loadEvolitConfig } from "./config.js";
6
+ import { createDeploymentRuntime } from "./deployment-runtime.js";
7
+ import { APP_DIRECTORY, BUILD_DIRECTORY, INTERNAL_DIRECTORY, MANIFEST_FILENAME } from "./constants.js";
8
+ import { normalizeClientAssetManifest } from "./client-assets.js";
9
+ import { pathExists, readJson } from "./fs-utils.js";
10
+ import { createDefaultRouteCacheKey, resolveResponseCacheRuntime } from "./response-cache.js";
11
+
12
+ function getPort(explicitPort) {
13
+ const port = explicitPort ?? process.env.PORT ?? "3000";
14
+ return Number.parseInt(String(port), 10);
15
+ }
16
+
17
+ async function writeResponseBody(response, body) {
18
+ if (!body || typeof body.getReader !== "function") {
19
+ response.end(body);
20
+ return;
21
+ }
22
+
23
+ const reader = body.getReader();
24
+ try {
25
+ while (true) {
26
+ const { done, value } = await reader.read();
27
+ if (done) {
28
+ break;
29
+ }
30
+ response.write(value);
31
+ }
32
+ } finally {
33
+ reader.releaseLock();
34
+ }
35
+ response.end();
36
+ }
37
+
38
+ async function createRecursiveDirectoryWatcher(rootDirectory, onChange) {
39
+ const watchers = new Map();
40
+ let closed = false;
41
+
42
+ async function watchDirectory(directory) {
43
+ if (closed || watchers.has(directory)) {
44
+ return;
45
+ }
46
+
47
+ let watcher;
48
+ try {
49
+ watcher = watch(directory, (eventType, fileName) => {
50
+ onChange();
51
+
52
+ if (eventType === "rename" && fileName) {
53
+ void watchNewDirectory(path.join(directory, fileName));
54
+ }
55
+ });
56
+ } catch (error) {
57
+ if (error?.code !== "ENOENT") {
58
+ throw error;
59
+ }
60
+ return;
61
+ }
62
+
63
+ // A deleted directory may emit an error after its watcher has been installed.
64
+ watcher.on("error", () => {});
65
+ watchers.set(directory, watcher);
66
+
67
+ let entries;
68
+ try {
69
+ entries = await fs.readdir(directory, { withFileTypes: true });
70
+ } catch (error) {
71
+ if (error?.code === "ENOENT") {
72
+ return;
73
+ }
74
+ throw error;
75
+ }
76
+
77
+ await Promise.all(
78
+ entries
79
+ .filter((entry) => entry.isDirectory())
80
+ .map((entry) => watchDirectory(path.join(directory, entry.name))),
81
+ );
82
+ }
83
+
84
+ async function watchNewDirectory(candidate) {
85
+ try {
86
+ const stat = await fs.stat(candidate);
87
+ if (stat.isDirectory()) {
88
+ await watchDirectory(candidate);
89
+ }
90
+ } catch (error) {
91
+ if (error?.code !== "ENOENT") {
92
+ throw error;
93
+ }
94
+ }
95
+ }
96
+
97
+ await watchDirectory(rootDirectory);
98
+
99
+ return {
100
+ close() {
101
+ closed = true;
102
+ for (const watcher of watchers.values()) {
103
+ watcher.close();
104
+ }
105
+ watchers.clear();
106
+ },
107
+ };
108
+ }
109
+
110
+ async function createServer(projectRoot, mode, explicitPort, options = {}) {
111
+ const evolitConfig = options.evolitConfig ?? await loadEvolitConfig(projectRoot);
112
+ const responseCacheRuntime = options.responseCacheStore
113
+ ? {
114
+ store: options.responseCacheStore,
115
+ createKey: options.createResponseCacheKey ?? (({ request }) => createDefaultRouteCacheKey(request)),
116
+ }
117
+ : await resolveResponseCacheRuntime(projectRoot, mode, evolitConfig);
118
+ const deploymentRuntime = await createDeploymentRuntime({
119
+ projectRoot,
120
+ mode,
121
+ assetManifest: normalizeClientAssetManifest(options.assetManifest),
122
+ responseCacheRuntime,
123
+ });
124
+ const port = getPort(explicitPort);
125
+ let invalidationTimer = null;
126
+ let pendingInvalidation = null;
127
+ let resolvePendingInvalidation = null;
128
+ let invalidationPromise = Promise.resolve();
129
+
130
+ function scheduleDevelopmentInvalidation() {
131
+ if (invalidationTimer) {
132
+ clearTimeout(invalidationTimer);
133
+ }
134
+
135
+ if (!pendingInvalidation) {
136
+ pendingInvalidation = new Promise((resolve) => {
137
+ resolvePendingInvalidation = resolve;
138
+ });
139
+ }
140
+
141
+ invalidationTimer = setTimeout(() => {
142
+ invalidationTimer = null;
143
+ const resolve = resolvePendingInvalidation;
144
+ pendingInvalidation = null;
145
+ resolvePendingInvalidation = null;
146
+ invalidationPromise = invalidationPromise
147
+ .catch(() => {})
148
+ .then(() => deploymentRuntime.invalidateDevelopmentState());
149
+ invalidationPromise.then(resolve, resolve);
150
+ }, 40);
151
+ }
152
+
153
+ const watcher = mode === "development"
154
+ ? await createRecursiveDirectoryWatcher(
155
+ path.join(projectRoot, APP_DIRECTORY),
156
+ scheduleDevelopmentInvalidation,
157
+ )
158
+ : null;
159
+
160
+ const server = http.createServer(async (req, res) => {
161
+ try {
162
+ await pendingInvalidation;
163
+ await invalidationPromise;
164
+ const origin = `http://${req.headers.host ?? `localhost:${port}`}`;
165
+ const method = req.method ?? "GET";
166
+ const request = new Request(new URL(req.url ?? "/", origin), {
167
+ method,
168
+ headers: req.headers,
169
+ ...(method === "GET" || method === "HEAD" ? {} : { body: req, duplex: "half" }),
170
+ });
171
+ const response = await deploymentRuntime.handle(request);
172
+
173
+ res.writeHead(response.status, response.headers);
174
+ await writeResponseBody(res, response.body);
175
+ } catch (error) {
176
+ res.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
177
+ res.end(error instanceof Error ? error.stack ?? error.message : String(error));
178
+ }
179
+ });
180
+
181
+ return {
182
+ port,
183
+ deploymentRuntime,
184
+ async listen() {
185
+ await new Promise((resolve) => {
186
+ server.listen(port, resolve);
187
+ });
188
+ return server;
189
+ },
190
+ async close() {
191
+ if (invalidationTimer) {
192
+ clearTimeout(invalidationTimer);
193
+ }
194
+ watcher?.close();
195
+ await new Promise((resolve, reject) => {
196
+ server.close((error) => {
197
+ if (error) {
198
+ reject(error);
199
+ return;
200
+ }
201
+ resolve();
202
+ });
203
+ });
204
+ await deploymentRuntime.close?.();
205
+ },
206
+ };
207
+ }
208
+
209
+ export async function createDevServer(projectRoot, options = {}) {
210
+ return createServer(projectRoot, "development", options.port, options);
211
+ }
212
+
213
+ export async function createStartServer(projectRoot, options = {}) {
214
+ const manifestPath = path.join(
215
+ projectRoot,
216
+ INTERNAL_DIRECTORY,
217
+ BUILD_DIRECTORY,
218
+ MANIFEST_FILENAME,
219
+ );
220
+
221
+ if (!(await pathExists(manifestPath))) {
222
+ throw new Error("Build output not found. Run `yarn build` before `yarn start`.");
223
+ }
224
+
225
+ const manifest = await readJson(manifestPath);
226
+ return createServer(projectRoot, "production", options.port, {
227
+ ...options,
228
+ assetManifest: manifest.clientAssets ?? null,
229
+ });
230
+ }
@@ -0,0 +1,189 @@
1
+ import { renderBootstrap, renderDocument } from "@litsx/ssr";
2
+
3
+ function escapeHtmlAttribute(value) {
4
+ return String(value)
5
+ .replaceAll("&", "&")
6
+ .replaceAll('"', """)
7
+ .replaceAll("<", "&lt;")
8
+ .replaceAll(">", "&gt;");
9
+ }
10
+
11
+ function normalizeHeadMarkup(value) {
12
+ if (Array.isArray(value)) {
13
+ return value.filter(Boolean).join("\n");
14
+ }
15
+
16
+ return typeof value === "string" ? value : "";
17
+ }
18
+
19
+ function resolveDocumentMetadata(routeMetadata = {}, adapterOptions = {}) {
20
+ const description =
21
+ routeMetadata.description ?? adapterOptions.description ?? "A LitSX application";
22
+ const extraHead = [
23
+ description
24
+ ? `<meta name="description" content="${escapeHtmlAttribute(description)}" />`
25
+ : "",
26
+ normalizeHeadMarkup(adapterOptions.head),
27
+ normalizeHeadMarkup(routeMetadata.head),
28
+ ]
29
+ .filter(Boolean)
30
+ .join("\n");
31
+
32
+ return {
33
+ lang: String(routeMetadata.lang ?? adapterOptions.lang ?? "en"),
34
+ title: String(routeMetadata.title ?? adapterOptions.title ?? "evolit"),
35
+ head: extraHead,
36
+ htmlAttributes: {
37
+ ...(adapterOptions.htmlAttributes ?? {}),
38
+ ...(routeMetadata.htmlAttributes ?? {}),
39
+ },
40
+ bodyAttributes: {
41
+ ...(adapterOptions.bodyAttributes ?? {}),
42
+ ...(routeMetadata.bodyAttributes ?? {}),
43
+ },
44
+ };
45
+ }
46
+
47
+ function createHtmlDocument({ body, metadata = {} }) {
48
+ const title = metadata.title ? String(metadata.title) : "evolit";
49
+ const description =
50
+ metadata.description ? String(metadata.description) : "A LitSX application";
51
+
52
+ return `<!DOCTYPE html>
53
+ <html lang="${escapeHtmlAttribute(metadata.lang ?? "en")}">
54
+ <head>
55
+ <meta charset="utf-8" />
56
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
57
+ <title>${title}</title>${description ? `
58
+ <meta name="description" content="${escapeHtmlAttribute(description)}" />` : ""}
59
+ ${metadata.head ? ` ${String(metadata.head).split("\n").join("\n ")}` : ""}
60
+ </head>
61
+ <body>${body}</body>
62
+ </html>`;
63
+ }
64
+
65
+ function createRouteHeaders(routeResult, headers = {}) {
66
+ return {
67
+ ...headers,
68
+ ...(routeResult.responseHeaders ?? {}),
69
+ };
70
+ }
71
+
72
+ function responseHeadersToObject(headers) {
73
+ const values = Object.fromEntries(headers.entries());
74
+ if (typeof headers.getSetCookie === "function") {
75
+ const cookies = headers.getSetCookie();
76
+ if (cookies.length > 0) {
77
+ values["set-cookie"] = cookies;
78
+ }
79
+ }
80
+ return values;
81
+ }
82
+
83
+ export function createSsrAdapter(options = {}) {
84
+ return {
85
+ name: options.name ?? "litsx-ssr",
86
+ async renderRouteTree(routeResult) {
87
+ if (routeResult.type === "handler") {
88
+ return {
89
+ status: routeResult.response.status,
90
+ headers: responseHeadersToObject(routeResult.response.headers),
91
+ body: routeResult.response.body,
92
+ };
93
+ }
94
+
95
+ if (routeResult.type === "not-found" && !routeResult.tree) {
96
+ return {
97
+ status: 404,
98
+ headers: createRouteHeaders(routeResult, { "content-type": "text/html; charset=utf-8" }),
99
+ body: "<!DOCTYPE html><html><body><h1>404</h1><p>Route not found.</p></body></html>",
100
+ };
101
+ }
102
+
103
+ if (routeResult.type === "redirect") {
104
+ return {
105
+ status: routeResult.status ?? 307,
106
+ headers: createRouteHeaders(routeResult, {
107
+ location: routeResult.location,
108
+ "content-type": "text/html; charset=utf-8",
109
+ }),
110
+ body: "<!DOCTYPE html><html><body><p>Redirecting...</p></body></html>",
111
+ };
112
+ }
113
+
114
+ if (typeof routeResult.tree === "string") {
115
+ const metadata = resolveDocumentMetadata(routeResult.metadata, options);
116
+ return {
117
+ status: routeResult.status ?? 200,
118
+ headers: createRouteHeaders(routeResult, { "content-type": "text/html; charset=utf-8" }),
119
+ body: createHtmlDocument({
120
+ body: routeResult.tree,
121
+ metadata: {
122
+ ...routeResult.metadata,
123
+ ...metadata,
124
+ description: routeResult.metadata?.description ?? options.description,
125
+ },
126
+ }),
127
+ };
128
+ }
129
+
130
+ const documentOptions = resolveDocumentMetadata(routeResult.metadata, options);
131
+ const bootstrap = options.bootstrap ?? false;
132
+ const result = await renderDocument(routeResult.tree, {
133
+ ...documentOptions,
134
+ assetResolver: options.assetResolver,
135
+ bootstrap,
136
+ clientEntry: options.clientEntry,
137
+ template: options.template,
138
+ });
139
+
140
+ const generatedBootstrap =
141
+ !options.bootstrap && !options.clientEntry && typeof options.resolveBootstrap === "function"
142
+ ? await options.resolveBootstrap({ routeResult, result })
143
+ : "";
144
+ const additionalHead =
145
+ typeof options.resolveAdditionalHead === "function"
146
+ ? await options.resolveAdditionalHead({ routeResult, result })
147
+ : "";
148
+ const documentWithAdditionalHead = additionalHead
149
+ ? result.document.replace("</head>", `${additionalHead}\n</head>`)
150
+ : result.document;
151
+ const transformedDocument =
152
+ typeof options.transformDocument === "function"
153
+ ? await options.transformDocument({
154
+ routeResult,
155
+ result,
156
+ document: documentWithAdditionalHead,
157
+ })
158
+ : documentWithAdditionalHead;
159
+
160
+ if (generatedBootstrap) {
161
+ const bootstrapMarkup = renderBootstrap({
162
+ bootstrap: {
163
+ content: generatedBootstrap,
164
+ },
165
+ });
166
+ const documentWithBootstrap = transformedDocument.replace(
167
+ "</body>",
168
+ `${bootstrapMarkup}\n</body>`,
169
+ );
170
+
171
+ return {
172
+ status: routeResult.status ?? 200,
173
+ headers: createRouteHeaders(routeResult, { "content-type": "text/html; charset=utf-8" }),
174
+ body: documentWithBootstrap,
175
+ };
176
+ }
177
+
178
+ return {
179
+ status: routeResult.status ?? 200,
180
+ headers: createRouteHeaders(routeResult, { "content-type": "text/html; charset=utf-8" }),
181
+ body: transformedDocument,
182
+ };
183
+ },
184
+ };
185
+ }
186
+
187
+ export async function renderRouteTreeWithAdapter(routeResult, adapter = createSsrAdapter()) {
188
+ return adapter.renderRouteTree(routeResult);
189
+ }
@@ -0,0 +1,20 @@
1
+ export const metadata = {
2
+ title: "About | evolit",
3
+ description: "What evolit is trying to become",
4
+ };
5
+
6
+ export default async function AboutPage() {
7
+ return (
8
+ <section>
9
+ <h1>About evolit</h1>
10
+ <p>
11
+ The goal is not to clone Next.js mechanically. The goal is to offer the same level of
12
+ application ergonomics for LitSX-authored user interfaces: routing, layouts, SSR,
13
+ build orchestration, and Web Platform server APIs.
14
+ </p>
15
+ <p>
16
+ This starter is the first end-to-end slice needed to evolve that runtime in the open.
17
+ </p>
18
+ </section>
19
+ );
20
+ }
@@ -0,0 +1 @@
1
+ declare module "*.css";
@@ -0,0 +1,35 @@
1
+ export default function FeatureCard({ title, body }) {
2
+ static styles = `
3
+ :host {
4
+ display: block;
5
+ }
6
+
7
+ .card {
8
+ padding: 24px;
9
+ border-radius: 18px;
10
+ background: rgba(255, 255, 255, 0.06);
11
+ border: 1px solid rgba(255, 255, 255, 0.08);
12
+ backdrop-filter: blur(12px);
13
+ }
14
+
15
+ .title {
16
+ margin: 0 0 12px;
17
+ font-size: 1.25rem;
18
+ }
19
+
20
+ .body {
21
+ margin: 0;
22
+ line-height: 1.7;
23
+ color: rgba(248, 247, 241, 0.78);
24
+ }
25
+ `;
26
+
27
+ return (
28
+ <article class="card">
29
+ <h2 class="title">{title}</h2>
30
+ <p class="body">
31
+ {body}
32
+ </p>
33
+ </article>
34
+ );
35
+ }
@@ -0,0 +1,27 @@
1
+ main {
2
+ min-height: 100vh;
3
+ padding: 48px 24px;
4
+ color: #f8f7f1;
5
+ background: radial-gradient(circle at top, rgba(239, 148, 89, 0.22), transparent 34%), linear-gradient(180deg, #10161f 0%, #182332 100%);
6
+ font-family: Iowan Old Style, Palatino Linotype, Book Antiqua, Georgia, serif;
7
+ }
8
+
9
+ .shell { max-width: 960px; margin: 0 auto; }
10
+ header { margin-bottom: 40px; }
11
+ .wordmark {
12
+ display: inline-block;
13
+ padding: 8px 12px;
14
+ border: 1px solid rgba(248, 247, 241, 0.18);
15
+ letter-spacing: 0.16em;
16
+ text-transform: uppercase;
17
+ font-size: 12px;
18
+ }
19
+
20
+ .intro { display: grid; gap: 20px; margin-bottom: 40px; }
21
+ .eyebrow { margin: 0; letter-spacing: 0.18em; text-transform: uppercase; font-size: 12px; color: rgba(248, 247, 241, 0.56); }
22
+ h1 { margin: 0; font-size: clamp(3rem, 7vw, 5.5rem); line-height: 0.94; }
23
+ .summary { max-width: 720px; margin: 0; line-height: 1.8; color: rgba(248, 247, 241, 0.76); }
24
+ .features { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
25
+
26
+ section { max-width: 720px; display: grid; gap: 20px; }
27
+ section p { margin: 0; line-height: 1.8; color: rgba(248, 247, 241, 0.78); }
@@ -0,0 +1,18 @@
1
+ export const metadata = {
2
+ title: "evolit",
3
+ description: "A Next.js-style runtime for LitSX",
4
+ };
5
+
6
+ export default async function RootLayout({ children }) {
7
+ return (
8
+ <main>
9
+ <div class="shell">
10
+ <header>
11
+ <div class="wordmark">evolit</div>
12
+ </header>
13
+ {children}
14
+ </div>
15
+ </main>
16
+ );
17
+ }
18
+ import "./global.css";
@@ -0,0 +1,36 @@
1
+ import FeatureCard from "./components/feature-card.litsx";
2
+
3
+ export default async function HomePage() {
4
+ return (
5
+ <section>
6
+ <div class="intro">
7
+ <p class="eyebrow">LitSX application framework</p>
8
+ <h1>
9
+ Next.js ideas,
10
+ <br />
11
+ LitSX authored.
12
+ </h1>
13
+ <p class="summary">
14
+ This starter proves the first framework contract: routes from <code>app/</code>,
15
+ nested layouts, request-aware page modules, and server rendering for LitSX-authored
16
+ UI.
17
+ </p>
18
+ </div>
19
+
20
+ <div class="features">
21
+ <FeatureCard
22
+ title="File Routing"
23
+ body="Every app/page.litsx becomes an addressable route, with dynamic segments reserved for the next iteration."
24
+ />
25
+ <FeatureCard
26
+ title="SSR Boundary"
27
+ body="The framework now renders route trees through @litsx/ssr while keeping the adapter boundary internal."
28
+ />
29
+ <FeatureCard
30
+ title="Compiler Reuse"
31
+ body="Authored .litsx source is compiled through the public @litsx/compiler facade instead of a parallel transform path."
32
+ />
33
+ </div>
34
+ </section>
35
+ );
36
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "allowJs": true,
7
+ "checkJs": false,
8
+ "strict": false,
9
+ "jsx": "preserve",
10
+ "jsxImportSource": "@litsx/core",
11
+ "allowArbitraryExtensions": true,
12
+ "plugins": [
13
+ {
14
+ "name": "@litsx/typescript"
15
+ }
16
+ ]
17
+ },
18
+ "include": [
19
+ "app/**/*",
20
+ "evolit.config.js"
21
+ ]
22
+ }