rwsdk 1.0.8 → 1.1.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/lib/e2e/testHarness.mjs +6 -1
- package/dist/lib/normalizeModulePath.d.mts +2 -1
- package/dist/lib/normalizeModulePath.mjs +20 -13
- package/dist/lib/normalizeModulePath.test.mjs +19 -0
- package/dist/lib/smokeTests/codeUpdates.mjs +6 -6
- package/dist/lib/smokeTests/release.mjs +35 -0
- package/dist/runtime/server.d.ts +28 -0
- package/dist/runtime/server.js +41 -2
- package/dist/runtime/server.test.d.ts +1 -0
- package/dist/runtime/server.test.js +130 -0
- package/dist/vite/redwoodPlugin.mjs +4 -3
- package/package.json +1 -1
|
@@ -231,10 +231,15 @@ export function createDeployment() {
|
|
|
231
231
|
? match[1]
|
|
232
232
|
: Math.random().toString(36).substring(2, 15);
|
|
233
233
|
const deployResult = await runRelease(projectDir, projectDir, resourceUniqueKey);
|
|
234
|
+
// A fresh *.workers.dev subdomain can return 200 with Cloudflare's
|
|
235
|
+
// "There is nothing here yet" placeholder before the worker code
|
|
236
|
+
// propagates globally. Wait until the response body contains the
|
|
237
|
+
// rwsdk-rendered marker so tests don't run against the placeholder.
|
|
234
238
|
await poll(async () => {
|
|
235
239
|
try {
|
|
236
240
|
const response = await fetch(deployResult.url);
|
|
237
|
-
|
|
241
|
+
const body = await response.text();
|
|
242
|
+
return body.includes("__RWSDK_CONTEXT");
|
|
238
243
|
}
|
|
239
244
|
catch (e) {
|
|
240
245
|
return false;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
/**
|
|
2
3
|
* Find the number of common ancestor segments between two absolute paths.
|
|
3
4
|
* Returns the count of shared directory segments from the root.
|
|
@@ -28,4 +29,4 @@ export declare function findCommonAncestorDepth(path1: string, path2: string): n
|
|
|
28
29
|
export declare function normalizeModulePath(modulePath: string, projectRootDir: string, options?: {
|
|
29
30
|
absolute?: boolean;
|
|
30
31
|
isViteStyle?: boolean;
|
|
31
|
-
}): string;
|
|
32
|
+
}, _path?: typeof path): string;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { normalizePath
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { normalizePath } from "vite";
|
|
3
|
+
const windowsDriveAbsoluteRE = /^[A-Za-z]:\//;
|
|
4
|
+
// Vite's normalizePath only converts backslashes on Windows. This wrapper
|
|
5
|
+
// ensures forward slashes regardless of platform, which matters when win32
|
|
6
|
+
// path functions are injected for testing on Linux.
|
|
7
|
+
const normalizePathSeparators = (p) => normalizePath(p.replace(/\\/g, "/"));
|
|
3
8
|
/**
|
|
4
9
|
* Find the number of common ancestor segments between two absolute paths.
|
|
5
10
|
* Returns the count of shared directory segments from the root.
|
|
@@ -41,16 +46,16 @@ export function findCommonAncestorDepth(path1, path2) {
|
|
|
41
46
|
* /opt/tools/logger.ts → /opt/tools/logger.ts (resolved as Vite-style)
|
|
42
47
|
* /src/page.tsx, { absolute: true } → /Users/justin/my-app/src/page.tsx
|
|
43
48
|
*/
|
|
44
|
-
export function normalizeModulePath(modulePath, projectRootDir, options = {}) {
|
|
49
|
+
export function normalizeModulePath(modulePath, projectRootDir, options = {}, _path = path) {
|
|
45
50
|
modulePath = normalizePathSeparators(modulePath);
|
|
46
|
-
projectRootDir = normalizePathSeparators(
|
|
51
|
+
projectRootDir = normalizePathSeparators(_path.resolve(projectRootDir));
|
|
47
52
|
// Handle empty string or current directory
|
|
48
53
|
if (modulePath === "" || modulePath === ".") {
|
|
49
54
|
return options.absolute ? projectRootDir : "/";
|
|
50
55
|
}
|
|
51
56
|
// For relative paths, resolve them first
|
|
52
57
|
let resolved;
|
|
53
|
-
if (
|
|
58
|
+
if (_path.isAbsolute(modulePath)) {
|
|
54
59
|
if (modulePath.startsWith(projectRootDir + "/") ||
|
|
55
60
|
modulePath === projectRootDir) {
|
|
56
61
|
// Path starts with project root - it's a real absolute path inside project
|
|
@@ -61,7 +66,7 @@ export function normalizeModulePath(modulePath, projectRootDir, options = {}) {
|
|
|
61
66
|
if (options.isViteStyle !== undefined) {
|
|
62
67
|
// User explicitly specified whether this should be treated as Vite-style
|
|
63
68
|
if (options.isViteStyle) {
|
|
64
|
-
resolved =
|
|
69
|
+
resolved = _path.resolve(projectRootDir, modulePath.slice(1));
|
|
65
70
|
}
|
|
66
71
|
else {
|
|
67
72
|
resolved = modulePath;
|
|
@@ -70,19 +75,20 @@ export function normalizeModulePath(modulePath, projectRootDir, options = {}) {
|
|
|
70
75
|
else {
|
|
71
76
|
// Fall back to heuristics using common ancestor depth
|
|
72
77
|
const commonDepth = findCommonAncestorDepth(modulePath, projectRootDir);
|
|
73
|
-
if (commonDepth > 0) {
|
|
74
|
-
// Paths share meaningful common ancestor
|
|
78
|
+
if (commonDepth > 0 || windowsDriveAbsoluteRE.test(modulePath)) {
|
|
79
|
+
// Paths share meaningful common ancestor, or this is a Windows absolute
|
|
80
|
+
// path on a different drive — treat as a real external absolute path
|
|
75
81
|
resolved = modulePath;
|
|
76
82
|
}
|
|
77
83
|
else {
|
|
78
84
|
// No meaningful common ancestor - assume Vite-style path within project
|
|
79
|
-
resolved =
|
|
85
|
+
resolved = _path.resolve(projectRootDir, modulePath.slice(1));
|
|
80
86
|
}
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
else {
|
|
85
|
-
resolved =
|
|
91
|
+
resolved = _path.resolve(projectRootDir, modulePath);
|
|
86
92
|
}
|
|
87
93
|
resolved = normalizePathSeparators(resolved);
|
|
88
94
|
// If absolute option is set, always return absolute paths
|
|
@@ -90,9 +96,10 @@ export function normalizeModulePath(modulePath, projectRootDir, options = {}) {
|
|
|
90
96
|
return resolved;
|
|
91
97
|
}
|
|
92
98
|
// Check if the resolved path is within the project root
|
|
93
|
-
const relative =
|
|
94
|
-
// If the path goes outside the project root (starts with ..)
|
|
95
|
-
|
|
99
|
+
const relative = _path.relative(projectRootDir, resolved);
|
|
100
|
+
// If the path goes outside the project root (starts with ..) or is on a
|
|
101
|
+
// different drive (Windows: path.relative returns an absolute path), return absolute
|
|
102
|
+
if (relative.startsWith("..") || _path.isAbsolute(relative)) {
|
|
96
103
|
return resolved;
|
|
97
104
|
}
|
|
98
105
|
// Path is within project root, return as Vite-style relative path
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { win32 as windowsPath } from "node:path";
|
|
1
2
|
import { describe, expect, it } from "vitest";
|
|
2
3
|
import { findCommonAncestorDepth, normalizeModulePath, } from "./normalizeModulePath.mjs";
|
|
3
4
|
describe("findCommonAncestorDepth", () => {
|
|
@@ -220,3 +221,21 @@ describe("normalizeModulePath", () => {
|
|
|
220
221
|
});
|
|
221
222
|
});
|
|
222
223
|
});
|
|
224
|
+
describe("Windows paths (path.win32)", () => {
|
|
225
|
+
const root = "C:/Projects/app";
|
|
226
|
+
it("Windows absolute path inside project", () => {
|
|
227
|
+
expect(normalizeModulePath("C:/Projects/app/src/page.tsx", root, {}, windowsPath)).toBe("/src/page.tsx");
|
|
228
|
+
});
|
|
229
|
+
it("Windows absolute path outside project (same drive)", () => {
|
|
230
|
+
expect(normalizeModulePath("C:/other/utils.ts", root, {}, windowsPath)).toBe("C:/other/utils.ts");
|
|
231
|
+
});
|
|
232
|
+
it("Cross-drive path is treated as external", () => {
|
|
233
|
+
expect(normalizeModulePath("D:/other/utils.ts", root, {}, windowsPath)).toBe("D:/other/utils.ts");
|
|
234
|
+
});
|
|
235
|
+
it("Relative path resolves correctly", () => {
|
|
236
|
+
expect(normalizeModulePath("src/page.tsx", root, {}, windowsPath)).toBe("/src/page.tsx");
|
|
237
|
+
});
|
|
238
|
+
it("Windows absolute path inside project with absolute option", () => {
|
|
239
|
+
expect(normalizeModulePath("C:/Projects/app/src/page.tsx", root, { absolute: true }, windowsPath)).toBe("C:/Projects/app/src/page.tsx");
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -75,9 +75,9 @@ export async function createSmokeTestStylesheets(targetDir) {
|
|
|
75
75
|
const urlStylesPath = join(appDir, "smokeTestUrlStyles.css");
|
|
76
76
|
log("Creating smokeTestUrlStyles.css at: %s", urlStylesPath);
|
|
77
77
|
await fs.writeFile(urlStylesPath, smokeTestUrlStylesCssTemplate);
|
|
78
|
-
// Modify
|
|
79
|
-
const documentPath = join(appDir, "
|
|
80
|
-
log("Modifying
|
|
78
|
+
// Modify document.tsx to include the URL stylesheet using CSS URL import
|
|
79
|
+
const documentPath = join(appDir, "document.tsx");
|
|
80
|
+
log("Modifying document.tsx to include URL stylesheet at: %s", documentPath);
|
|
81
81
|
try {
|
|
82
82
|
const documentContent = await fs.readFile(documentPath, "utf-8");
|
|
83
83
|
const s = new MagicString(documentContent);
|
|
@@ -90,14 +90,14 @@ export async function createSmokeTestStylesheets(targetDir) {
|
|
|
90
90
|
if (headTagEnd !== -1) {
|
|
91
91
|
s.appendLeft(headTagEnd, ' <link rel="stylesheet" href={smokeTestUrlStyles} />\n');
|
|
92
92
|
await fs.writeFile(documentPath, s.toString(), "utf-8");
|
|
93
|
-
log("Successfully modified
|
|
93
|
+
log("Successfully modified document.tsx with CSS URL import pattern");
|
|
94
94
|
}
|
|
95
95
|
else {
|
|
96
|
-
log("Could not find </head> tag in
|
|
96
|
+
log("Could not find </head> tag in document.tsx");
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
catch (e) {
|
|
100
|
-
log("Could not modify
|
|
100
|
+
log("Could not modify document.tsx: %s", e);
|
|
101
101
|
}
|
|
102
102
|
log("Smoke test stylesheets created successfully");
|
|
103
103
|
}
|
|
@@ -9,6 +9,35 @@ export { $expect, deleteD1Database, deleteWorker, isRelatedToTest, listD1Databas
|
|
|
9
9
|
export async function runRelease(cwd, projectDir, resourceUniqueKey) {
|
|
10
10
|
return runE2ERelease(cwd, projectDir, resourceUniqueKey);
|
|
11
11
|
}
|
|
12
|
+
async function waitForDeploymentContent(baseUrl, { timeoutMs = 60_000, intervalMs = 2_000, } = {}) {
|
|
13
|
+
const marker = "__RWSDK_CONTEXT";
|
|
14
|
+
const deadline = Date.now() + timeoutMs;
|
|
15
|
+
let attempt = 0;
|
|
16
|
+
let lastStatus;
|
|
17
|
+
let lastBytes = 0;
|
|
18
|
+
while (Date.now() < deadline) {
|
|
19
|
+
attempt += 1;
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(baseUrl);
|
|
22
|
+
const body = await res.text();
|
|
23
|
+
lastStatus = res.status;
|
|
24
|
+
lastBytes = body.length;
|
|
25
|
+
if (body.includes(marker)) {
|
|
26
|
+
log("Deployment content verified at %s after %d attempt(s)", baseUrl, attempt);
|
|
27
|
+
console.log(`✅ Deployment content ready at ${baseUrl} (attempt ${attempt})`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
log("Attempt %d: %s returned %d (%d bytes), no app marker yet", attempt, baseUrl, res.status, body.length);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
log("Attempt %d: fetch failed for %s: %O", attempt, baseUrl, err);
|
|
34
|
+
}
|
|
35
|
+
await setTimeout(intervalMs);
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`Deployment at ${baseUrl} did not serve app content within ${timeoutMs}ms ` +
|
|
38
|
+
`(last status ${lastStatus ?? "n/a"}, ${lastBytes} bytes). ` +
|
|
39
|
+
`Likely Cloudflare *.workers.dev propagation still in progress.`);
|
|
40
|
+
}
|
|
12
41
|
/**
|
|
13
42
|
* Runs tests against the production deployment
|
|
14
43
|
*/
|
|
@@ -23,6 +52,12 @@ export async function runReleaseTest(artifactDir, resources, browserPath, headle
|
|
|
23
52
|
await setTimeout(1000);
|
|
24
53
|
// DRY: check both root and custom path
|
|
25
54
|
await checkServerUp(url, "/");
|
|
55
|
+
// A fresh *.workers.dev subdomain can return 200 with Cloudflare's
|
|
56
|
+
// "There is nothing here yet" placeholder before the worker code is
|
|
57
|
+
// globally propagated. Poll the URL until the response body contains
|
|
58
|
+
// an rwsdk-rendered marker so we don't run the browser tests against
|
|
59
|
+
// the placeholder.
|
|
60
|
+
await waitForDeploymentContent(url);
|
|
26
61
|
// Now run the tests with the custom path
|
|
27
62
|
const testUrl = new URL("/__smoke_test", url).toString();
|
|
28
63
|
await checkUrl(testUrl, artifactDir, browserPath, headless, bail, skipClient, "Production", realtime, resources.targetDir, // Add target directory parameter
|
package/dist/runtime/server.d.ts
CHANGED
|
@@ -11,6 +11,34 @@ type WrappedServerFunction<TArgs extends any[] = any[], TResult = any> = {
|
|
|
11
11
|
(...args: TArgs): Promise<TResult>;
|
|
12
12
|
method?: "GET" | "POST";
|
|
13
13
|
};
|
|
14
|
+
export type ServerFunctionWrap = (fn: Function, args: any[], type: "action" | "query") => Promise<any>;
|
|
15
|
+
/**
|
|
16
|
+
* Register a wrapper that runs around every server action and query handler.
|
|
17
|
+
*
|
|
18
|
+
* Call this once in your worker entry point. The wrapper receives the main
|
|
19
|
+
* handler function, its arguments, and the type ("action" or "query").
|
|
20
|
+
* Interruptors run *outside* the wrapper.
|
|
21
|
+
*
|
|
22
|
+
* Throws if called more than once.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { registerServerFunctionWrap } from "rwsdk/worker";
|
|
27
|
+
* import * as Sentry from "@sentry/cloudflare";
|
|
28
|
+
*
|
|
29
|
+
* registerServerFunctionWrap((fn, args, type) =>
|
|
30
|
+
* Sentry.startSpan(
|
|
31
|
+
* { name: fn.name, op: `function.rsc_${type}` },
|
|
32
|
+
* () => fn(...args)
|
|
33
|
+
* )
|
|
34
|
+
* );
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function registerServerFunctionWrap(wrap: ServerFunctionWrap): void;
|
|
38
|
+
/**
|
|
39
|
+
* @internal Reset the global wrap — only for tests.
|
|
40
|
+
*/
|
|
41
|
+
export declare function __resetServerFunctionWrap(): void;
|
|
14
42
|
/**
|
|
15
43
|
* Wrap a function to be used as a server query.
|
|
16
44
|
*
|
package/dist/runtime/server.js
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
import { requestInfo } from "./requestInfo/worker";
|
|
2
|
+
let globalWrap;
|
|
3
|
+
/**
|
|
4
|
+
* Register a wrapper that runs around every server action and query handler.
|
|
5
|
+
*
|
|
6
|
+
* Call this once in your worker entry point. The wrapper receives the main
|
|
7
|
+
* handler function, its arguments, and the type ("action" or "query").
|
|
8
|
+
* Interruptors run *outside* the wrapper.
|
|
9
|
+
*
|
|
10
|
+
* Throws if called more than once.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { registerServerFunctionWrap } from "rwsdk/worker";
|
|
15
|
+
* import * as Sentry from "@sentry/cloudflare";
|
|
16
|
+
*
|
|
17
|
+
* registerServerFunctionWrap((fn, args, type) =>
|
|
18
|
+
* Sentry.startSpan(
|
|
19
|
+
* { name: fn.name, op: `function.rsc_${type}` },
|
|
20
|
+
* () => fn(...args)
|
|
21
|
+
* )
|
|
22
|
+
* );
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function registerServerFunctionWrap(wrap) {
|
|
26
|
+
if (globalWrap) {
|
|
27
|
+
throw new Error("registerServerFunctionWrap() has already been called. " +
|
|
28
|
+
"Only one wrapper can be registered.");
|
|
29
|
+
}
|
|
30
|
+
globalWrap = wrap;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @internal Reset the global wrap — only for tests.
|
|
34
|
+
*/
|
|
35
|
+
export function __resetServerFunctionWrap() {
|
|
36
|
+
globalWrap = undefined;
|
|
37
|
+
}
|
|
2
38
|
function createServerFunction(fns, mainFn, options) {
|
|
3
39
|
const wrapped = async (...args) => {
|
|
4
40
|
const { request, ctx } = requestInfo;
|
|
@@ -11,6 +47,9 @@ function createServerFunction(fns, mainFn, options) {
|
|
|
11
47
|
return result;
|
|
12
48
|
}
|
|
13
49
|
}
|
|
50
|
+
if (globalWrap) {
|
|
51
|
+
return globalWrap(mainFn, args, options?.__type ?? "action");
|
|
52
|
+
}
|
|
14
53
|
return mainFn(...args);
|
|
15
54
|
};
|
|
16
55
|
wrapped.method = options?.method ?? "POST";
|
|
@@ -45,7 +84,7 @@ export function serverQuery(fnsOrFn, options) {
|
|
|
45
84
|
mainFn = fnsOrFn;
|
|
46
85
|
}
|
|
47
86
|
const method = options?.method ?? "GET"; // Default to GET for query
|
|
48
|
-
const wrapped = createServerFunction(fns, mainFn, { ...options, method });
|
|
87
|
+
const wrapped = createServerFunction(fns, mainFn, { ...options, method, __type: "query" });
|
|
49
88
|
wrapped.method = method;
|
|
50
89
|
return wrapped;
|
|
51
90
|
}
|
|
@@ -78,7 +117,7 @@ export function serverAction(fnsOrFn, options) {
|
|
|
78
117
|
mainFn = fnsOrFn;
|
|
79
118
|
}
|
|
80
119
|
const method = options?.method ?? "POST"; // Default to POST for action
|
|
81
|
-
const wrapped = createServerFunction(fns, mainFn, { ...options, method });
|
|
120
|
+
const wrapped = createServerFunction(fns, mainFn, { ...options, method, __type: "action" });
|
|
82
121
|
wrapped.method = method;
|
|
83
122
|
return wrapped;
|
|
84
123
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
2
|
+
let mockRequestInfo;
|
|
3
|
+
vi.mock("./requestInfo/worker", () => ({
|
|
4
|
+
get requestInfo() {
|
|
5
|
+
return mockRequestInfo;
|
|
6
|
+
},
|
|
7
|
+
}));
|
|
8
|
+
import { serverAction, serverQuery, registerServerFunctionWrap, __resetServerFunctionWrap, } from "./server";
|
|
9
|
+
describe("registerServerFunctionWrap", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
__resetServerFunctionWrap();
|
|
12
|
+
mockRequestInfo = {
|
|
13
|
+
request: new Request("https://test.example/"),
|
|
14
|
+
ctx: {},
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
it("wraps serverAction handlers", async () => {
|
|
18
|
+
const wrapSpy = vi.fn((fn, args, _type) => fn(...args));
|
|
19
|
+
registerServerFunctionWrap(wrapSpy);
|
|
20
|
+
const action = serverAction(async function createThing(name) {
|
|
21
|
+
return `created ${name}`;
|
|
22
|
+
});
|
|
23
|
+
const result = await action("foo");
|
|
24
|
+
expect(wrapSpy).toHaveBeenCalledOnce();
|
|
25
|
+
expect(wrapSpy.mock.calls[0][2]).toBe("action");
|
|
26
|
+
expect(result).toBe("created foo");
|
|
27
|
+
});
|
|
28
|
+
it("wraps serverQuery handlers", async () => {
|
|
29
|
+
const wrapSpy = vi.fn((fn, args, _type) => fn(...args));
|
|
30
|
+
registerServerFunctionWrap(wrapSpy);
|
|
31
|
+
const query = serverQuery(async function getThings() {
|
|
32
|
+
return [1, 2, 3];
|
|
33
|
+
});
|
|
34
|
+
const result = await query();
|
|
35
|
+
expect(wrapSpy).toHaveBeenCalledOnce();
|
|
36
|
+
expect(wrapSpy.mock.calls[0][2]).toBe("query");
|
|
37
|
+
expect(result).toEqual([1, 2, 3]);
|
|
38
|
+
});
|
|
39
|
+
it("passes the main function and args to the wrapper", async () => {
|
|
40
|
+
const wrapSpy = vi.fn((fn, args, _type) => fn(...args));
|
|
41
|
+
registerServerFunctionWrap(wrapSpy);
|
|
42
|
+
const action = serverAction(async function multiply(a, b) {
|
|
43
|
+
return a * b;
|
|
44
|
+
});
|
|
45
|
+
await action(3, 7);
|
|
46
|
+
const [fn, args] = wrapSpy.mock.calls[0];
|
|
47
|
+
expect(fn.name).toBe("multiply");
|
|
48
|
+
expect(args).toEqual([3, 7]);
|
|
49
|
+
});
|
|
50
|
+
it("interruptors run outside the wrapper", async () => {
|
|
51
|
+
const order = [];
|
|
52
|
+
registerServerFunctionWrap((fn, args, _type) => {
|
|
53
|
+
order.push("wrap:start");
|
|
54
|
+
const result = fn(...args);
|
|
55
|
+
order.push("wrap:end");
|
|
56
|
+
return result;
|
|
57
|
+
});
|
|
58
|
+
const interruptor = async () => {
|
|
59
|
+
order.push("interruptor");
|
|
60
|
+
};
|
|
61
|
+
const action = serverAction([
|
|
62
|
+
interruptor,
|
|
63
|
+
async function doWork() {
|
|
64
|
+
order.push("handler");
|
|
65
|
+
return "done";
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
await action();
|
|
69
|
+
expect(order).toEqual([
|
|
70
|
+
"interruptor",
|
|
71
|
+
"wrap:start",
|
|
72
|
+
"handler",
|
|
73
|
+
"wrap:end",
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
it("wrapper is not called when an interruptor short-circuits", async () => {
|
|
77
|
+
const wrapSpy = vi.fn((fn, args, _type) => fn(...args));
|
|
78
|
+
registerServerFunctionWrap(wrapSpy);
|
|
79
|
+
const action = serverAction([
|
|
80
|
+
async () => new Response("blocked", { status: 403 }),
|
|
81
|
+
async function neverCalled() {
|
|
82
|
+
return "nope";
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
const result = await action();
|
|
86
|
+
expect(wrapSpy).not.toHaveBeenCalled();
|
|
87
|
+
expect(result).toBeInstanceOf(Response);
|
|
88
|
+
});
|
|
89
|
+
it("throws if called more than once", () => {
|
|
90
|
+
registerServerFunctionWrap(async (fn, args) => fn(...args));
|
|
91
|
+
expect(() => {
|
|
92
|
+
registerServerFunctionWrap(async (fn, args) => fn(...args));
|
|
93
|
+
}).toThrow("registerServerFunctionWrap() has already been called");
|
|
94
|
+
});
|
|
95
|
+
it("without registration, handlers work normally", async () => {
|
|
96
|
+
const action = serverAction(async function echo(msg) {
|
|
97
|
+
return msg;
|
|
98
|
+
});
|
|
99
|
+
const result = await action("hello");
|
|
100
|
+
expect(result).toBe("hello");
|
|
101
|
+
});
|
|
102
|
+
it("wrapper can transform the return value", async () => {
|
|
103
|
+
registerServerFunctionWrap(async (fn, args, _type) => {
|
|
104
|
+
const result = await fn(...args);
|
|
105
|
+
return { wrapped: true, result };
|
|
106
|
+
});
|
|
107
|
+
const action = serverAction(async function getValue() {
|
|
108
|
+
return 42;
|
|
109
|
+
});
|
|
110
|
+
const result = await action();
|
|
111
|
+
expect(result).toEqual({ wrapped: true, result: 42 });
|
|
112
|
+
});
|
|
113
|
+
it("works with array-style serverAction (interruptors + handler)", async () => {
|
|
114
|
+
const wrapSpy = vi.fn((fn, args, _type) => fn(...args));
|
|
115
|
+
registerServerFunctionWrap(wrapSpy);
|
|
116
|
+
const action = serverAction([
|
|
117
|
+
async ({ ctx }) => {
|
|
118
|
+
ctx.authed = true;
|
|
119
|
+
},
|
|
120
|
+
async function save(data) {
|
|
121
|
+
return `saved ${data}`;
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
const result = await action("test");
|
|
125
|
+
expect(result).toBe("saved test");
|
|
126
|
+
expect(wrapSpy).toHaveBeenCalledOnce();
|
|
127
|
+
const [fn] = wrapSpy.mock.calls[0];
|
|
128
|
+
expect(fn.name).toBe("save");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cloudflare } from "@cloudflare/vite-plugin";
|
|
2
|
-
import { resolve } from "node:path
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { normalizePath } from "vite";
|
|
3
4
|
import { unstable_readConfig } from "wrangler";
|
|
4
5
|
import { devServerConstantPlugin } from "./devServerConstant.mjs";
|
|
5
6
|
import { hasOwnCloudflareVitePlugin } from "./hasOwnCloudflareVitePlugin.mjs";
|
|
@@ -32,10 +33,10 @@ import { useServerLookupPlugin } from "./useServerLookupPlugin.mjs";
|
|
|
32
33
|
import { vitePreamblePlugin } from "./vitePreamblePlugin.mjs";
|
|
33
34
|
export const determineWorkerEntryPathname = async ({ projectRootDir, workerConfigPath, options, readConfig = unstable_readConfig, }) => {
|
|
34
35
|
if (options.entry?.worker) {
|
|
35
|
-
return resolve(projectRootDir, options.entry.worker);
|
|
36
|
+
return normalizePath(resolve(projectRootDir, options.entry.worker));
|
|
36
37
|
}
|
|
37
38
|
const workerConfig = readConfig({ config: workerConfigPath });
|
|
38
|
-
return resolve(projectRootDir, workerConfig.main ?? "src/worker.tsx");
|
|
39
|
+
return normalizePath(resolve(projectRootDir, workerConfig.main ?? "src/worker.tsx"));
|
|
39
40
|
};
|
|
40
41
|
const clientFiles = new Set();
|
|
41
42
|
const serverFiles = new Set();
|