@jay-framework/dev-server 0.15.6 → 0.16.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/dist/index.d.ts +61 -1
- package/dist/index.js +387 -25
- package/package.json +14 -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
|
|
|
@@ -207,4 +267,4 @@ declare function createViteForCli(options: {
|
|
|
207
267
|
tsConfigFilePath?: string;
|
|
208
268
|
}): Promise<ViteDevServer>;
|
|
209
269
|
|
|
210
|
-
export { ACTION_ENDPOINT_BASE, type ActionRouterOptions, type CreateViteServerOptions, type DevServer, type DevServerOptions, type DevServerRoute, actionBodyParser, createActionRouter, createViteForCli, createViteServer, mkDevServer };
|
|
270
|
+
export { ACTION_ENDPOINT_BASE, 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,14 @@ 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 { randomUUID } from "node:crypto";
|
|
24
|
+
import { createJayService } from "@jay-framework/fullstack-component";
|
|
21
25
|
const s$1 = createRequire(import.meta.url), e$1 = s$1("typescript"), c$1 = new Proxy(e$1, {
|
|
22
26
|
get(t, r) {
|
|
23
27
|
return t[r];
|
|
@@ -927,9 +931,16 @@ function extractActionFromExpression(node) {
|
|
|
927
931
|
}
|
|
928
932
|
if (c$1.isIdentifier(expr)) {
|
|
929
933
|
const funcName = expr.text;
|
|
930
|
-
if (funcName === "makeJayAction" || funcName === "makeJayQuery") {
|
|
934
|
+
if (funcName === "makeJayAction" || funcName === "makeJayQuery" || funcName === "makeJayStream") {
|
|
931
935
|
const nameArg = current.arguments[0];
|
|
932
936
|
if (nameArg && c$1.isStringLiteral(nameArg)) {
|
|
937
|
+
if (funcName === "makeJayStream") {
|
|
938
|
+
return {
|
|
939
|
+
actionName: nameArg.text,
|
|
940
|
+
method: "POST",
|
|
941
|
+
isStreaming: true
|
|
942
|
+
};
|
|
943
|
+
}
|
|
933
944
|
method = funcName === "makeJayQuery" ? "GET" : "POST";
|
|
934
945
|
if (explicitMethod) {
|
|
935
946
|
method = explicitMethod;
|
|
@@ -1331,14 +1342,27 @@ function jayStackCompiler(options = {}) {
|
|
|
1331
1342
|
getLogger().warn(`[action-transform] No actions found in ${actualPath}`);
|
|
1332
1343
|
return null;
|
|
1333
1344
|
}
|
|
1345
|
+
const hasRegularActions = actions.some((a) => !a.isStreaming);
|
|
1346
|
+
const hasStreamActions = actions.some((a) => a.isStreaming);
|
|
1347
|
+
const importNames = [];
|
|
1348
|
+
if (hasRegularActions)
|
|
1349
|
+
importNames.push("createActionCaller");
|
|
1350
|
+
if (hasStreamActions)
|
|
1351
|
+
importNames.push("createStreamCaller");
|
|
1334
1352
|
const lines = [
|
|
1335
|
-
`import {
|
|
1353
|
+
`import { ${importNames.join(", ")} } from '@jay-framework/stack-client-runtime';`,
|
|
1336
1354
|
""
|
|
1337
1355
|
];
|
|
1338
1356
|
for (const action of actions) {
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1357
|
+
if (action.isStreaming) {
|
|
1358
|
+
lines.push(
|
|
1359
|
+
`export const ${action.exportName} = createStreamCaller('${action.actionName}');`
|
|
1360
|
+
);
|
|
1361
|
+
} else {
|
|
1362
|
+
lines.push(
|
|
1363
|
+
`export const ${action.exportName} = createActionCaller('${action.actionName}', '${action.method}');`
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1342
1366
|
}
|
|
1343
1367
|
if (code.includes("ActionError")) {
|
|
1344
1368
|
lines.push(
|
|
@@ -1758,6 +1782,22 @@ function createActionRouter(options) {
|
|
|
1758
1782
|
requestMethod,
|
|
1759
1783
|
ACTION_ENDPOINT_BASE + "/" + actionName
|
|
1760
1784
|
);
|
|
1785
|
+
if (registry.isStreaming(actionName)) {
|
|
1786
|
+
res.setHeader("Content-Type", "application/x-ndjson");
|
|
1787
|
+
res.setHeader("Transfer-Encoding", "chunked");
|
|
1788
|
+
try {
|
|
1789
|
+
const generator = registry.executeStream(actionName, input);
|
|
1790
|
+
for await (const chunk of generator) {
|
|
1791
|
+
res.write(JSON.stringify({ chunk }) + "\n");
|
|
1792
|
+
}
|
|
1793
|
+
res.write(JSON.stringify({ done: true }) + "\n");
|
|
1794
|
+
} catch (err) {
|
|
1795
|
+
res.write(JSON.stringify({ error: err.message }) + "\n");
|
|
1796
|
+
}
|
|
1797
|
+
res.end();
|
|
1798
|
+
timing?.end();
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1761
1801
|
const result = await registry.execute(actionName, input);
|
|
1762
1802
|
if (requestMethod === "GET" && result.success) {
|
|
1763
1803
|
const cacheHeaders = registry.getCacheHeaders(actionName);
|
|
@@ -1831,6 +1871,124 @@ function actionBodyParser() {
|
|
|
1831
1871
|
});
|
|
1832
1872
|
};
|
|
1833
1873
|
}
|
|
1874
|
+
class FreezeStore {
|
|
1875
|
+
constructor(buildFolder) {
|
|
1876
|
+
__publicField(this, "dir");
|
|
1877
|
+
this.dir = path.join(buildFolder, "freezes");
|
|
1878
|
+
}
|
|
1879
|
+
async save(route, viewState, routePattern) {
|
|
1880
|
+
await fs$1.mkdir(this.dir, { recursive: true });
|
|
1881
|
+
const entry = {
|
|
1882
|
+
id: randomUUID().slice(0, 8),
|
|
1883
|
+
route,
|
|
1884
|
+
...routePattern && { routePattern },
|
|
1885
|
+
viewState,
|
|
1886
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1887
|
+
};
|
|
1888
|
+
await fs$1.writeFile(
|
|
1889
|
+
path.join(this.dir, `${entry.id}.json`),
|
|
1890
|
+
JSON.stringify(entry, null, 2),
|
|
1891
|
+
"utf-8"
|
|
1892
|
+
);
|
|
1893
|
+
return entry;
|
|
1894
|
+
}
|
|
1895
|
+
async get(id) {
|
|
1896
|
+
try {
|
|
1897
|
+
const content = await fs$1.readFile(path.join(this.dir, `${id}.json`), "utf-8");
|
|
1898
|
+
return JSON.parse(content);
|
|
1899
|
+
} catch {
|
|
1900
|
+
return void 0;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
async list(route) {
|
|
1904
|
+
try {
|
|
1905
|
+
const files = await fs$1.readdir(this.dir);
|
|
1906
|
+
const entries = [];
|
|
1907
|
+
for (const file of files) {
|
|
1908
|
+
if (!file.endsWith(".json"))
|
|
1909
|
+
continue;
|
|
1910
|
+
try {
|
|
1911
|
+
const content = await fs$1.readFile(path.join(this.dir, file), "utf-8");
|
|
1912
|
+
const entry = JSON.parse(content);
|
|
1913
|
+
if (!route || entry.routePattern === route || entry.route === route) {
|
|
1914
|
+
entries.push(entry);
|
|
1915
|
+
}
|
|
1916
|
+
} catch {
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
return entries.sort(
|
|
1920
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
1921
|
+
);
|
|
1922
|
+
} catch {
|
|
1923
|
+
return [];
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
async rename(id, name) {
|
|
1927
|
+
const entry = await this.get(id);
|
|
1928
|
+
if (!entry)
|
|
1929
|
+
return false;
|
|
1930
|
+
entry.name = name;
|
|
1931
|
+
await fs$1.writeFile(
|
|
1932
|
+
path.join(this.dir, `${id}.json`),
|
|
1933
|
+
JSON.stringify(entry, null, 2),
|
|
1934
|
+
"utf-8"
|
|
1935
|
+
);
|
|
1936
|
+
return true;
|
|
1937
|
+
}
|
|
1938
|
+
async delete(id) {
|
|
1939
|
+
try {
|
|
1940
|
+
await fs$1.unlink(path.join(this.dir, `${id}.json`));
|
|
1941
|
+
return true;
|
|
1942
|
+
} catch {
|
|
1943
|
+
return false;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
const DEV_SERVER_SERVICE = createJayService("DevServerService");
|
|
1948
|
+
class DevServerService {
|
|
1949
|
+
constructor(routes, vite, pagesBase, projectBase, jayRollupConfig, _freezeStore) {
|
|
1950
|
+
this.routes = routes;
|
|
1951
|
+
this.vite = vite;
|
|
1952
|
+
this.pagesBase = pagesBase;
|
|
1953
|
+
this.projectBase = projectBase;
|
|
1954
|
+
this.jayRollupConfig = jayRollupConfig;
|
|
1955
|
+
this._freezeStore = _freezeStore;
|
|
1956
|
+
}
|
|
1957
|
+
get freezeStore() {
|
|
1958
|
+
return this._freezeStore;
|
|
1959
|
+
}
|
|
1960
|
+
/** List all page routes in the project. */
|
|
1961
|
+
listRoutes() {
|
|
1962
|
+
return this.routes.map((r) => ({
|
|
1963
|
+
path: r.path,
|
|
1964
|
+
jayHtmlPath: r.fsRoute.jayHtmlPath,
|
|
1965
|
+
compPath: r.fsRoute.compPath
|
|
1966
|
+
}));
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Run loadParams for a route, yielding param batches as an async generator.
|
|
1970
|
+
* Loads all page parts (page component + keyed headless components) and
|
|
1971
|
+
* calls loadParams on each one that defines it.
|
|
1972
|
+
*/
|
|
1973
|
+
async *loadRouteParams(routePath) {
|
|
1974
|
+
const matched = this.routes.find((r) => r.path === routePath);
|
|
1975
|
+
if (!matched) {
|
|
1976
|
+
getLogger().error(`[loadRouteParams] Route [${routePath}] not found`);
|
|
1977
|
+
throw new Error(`Route "${routePath}" not found`);
|
|
1978
|
+
}
|
|
1979
|
+
const loaded = await loadPageParts(
|
|
1980
|
+
this.vite,
|
|
1981
|
+
matched.fsRoute,
|
|
1982
|
+
this.pagesBase,
|
|
1983
|
+
this.projectBase,
|
|
1984
|
+
this.jayRollupConfig
|
|
1985
|
+
);
|
|
1986
|
+
if (!loaded.val) {
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
yield* runLoadParams(loaded.val.parts);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1834
1992
|
let _watchLinkedFiles = () => {
|
|
1835
1993
|
};
|
|
1836
1994
|
async function initRoutes(pagesBaseFolder) {
|
|
@@ -1839,6 +1997,82 @@ async function initRoutes(pagesBaseFolder) {
|
|
|
1839
1997
|
compFilename: "page.ts"
|
|
1840
1998
|
});
|
|
1841
1999
|
}
|
|
2000
|
+
async function scanPluginRoutes(projectRoot, projectRoutes) {
|
|
2001
|
+
const plugins = await scanPlugins({ projectRoot, includeDevDeps: true });
|
|
2002
|
+
const projectPaths = new Set(projectRoutes.map((r) => r.rawRoute));
|
|
2003
|
+
const pluginRoutes = [];
|
|
2004
|
+
for (const [, plugin] of plugins) {
|
|
2005
|
+
if (!plugin.manifest.routes)
|
|
2006
|
+
continue;
|
|
2007
|
+
for (const route of plugin.manifest.routes) {
|
|
2008
|
+
if (projectPaths.has(route.path)) {
|
|
2009
|
+
getLogger().info(
|
|
2010
|
+
`[Routes] Plugin "${plugin.name}" route ${route.path} skipped — project route takes precedence`
|
|
2011
|
+
);
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
const jayHtmlPath = resolvePluginExport(plugin.pluginPath, route.jayHtml);
|
|
2015
|
+
if (!jayHtmlPath) {
|
|
2016
|
+
getLogger().warn(
|
|
2017
|
+
`[Routes] Plugin "${plugin.name}" route ${route.path}: jayHtml "${route.jayHtml}" not found`
|
|
2018
|
+
);
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
2021
|
+
const compPath = route.component.startsWith(".") ? path__default.resolve(plugin.pluginPath, route.component) : resolvePluginModule(plugin);
|
|
2022
|
+
pluginRoutes.push(createRoute(route.path, jayHtmlPath, compPath));
|
|
2023
|
+
getLogger().info(`[Routes] Plugin "${plugin.name}" provides route ${route.path}`);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
return pluginRoutes;
|
|
2027
|
+
}
|
|
2028
|
+
function resolvePluginExport(pluginPath, exportSubpath) {
|
|
2029
|
+
const normalized = exportSubpath.replace(/^\.\//, "");
|
|
2030
|
+
const packageJsonPath = path__default.join(pluginPath, "package.json");
|
|
2031
|
+
try {
|
|
2032
|
+
const packageJson = JSON.parse(fs__default$1.readFileSync(packageJsonPath, "utf-8"));
|
|
2033
|
+
if (packageJson.exports) {
|
|
2034
|
+
const exportKey = "./" + normalized;
|
|
2035
|
+
const exportValue = packageJson.exports[exportKey];
|
|
2036
|
+
if (exportValue) {
|
|
2037
|
+
const resolved = typeof exportValue === "string" ? exportValue : exportValue.default || exportValue.import || exportValue.require;
|
|
2038
|
+
if (resolved) {
|
|
2039
|
+
const fullPath = path__default.join(pluginPath, resolved);
|
|
2040
|
+
return fullPath;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
} catch {
|
|
2045
|
+
}
|
|
2046
|
+
for (const dir of ["dist", "lib", ""]) {
|
|
2047
|
+
const candidate = path__default.join(pluginPath, dir, normalized);
|
|
2048
|
+
try {
|
|
2049
|
+
fs__default$1.accessSync(candidate);
|
|
2050
|
+
return candidate;
|
|
2051
|
+
} catch {
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
return void 0;
|
|
2055
|
+
}
|
|
2056
|
+
function resolvePluginModule(plugin) {
|
|
2057
|
+
const modulePath = plugin.manifest.module || "index";
|
|
2058
|
+
for (const ext of [".ts", ".js", "/index.ts", "/index.js"]) {
|
|
2059
|
+
const candidate = path__default.join(plugin.pluginPath, modulePath + ext);
|
|
2060
|
+
try {
|
|
2061
|
+
fs__default$1.accessSync(candidate);
|
|
2062
|
+
return candidate;
|
|
2063
|
+
} catch {
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
for (const ext of [".ts", ".js"]) {
|
|
2067
|
+
const candidate = path__default.join(plugin.pluginPath, "lib", path__default.basename(modulePath) + ext);
|
|
2068
|
+
try {
|
|
2069
|
+
fs__default$1.accessSync(candidate);
|
|
2070
|
+
return candidate;
|
|
2071
|
+
} catch {
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
return path__default.join(plugin.pluginPath, modulePath);
|
|
2075
|
+
}
|
|
1842
2076
|
function defaults(options) {
|
|
1843
2077
|
const publicBaseUrlPath = options.publicBaseUrlPath || process.env.BASE || "/";
|
|
1844
2078
|
const projectRootFolder = options.projectRootFolder || ".";
|
|
@@ -1900,7 +2134,7 @@ function filterPluginsForPage(allPluginClientInits, allPluginsWithInit, usedPack
|
|
|
1900
2134
|
return pluginInfo && expandedPackages.has(pluginInfo.packageName);
|
|
1901
2135
|
});
|
|
1902
2136
|
}
|
|
1903
|
-
function mkRoute(route, vite, slowlyPhase, options, slowRenderCache, projectInit, allPluginsWithInit = [], allPluginClientInits = []) {
|
|
2137
|
+
function mkRoute(route, vite, slowlyPhase, options, slowRenderCache, freezeStore, projectInit, allPluginsWithInit = [], allPluginClientInits = []) {
|
|
1904
2138
|
const routePath = routeToExpressRoute(route);
|
|
1905
2139
|
const handler = async (req, res) => {
|
|
1906
2140
|
const timing = getDevLogger()?.startRequest(req.method, req.path);
|
|
@@ -1916,6 +2150,23 @@ function mkRoute(route, vite, slowlyPhase, options, slowRenderCache, projectInit
|
|
|
1916
2150
|
for (const [key, value] of urlObj.searchParams) {
|
|
1917
2151
|
query[key] = value;
|
|
1918
2152
|
}
|
|
2153
|
+
const freezeId = query["_jay_freeze"];
|
|
2154
|
+
if (freezeId && freezeStore) {
|
|
2155
|
+
timing?.annotate("[FROZEN]");
|
|
2156
|
+
await handleFrozenRequest(
|
|
2157
|
+
vite,
|
|
2158
|
+
route,
|
|
2159
|
+
options,
|
|
2160
|
+
freezeStore,
|
|
2161
|
+
slowRenderCache,
|
|
2162
|
+
freezeId,
|
|
2163
|
+
pageParams,
|
|
2164
|
+
query["format"] === "fragment" ? "fragment" : "page",
|
|
2165
|
+
res,
|
|
2166
|
+
timing
|
|
2167
|
+
);
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
1919
2170
|
if (options.disableSSR) {
|
|
1920
2171
|
await handleClientOnlyRequest(
|
|
1921
2172
|
vite,
|
|
@@ -2048,6 +2299,7 @@ async function handleCachedRequest(vite, route, options, cachedEntry, pageParams
|
|
|
2048
2299
|
projectInit,
|
|
2049
2300
|
pluginsForPage,
|
|
2050
2301
|
options,
|
|
2302
|
+
routeToExpressRoute(route),
|
|
2051
2303
|
cachedEntry.slowViewState,
|
|
2052
2304
|
timing,
|
|
2053
2305
|
cachedEntry.preRenderedContent,
|
|
@@ -2227,14 +2479,15 @@ async function handleClientOnlyRequest(vite, route, options, slowlyPhase, pagePa
|
|
|
2227
2479
|
projectInit,
|
|
2228
2480
|
pluginsForPage,
|
|
2229
2481
|
{
|
|
2230
|
-
enableAutomation: !options.disableAutomation
|
|
2482
|
+
enableAutomation: !options.disableAutomation,
|
|
2483
|
+
routePattern: routeToExpressRoute(route)
|
|
2231
2484
|
}
|
|
2232
2485
|
);
|
|
2233
2486
|
if (options.buildFolder) {
|
|
2234
2487
|
const pageName = !url || url === "/" ? "index" : url.replace(/^\//, "").replace(/\//g, "-");
|
|
2235
2488
|
const clientScriptDir = path__default.join(options.buildFolder, "debug", "client-entry");
|
|
2236
|
-
await
|
|
2237
|
-
await
|
|
2489
|
+
await fs__default.mkdir(clientScriptDir, { recursive: true });
|
|
2490
|
+
await fs__default.writeFile(path__default.join(clientScriptDir, `${pageName}.html`), pageHtml, "utf-8");
|
|
2238
2491
|
}
|
|
2239
2492
|
const viteStart = Date.now();
|
|
2240
2493
|
const compiledPageHtml = await vite.transformIndexHtml(!!url ? url : "/", pageHtml);
|
|
@@ -2242,11 +2495,11 @@ async function handleClientOnlyRequest(vite, route, options, slowlyPhase, pagePa
|
|
|
2242
2495
|
res.status(200).set({ "Content-Type": "text/html" }).send(compiledPageHtml);
|
|
2243
2496
|
timing?.end();
|
|
2244
2497
|
}
|
|
2245
|
-
async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, pageParts, viewState, carryForward, clientTrackByMap, projectInit, pluginsForPage, options, slowViewState, timing, preLoadedContent, headTags) {
|
|
2498
|
+
async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, pageParts, viewState, carryForward, clientTrackByMap, projectInit, pluginsForPage, options, routePattern, slowViewState, timing, preLoadedContent, headTags) {
|
|
2246
2499
|
let pageHtml;
|
|
2247
2500
|
const routeDir = path__default.dirname(path__default.relative(options.pagesRootFolder, sourceJayHtmlPath));
|
|
2248
2501
|
try {
|
|
2249
|
-
let jayHtmlContent = preLoadedContent ?? await
|
|
2502
|
+
let jayHtmlContent = preLoadedContent ?? await fs__default.readFile(jayHtmlPath, "utf-8");
|
|
2250
2503
|
const jayHtmlFilename = path__default.basename(jayHtmlPath);
|
|
2251
2504
|
const jayHtmlDir = path__default.dirname(jayHtmlPath);
|
|
2252
2505
|
const sourceDir = path__default.dirname(sourceJayHtmlPath);
|
|
@@ -2270,7 +2523,8 @@ async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, page
|
|
|
2270
2523
|
pluginsForPage,
|
|
2271
2524
|
{
|
|
2272
2525
|
enableAutomation: !options.disableAutomation,
|
|
2273
|
-
slowViewState
|
|
2526
|
+
slowViewState,
|
|
2527
|
+
routePattern
|
|
2274
2528
|
},
|
|
2275
2529
|
// Pass source directory for headfull FS file resolution when using pre-rendered path
|
|
2276
2530
|
jayHtmlDir !== sourceDir ? sourceDir : void 0,
|
|
@@ -2289,15 +2543,16 @@ async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, page
|
|
|
2289
2543
|
pluginsForPage,
|
|
2290
2544
|
{
|
|
2291
2545
|
enableAutomation: !options.disableAutomation,
|
|
2292
|
-
slowViewState
|
|
2546
|
+
slowViewState,
|
|
2547
|
+
routePattern
|
|
2293
2548
|
}
|
|
2294
2549
|
);
|
|
2295
2550
|
}
|
|
2296
2551
|
if (options.buildFolder) {
|
|
2297
2552
|
const pageName = !url || url === "/" ? "index" : url.replace(/^\//, "").replace(/\//g, "-");
|
|
2298
2553
|
const clientScriptDir = path__default.join(options.buildFolder, "debug", "client-entry");
|
|
2299
|
-
await
|
|
2300
|
-
await
|
|
2554
|
+
await fs__default.mkdir(clientScriptDir, { recursive: true });
|
|
2555
|
+
await fs__default.writeFile(path__default.join(clientScriptDir, `${pageName}.html`), pageHtml, "utf-8");
|
|
2301
2556
|
}
|
|
2302
2557
|
const viteStart = Date.now();
|
|
2303
2558
|
const compiledPageHtml = await vite.transformIndexHtml(!!url ? url : "/", pageHtml);
|
|
@@ -2305,12 +2560,62 @@ async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, page
|
|
|
2305
2560
|
res.status(200).set({ "Content-Type": "text/html" }).send(compiledPageHtml);
|
|
2306
2561
|
timing?.end();
|
|
2307
2562
|
}
|
|
2563
|
+
async function handleFrozenRequest(vite, route, options, freezeStore, slowRenderCache, freezeId, pageParams, format, res, timing) {
|
|
2564
|
+
const entry = await freezeStore.get(freezeId);
|
|
2565
|
+
if (!entry) {
|
|
2566
|
+
getLogger().warn(`[Freeze] Freeze "${freezeId}" not found`);
|
|
2567
|
+
res.status(404).send(`Freeze "${freezeId}" not found`);
|
|
2568
|
+
timing?.end();
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
const label = entry.name ? `"${entry.name}" (${freezeId})` : freezeId;
|
|
2572
|
+
getLogger().info(`[Freeze] Serving frozen page ${label} for ${route.rawRoute} [${format}]`);
|
|
2573
|
+
try {
|
|
2574
|
+
const cachedEntry = await slowRenderCache.get(route.jayHtmlPath, pageParams);
|
|
2575
|
+
const jayHtmlPath = cachedEntry?.preRenderedPath ?? route.jayHtmlPath;
|
|
2576
|
+
const jayHtmlContent = cachedEntry?.preRenderedContent ?? await fs__default.readFile(jayHtmlPath, "utf-8");
|
|
2577
|
+
const jayHtmlFilename = path__default.basename(jayHtmlPath);
|
|
2578
|
+
const jayHtmlDir = path__default.dirname(jayHtmlPath);
|
|
2579
|
+
const sourceDir = path__default.dirname(route.jayHtmlPath);
|
|
2580
|
+
const routeDir = path__default.dirname(path__default.relative(options.pagesRootFolder, route.jayHtmlPath));
|
|
2581
|
+
const { injectHeadfullFSTemplates: injectHeadfullFSTemplates2 } = await import("@jay-framework/compiler-jay-html");
|
|
2582
|
+
const { JAY_IMPORT_RESOLVER: JAY_IMPORT_RESOLVER2 } = await import("@jay-framework/compiler-jay-html");
|
|
2583
|
+
const fullJayHtml = injectHeadfullFSTemplates2(
|
|
2584
|
+
jayHtmlContent,
|
|
2585
|
+
sourceDir,
|
|
2586
|
+
JAY_IMPORT_RESOLVER2
|
|
2587
|
+
);
|
|
2588
|
+
const html = await generateFrozenPageHtml(
|
|
2589
|
+
vite,
|
|
2590
|
+
fullJayHtml,
|
|
2591
|
+
jayHtmlFilename,
|
|
2592
|
+
jayHtmlDir,
|
|
2593
|
+
entry.viewState,
|
|
2594
|
+
options.buildFolder,
|
|
2595
|
+
options.projectRootFolder,
|
|
2596
|
+
routeDir,
|
|
2597
|
+
options.jayRollupConfig?.tsConfigFilePath,
|
|
2598
|
+
void 0,
|
|
2599
|
+
format,
|
|
2600
|
+
entry.name
|
|
2601
|
+
);
|
|
2602
|
+
const headers = { "Content-Type": "text/html" };
|
|
2603
|
+
if (format === "fragment") {
|
|
2604
|
+
headers["Access-Control-Allow-Origin"] = "*";
|
|
2605
|
+
}
|
|
2606
|
+
res.status(200).set(headers).send(html);
|
|
2607
|
+
} catch (err) {
|
|
2608
|
+
getLogger().warn(`[Freeze] Failed to render frozen page: ${err.message}`);
|
|
2609
|
+
res.status(500).send(`Failed to render frozen page: ${err.message}`);
|
|
2610
|
+
}
|
|
2611
|
+
timing?.end();
|
|
2612
|
+
}
|
|
2308
2613
|
async function preRenderJayHtml(route, slowViewState, headlessContracts, headlessInstanceComponents, partKeys = []) {
|
|
2309
|
-
const jayHtmlContent = await
|
|
2614
|
+
const jayHtmlContent = await fs__default.readFile(route.jayHtmlPath, "utf-8");
|
|
2310
2615
|
const contractPath = route.jayHtmlPath.replace(".jay-html", ".jay-contract");
|
|
2311
2616
|
let contract;
|
|
2312
2617
|
try {
|
|
2313
|
-
const contractContent = await
|
|
2618
|
+
const contractContent = await fs__default.readFile(contractPath, "utf-8");
|
|
2314
2619
|
const parseResult = parseContract(contractContent, path__default.basename(contractPath));
|
|
2315
2620
|
if (parseResult.val) {
|
|
2316
2621
|
contract = parseResult.val;
|
|
@@ -2452,8 +2757,15 @@ async function mkDevServer(rawOptions) {
|
|
|
2452
2757
|
const options = defaults(rawOptions);
|
|
2453
2758
|
const { publicBaseUrlPath, pagesRootFolder, projectRootFolder, buildFolder, jayRollupConfig } = options;
|
|
2454
2759
|
if (buildFolder) {
|
|
2455
|
-
|
|
2456
|
-
|
|
2760
|
+
try {
|
|
2761
|
+
const entries = await fs__default.readdir(buildFolder).catch(() => []);
|
|
2762
|
+
for (const entry of entries) {
|
|
2763
|
+
if (entry === "freezes")
|
|
2764
|
+
continue;
|
|
2765
|
+
await fs__default.rm(path__default.join(buildFolder, entry), { recursive: true, force: true });
|
|
2766
|
+
}
|
|
2767
|
+
} catch {
|
|
2768
|
+
}
|
|
2457
2769
|
}
|
|
2458
2770
|
const viteLogLevel = options.logLevel === "silent" ? "silent" : options.logLevel === "verbose" ? "info" : "warn";
|
|
2459
2771
|
const lifecycleManager = new ServiceLifecycleManager(projectRootFolder);
|
|
@@ -2471,8 +2783,10 @@ async function mkDevServer(rawOptions) {
|
|
|
2471
2783
|
await materializeDynamicContracts(projectRootFolder, vite);
|
|
2472
2784
|
setupServiceHotReload(vite, lifecycleManager);
|
|
2473
2785
|
setupActionRouter(vite);
|
|
2474
|
-
const
|
|
2475
|
-
const
|
|
2786
|
+
const projectRoutes = await initRoutes(pagesRootFolder);
|
|
2787
|
+
const filteredProjectRoutes = buildFolder ? projectRoutes.filter((route) => !route.jayHtmlPath.startsWith(buildFolder)) : projectRoutes;
|
|
2788
|
+
const pluginRoutes = await scanPluginRoutes(projectRootFolder, filteredProjectRoutes);
|
|
2789
|
+
const routes = [...filteredProjectRoutes, ...pluginRoutes];
|
|
2476
2790
|
const slowlyPhase = new DevSlowlyChangingPhase();
|
|
2477
2791
|
const slowRenderCacheDir = path__default.join(buildFolder, "pre-rendered");
|
|
2478
2792
|
const slowRenderCache = new SlowRenderCache(slowRenderCacheDir, pagesRootFolder);
|
|
@@ -2484,6 +2798,10 @@ async function mkDevServer(rawOptions) {
|
|
|
2484
2798
|
const projectInit = lifecycleManager.getProjectInit() ?? void 0;
|
|
2485
2799
|
const pluginsWithInit = lifecycleManager.getPluginsWithInit();
|
|
2486
2800
|
const pluginClientInits = preparePluginClientInits(pluginsWithInit);
|
|
2801
|
+
const freezeStore = buildFolder ? new FreezeStore(buildFolder) : void 0;
|
|
2802
|
+
if (freezeStore) {
|
|
2803
|
+
setupFreezeEndpoint(vite, freezeStore);
|
|
2804
|
+
}
|
|
2487
2805
|
const devServerRoutes = routes.map(
|
|
2488
2806
|
(route) => mkRoute(
|
|
2489
2807
|
route,
|
|
@@ -2491,16 +2809,28 @@ async function mkDevServer(rawOptions) {
|
|
|
2491
2809
|
slowlyPhase,
|
|
2492
2810
|
options,
|
|
2493
2811
|
slowRenderCache,
|
|
2812
|
+
freezeStore,
|
|
2494
2813
|
projectInit,
|
|
2495
2814
|
pluginsWithInit,
|
|
2496
2815
|
pluginClientInits
|
|
2497
2816
|
)
|
|
2498
2817
|
);
|
|
2818
|
+
const service = new DevServerService(
|
|
2819
|
+
devServerRoutes,
|
|
2820
|
+
vite,
|
|
2821
|
+
options.pagesRootFolder,
|
|
2822
|
+
options.projectRootFolder,
|
|
2823
|
+
options.jayRollupConfig,
|
|
2824
|
+
freezeStore
|
|
2825
|
+
);
|
|
2826
|
+
registerService(DEV_SERVER_SERVICE, service);
|
|
2499
2827
|
return {
|
|
2500
2828
|
server: vite.middlewares,
|
|
2501
2829
|
viteServer: vite,
|
|
2502
2830
|
routes: devServerRoutes,
|
|
2503
|
-
lifecycleManager
|
|
2831
|
+
lifecycleManager,
|
|
2832
|
+
freezeStore,
|
|
2833
|
+
service
|
|
2504
2834
|
};
|
|
2505
2835
|
}
|
|
2506
2836
|
function setupGracefulShutdown(lifecycleManager) {
|
|
@@ -2539,6 +2869,35 @@ function setupActionRouter(vite) {
|
|
|
2539
2869
|
vite.middlewares.use(ACTION_ENDPOINT_BASE, createActionRouter());
|
|
2540
2870
|
getLogger().info(`[Actions] Action router mounted at ${ACTION_ENDPOINT_BASE}`);
|
|
2541
2871
|
}
|
|
2872
|
+
function setupFreezeEndpoint(vite, freezeStore) {
|
|
2873
|
+
vite.middlewares.use((req, res, next) => {
|
|
2874
|
+
if (req.method === "POST" && (req.url === "/_jay/freeze" || req.originalUrl === "/_jay/freeze")) {
|
|
2875
|
+
let body = "";
|
|
2876
|
+
req.on("data", (chunk) => body += chunk);
|
|
2877
|
+
req.on("end", async () => {
|
|
2878
|
+
try {
|
|
2879
|
+
const { route, routePattern, viewState } = JSON.parse(body);
|
|
2880
|
+
if (!route || !viewState) {
|
|
2881
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2882
|
+
res.end(JSON.stringify({ error: "Missing route or viewState" }));
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
const entry = await freezeStore.save(route, viewState, routePattern);
|
|
2886
|
+
getLogger().info(`[Freeze] Saved freeze "${entry.id}" for ${route}`);
|
|
2887
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2888
|
+
res.end(JSON.stringify(entry));
|
|
2889
|
+
} catch (err) {
|
|
2890
|
+
getLogger().warn(`[Freeze] Failed to save: ${err.message}`);
|
|
2891
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2892
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2893
|
+
}
|
|
2894
|
+
});
|
|
2895
|
+
} else {
|
|
2896
|
+
next();
|
|
2897
|
+
}
|
|
2898
|
+
});
|
|
2899
|
+
getLogger().info("[Freeze] Freeze endpoint mounted at /_jay/freeze");
|
|
2900
|
+
}
|
|
2542
2901
|
function setupSlowRenderCacheInvalidation(vite, cache, pagesRootFolder, projectRootFolder) {
|
|
2543
2902
|
const watchedFiles = /* @__PURE__ */ new Set();
|
|
2544
2903
|
const watchLinkedFiles = (files) => {
|
|
@@ -2600,6 +2959,9 @@ function setupSlowRenderCacheInvalidation(vite, cache, pagesRootFolder, projectR
|
|
|
2600
2959
|
}
|
|
2601
2960
|
export {
|
|
2602
2961
|
ACTION_ENDPOINT_BASE,
|
|
2962
|
+
DEV_SERVER_SERVICE,
|
|
2963
|
+
DevServerService,
|
|
2964
|
+
FreezeStore,
|
|
2603
2965
|
actionBodyParser,
|
|
2604
2966
|
createActionRouter,
|
|
2605
2967
|
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.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,22 +23,22 @@
|
|
|
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.0",
|
|
27
|
+
"@jay-framework/compiler-shared": "^0.16.0",
|
|
28
|
+
"@jay-framework/component": "^0.16.0",
|
|
29
|
+
"@jay-framework/fullstack-component": "^0.16.0",
|
|
30
|
+
"@jay-framework/logger": "^0.16.0",
|
|
31
|
+
"@jay-framework/runtime": "^0.16.0",
|
|
32
|
+
"@jay-framework/stack-client-runtime": "^0.16.0",
|
|
33
|
+
"@jay-framework/stack-route-scanner": "^0.16.0",
|
|
34
|
+
"@jay-framework/stack-server-runtime": "^0.16.0",
|
|
35
|
+
"@jay-framework/view-state-merge": "^0.16.0",
|
|
36
36
|
"vite": "^5.0.11"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@jay-framework/dev-environment": "^0.
|
|
40
|
-
"@jay-framework/jay-cli": "^0.
|
|
41
|
-
"@jay-framework/stack-client-runtime": "^0.
|
|
39
|
+
"@jay-framework/dev-environment": "^0.16.0",
|
|
40
|
+
"@jay-framework/jay-cli": "^0.16.0",
|
|
41
|
+
"@jay-framework/stack-client-runtime": "^0.16.0",
|
|
42
42
|
"@playwright/test": "^1.58.2",
|
|
43
43
|
"@types/express": "^5.0.2",
|
|
44
44
|
"@types/node": "^22.15.21",
|