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/LICENSE +201 -0
- package/README.md +352 -0
- package/package.json +63 -0
- package/src/app-discovery.js +312 -0
- package/src/build.js +417 -0
- package/src/cli.js +83 -0
- package/src/client-assets.js +1881 -0
- package/src/compiler.js +353 -0
- package/src/config.js +20 -0
- package/src/constants.js +49 -0
- package/src/deployment-runtime.js +463 -0
- package/src/fs-utils.js +71 -0
- package/src/index.js +167 -0
- package/src/render.js +516 -0
- package/src/request-context-browser.js +11 -0
- package/src/request-context.js +230 -0
- package/src/response-cache.js +214 -0
- package/src/route-config.js +60 -0
- package/src/scaffold.js +107 -0
- package/src/server-api.js +51 -0
- package/src/server.js +230 -0
- package/src/ssr-adapter.js +189 -0
- package/templates/default/app/about/page.litsx +20 -0
- package/templates/default/app/assets.d.ts +1 -0
- package/templates/default/app/components/feature-card.litsx +35 -0
- package/templates/default/app/global.css +27 -0
- package/templates/default/app/layout.litsx +18 -0
- package/templates/default/app/page.litsx +36 -0
- package/templates/default/jsconfig.json +22 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
const REQUEST_CONTEXT_STORAGE = Symbol.for("evolit.request-context.storage");
|
|
4
|
+
const HTTP_SIGNAL_CLASS = Symbol.for("evolit.request-context.http-signal");
|
|
5
|
+
|
|
6
|
+
const requestContextStorage = globalThis[REQUEST_CONTEXT_STORAGE] ??= new AsyncLocalStorage();
|
|
7
|
+
const EvolitHttpSignal = globalThis[HTTP_SIGNAL_CLASS] ??= class EvolitHttpSignal extends Error {
|
|
8
|
+
constructor(type, options = {}) {
|
|
9
|
+
super(type);
|
|
10
|
+
this.type = type;
|
|
11
|
+
this.status = options.status;
|
|
12
|
+
this.location = options.location;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function getActiveContext() {
|
|
17
|
+
const context = requestContextStorage.getStore();
|
|
18
|
+
if (!context) {
|
|
19
|
+
throw new Error("Request APIs can only be called while rendering a evolit route.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return context;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseCookieHeader(value) {
|
|
26
|
+
const values = new Map();
|
|
27
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
28
|
+
return values;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const entry of value.split(";")) {
|
|
32
|
+
const separator = entry.indexOf("=");
|
|
33
|
+
if (separator === -1) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const name = entry.slice(0, separator).trim();
|
|
38
|
+
if (!name) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
values.set(name, decodeURIComponent(entry.slice(separator + 1).trim()));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return values;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function serializeCookie(name, value, options = {}) {
|
|
49
|
+
const attributes = [`${name}=${encodeURIComponent(value)}`];
|
|
50
|
+
if (options.maxAge != null) attributes.push(`Max-Age=${Math.floor(options.maxAge)}`);
|
|
51
|
+
if (options.domain) attributes.push(`Domain=${options.domain}`);
|
|
52
|
+
attributes.push(`Path=${options.path ?? "/"}`);
|
|
53
|
+
if (options.expires) attributes.push(`Expires=${new Date(options.expires).toUTCString()}`);
|
|
54
|
+
if (options.httpOnly) attributes.push("HttpOnly");
|
|
55
|
+
if (options.secure) attributes.push("Secure");
|
|
56
|
+
if (options.sameSite) attributes.push(`SameSite=${options.sameSite}`);
|
|
57
|
+
return attributes.join("; ");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createCookieStore(context) {
|
|
61
|
+
const requestCookies = parseCookieHeader(context.request.headers.get("cookie"));
|
|
62
|
+
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
get(name) {
|
|
65
|
+
const value = requestCookies.get(name);
|
|
66
|
+
return value == null ? undefined : { name, value };
|
|
67
|
+
},
|
|
68
|
+
getAll(name) {
|
|
69
|
+
if (name) {
|
|
70
|
+
const cookie = this.get(name);
|
|
71
|
+
return cookie ? [cookie] : [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return [...requestCookies.entries()].map(([cookieName, value]) => ({ name: cookieName, value }));
|
|
75
|
+
},
|
|
76
|
+
has(name) {
|
|
77
|
+
return requestCookies.has(name);
|
|
78
|
+
},
|
|
79
|
+
set(name, value, options = {}) {
|
|
80
|
+
context.responseCookies.push(serializeCookie(name, String(value), options));
|
|
81
|
+
requestCookies.set(name, String(value));
|
|
82
|
+
context.didUseDynamicRequestData = true;
|
|
83
|
+
},
|
|
84
|
+
delete(name, options = {}) {
|
|
85
|
+
context.responseCookies.push(serializeCookie(name, "", { ...options, maxAge: 0 }));
|
|
86
|
+
requestCookies.delete(name);
|
|
87
|
+
context.didUseDynamicRequestData = true;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function createRouteRequest(context) {
|
|
93
|
+
return new Proxy(context.request, {
|
|
94
|
+
get(target, property) {
|
|
95
|
+
if (property !== "constructor" && property !== Symbol.toStringTag) {
|
|
96
|
+
context.didUseDynamicRequestData = true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const value = Reflect.get(target, property, target);
|
|
100
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createRequestContext({ request, params = {}, searchParams = {} }) {
|
|
106
|
+
const context = {
|
|
107
|
+
request,
|
|
108
|
+
params: Object.freeze({ ...params }),
|
|
109
|
+
searchParams: Object.freeze({ ...searchParams }),
|
|
110
|
+
responseHeaders: new Headers(),
|
|
111
|
+
responseCookies: [],
|
|
112
|
+
didUseDynamicRequestData: false,
|
|
113
|
+
};
|
|
114
|
+
context.cookieStore = createCookieStore(context);
|
|
115
|
+
context.routeRequest = createRouteRequest(context);
|
|
116
|
+
return context;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function runWithRequestContext(context, callback) {
|
|
120
|
+
return requestContextStorage.run(context, callback);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Returns the incoming request headers and marks the current route dynamic.
|
|
125
|
+
*
|
|
126
|
+
* @returns {Headers} The incoming request headers.
|
|
127
|
+
*/
|
|
128
|
+
export function headers() {
|
|
129
|
+
const context = getActiveContext();
|
|
130
|
+
context.didUseDynamicRequestData = true;
|
|
131
|
+
return context.request.headers;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Returns the request cookie store and marks the current route dynamic.
|
|
136
|
+
* Mutations are merged into the outgoing response.
|
|
137
|
+
*
|
|
138
|
+
* @returns {object} A cookie store with `get`, `getAll`, `has`, `set`, and `delete`.
|
|
139
|
+
*/
|
|
140
|
+
export function cookies() {
|
|
141
|
+
const context = getActiveContext();
|
|
142
|
+
context.didUseDynamicRequestData = true;
|
|
143
|
+
return context.cookieStore;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Returns the incoming request URL and marks the current route dynamic.
|
|
148
|
+
*
|
|
149
|
+
* @returns {URL} The request URL.
|
|
150
|
+
*/
|
|
151
|
+
export function requestUrl() {
|
|
152
|
+
const context = getActiveContext();
|
|
153
|
+
context.didUseDynamicRequestData = true;
|
|
154
|
+
return new URL(context.request.url);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Returns mutable headers merged into the response after route rendering.
|
|
159
|
+
*
|
|
160
|
+
* @returns {Headers} Response headers.
|
|
161
|
+
*/
|
|
162
|
+
export function responseHeaders() {
|
|
163
|
+
return getActiveContext().responseHeaders;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Stops route execution and responds with a redirect.
|
|
168
|
+
*
|
|
169
|
+
* @param {string} location Redirect destination.
|
|
170
|
+
* @param {number} [status=307] Redirect HTTP status.
|
|
171
|
+
* @returns {never}
|
|
172
|
+
*/
|
|
173
|
+
export function redirect(location, status = 307) {
|
|
174
|
+
throw new EvolitHttpSignal("redirect", { location: String(location), status });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Stops route execution and responds with a permanent HTTP 308 redirect.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} location Redirect destination.
|
|
181
|
+
* @returns {never}
|
|
182
|
+
*/
|
|
183
|
+
export function permanentRedirect(location) {
|
|
184
|
+
redirect(location, 308);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Stops route execution with a 404 signal for the nearest `not-found.litsx` boundary.
|
|
189
|
+
*
|
|
190
|
+
* @returns {never}
|
|
191
|
+
*/
|
|
192
|
+
export function notFound() {
|
|
193
|
+
throw new EvolitHttpSignal("not-found", { status: 404 });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function isEvolitHttpSignal(error) {
|
|
197
|
+
return error instanceof EvolitHttpSignal;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function getRequestContextResponse(context) {
|
|
201
|
+
const headers = Object.fromEntries(context.responseHeaders.entries());
|
|
202
|
+
if (context.responseCookies.length > 0) {
|
|
203
|
+
headers["set-cookie"] = context.responseCookies;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
headers,
|
|
208
|
+
didUseDynamicRequestData: context.didUseDynamicRequestData,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function applyRequestContextToResponse(response, context) {
|
|
213
|
+
if (!(response instanceof Response)) {
|
|
214
|
+
throw new Error("Expected a route handler to return a Web Response.");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const headers = new Headers(response.headers);
|
|
218
|
+
for (const [name, value] of context.responseHeaders.entries()) {
|
|
219
|
+
headers.set(name, value);
|
|
220
|
+
}
|
|
221
|
+
for (const cookie of context.responseCookies) {
|
|
222
|
+
headers.append("set-cookie", cookie);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return new Response(response.body, {
|
|
226
|
+
status: response.status,
|
|
227
|
+
statusText: response.statusText,
|
|
228
|
+
headers,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { BUILD_DIRECTORY, INTERNAL_DIRECTORY, ROUTE_CACHE_DIRECTORY } from "./constants.js";
|
|
5
|
+
import { ensureDirectory, pathExists, readJson, writeJson } from "./fs-utils.js";
|
|
6
|
+
|
|
7
|
+
function createCacheFileName(cacheKey) {
|
|
8
|
+
return `${crypto.createHash("sha1").update(cacheKey).digest("hex")}.json`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function createCacheObjectKey(cacheKey, prefix = "") {
|
|
12
|
+
const normalizedPrefix = typeof prefix === "string" ? prefix.replace(/\/+$/g, "") : "";
|
|
13
|
+
const fileName = createCacheFileName(cacheKey);
|
|
14
|
+
return normalizedPrefix ? `${normalizedPrefix}/${fileName}` : fileName;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createCachedRouteResponse(response, cachePolicy, now = new Date()) {
|
|
18
|
+
const ttlSeconds = cachePolicy?.mode === "revalidate" ? cachePolicy.ttlSeconds : null;
|
|
19
|
+
const createdAt = now.toISOString();
|
|
20
|
+
const expiresAt = ttlSeconds == null
|
|
21
|
+
? null
|
|
22
|
+
: new Date(now.getTime() + (ttlSeconds * 1000)).toISOString();
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
status: response.status,
|
|
26
|
+
headers: { ...(response.headers ?? {}) },
|
|
27
|
+
body: String(response.body ?? ""),
|
|
28
|
+
createdAt,
|
|
29
|
+
expiresAt,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isCachedRouteResponseFresh(entry, now = new Date()) {
|
|
34
|
+
if (!entry || typeof entry !== "object") {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!entry.expiresAt) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new Date(entry.expiresAt).getTime() > now.getTime();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class MemoryResponseCacheStore {
|
|
46
|
+
#entries = new Map();
|
|
47
|
+
|
|
48
|
+
async get(cacheKey) {
|
|
49
|
+
return this.#entries.get(cacheKey) ?? null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async put(cacheKey, entry) {
|
|
53
|
+
this.#entries.set(cacheKey, entry);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async delete(cacheKey) {
|
|
57
|
+
this.#entries.delete(cacheKey);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async clear() {
|
|
61
|
+
this.#entries.clear();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class FileSystemResponseCacheStore {
|
|
66
|
+
#rootDirectory;
|
|
67
|
+
|
|
68
|
+
constructor(rootDirectory) {
|
|
69
|
+
this.#rootDirectory = rootDirectory;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getRootDirectory() {
|
|
73
|
+
return this.#rootDirectory;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getFilePath(cacheKey) {
|
|
77
|
+
return path.join(this.#rootDirectory, createCacheFileName(cacheKey));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async get(cacheKey) {
|
|
81
|
+
const filePath = this.getFilePath(cacheKey);
|
|
82
|
+
if (!(await pathExists(filePath))) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const entry = await readJson(filePath);
|
|
87
|
+
return entry?.key === cacheKey ? entry.value ?? null : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async put(cacheKey, entry) {
|
|
91
|
+
const filePath = this.getFilePath(cacheKey);
|
|
92
|
+
await ensureDirectory(path.dirname(filePath));
|
|
93
|
+
await writeJson(filePath, {
|
|
94
|
+
key: cacheKey,
|
|
95
|
+
value: entry,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async delete(cacheKey) {
|
|
100
|
+
const filePath = this.getFilePath(cacheKey);
|
|
101
|
+
if (await pathExists(filePath)) {
|
|
102
|
+
await fs.unlink(filePath);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class ObjectStorageResponseCacheStore {
|
|
108
|
+
#prefix;
|
|
109
|
+
#getObject;
|
|
110
|
+
#putObject;
|
|
111
|
+
#deleteObject;
|
|
112
|
+
|
|
113
|
+
constructor(options = {}) {
|
|
114
|
+
this.#prefix = options.prefix ?? "";
|
|
115
|
+
this.#getObject = options.getObject;
|
|
116
|
+
this.#putObject = options.putObject;
|
|
117
|
+
this.#deleteObject = options.deleteObject ?? null;
|
|
118
|
+
|
|
119
|
+
if (typeof this.#getObject !== "function") {
|
|
120
|
+
throw new Error("Expected ObjectStorageResponseCacheStore options.getObject to be a function.");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof this.#putObject !== "function") {
|
|
124
|
+
throw new Error("Expected ObjectStorageResponseCacheStore options.putObject to be a function.");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
getObjectKey(cacheKey) {
|
|
129
|
+
return createCacheObjectKey(cacheKey, this.#prefix);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async get(cacheKey) {
|
|
133
|
+
const serializedEntry = await this.#getObject(this.getObjectKey(cacheKey));
|
|
134
|
+
if (typeof serializedEntry !== "string" || serializedEntry.length === 0) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const entry = JSON.parse(serializedEntry);
|
|
139
|
+
return entry?.key === cacheKey ? entry.value ?? null : null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async put(cacheKey, entry) {
|
|
143
|
+
await this.#putObject(
|
|
144
|
+
this.getObjectKey(cacheKey),
|
|
145
|
+
`${JSON.stringify({
|
|
146
|
+
key: cacheKey,
|
|
147
|
+
value: entry,
|
|
148
|
+
}, null, 2)}\n`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async delete(cacheKey) {
|
|
153
|
+
if (typeof this.#deleteObject !== "function") {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await this.#deleteObject(this.getObjectKey(cacheKey));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function createDefaultRouteCacheKey(request) {
|
|
162
|
+
const url = new URL(request.url);
|
|
163
|
+
return `${url.pathname}${url.search}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function getRouteCacheArtifactFileName(cacheKey) {
|
|
167
|
+
return createCacheFileName(cacheKey);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function createDefaultResponseCacheStore(projectRoot, mode) {
|
|
171
|
+
if (mode === "development") {
|
|
172
|
+
return new MemoryResponseCacheStore();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return new FileSystemResponseCacheStore(
|
|
176
|
+
path.join(projectRoot, INTERNAL_DIRECTORY, BUILD_DIRECTORY, ROUTE_CACHE_DIRECTORY),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isResponseCacheStore(value) {
|
|
181
|
+
return (
|
|
182
|
+
value != null &&
|
|
183
|
+
typeof value === "object" &&
|
|
184
|
+
typeof value.get === "function" &&
|
|
185
|
+
typeof value.put === "function"
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function resolveResponseCacheRuntime(projectRoot, mode, evolitConfig = {}) {
|
|
190
|
+
const defaultStore = createDefaultResponseCacheStore(projectRoot, mode);
|
|
191
|
+
const responseCacheConfig = evolitConfig?.responseCache ?? {};
|
|
192
|
+
|
|
193
|
+
let store = defaultStore;
|
|
194
|
+
if (typeof responseCacheConfig?.createStore === "function") {
|
|
195
|
+
store = await responseCacheConfig.createStore({
|
|
196
|
+
projectRoot,
|
|
197
|
+
mode,
|
|
198
|
+
defaultStore,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!isResponseCacheStore(store)) {
|
|
203
|
+
throw new Error("Expected responseCache.createStore() to return an object with get() and put() methods.");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const createKey = typeof responseCacheConfig?.createKey === "function"
|
|
207
|
+
? responseCacheConfig.createKey
|
|
208
|
+
: ({ request }) => createDefaultRouteCacheKey(request);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
store,
|
|
212
|
+
createKey,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
function normalizeRevalidatePolicy(cacheConfig) {
|
|
2
|
+
if (!cacheConfig || typeof cacheConfig !== "object" || Array.isArray(cacheConfig)) {
|
|
3
|
+
throw new Error("Expected routeConfig.cache to be an object when using revalidate mode.");
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const ttlSeconds = cacheConfig.revalidate;
|
|
7
|
+
if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0) {
|
|
8
|
+
throw new Error("Expected routeConfig.cache.revalidate to be a positive integer.");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
mode: "revalidate",
|
|
13
|
+
ttlSeconds,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeRouteCachePolicy(routeConfig = {}) {
|
|
18
|
+
const cacheConfig = routeConfig?.cache ?? "dynamic";
|
|
19
|
+
|
|
20
|
+
if (cacheConfig === "dynamic") {
|
|
21
|
+
return { mode: "dynamic" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (cacheConfig === "static") {
|
|
25
|
+
return { mode: "static" };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return normalizeRevalidatePolicy(cacheConfig);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function mergeRouteConfig(modules) {
|
|
32
|
+
return modules.reduce((config, moduleRecord) => {
|
|
33
|
+
if (!moduleRecord || moduleRecord.routeConfig == null) {
|
|
34
|
+
return config;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof moduleRecord.routeConfig !== "object" || Array.isArray(moduleRecord.routeConfig)) {
|
|
38
|
+
throw new Error("Expected routeConfig to export an object.");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
...config,
|
|
43
|
+
...moduleRecord.routeConfig,
|
|
44
|
+
};
|
|
45
|
+
}, {});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function serializeRouteCachePolicy(cachePolicy) {
|
|
49
|
+
if (!cachePolicy || cachePolicy.mode === "dynamic") {
|
|
50
|
+
return "dynamic";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (cachePolicy.mode === "static") {
|
|
54
|
+
return "static";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
revalidate: cachePolicy.ttlSeconds,
|
|
59
|
+
};
|
|
60
|
+
}
|
package/src/scaffold.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { copyDirectory, ensureDirectory, pathExists, writeJson } from "./fs-utils.js";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const { version: frameworkVersion } = require("../package.json");
|
|
10
|
+
const TEMPLATE_ROOT = path.resolve(__dirname, "../templates/default");
|
|
11
|
+
|
|
12
|
+
async function writeSitePackageJson(targetDirectory, siteName) {
|
|
13
|
+
const packageJsonPath = path.join(targetDirectory, "package.json");
|
|
14
|
+
const packageJson = {
|
|
15
|
+
name: siteName,
|
|
16
|
+
version: "0.1.0",
|
|
17
|
+
private: true,
|
|
18
|
+
type: "module",
|
|
19
|
+
scripts: {
|
|
20
|
+
dev: "evolit dev",
|
|
21
|
+
build: "evolit build",
|
|
22
|
+
start: "evolit start",
|
|
23
|
+
typecheck: "litsx-tsc -p jsconfig.json --noEmit",
|
|
24
|
+
},
|
|
25
|
+
dependencies: {
|
|
26
|
+
"@litsx/core": "0.17.0-canary-feat-ssr-20260726130435",
|
|
27
|
+
evolit: frameworkVersion,
|
|
28
|
+
},
|
|
29
|
+
devDependencies: {
|
|
30
|
+
"@litsx/typescript": "^0.9.0",
|
|
31
|
+
"typescript": "^6.0.0",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
await writeJson(packageJsonPath, packageJson);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function scaffoldSite(targetDirectory, options = {}) {
|
|
39
|
+
const absoluteTargetDirectory = path.resolve(targetDirectory);
|
|
40
|
+
const siteName = options.name ?? path.basename(absoluteTargetDirectory);
|
|
41
|
+
|
|
42
|
+
if (await pathExists(absoluteTargetDirectory)) {
|
|
43
|
+
const entries = await fs.readdir(absoluteTargetDirectory);
|
|
44
|
+
if (entries.length > 0) {
|
|
45
|
+
throw new Error(`Target directory is not empty: ${absoluteTargetDirectory}`);
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
await ensureDirectory(absoluteTargetDirectory);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await copyDirectory(TEMPLATE_ROOT, absoluteTargetDirectory);
|
|
52
|
+
await writeSitePackageJson(absoluteTargetDirectory, siteName);
|
|
53
|
+
await fs.writeFile(
|
|
54
|
+
path.join(absoluteTargetDirectory, "evolit.config.js"),
|
|
55
|
+
[
|
|
56
|
+
"export default {",
|
|
57
|
+
" // Reserved for framework configuration.",
|
|
58
|
+
" // Response cache adapters and deployment-specific runtime hooks can live here.",
|
|
59
|
+
" // Example for object storage adapters such as S3:",
|
|
60
|
+
" // import { ObjectStorageResponseCacheStore } from \"evolit\";",
|
|
61
|
+
" // import {",
|
|
62
|
+
" // DeleteObjectCommand,",
|
|
63
|
+
" // GetObjectCommand,",
|
|
64
|
+
" // PutObjectCommand,",
|
|
65
|
+
" // S3Client,",
|
|
66
|
+
" // } from \"@aws-sdk/client-s3\";",
|
|
67
|
+
" //",
|
|
68
|
+
" // const s3 = new S3Client({ region: process.env.AWS_REGION });",
|
|
69
|
+
" // const bucket = process.env.EVOLIT_CACHE_BUCKET;",
|
|
70
|
+
" // responseCache: {",
|
|
71
|
+
" // async createStore() {",
|
|
72
|
+
" // return new ObjectStorageResponseCacheStore({",
|
|
73
|
+
" // prefix: \"routes\",",
|
|
74
|
+
" // async getObject(key) {",
|
|
75
|
+
" // try {",
|
|
76
|
+
" // const result = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));",
|
|
77
|
+
" // return await result.Body.transformToString();",
|
|
78
|
+
" // } catch (error) {",
|
|
79
|
+
" // if (error?.name === \"NoSuchKey\") return null;",
|
|
80
|
+
" // throw error;",
|
|
81
|
+
" // }",
|
|
82
|
+
" // },",
|
|
83
|
+
" // async putObject(key, value) {",
|
|
84
|
+
" // await s3.send(new PutObjectCommand({",
|
|
85
|
+
" // Bucket: bucket,",
|
|
86
|
+
" // Key: key,",
|
|
87
|
+
" // Body: value,",
|
|
88
|
+
" // ContentType: \"application/json; charset=utf-8\",",
|
|
89
|
+
" // }));",
|
|
90
|
+
" // },",
|
|
91
|
+
" // async deleteObject(key) {",
|
|
92
|
+
" // await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));",
|
|
93
|
+
" // },",
|
|
94
|
+
" // });",
|
|
95
|
+
" // },",
|
|
96
|
+
" // createKey({ request }) {",
|
|
97
|
+
" // return new URL(request.url).pathname;",
|
|
98
|
+
" // },",
|
|
99
|
+
" // },",
|
|
100
|
+
"};",
|
|
101
|
+
"",
|
|
102
|
+
].join("\n"),
|
|
103
|
+
"utf8",
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return absoluteTargetDirectory;
|
|
107
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads request cookies and exposes mutations for the outgoing response.
|
|
3
|
+
*
|
|
4
|
+
* @returns {object} A cookie store with `get`, `getAll`, `has`, `set`, and `delete`.
|
|
5
|
+
*/
|
|
6
|
+
export { cookies } from "./request-context.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns the incoming request headers. Reading headers makes the current route dynamic.
|
|
10
|
+
*
|
|
11
|
+
* @returns {Headers} The incoming request headers.
|
|
12
|
+
*/
|
|
13
|
+
export { headers } from "./request-context.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Signals a 404 response and renders the nearest `not-found.litsx` boundary when available.
|
|
17
|
+
*
|
|
18
|
+
* @returns {never}
|
|
19
|
+
*/
|
|
20
|
+
export { notFound } from "./request-context.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Signals a permanent redirect with HTTP status 308.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} location Redirect destination.
|
|
26
|
+
* @returns {never}
|
|
27
|
+
*/
|
|
28
|
+
export { permanentRedirect } from "./request-context.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Signals a redirect response. The default status is 307.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} location Redirect destination.
|
|
34
|
+
* @param {number} [status=307] Redirect HTTP status.
|
|
35
|
+
* @returns {never}
|
|
36
|
+
*/
|
|
37
|
+
export { redirect } from "./request-context.js";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns the incoming request URL. Reading it makes the current route dynamic.
|
|
41
|
+
*
|
|
42
|
+
* @returns {URL} The incoming request URL.
|
|
43
|
+
*/
|
|
44
|
+
export { requestUrl } from "./request-context.js";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns mutable response headers for the current route response.
|
|
48
|
+
*
|
|
49
|
+
* @returns {Headers} Headers merged into the outgoing response.
|
|
50
|
+
*/
|
|
51
|
+
export { responseHeaders } from "./request-context.js";
|