@jay-framework/dev-server 0.16.0 → 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 +13 -3
- package/dist/index.js +163 -8
- package/package.json +16 -14
package/dist/index.d.ts
CHANGED
|
@@ -220,10 +220,20 @@ interface ActionRouterOptions {
|
|
|
220
220
|
*/
|
|
221
221
|
declare function createActionRouter(options?: ActionRouterOptions): RequestHandler$1;
|
|
222
222
|
/**
|
|
223
|
-
*
|
|
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()).
|
|
224
234
|
* Should be applied before the action router.
|
|
225
235
|
*/
|
|
226
|
-
declare function actionBodyParser(): RequestHandler$1;
|
|
236
|
+
declare function actionBodyParser(options: ActionBodyParserOptions): RequestHandler$1;
|
|
227
237
|
|
|
228
238
|
/**
|
|
229
239
|
* Vite Factory
|
|
@@ -267,4 +277,4 @@ declare function createViteForCli(options: {
|
|
|
267
277
|
tsConfigFilePath?: string;
|
|
268
278
|
}): Promise<ViteDevServer>;
|
|
269
279
|
|
|
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 };
|
|
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
|
@@ -20,6 +20,10 @@ import { discoverPluginsWithInit, sortPluginsByDependencies, executePluginServer
|
|
|
20
20
|
import * as fs$1 from "node:fs/promises";
|
|
21
21
|
import fs__default from "node:fs/promises";
|
|
22
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";
|
|
23
27
|
import { randomUUID } from "node:crypto";
|
|
24
28
|
import { createJayService } from "@jay-framework/fullstack-component";
|
|
25
29
|
const s$1 = createRequire(import.meta.url), e$1 = s$1("typescript"), c$1 = new Proxy(e$1, {
|
|
@@ -915,6 +919,7 @@ function extractActionFromExpression(node) {
|
|
|
915
919
|
let current = node;
|
|
916
920
|
let method = "POST";
|
|
917
921
|
let explicitMethod = null;
|
|
922
|
+
let acceptsFiles = false;
|
|
918
923
|
while (c$1.isCallExpression(current)) {
|
|
919
924
|
const expr = current.expression;
|
|
920
925
|
if (c$1.isPropertyAccessExpression(expr) && expr.name.text === "withMethod") {
|
|
@@ -925,6 +930,11 @@ function extractActionFromExpression(node) {
|
|
|
925
930
|
current = expr.expression;
|
|
926
931
|
continue;
|
|
927
932
|
}
|
|
933
|
+
if (c$1.isPropertyAccessExpression(expr) && expr.name.text === "withFiles") {
|
|
934
|
+
acceptsFiles = true;
|
|
935
|
+
current = expr.expression;
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
928
938
|
if (c$1.isPropertyAccessExpression(expr) && ["withServices", "withCaching", "withHandler", "withTimeout"].includes(expr.name.text)) {
|
|
929
939
|
current = expr.expression;
|
|
930
940
|
continue;
|
|
@@ -938,7 +948,8 @@ function extractActionFromExpression(node) {
|
|
|
938
948
|
return {
|
|
939
949
|
actionName: nameArg.text,
|
|
940
950
|
method: "POST",
|
|
941
|
-
isStreaming: true
|
|
951
|
+
isStreaming: true,
|
|
952
|
+
...acceptsFiles && { acceptsFiles: true }
|
|
942
953
|
};
|
|
943
954
|
}
|
|
944
955
|
method = funcName === "makeJayQuery" ? "GET" : "POST";
|
|
@@ -947,7 +958,8 @@ function extractActionFromExpression(node) {
|
|
|
947
958
|
}
|
|
948
959
|
return {
|
|
949
960
|
actionName: nameArg.text,
|
|
950
|
-
method
|
|
961
|
+
method,
|
|
962
|
+
...acceptsFiles && { acceptsFiles: true }
|
|
951
963
|
};
|
|
952
964
|
}
|
|
953
965
|
}
|
|
@@ -1354,13 +1366,14 @@ function jayStackCompiler(options = {}) {
|
|
|
1354
1366
|
""
|
|
1355
1367
|
];
|
|
1356
1368
|
for (const action of actions) {
|
|
1369
|
+
const filesOpt = action.acceptsFiles ? ", { acceptsFiles: true }" : "";
|
|
1357
1370
|
if (action.isStreaming) {
|
|
1358
1371
|
lines.push(
|
|
1359
|
-
`export const ${action.exportName} = createStreamCaller('${action.actionName}');`
|
|
1372
|
+
`export const ${action.exportName} = createStreamCaller('${action.actionName}'${filesOpt});`
|
|
1360
1373
|
);
|
|
1361
1374
|
} else {
|
|
1362
1375
|
lines.push(
|
|
1363
|
-
`export const ${action.exportName} = createActionCaller('${action.actionName}', '${action.method}');`
|
|
1376
|
+
`export const ${action.exportName} = createActionCaller('${action.actionName}', '${action.method}'${filesOpt});`
|
|
1364
1377
|
);
|
|
1365
1378
|
}
|
|
1366
1379
|
}
|
|
@@ -1782,6 +1795,11 @@ function createActionRouter(options) {
|
|
|
1782
1795
|
requestMethod,
|
|
1783
1796
|
ACTION_ENDPOINT_BASE + "/" + actionName
|
|
1784
1797
|
);
|
|
1798
|
+
const tempDir = req._jayTempDir;
|
|
1799
|
+
const cleanup = () => {
|
|
1800
|
+
if (tempDir)
|
|
1801
|
+
cleanupTempDir(tempDir);
|
|
1802
|
+
};
|
|
1785
1803
|
if (registry.isStreaming(actionName)) {
|
|
1786
1804
|
res.setHeader("Content-Type", "application/x-ndjson");
|
|
1787
1805
|
res.setHeader("Transfer-Encoding", "chunked");
|
|
@@ -1794,11 +1812,13 @@ function createActionRouter(options) {
|
|
|
1794
1812
|
} catch (err) {
|
|
1795
1813
|
res.write(JSON.stringify({ error: err.message }) + "\n");
|
|
1796
1814
|
}
|
|
1815
|
+
cleanup();
|
|
1797
1816
|
res.end();
|
|
1798
1817
|
timing?.end();
|
|
1799
1818
|
return;
|
|
1800
1819
|
}
|
|
1801
1820
|
const result = await registry.execute(actionName, input);
|
|
1821
|
+
cleanup();
|
|
1802
1822
|
if (requestMethod === "GET" && result.success) {
|
|
1803
1823
|
const cacheHeaders = registry.getCacheHeaders(actionName);
|
|
1804
1824
|
if (cacheHeaders) {
|
|
@@ -1839,7 +1859,106 @@ function getStatusCodeForError(code, isActionError) {
|
|
|
1839
1859
|
return 500;
|
|
1840
1860
|
}
|
|
1841
1861
|
}
|
|
1842
|
-
|
|
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;
|
|
1843
1962
|
return (req, res, next) => {
|
|
1844
1963
|
if (!req.path.startsWith(ACTION_ENDPOINT_BASE)) {
|
|
1845
1964
|
next();
|
|
@@ -1849,6 +1968,42 @@ function actionBodyParser() {
|
|
|
1849
1968
|
next();
|
|
1850
1969
|
return;
|
|
1851
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
|
+
}
|
|
1852
2007
|
let body = "";
|
|
1853
2008
|
req.setEncoding("utf8");
|
|
1854
2009
|
req.on("data", (chunk) => {
|
|
@@ -2782,7 +2937,7 @@ async function mkDevServer(rawOptions) {
|
|
|
2782
2937
|
await lifecycleManager.initialize();
|
|
2783
2938
|
await materializeDynamicContracts(projectRootFolder, vite);
|
|
2784
2939
|
setupServiceHotReload(vite, lifecycleManager);
|
|
2785
|
-
setupActionRouter(vite);
|
|
2940
|
+
setupActionRouter(vite, buildFolder);
|
|
2786
2941
|
const projectRoutes = await initRoutes(pagesRootFolder);
|
|
2787
2942
|
const filteredProjectRoutes = buildFolder ? projectRoutes.filter((route) => !route.jayHtmlPath.startsWith(buildFolder)) : projectRoutes;
|
|
2788
2943
|
const pluginRoutes = await scanPluginRoutes(projectRootFolder, filteredProjectRoutes);
|
|
@@ -2864,8 +3019,8 @@ function setupServiceHotReload(vite, lifecycleManager) {
|
|
|
2864
3019
|
}
|
|
2865
3020
|
});
|
|
2866
3021
|
}
|
|
2867
|
-
function setupActionRouter(vite) {
|
|
2868
|
-
vite.middlewares.use(actionBodyParser());
|
|
3022
|
+
function setupActionRouter(vite, buildFolder) {
|
|
3023
|
+
vite.middlewares.use(actionBodyParser({ buildFolder }));
|
|
2869
3024
|
vite.middlewares.use(ACTION_ENDPOINT_BASE, createActionRouter());
|
|
2870
3025
|
getLogger().info(`[Actions] Action router mounted at ${ACTION_ENDPOINT_BASE}`);
|
|
2871
3026
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jay-framework/dev-server",
|
|
3
|
-
"version": "0.16.
|
|
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.16.
|
|
27
|
-
"@jay-framework/compiler-shared": "^0.16.
|
|
28
|
-
"@jay-framework/component": "^0.16.
|
|
29
|
-
"@jay-framework/fullstack-component": "^0.16.
|
|
30
|
-
"@jay-framework/logger": "^0.16.
|
|
31
|
-
"@jay-framework/runtime": "^0.16.
|
|
32
|
-
"@jay-framework/stack-client-runtime": "^0.16.
|
|
33
|
-
"@jay-framework/stack-route-scanner": "^0.16.
|
|
34
|
-
"@jay-framework/stack-server-runtime": "^0.16.
|
|
35
|
-
"@jay-framework/view-state-merge": "^0.16.
|
|
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.16.
|
|
40
|
-
"@jay-framework/jay-cli": "^0.16.
|
|
41
|
-
"@jay-framework/stack-client-runtime": "^0.16.
|
|
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",
|