@jay-framework/dev-server 0.15.6 → 0.16.1
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/dist/index.d.ts +73 -3
- package/dist/index.js +547 -30
- package/package.json +16 -14
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { JayRollupConfig } from '@jay-framework/rollup-plugin';
|
|
|
5
5
|
import { LogLevel } from '@jay-framework/logger';
|
|
6
6
|
import { Server } from 'node:http';
|
|
7
7
|
import { ProjectClientInitInfo, PluginWithInit, ActionRegistry } from '@jay-framework/stack-server-runtime';
|
|
8
|
+
import * as _jay_framework_fullstack_component from '@jay-framework/fullstack-component';
|
|
8
9
|
import { RequestHandler as RequestHandler$1 } from 'express';
|
|
9
10
|
import { JayRollupConfig as JayRollupConfig$1 } from '@jay-framework/compiler-jay-stack';
|
|
10
11
|
|
|
@@ -103,6 +104,62 @@ declare class ServiceLifecycleManager {
|
|
|
103
104
|
isReady(): boolean;
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Page Freeze — Design Log #127
|
|
109
|
+
*
|
|
110
|
+
* Captures a page's ViewState at a point in time and serves it as a
|
|
111
|
+
* static SSR snapshot. Enables designers to create multiple frozen
|
|
112
|
+
* views of a page in different states for side-by-side comparison.
|
|
113
|
+
*/
|
|
114
|
+
interface FreezeEntry {
|
|
115
|
+
id: string;
|
|
116
|
+
name?: string;
|
|
117
|
+
/** The concrete URL path (e.g., /products/kitan) */
|
|
118
|
+
route: string;
|
|
119
|
+
/** The route pattern (e.g., /products/kitan{/:category}) */
|
|
120
|
+
routePattern?: string;
|
|
121
|
+
viewState: object;
|
|
122
|
+
createdAt: string;
|
|
123
|
+
}
|
|
124
|
+
declare class FreezeStore {
|
|
125
|
+
private readonly dir;
|
|
126
|
+
constructor(buildFolder: string);
|
|
127
|
+
save(route: string, viewState: object, routePattern?: string): Promise<FreezeEntry>;
|
|
128
|
+
get(id: string): Promise<FreezeEntry | undefined>;
|
|
129
|
+
list(route?: string): Promise<FreezeEntry[]>;
|
|
130
|
+
rename(id: string, name: string): Promise<boolean>;
|
|
131
|
+
delete(id: string): Promise<boolean>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Service marker for DevServerService.
|
|
136
|
+
* Use with `.withServices(DEV_SERVER_SERVICE)` in actions and components.
|
|
137
|
+
*/
|
|
138
|
+
declare const DEV_SERVER_SERVICE: _jay_framework_fullstack_component.ServiceMarker<DevServerService>;
|
|
139
|
+
interface RouteInfo {
|
|
140
|
+
path: string;
|
|
141
|
+
jayHtmlPath: string;
|
|
142
|
+
compPath: string;
|
|
143
|
+
}
|
|
144
|
+
declare class DevServerService {
|
|
145
|
+
private routes;
|
|
146
|
+
private vite;
|
|
147
|
+
private pagesBase;
|
|
148
|
+
private projectBase;
|
|
149
|
+
private jayRollupConfig;
|
|
150
|
+
private _freezeStore?;
|
|
151
|
+
constructor(routes: DevServerRoute[], vite: ViteDevServer, pagesBase: string, projectBase: string, jayRollupConfig: JayRollupConfig, _freezeStore?: FreezeStore);
|
|
152
|
+
get freezeStore(): FreezeStore | undefined;
|
|
153
|
+
/** List all page routes in the project. */
|
|
154
|
+
listRoutes(): RouteInfo[];
|
|
155
|
+
/**
|
|
156
|
+
* Run loadParams for a route, yielding param batches as an async generator.
|
|
157
|
+
* Loads all page parts (page component + keyed headless components) and
|
|
158
|
+
* calls loadParams on each one that defines it.
|
|
159
|
+
*/
|
|
160
|
+
loadRouteParams(routePath: string): AsyncGenerator<Record<string, string>[]>;
|
|
161
|
+
}
|
|
162
|
+
|
|
106
163
|
interface DevServerRoute {
|
|
107
164
|
path: string;
|
|
108
165
|
handler: RequestHandler;
|
|
@@ -113,6 +170,9 @@ interface DevServer {
|
|
|
113
170
|
viteServer: ViteDevServer;
|
|
114
171
|
routes: DevServerRoute[];
|
|
115
172
|
lifecycleManager: ServiceLifecycleManager;
|
|
173
|
+
freezeStore?: FreezeStore;
|
|
174
|
+
/** Public API for design board applications and CLI (DL#128) */
|
|
175
|
+
service: DevServerService;
|
|
116
176
|
}
|
|
117
177
|
declare function mkDevServer(rawOptions: DevServerOptions): Promise<DevServer>;
|
|
118
178
|
|
|
@@ -160,10 +220,20 @@ interface ActionRouterOptions {
|
|
|
160
220
|
*/
|
|
161
221
|
declare function createActionRouter(options?: ActionRouterOptions): RequestHandler$1;
|
|
162
222
|
/**
|
|
163
|
-
*
|
|
223
|
+
* Options for the action body parser middleware.
|
|
224
|
+
*/
|
|
225
|
+
interface ActionBodyParserOptions {
|
|
226
|
+
/** Build folder for temp file storage (DL#131) */
|
|
227
|
+
buildFolder: string;
|
|
228
|
+
/** Action registry to check for acceptsFiles (default: global actionRegistry) */
|
|
229
|
+
registry?: ActionRegistry;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Express middleware to parse request body for action requests.
|
|
233
|
+
* Supports JSON (default) and multipart/form-data (for actions with .withFiles()).
|
|
164
234
|
* Should be applied before the action router.
|
|
165
235
|
*/
|
|
166
|
-
declare function actionBodyParser(): RequestHandler$1;
|
|
236
|
+
declare function actionBodyParser(options: ActionBodyParserOptions): RequestHandler$1;
|
|
167
237
|
|
|
168
238
|
/**
|
|
169
239
|
* Vite Factory
|
|
@@ -207,4 +277,4 @@ declare function createViteForCli(options: {
|
|
|
207
277
|
tsConfigFilePath?: string;
|
|
208
278
|
}): Promise<ViteDevServer>;
|
|
209
279
|
|
|
210
|
-
export { ACTION_ENDPOINT_BASE, type ActionRouterOptions, type CreateViteServerOptions, type DevServer, type DevServerOptions, type DevServerRoute, actionBodyParser, createActionRouter, createViteForCli, createViteServer, mkDevServer };
|
|
280
|
+
export { ACTION_ENDPOINT_BASE, type ActionBodyParserOptions, type ActionRouterOptions, type CreateViteServerOptions, DEV_SERVER_SERVICE, type DevServer, type DevServerOptions, type DevServerRoute, DevServerService, type FreezeEntry, FreezeStore, type RouteInfo, actionBodyParser, createActionRouter, createViteForCli, createViteServer, mkDevServer };
|
package/dist/index.js
CHANGED
|
@@ -14,10 +14,18 @@ import { JAY_IMPORT_RESOLVER, injectHeadfullFSTemplates, parseContract, slowRend
|
|
|
14
14
|
import { getLogger, getDevLogger } from "@jay-framework/logger";
|
|
15
15
|
import { createRequire as createRequire$1 } from "node:module";
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
|
-
import
|
|
18
|
-
import {
|
|
19
|
-
import
|
|
17
|
+
import fs__default$1 from "node:fs";
|
|
18
|
+
import { scanRoutes, createRoute, routeToExpressRoute } from "@jay-framework/stack-route-scanner";
|
|
19
|
+
import { discoverPluginsWithInit, sortPluginsByDependencies, executePluginServerInits, runInitCallbacks, actionRegistry, discoverAndRegisterActions, discoverAllPluginActions, runShutdownCallbacks, clearLifecycleCallbacks, clearServiceRegistry, clearClientInitData, loadPageParts, runLoadParams, DevSlowlyChangingPhase, SlowRenderCache, preparePluginClientInits, registerService, clearServerElementCache, scanPlugins, getServiceRegistry, materializeContracts, renderFastChangingData, mergeHeadTags, generateClientScript, getClientInitData, generateSSRPageHtml, generateFrozenPageHtml, validateForEachInstances, slowRenderInstances } from "@jay-framework/stack-server-runtime";
|
|
20
|
+
import * as fs$1 from "node:fs/promises";
|
|
21
|
+
import fs__default from "node:fs/promises";
|
|
20
22
|
import { pathToFileURL } from "node:url";
|
|
23
|
+
import Busboy from "busboy";
|
|
24
|
+
import fs$2 from "fs";
|
|
25
|
+
import path$1 from "path";
|
|
26
|
+
import crypto from "crypto";
|
|
27
|
+
import { randomUUID } from "node:crypto";
|
|
28
|
+
import { createJayService } from "@jay-framework/fullstack-component";
|
|
21
29
|
const s$1 = createRequire(import.meta.url), e$1 = s$1("typescript"), c$1 = new Proxy(e$1, {
|
|
22
30
|
get(t, r) {
|
|
23
31
|
return t[r];
|
|
@@ -911,6 +919,7 @@ function extractActionFromExpression(node) {
|
|
|
911
919
|
let current = node;
|
|
912
920
|
let method = "POST";
|
|
913
921
|
let explicitMethod = null;
|
|
922
|
+
let acceptsFiles = false;
|
|
914
923
|
while (c$1.isCallExpression(current)) {
|
|
915
924
|
const expr = current.expression;
|
|
916
925
|
if (c$1.isPropertyAccessExpression(expr) && expr.name.text === "withMethod") {
|
|
@@ -921,22 +930,36 @@ function extractActionFromExpression(node) {
|
|
|
921
930
|
current = expr.expression;
|
|
922
931
|
continue;
|
|
923
932
|
}
|
|
933
|
+
if (c$1.isPropertyAccessExpression(expr) && expr.name.text === "withFiles") {
|
|
934
|
+
acceptsFiles = true;
|
|
935
|
+
current = expr.expression;
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
924
938
|
if (c$1.isPropertyAccessExpression(expr) && ["withServices", "withCaching", "withHandler", "withTimeout"].includes(expr.name.text)) {
|
|
925
939
|
current = expr.expression;
|
|
926
940
|
continue;
|
|
927
941
|
}
|
|
928
942
|
if (c$1.isIdentifier(expr)) {
|
|
929
943
|
const funcName = expr.text;
|
|
930
|
-
if (funcName === "makeJayAction" || funcName === "makeJayQuery") {
|
|
944
|
+
if (funcName === "makeJayAction" || funcName === "makeJayQuery" || funcName === "makeJayStream") {
|
|
931
945
|
const nameArg = current.arguments[0];
|
|
932
946
|
if (nameArg && c$1.isStringLiteral(nameArg)) {
|
|
947
|
+
if (funcName === "makeJayStream") {
|
|
948
|
+
return {
|
|
949
|
+
actionName: nameArg.text,
|
|
950
|
+
method: "POST",
|
|
951
|
+
isStreaming: true,
|
|
952
|
+
...acceptsFiles && { acceptsFiles: true }
|
|
953
|
+
};
|
|
954
|
+
}
|
|
933
955
|
method = funcName === "makeJayQuery" ? "GET" : "POST";
|
|
934
956
|
if (explicitMethod) {
|
|
935
957
|
method = explicitMethod;
|
|
936
958
|
}
|
|
937
959
|
return {
|
|
938
960
|
actionName: nameArg.text,
|
|
939
|
-
method
|
|
961
|
+
method,
|
|
962
|
+
...acceptsFiles && { acceptsFiles: true }
|
|
940
963
|
};
|
|
941
964
|
}
|
|
942
965
|
}
|
|
@@ -1331,14 +1354,28 @@ function jayStackCompiler(options = {}) {
|
|
|
1331
1354
|
getLogger().warn(`[action-transform] No actions found in ${actualPath}`);
|
|
1332
1355
|
return null;
|
|
1333
1356
|
}
|
|
1357
|
+
const hasRegularActions = actions.some((a) => !a.isStreaming);
|
|
1358
|
+
const hasStreamActions = actions.some((a) => a.isStreaming);
|
|
1359
|
+
const importNames = [];
|
|
1360
|
+
if (hasRegularActions)
|
|
1361
|
+
importNames.push("createActionCaller");
|
|
1362
|
+
if (hasStreamActions)
|
|
1363
|
+
importNames.push("createStreamCaller");
|
|
1334
1364
|
const lines = [
|
|
1335
|
-
`import {
|
|
1365
|
+
`import { ${importNames.join(", ")} } from '@jay-framework/stack-client-runtime';`,
|
|
1336
1366
|
""
|
|
1337
1367
|
];
|
|
1338
1368
|
for (const action of actions) {
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1369
|
+
const filesOpt = action.acceptsFiles ? ", { acceptsFiles: true }" : "";
|
|
1370
|
+
if (action.isStreaming) {
|
|
1371
|
+
lines.push(
|
|
1372
|
+
`export const ${action.exportName} = createStreamCaller('${action.actionName}'${filesOpt});`
|
|
1373
|
+
);
|
|
1374
|
+
} else {
|
|
1375
|
+
lines.push(
|
|
1376
|
+
`export const ${action.exportName} = createActionCaller('${action.actionName}', '${action.method}'${filesOpt});`
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1342
1379
|
}
|
|
1343
1380
|
if (code.includes("ActionError")) {
|
|
1344
1381
|
lines.push(
|
|
@@ -1758,7 +1795,30 @@ function createActionRouter(options) {
|
|
|
1758
1795
|
requestMethod,
|
|
1759
1796
|
ACTION_ENDPOINT_BASE + "/" + actionName
|
|
1760
1797
|
);
|
|
1798
|
+
const tempDir = req._jayTempDir;
|
|
1799
|
+
const cleanup = () => {
|
|
1800
|
+
if (tempDir)
|
|
1801
|
+
cleanupTempDir(tempDir);
|
|
1802
|
+
};
|
|
1803
|
+
if (registry.isStreaming(actionName)) {
|
|
1804
|
+
res.setHeader("Content-Type", "application/x-ndjson");
|
|
1805
|
+
res.setHeader("Transfer-Encoding", "chunked");
|
|
1806
|
+
try {
|
|
1807
|
+
const generator = registry.executeStream(actionName, input);
|
|
1808
|
+
for await (const chunk of generator) {
|
|
1809
|
+
res.write(JSON.stringify({ chunk }) + "\n");
|
|
1810
|
+
}
|
|
1811
|
+
res.write(JSON.stringify({ done: true }) + "\n");
|
|
1812
|
+
} catch (err) {
|
|
1813
|
+
res.write(JSON.stringify({ error: err.message }) + "\n");
|
|
1814
|
+
}
|
|
1815
|
+
cleanup();
|
|
1816
|
+
res.end();
|
|
1817
|
+
timing?.end();
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1761
1820
|
const result = await registry.execute(actionName, input);
|
|
1821
|
+
cleanup();
|
|
1762
1822
|
if (requestMethod === "GET" && result.success) {
|
|
1763
1823
|
const cacheHeaders = registry.getCacheHeaders(actionName);
|
|
1764
1824
|
if (cacheHeaders) {
|
|
@@ -1799,7 +1859,106 @@ function getStatusCodeForError(code, isActionError) {
|
|
|
1799
1859
|
return 500;
|
|
1800
1860
|
}
|
|
1801
1861
|
}
|
|
1802
|
-
|
|
1862
|
+
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
1863
|
+
const DEFAULT_MAX_FILES = 10;
|
|
1864
|
+
function parseMultipart(req, tempDir, maxFileSize, maxFiles) {
|
|
1865
|
+
return new Promise((resolve, reject) => {
|
|
1866
|
+
fs$2.mkdirSync(tempDir, { recursive: true });
|
|
1867
|
+
const files = {};
|
|
1868
|
+
let jsonData = {};
|
|
1869
|
+
let fileCount = 0;
|
|
1870
|
+
let errored = false;
|
|
1871
|
+
const pendingWrites = [];
|
|
1872
|
+
const bb = Busboy({
|
|
1873
|
+
headers: req.headers,
|
|
1874
|
+
limits: {
|
|
1875
|
+
fileSize: maxFileSize,
|
|
1876
|
+
files: maxFiles
|
|
1877
|
+
}
|
|
1878
|
+
});
|
|
1879
|
+
bb.on("file", (fieldname, stream, info) => {
|
|
1880
|
+
if (errored)
|
|
1881
|
+
return;
|
|
1882
|
+
fileCount++;
|
|
1883
|
+
const filename = info.filename || `upload-${fileCount}`;
|
|
1884
|
+
const tempPath = path$1.join(tempDir, `${fileCount}-${filename}`);
|
|
1885
|
+
let size = 0;
|
|
1886
|
+
let truncated = false;
|
|
1887
|
+
const writeStream = fs$2.createWriteStream(tempPath);
|
|
1888
|
+
stream.pipe(writeStream);
|
|
1889
|
+
stream.on("data", (data) => {
|
|
1890
|
+
size += data.length;
|
|
1891
|
+
});
|
|
1892
|
+
stream.on("limit", () => {
|
|
1893
|
+
truncated = true;
|
|
1894
|
+
});
|
|
1895
|
+
pendingWrites.push(
|
|
1896
|
+
new Promise((resolveWrite, rejectWrite) => {
|
|
1897
|
+
writeStream.on("close", () => {
|
|
1898
|
+
if (truncated) {
|
|
1899
|
+
rejectWrite(
|
|
1900
|
+
new Error(
|
|
1901
|
+
`File "${filename}" exceeds maximum size of ${maxFileSize} bytes`
|
|
1902
|
+
)
|
|
1903
|
+
);
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
const jayFile = {
|
|
1907
|
+
name: filename,
|
|
1908
|
+
type: info.mimeType,
|
|
1909
|
+
size,
|
|
1910
|
+
path: tempPath
|
|
1911
|
+
};
|
|
1912
|
+
if (files[fieldname]) {
|
|
1913
|
+
const existing = files[fieldname];
|
|
1914
|
+
if (Array.isArray(existing)) {
|
|
1915
|
+
existing.push(jayFile);
|
|
1916
|
+
} else {
|
|
1917
|
+
files[fieldname] = [existing, jayFile];
|
|
1918
|
+
}
|
|
1919
|
+
} else {
|
|
1920
|
+
files[fieldname] = jayFile;
|
|
1921
|
+
}
|
|
1922
|
+
resolveWrite();
|
|
1923
|
+
});
|
|
1924
|
+
})
|
|
1925
|
+
);
|
|
1926
|
+
});
|
|
1927
|
+
bb.on("field", (fieldname, value) => {
|
|
1928
|
+
if (errored)
|
|
1929
|
+
return;
|
|
1930
|
+
if (fieldname === "_json") {
|
|
1931
|
+
try {
|
|
1932
|
+
jsonData = JSON.parse(value);
|
|
1933
|
+
} catch {
|
|
1934
|
+
errored = true;
|
|
1935
|
+
reject(new Error("Invalid JSON in _json field"));
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
bb.on("close", () => {
|
|
1940
|
+
if (errored)
|
|
1941
|
+
return;
|
|
1942
|
+
Promise.all(pendingWrites).then(() => resolve({ body: { ...jsonData, ...files }, tempDir })).catch((err) => reject(err));
|
|
1943
|
+
});
|
|
1944
|
+
bb.on("error", (err) => {
|
|
1945
|
+
if (!errored) {
|
|
1946
|
+
errored = true;
|
|
1947
|
+
reject(err);
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
req.pipe(bb);
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
function cleanupTempDir(dir) {
|
|
1954
|
+
try {
|
|
1955
|
+
fs$2.rmSync(dir, { recursive: true, force: true });
|
|
1956
|
+
} catch {
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
function actionBodyParser(options) {
|
|
1960
|
+
const { buildFolder, registry: reg } = options;
|
|
1961
|
+
const registryToUse = reg ?? actionRegistry;
|
|
1803
1962
|
return (req, res, next) => {
|
|
1804
1963
|
if (!req.path.startsWith(ACTION_ENDPOINT_BASE)) {
|
|
1805
1964
|
next();
|
|
@@ -1809,6 +1968,42 @@ function actionBodyParser() {
|
|
|
1809
1968
|
next();
|
|
1810
1969
|
return;
|
|
1811
1970
|
}
|
|
1971
|
+
const contentType = req.headers["content-type"] || "";
|
|
1972
|
+
if (contentType.startsWith("multipart/form-data")) {
|
|
1973
|
+
const actionName = req.path.slice(ACTION_ENDPOINT_BASE.length + 1);
|
|
1974
|
+
const action = registryToUse.get(actionName);
|
|
1975
|
+
if (!action?.acceptsFiles) {
|
|
1976
|
+
res.status(400).json({
|
|
1977
|
+
success: false,
|
|
1978
|
+
error: {
|
|
1979
|
+
code: "FILES_NOT_ACCEPTED",
|
|
1980
|
+
message: `Action '${actionName}' does not accept file uploads. Use .withFiles() on the action builder.`,
|
|
1981
|
+
isActionError: false
|
|
1982
|
+
}
|
|
1983
|
+
});
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
const requestId = crypto.randomUUID();
|
|
1987
|
+
const tempDir = path$1.join(buildFolder, ".tmp", "actions", requestId);
|
|
1988
|
+
const maxFileSize = action.fileOptions?.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
1989
|
+
const maxFiles = action.fileOptions?.maxFiles ?? DEFAULT_MAX_FILES;
|
|
1990
|
+
parseMultipart(req, tempDir, maxFileSize, maxFiles).then(({ body: body2, tempDir: td }) => {
|
|
1991
|
+
req.body = body2;
|
|
1992
|
+
req._jayTempDir = td;
|
|
1993
|
+
next();
|
|
1994
|
+
}).catch((err) => {
|
|
1995
|
+
cleanupTempDir(tempDir);
|
|
1996
|
+
res.status(400).json({
|
|
1997
|
+
success: false,
|
|
1998
|
+
error: {
|
|
1999
|
+
code: "MULTIPART_PARSE_ERROR",
|
|
2000
|
+
message: err.message,
|
|
2001
|
+
isActionError: false
|
|
2002
|
+
}
|
|
2003
|
+
});
|
|
2004
|
+
});
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
1812
2007
|
let body = "";
|
|
1813
2008
|
req.setEncoding("utf8");
|
|
1814
2009
|
req.on("data", (chunk) => {
|
|
@@ -1831,6 +2026,124 @@ function actionBodyParser() {
|
|
|
1831
2026
|
});
|
|
1832
2027
|
};
|
|
1833
2028
|
}
|
|
2029
|
+
class FreezeStore {
|
|
2030
|
+
constructor(buildFolder) {
|
|
2031
|
+
__publicField(this, "dir");
|
|
2032
|
+
this.dir = path.join(buildFolder, "freezes");
|
|
2033
|
+
}
|
|
2034
|
+
async save(route, viewState, routePattern) {
|
|
2035
|
+
await fs$1.mkdir(this.dir, { recursive: true });
|
|
2036
|
+
const entry = {
|
|
2037
|
+
id: randomUUID().slice(0, 8),
|
|
2038
|
+
route,
|
|
2039
|
+
...routePattern && { routePattern },
|
|
2040
|
+
viewState,
|
|
2041
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2042
|
+
};
|
|
2043
|
+
await fs$1.writeFile(
|
|
2044
|
+
path.join(this.dir, `${entry.id}.json`),
|
|
2045
|
+
JSON.stringify(entry, null, 2),
|
|
2046
|
+
"utf-8"
|
|
2047
|
+
);
|
|
2048
|
+
return entry;
|
|
2049
|
+
}
|
|
2050
|
+
async get(id) {
|
|
2051
|
+
try {
|
|
2052
|
+
const content = await fs$1.readFile(path.join(this.dir, `${id}.json`), "utf-8");
|
|
2053
|
+
return JSON.parse(content);
|
|
2054
|
+
} catch {
|
|
2055
|
+
return void 0;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
async list(route) {
|
|
2059
|
+
try {
|
|
2060
|
+
const files = await fs$1.readdir(this.dir);
|
|
2061
|
+
const entries = [];
|
|
2062
|
+
for (const file of files) {
|
|
2063
|
+
if (!file.endsWith(".json"))
|
|
2064
|
+
continue;
|
|
2065
|
+
try {
|
|
2066
|
+
const content = await fs$1.readFile(path.join(this.dir, file), "utf-8");
|
|
2067
|
+
const entry = JSON.parse(content);
|
|
2068
|
+
if (!route || entry.routePattern === route || entry.route === route) {
|
|
2069
|
+
entries.push(entry);
|
|
2070
|
+
}
|
|
2071
|
+
} catch {
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
return entries.sort(
|
|
2075
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
2076
|
+
);
|
|
2077
|
+
} catch {
|
|
2078
|
+
return [];
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
async rename(id, name) {
|
|
2082
|
+
const entry = await this.get(id);
|
|
2083
|
+
if (!entry)
|
|
2084
|
+
return false;
|
|
2085
|
+
entry.name = name;
|
|
2086
|
+
await fs$1.writeFile(
|
|
2087
|
+
path.join(this.dir, `${id}.json`),
|
|
2088
|
+
JSON.stringify(entry, null, 2),
|
|
2089
|
+
"utf-8"
|
|
2090
|
+
);
|
|
2091
|
+
return true;
|
|
2092
|
+
}
|
|
2093
|
+
async delete(id) {
|
|
2094
|
+
try {
|
|
2095
|
+
await fs$1.unlink(path.join(this.dir, `${id}.json`));
|
|
2096
|
+
return true;
|
|
2097
|
+
} catch {
|
|
2098
|
+
return false;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
const DEV_SERVER_SERVICE = createJayService("DevServerService");
|
|
2103
|
+
class DevServerService {
|
|
2104
|
+
constructor(routes, vite, pagesBase, projectBase, jayRollupConfig, _freezeStore) {
|
|
2105
|
+
this.routes = routes;
|
|
2106
|
+
this.vite = vite;
|
|
2107
|
+
this.pagesBase = pagesBase;
|
|
2108
|
+
this.projectBase = projectBase;
|
|
2109
|
+
this.jayRollupConfig = jayRollupConfig;
|
|
2110
|
+
this._freezeStore = _freezeStore;
|
|
2111
|
+
}
|
|
2112
|
+
get freezeStore() {
|
|
2113
|
+
return this._freezeStore;
|
|
2114
|
+
}
|
|
2115
|
+
/** List all page routes in the project. */
|
|
2116
|
+
listRoutes() {
|
|
2117
|
+
return this.routes.map((r) => ({
|
|
2118
|
+
path: r.path,
|
|
2119
|
+
jayHtmlPath: r.fsRoute.jayHtmlPath,
|
|
2120
|
+
compPath: r.fsRoute.compPath
|
|
2121
|
+
}));
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Run loadParams for a route, yielding param batches as an async generator.
|
|
2125
|
+
* Loads all page parts (page component + keyed headless components) and
|
|
2126
|
+
* calls loadParams on each one that defines it.
|
|
2127
|
+
*/
|
|
2128
|
+
async *loadRouteParams(routePath) {
|
|
2129
|
+
const matched = this.routes.find((r) => r.path === routePath);
|
|
2130
|
+
if (!matched) {
|
|
2131
|
+
getLogger().error(`[loadRouteParams] Route [${routePath}] not found`);
|
|
2132
|
+
throw new Error(`Route "${routePath}" not found`);
|
|
2133
|
+
}
|
|
2134
|
+
const loaded = await loadPageParts(
|
|
2135
|
+
this.vite,
|
|
2136
|
+
matched.fsRoute,
|
|
2137
|
+
this.pagesBase,
|
|
2138
|
+
this.projectBase,
|
|
2139
|
+
this.jayRollupConfig
|
|
2140
|
+
);
|
|
2141
|
+
if (!loaded.val) {
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
yield* runLoadParams(loaded.val.parts);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
1834
2147
|
let _watchLinkedFiles = () => {
|
|
1835
2148
|
};
|
|
1836
2149
|
async function initRoutes(pagesBaseFolder) {
|
|
@@ -1839,6 +2152,82 @@ async function initRoutes(pagesBaseFolder) {
|
|
|
1839
2152
|
compFilename: "page.ts"
|
|
1840
2153
|
});
|
|
1841
2154
|
}
|
|
2155
|
+
async function scanPluginRoutes(projectRoot, projectRoutes) {
|
|
2156
|
+
const plugins = await scanPlugins({ projectRoot, includeDevDeps: true });
|
|
2157
|
+
const projectPaths = new Set(projectRoutes.map((r) => r.rawRoute));
|
|
2158
|
+
const pluginRoutes = [];
|
|
2159
|
+
for (const [, plugin] of plugins) {
|
|
2160
|
+
if (!plugin.manifest.routes)
|
|
2161
|
+
continue;
|
|
2162
|
+
for (const route of plugin.manifest.routes) {
|
|
2163
|
+
if (projectPaths.has(route.path)) {
|
|
2164
|
+
getLogger().info(
|
|
2165
|
+
`[Routes] Plugin "${plugin.name}" route ${route.path} skipped — project route takes precedence`
|
|
2166
|
+
);
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
const jayHtmlPath = resolvePluginExport(plugin.pluginPath, route.jayHtml);
|
|
2170
|
+
if (!jayHtmlPath) {
|
|
2171
|
+
getLogger().warn(
|
|
2172
|
+
`[Routes] Plugin "${plugin.name}" route ${route.path}: jayHtml "${route.jayHtml}" not found`
|
|
2173
|
+
);
|
|
2174
|
+
continue;
|
|
2175
|
+
}
|
|
2176
|
+
const compPath = route.component.startsWith(".") ? path__default.resolve(plugin.pluginPath, route.component) : resolvePluginModule(plugin);
|
|
2177
|
+
pluginRoutes.push(createRoute(route.path, jayHtmlPath, compPath));
|
|
2178
|
+
getLogger().info(`[Routes] Plugin "${plugin.name}" provides route ${route.path}`);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
return pluginRoutes;
|
|
2182
|
+
}
|
|
2183
|
+
function resolvePluginExport(pluginPath, exportSubpath) {
|
|
2184
|
+
const normalized = exportSubpath.replace(/^\.\//, "");
|
|
2185
|
+
const packageJsonPath = path__default.join(pluginPath, "package.json");
|
|
2186
|
+
try {
|
|
2187
|
+
const packageJson = JSON.parse(fs__default$1.readFileSync(packageJsonPath, "utf-8"));
|
|
2188
|
+
if (packageJson.exports) {
|
|
2189
|
+
const exportKey = "./" + normalized;
|
|
2190
|
+
const exportValue = packageJson.exports[exportKey];
|
|
2191
|
+
if (exportValue) {
|
|
2192
|
+
const resolved = typeof exportValue === "string" ? exportValue : exportValue.default || exportValue.import || exportValue.require;
|
|
2193
|
+
if (resolved) {
|
|
2194
|
+
const fullPath = path__default.join(pluginPath, resolved);
|
|
2195
|
+
return fullPath;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
} catch {
|
|
2200
|
+
}
|
|
2201
|
+
for (const dir of ["dist", "lib", ""]) {
|
|
2202
|
+
const candidate = path__default.join(pluginPath, dir, normalized);
|
|
2203
|
+
try {
|
|
2204
|
+
fs__default$1.accessSync(candidate);
|
|
2205
|
+
return candidate;
|
|
2206
|
+
} catch {
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
return void 0;
|
|
2210
|
+
}
|
|
2211
|
+
function resolvePluginModule(plugin) {
|
|
2212
|
+
const modulePath = plugin.manifest.module || "index";
|
|
2213
|
+
for (const ext of [".ts", ".js", "/index.ts", "/index.js"]) {
|
|
2214
|
+
const candidate = path__default.join(plugin.pluginPath, modulePath + ext);
|
|
2215
|
+
try {
|
|
2216
|
+
fs__default$1.accessSync(candidate);
|
|
2217
|
+
return candidate;
|
|
2218
|
+
} catch {
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
for (const ext of [".ts", ".js"]) {
|
|
2222
|
+
const candidate = path__default.join(plugin.pluginPath, "lib", path__default.basename(modulePath) + ext);
|
|
2223
|
+
try {
|
|
2224
|
+
fs__default$1.accessSync(candidate);
|
|
2225
|
+
return candidate;
|
|
2226
|
+
} catch {
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
return path__default.join(plugin.pluginPath, modulePath);
|
|
2230
|
+
}
|
|
1842
2231
|
function defaults(options) {
|
|
1843
2232
|
const publicBaseUrlPath = options.publicBaseUrlPath || process.env.BASE || "/";
|
|
1844
2233
|
const projectRootFolder = options.projectRootFolder || ".";
|
|
@@ -1900,7 +2289,7 @@ function filterPluginsForPage(allPluginClientInits, allPluginsWithInit, usedPack
|
|
|
1900
2289
|
return pluginInfo && expandedPackages.has(pluginInfo.packageName);
|
|
1901
2290
|
});
|
|
1902
2291
|
}
|
|
1903
|
-
function mkRoute(route, vite, slowlyPhase, options, slowRenderCache, projectInit, allPluginsWithInit = [], allPluginClientInits = []) {
|
|
2292
|
+
function mkRoute(route, vite, slowlyPhase, options, slowRenderCache, freezeStore, projectInit, allPluginsWithInit = [], allPluginClientInits = []) {
|
|
1904
2293
|
const routePath = routeToExpressRoute(route);
|
|
1905
2294
|
const handler = async (req, res) => {
|
|
1906
2295
|
const timing = getDevLogger()?.startRequest(req.method, req.path);
|
|
@@ -1916,6 +2305,23 @@ function mkRoute(route, vite, slowlyPhase, options, slowRenderCache, projectInit
|
|
|
1916
2305
|
for (const [key, value] of urlObj.searchParams) {
|
|
1917
2306
|
query[key] = value;
|
|
1918
2307
|
}
|
|
2308
|
+
const freezeId = query["_jay_freeze"];
|
|
2309
|
+
if (freezeId && freezeStore) {
|
|
2310
|
+
timing?.annotate("[FROZEN]");
|
|
2311
|
+
await handleFrozenRequest(
|
|
2312
|
+
vite,
|
|
2313
|
+
route,
|
|
2314
|
+
options,
|
|
2315
|
+
freezeStore,
|
|
2316
|
+
slowRenderCache,
|
|
2317
|
+
freezeId,
|
|
2318
|
+
pageParams,
|
|
2319
|
+
query["format"] === "fragment" ? "fragment" : "page",
|
|
2320
|
+
res,
|
|
2321
|
+
timing
|
|
2322
|
+
);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
1919
2325
|
if (options.disableSSR) {
|
|
1920
2326
|
await handleClientOnlyRequest(
|
|
1921
2327
|
vite,
|
|
@@ -2048,6 +2454,7 @@ async function handleCachedRequest(vite, route, options, cachedEntry, pageParams
|
|
|
2048
2454
|
projectInit,
|
|
2049
2455
|
pluginsForPage,
|
|
2050
2456
|
options,
|
|
2457
|
+
routeToExpressRoute(route),
|
|
2051
2458
|
cachedEntry.slowViewState,
|
|
2052
2459
|
timing,
|
|
2053
2460
|
cachedEntry.preRenderedContent,
|
|
@@ -2227,14 +2634,15 @@ async function handleClientOnlyRequest(vite, route, options, slowlyPhase, pagePa
|
|
|
2227
2634
|
projectInit,
|
|
2228
2635
|
pluginsForPage,
|
|
2229
2636
|
{
|
|
2230
|
-
enableAutomation: !options.disableAutomation
|
|
2637
|
+
enableAutomation: !options.disableAutomation,
|
|
2638
|
+
routePattern: routeToExpressRoute(route)
|
|
2231
2639
|
}
|
|
2232
2640
|
);
|
|
2233
2641
|
if (options.buildFolder) {
|
|
2234
2642
|
const pageName = !url || url === "/" ? "index" : url.replace(/^\//, "").replace(/\//g, "-");
|
|
2235
2643
|
const clientScriptDir = path__default.join(options.buildFolder, "debug", "client-entry");
|
|
2236
|
-
await
|
|
2237
|
-
await
|
|
2644
|
+
await fs__default.mkdir(clientScriptDir, { recursive: true });
|
|
2645
|
+
await fs__default.writeFile(path__default.join(clientScriptDir, `${pageName}.html`), pageHtml, "utf-8");
|
|
2238
2646
|
}
|
|
2239
2647
|
const viteStart = Date.now();
|
|
2240
2648
|
const compiledPageHtml = await vite.transformIndexHtml(!!url ? url : "/", pageHtml);
|
|
@@ -2242,11 +2650,11 @@ async function handleClientOnlyRequest(vite, route, options, slowlyPhase, pagePa
|
|
|
2242
2650
|
res.status(200).set({ "Content-Type": "text/html" }).send(compiledPageHtml);
|
|
2243
2651
|
timing?.end();
|
|
2244
2652
|
}
|
|
2245
|
-
async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, pageParts, viewState, carryForward, clientTrackByMap, projectInit, pluginsForPage, options, slowViewState, timing, preLoadedContent, headTags) {
|
|
2653
|
+
async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, pageParts, viewState, carryForward, clientTrackByMap, projectInit, pluginsForPage, options, routePattern, slowViewState, timing, preLoadedContent, headTags) {
|
|
2246
2654
|
let pageHtml;
|
|
2247
2655
|
const routeDir = path__default.dirname(path__default.relative(options.pagesRootFolder, sourceJayHtmlPath));
|
|
2248
2656
|
try {
|
|
2249
|
-
let jayHtmlContent = preLoadedContent ?? await
|
|
2657
|
+
let jayHtmlContent = preLoadedContent ?? await fs__default.readFile(jayHtmlPath, "utf-8");
|
|
2250
2658
|
const jayHtmlFilename = path__default.basename(jayHtmlPath);
|
|
2251
2659
|
const jayHtmlDir = path__default.dirname(jayHtmlPath);
|
|
2252
2660
|
const sourceDir = path__default.dirname(sourceJayHtmlPath);
|
|
@@ -2270,7 +2678,8 @@ async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, page
|
|
|
2270
2678
|
pluginsForPage,
|
|
2271
2679
|
{
|
|
2272
2680
|
enableAutomation: !options.disableAutomation,
|
|
2273
|
-
slowViewState
|
|
2681
|
+
slowViewState,
|
|
2682
|
+
routePattern
|
|
2274
2683
|
},
|
|
2275
2684
|
// Pass source directory for headfull FS file resolution when using pre-rendered path
|
|
2276
2685
|
jayHtmlDir !== sourceDir ? sourceDir : void 0,
|
|
@@ -2289,15 +2698,16 @@ async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, page
|
|
|
2289
2698
|
pluginsForPage,
|
|
2290
2699
|
{
|
|
2291
2700
|
enableAutomation: !options.disableAutomation,
|
|
2292
|
-
slowViewState
|
|
2701
|
+
slowViewState,
|
|
2702
|
+
routePattern
|
|
2293
2703
|
}
|
|
2294
2704
|
);
|
|
2295
2705
|
}
|
|
2296
2706
|
if (options.buildFolder) {
|
|
2297
2707
|
const pageName = !url || url === "/" ? "index" : url.replace(/^\//, "").replace(/\//g, "-");
|
|
2298
2708
|
const clientScriptDir = path__default.join(options.buildFolder, "debug", "client-entry");
|
|
2299
|
-
await
|
|
2300
|
-
await
|
|
2709
|
+
await fs__default.mkdir(clientScriptDir, { recursive: true });
|
|
2710
|
+
await fs__default.writeFile(path__default.join(clientScriptDir, `${pageName}.html`), pageHtml, "utf-8");
|
|
2301
2711
|
}
|
|
2302
2712
|
const viteStart = Date.now();
|
|
2303
2713
|
const compiledPageHtml = await vite.transformIndexHtml(!!url ? url : "/", pageHtml);
|
|
@@ -2305,12 +2715,62 @@ async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, page
|
|
|
2305
2715
|
res.status(200).set({ "Content-Type": "text/html" }).send(compiledPageHtml);
|
|
2306
2716
|
timing?.end();
|
|
2307
2717
|
}
|
|
2718
|
+
async function handleFrozenRequest(vite, route, options, freezeStore, slowRenderCache, freezeId, pageParams, format, res, timing) {
|
|
2719
|
+
const entry = await freezeStore.get(freezeId);
|
|
2720
|
+
if (!entry) {
|
|
2721
|
+
getLogger().warn(`[Freeze] Freeze "${freezeId}" not found`);
|
|
2722
|
+
res.status(404).send(`Freeze "${freezeId}" not found`);
|
|
2723
|
+
timing?.end();
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
const label = entry.name ? `"${entry.name}" (${freezeId})` : freezeId;
|
|
2727
|
+
getLogger().info(`[Freeze] Serving frozen page ${label} for ${route.rawRoute} [${format}]`);
|
|
2728
|
+
try {
|
|
2729
|
+
const cachedEntry = await slowRenderCache.get(route.jayHtmlPath, pageParams);
|
|
2730
|
+
const jayHtmlPath = cachedEntry?.preRenderedPath ?? route.jayHtmlPath;
|
|
2731
|
+
const jayHtmlContent = cachedEntry?.preRenderedContent ?? await fs__default.readFile(jayHtmlPath, "utf-8");
|
|
2732
|
+
const jayHtmlFilename = path__default.basename(jayHtmlPath);
|
|
2733
|
+
const jayHtmlDir = path__default.dirname(jayHtmlPath);
|
|
2734
|
+
const sourceDir = path__default.dirname(route.jayHtmlPath);
|
|
2735
|
+
const routeDir = path__default.dirname(path__default.relative(options.pagesRootFolder, route.jayHtmlPath));
|
|
2736
|
+
const { injectHeadfullFSTemplates: injectHeadfullFSTemplates2 } = await import("@jay-framework/compiler-jay-html");
|
|
2737
|
+
const { JAY_IMPORT_RESOLVER: JAY_IMPORT_RESOLVER2 } = await import("@jay-framework/compiler-jay-html");
|
|
2738
|
+
const fullJayHtml = injectHeadfullFSTemplates2(
|
|
2739
|
+
jayHtmlContent,
|
|
2740
|
+
sourceDir,
|
|
2741
|
+
JAY_IMPORT_RESOLVER2
|
|
2742
|
+
);
|
|
2743
|
+
const html = await generateFrozenPageHtml(
|
|
2744
|
+
vite,
|
|
2745
|
+
fullJayHtml,
|
|
2746
|
+
jayHtmlFilename,
|
|
2747
|
+
jayHtmlDir,
|
|
2748
|
+
entry.viewState,
|
|
2749
|
+
options.buildFolder,
|
|
2750
|
+
options.projectRootFolder,
|
|
2751
|
+
routeDir,
|
|
2752
|
+
options.jayRollupConfig?.tsConfigFilePath,
|
|
2753
|
+
void 0,
|
|
2754
|
+
format,
|
|
2755
|
+
entry.name
|
|
2756
|
+
);
|
|
2757
|
+
const headers = { "Content-Type": "text/html" };
|
|
2758
|
+
if (format === "fragment") {
|
|
2759
|
+
headers["Access-Control-Allow-Origin"] = "*";
|
|
2760
|
+
}
|
|
2761
|
+
res.status(200).set(headers).send(html);
|
|
2762
|
+
} catch (err) {
|
|
2763
|
+
getLogger().warn(`[Freeze] Failed to render frozen page: ${err.message}`);
|
|
2764
|
+
res.status(500).send(`Failed to render frozen page: ${err.message}`);
|
|
2765
|
+
}
|
|
2766
|
+
timing?.end();
|
|
2767
|
+
}
|
|
2308
2768
|
async function preRenderJayHtml(route, slowViewState, headlessContracts, headlessInstanceComponents, partKeys = []) {
|
|
2309
|
-
const jayHtmlContent = await
|
|
2769
|
+
const jayHtmlContent = await fs__default.readFile(route.jayHtmlPath, "utf-8");
|
|
2310
2770
|
const contractPath = route.jayHtmlPath.replace(".jay-html", ".jay-contract");
|
|
2311
2771
|
let contract;
|
|
2312
2772
|
try {
|
|
2313
|
-
const contractContent = await
|
|
2773
|
+
const contractContent = await fs__default.readFile(contractPath, "utf-8");
|
|
2314
2774
|
const parseResult = parseContract(contractContent, path__default.basename(contractPath));
|
|
2315
2775
|
if (parseResult.val) {
|
|
2316
2776
|
contract = parseResult.val;
|
|
@@ -2452,8 +2912,15 @@ async function mkDevServer(rawOptions) {
|
|
|
2452
2912
|
const options = defaults(rawOptions);
|
|
2453
2913
|
const { publicBaseUrlPath, pagesRootFolder, projectRootFolder, buildFolder, jayRollupConfig } = options;
|
|
2454
2914
|
if (buildFolder) {
|
|
2455
|
-
|
|
2456
|
-
|
|
2915
|
+
try {
|
|
2916
|
+
const entries = await fs__default.readdir(buildFolder).catch(() => []);
|
|
2917
|
+
for (const entry of entries) {
|
|
2918
|
+
if (entry === "freezes")
|
|
2919
|
+
continue;
|
|
2920
|
+
await fs__default.rm(path__default.join(buildFolder, entry), { recursive: true, force: true });
|
|
2921
|
+
}
|
|
2922
|
+
} catch {
|
|
2923
|
+
}
|
|
2457
2924
|
}
|
|
2458
2925
|
const viteLogLevel = options.logLevel === "silent" ? "silent" : options.logLevel === "verbose" ? "info" : "warn";
|
|
2459
2926
|
const lifecycleManager = new ServiceLifecycleManager(projectRootFolder);
|
|
@@ -2470,9 +2937,11 @@ async function mkDevServer(rawOptions) {
|
|
|
2470
2937
|
await lifecycleManager.initialize();
|
|
2471
2938
|
await materializeDynamicContracts(projectRootFolder, vite);
|
|
2472
2939
|
setupServiceHotReload(vite, lifecycleManager);
|
|
2473
|
-
setupActionRouter(vite);
|
|
2474
|
-
const
|
|
2475
|
-
const
|
|
2940
|
+
setupActionRouter(vite, buildFolder);
|
|
2941
|
+
const projectRoutes = await initRoutes(pagesRootFolder);
|
|
2942
|
+
const filteredProjectRoutes = buildFolder ? projectRoutes.filter((route) => !route.jayHtmlPath.startsWith(buildFolder)) : projectRoutes;
|
|
2943
|
+
const pluginRoutes = await scanPluginRoutes(projectRootFolder, filteredProjectRoutes);
|
|
2944
|
+
const routes = [...filteredProjectRoutes, ...pluginRoutes];
|
|
2476
2945
|
const slowlyPhase = new DevSlowlyChangingPhase();
|
|
2477
2946
|
const slowRenderCacheDir = path__default.join(buildFolder, "pre-rendered");
|
|
2478
2947
|
const slowRenderCache = new SlowRenderCache(slowRenderCacheDir, pagesRootFolder);
|
|
@@ -2484,6 +2953,10 @@ async function mkDevServer(rawOptions) {
|
|
|
2484
2953
|
const projectInit = lifecycleManager.getProjectInit() ?? void 0;
|
|
2485
2954
|
const pluginsWithInit = lifecycleManager.getPluginsWithInit();
|
|
2486
2955
|
const pluginClientInits = preparePluginClientInits(pluginsWithInit);
|
|
2956
|
+
const freezeStore = buildFolder ? new FreezeStore(buildFolder) : void 0;
|
|
2957
|
+
if (freezeStore) {
|
|
2958
|
+
setupFreezeEndpoint(vite, freezeStore);
|
|
2959
|
+
}
|
|
2487
2960
|
const devServerRoutes = routes.map(
|
|
2488
2961
|
(route) => mkRoute(
|
|
2489
2962
|
route,
|
|
@@ -2491,16 +2964,28 @@ async function mkDevServer(rawOptions) {
|
|
|
2491
2964
|
slowlyPhase,
|
|
2492
2965
|
options,
|
|
2493
2966
|
slowRenderCache,
|
|
2967
|
+
freezeStore,
|
|
2494
2968
|
projectInit,
|
|
2495
2969
|
pluginsWithInit,
|
|
2496
2970
|
pluginClientInits
|
|
2497
2971
|
)
|
|
2498
2972
|
);
|
|
2973
|
+
const service = new DevServerService(
|
|
2974
|
+
devServerRoutes,
|
|
2975
|
+
vite,
|
|
2976
|
+
options.pagesRootFolder,
|
|
2977
|
+
options.projectRootFolder,
|
|
2978
|
+
options.jayRollupConfig,
|
|
2979
|
+
freezeStore
|
|
2980
|
+
);
|
|
2981
|
+
registerService(DEV_SERVER_SERVICE, service);
|
|
2499
2982
|
return {
|
|
2500
2983
|
server: vite.middlewares,
|
|
2501
2984
|
viteServer: vite,
|
|
2502
2985
|
routes: devServerRoutes,
|
|
2503
|
-
lifecycleManager
|
|
2986
|
+
lifecycleManager,
|
|
2987
|
+
freezeStore,
|
|
2988
|
+
service
|
|
2504
2989
|
};
|
|
2505
2990
|
}
|
|
2506
2991
|
function setupGracefulShutdown(lifecycleManager) {
|
|
@@ -2534,11 +3019,40 @@ function setupServiceHotReload(vite, lifecycleManager) {
|
|
|
2534
3019
|
}
|
|
2535
3020
|
});
|
|
2536
3021
|
}
|
|
2537
|
-
function setupActionRouter(vite) {
|
|
2538
|
-
vite.middlewares.use(actionBodyParser());
|
|
3022
|
+
function setupActionRouter(vite, buildFolder) {
|
|
3023
|
+
vite.middlewares.use(actionBodyParser({ buildFolder }));
|
|
2539
3024
|
vite.middlewares.use(ACTION_ENDPOINT_BASE, createActionRouter());
|
|
2540
3025
|
getLogger().info(`[Actions] Action router mounted at ${ACTION_ENDPOINT_BASE}`);
|
|
2541
3026
|
}
|
|
3027
|
+
function setupFreezeEndpoint(vite, freezeStore) {
|
|
3028
|
+
vite.middlewares.use((req, res, next) => {
|
|
3029
|
+
if (req.method === "POST" && (req.url === "/_jay/freeze" || req.originalUrl === "/_jay/freeze")) {
|
|
3030
|
+
let body = "";
|
|
3031
|
+
req.on("data", (chunk) => body += chunk);
|
|
3032
|
+
req.on("end", async () => {
|
|
3033
|
+
try {
|
|
3034
|
+
const { route, routePattern, viewState } = JSON.parse(body);
|
|
3035
|
+
if (!route || !viewState) {
|
|
3036
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3037
|
+
res.end(JSON.stringify({ error: "Missing route or viewState" }));
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3040
|
+
const entry = await freezeStore.save(route, viewState, routePattern);
|
|
3041
|
+
getLogger().info(`[Freeze] Saved freeze "${entry.id}" for ${route}`);
|
|
3042
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3043
|
+
res.end(JSON.stringify(entry));
|
|
3044
|
+
} catch (err) {
|
|
3045
|
+
getLogger().warn(`[Freeze] Failed to save: ${err.message}`);
|
|
3046
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
3047
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
3048
|
+
}
|
|
3049
|
+
});
|
|
3050
|
+
} else {
|
|
3051
|
+
next();
|
|
3052
|
+
}
|
|
3053
|
+
});
|
|
3054
|
+
getLogger().info("[Freeze] Freeze endpoint mounted at /_jay/freeze");
|
|
3055
|
+
}
|
|
2542
3056
|
function setupSlowRenderCacheInvalidation(vite, cache, pagesRootFolder, projectRootFolder) {
|
|
2543
3057
|
const watchedFiles = /* @__PURE__ */ new Set();
|
|
2544
3058
|
const watchLinkedFiles = (files) => {
|
|
@@ -2600,6 +3114,9 @@ function setupSlowRenderCacheInvalidation(vite, cache, pagesRootFolder, projectR
|
|
|
2600
3114
|
}
|
|
2601
3115
|
export {
|
|
2602
3116
|
ACTION_ENDPOINT_BASE,
|
|
3117
|
+
DEV_SERVER_SERVICE,
|
|
3118
|
+
DevServerService,
|
|
3119
|
+
FreezeStore,
|
|
2603
3120
|
actionBodyParser,
|
|
2604
3121
|
createActionRouter,
|
|
2605
3122
|
createViteForCli,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jay-framework/dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,23 +23,25 @@
|
|
|
23
23
|
"test:watch": "vitest"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@jay-framework/compiler-jay-stack": "^0.
|
|
27
|
-
"@jay-framework/compiler-shared": "^0.
|
|
28
|
-
"@jay-framework/component": "^0.
|
|
29
|
-
"@jay-framework/fullstack-component": "^0.
|
|
30
|
-
"@jay-framework/logger": "^0.
|
|
31
|
-
"@jay-framework/runtime": "^0.
|
|
32
|
-
"@jay-framework/stack-client-runtime": "^0.
|
|
33
|
-
"@jay-framework/stack-route-scanner": "^0.
|
|
34
|
-
"@jay-framework/stack-server-runtime": "^0.
|
|
35
|
-
"@jay-framework/view-state-merge": "^0.
|
|
26
|
+
"@jay-framework/compiler-jay-stack": "^0.16.1",
|
|
27
|
+
"@jay-framework/compiler-shared": "^0.16.1",
|
|
28
|
+
"@jay-framework/component": "^0.16.1",
|
|
29
|
+
"@jay-framework/fullstack-component": "^0.16.1",
|
|
30
|
+
"@jay-framework/logger": "^0.16.1",
|
|
31
|
+
"@jay-framework/runtime": "^0.16.1",
|
|
32
|
+
"@jay-framework/stack-client-runtime": "^0.16.1",
|
|
33
|
+
"@jay-framework/stack-route-scanner": "^0.16.1",
|
|
34
|
+
"@jay-framework/stack-server-runtime": "^0.16.1",
|
|
35
|
+
"@jay-framework/view-state-merge": "^0.16.1",
|
|
36
|
+
"busboy": "^1.6.0",
|
|
36
37
|
"vite": "^5.0.11"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
|
-
"@jay-framework/dev-environment": "^0.
|
|
40
|
-
"@jay-framework/jay-cli": "^0.
|
|
41
|
-
"@jay-framework/stack-client-runtime": "^0.
|
|
40
|
+
"@jay-framework/dev-environment": "^0.16.1",
|
|
41
|
+
"@jay-framework/jay-cli": "^0.16.1",
|
|
42
|
+
"@jay-framework/stack-client-runtime": "^0.16.1",
|
|
42
43
|
"@playwright/test": "^1.58.2",
|
|
44
|
+
"@types/busboy": "^1.5.4",
|
|
43
45
|
"@types/express": "^5.0.2",
|
|
44
46
|
"@types/node": "^22.15.21",
|
|
45
47
|
"jsdom": "^23.2.0",
|