rwsdk 0.1.23 → 0.1.25

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.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Normalize a module path to a consistent form.
3
+ * Returns slash-prefixed paths for files within project root,
4
+ * or absolute paths for external files.
5
+ *
6
+ * Examples:
7
+ * /Users/justin/my-app/src/page.ts → /src/page.ts
8
+ * ../shared/utils.ts → /Users/justin/shared/utils.ts
9
+ * /src/page.ts (Vite-style) → /src/page.ts
10
+ * node_modules/foo/index.js → /node_modules/foo/index.js
11
+ *
12
+ * With { absolute: true }:
13
+ * /Users/justin/my-app/src/page.ts → /Users/justin/my-app/src/page.ts
14
+ *
15
+ * With { isViteStyle: false }:
16
+ * /opt/tools/logger.ts → /opt/tools/logger.ts (treated as external)
17
+ * /src/page.tsx → /src/page.tsx (treated as external)
18
+ *
19
+ * With { isViteStyle: true }:
20
+ * /opt/tools/logger.ts → /opt/tools/logger.ts (resolved as Vite-style)
21
+ * /src/page.tsx, { absolute: true } → /Users/justin/my-app/src/page.tsx
22
+ */
23
+ export declare function normalizeModulePath(modulePath: string, projectRootDir: string, options?: {
24
+ absolute?: boolean;
25
+ isViteStyle?: boolean;
26
+ }): string;
@@ -0,0 +1,101 @@
1
+ import * as path from "node:path";
2
+ import { normalizePath as normalizePathSeparators } from "vite";
3
+ /**
4
+ * Find the number of common ancestor segments between two absolute paths.
5
+ * Returns the count of shared directory segments from the root.
6
+ */
7
+ function findCommonAncestorDepth(path1, path2) {
8
+ const segments1 = path1.split("/").filter(Boolean);
9
+ const segments2 = path2.split("/").filter(Boolean);
10
+ let commonLength = 0;
11
+ const minLength = Math.min(segments1.length, segments2.length);
12
+ for (let i = 0; i < minLength; i++) {
13
+ if (segments1[i] === segments2[i]) {
14
+ commonLength++;
15
+ }
16
+ else {
17
+ break;
18
+ }
19
+ }
20
+ return commonLength;
21
+ }
22
+ /**
23
+ * Normalize a module path to a consistent form.
24
+ * Returns slash-prefixed paths for files within project root,
25
+ * or absolute paths for external files.
26
+ *
27
+ * Examples:
28
+ * /Users/justin/my-app/src/page.ts → /src/page.ts
29
+ * ../shared/utils.ts → /Users/justin/shared/utils.ts
30
+ * /src/page.ts (Vite-style) → /src/page.ts
31
+ * node_modules/foo/index.js → /node_modules/foo/index.js
32
+ *
33
+ * With { absolute: true }:
34
+ * /Users/justin/my-app/src/page.ts → /Users/justin/my-app/src/page.ts
35
+ *
36
+ * With { isViteStyle: false }:
37
+ * /opt/tools/logger.ts → /opt/tools/logger.ts (treated as external)
38
+ * /src/page.tsx → /src/page.tsx (treated as external)
39
+ *
40
+ * With { isViteStyle: true }:
41
+ * /opt/tools/logger.ts → /opt/tools/logger.ts (resolved as Vite-style)
42
+ * /src/page.tsx, { absolute: true } → /Users/justin/my-app/src/page.tsx
43
+ */
44
+ export function normalizeModulePath(modulePath, projectRootDir, options = {}) {
45
+ modulePath = normalizePathSeparators(modulePath);
46
+ projectRootDir = normalizePathSeparators(path.resolve(projectRootDir));
47
+ // Handle empty string or current directory
48
+ if (modulePath === "" || modulePath === ".") {
49
+ return options.absolute ? projectRootDir : "/";
50
+ }
51
+ // For relative paths, resolve them first
52
+ let resolved;
53
+ if (path.isAbsolute(modulePath)) {
54
+ if (modulePath.startsWith(projectRootDir + "/") ||
55
+ modulePath === projectRootDir) {
56
+ // Path starts with project root - it's a real absolute path inside project
57
+ resolved = modulePath;
58
+ }
59
+ else {
60
+ // Check how the path relates to the project root
61
+ if (options.isViteStyle !== undefined) {
62
+ // User explicitly specified whether this should be treated as Vite-style
63
+ if (options.isViteStyle) {
64
+ resolved = path.resolve(projectRootDir, modulePath.slice(1));
65
+ }
66
+ else {
67
+ resolved = modulePath;
68
+ }
69
+ }
70
+ else {
71
+ // Fall back to heuristics using common ancestor depth
72
+ const commonDepth = findCommonAncestorDepth(modulePath, projectRootDir);
73
+ if (commonDepth > 0) {
74
+ // Paths share meaningful common ancestor - treat as real absolute path
75
+ resolved = modulePath;
76
+ }
77
+ else {
78
+ // No meaningful common ancestor - assume Vite-style path within project
79
+ resolved = path.resolve(projectRootDir, modulePath.slice(1));
80
+ }
81
+ }
82
+ }
83
+ }
84
+ else {
85
+ resolved = path.resolve(projectRootDir, modulePath);
86
+ }
87
+ resolved = normalizePathSeparators(resolved);
88
+ // If absolute option is set, always return absolute paths
89
+ if (options.absolute) {
90
+ return resolved;
91
+ }
92
+ // Check if the resolved path is within the project root
93
+ const relative = path.relative(projectRootDir, resolved);
94
+ // If the path goes outside the project root (starts with ..), return absolute
95
+ if (relative.startsWith("..")) {
96
+ return resolved;
97
+ }
98
+ // Path is within project root, return as Vite-style relative path
99
+ const cleanRelative = relative === "." ? "" : relative;
100
+ return "/" + normalizePathSeparators(cleanRelative);
101
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,203 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { normalizeModulePath } from "./normalizeModulePath.mjs";
3
+ describe("normalizeModulePath", () => {
4
+ describe("1. Project-local paths", () => {
5
+ it("Relative file", () => {
6
+ expect(normalizeModulePath("src/page.tsx", "/Users/name/code/my-app")).toBe("/src/page.tsx");
7
+ });
8
+ it("Relative file in subdir", () => {
9
+ expect(normalizeModulePath("src/utils/index.ts", "/Users/name/code/my-app")).toBe("/src/utils/index.ts");
10
+ });
11
+ it("Relative file with ./", () => {
12
+ expect(normalizeModulePath("./src/page.tsx", "/Users/name/code/my-app")).toBe("/src/page.tsx");
13
+ });
14
+ it("Relative file with ../ (external)", () => {
15
+ expect(normalizeModulePath("../shared/foo.ts", "/Users/name/code/my-app")).toBe("/Users/name/code/shared/foo.ts");
16
+ });
17
+ });
18
+ describe("2. Vite-style absolute paths", () => {
19
+ it("Vite-style root import", () => {
20
+ expect(normalizeModulePath("/src/page.tsx", "/Users/name/code/my-app")).toBe("/src/page.tsx");
21
+ });
22
+ it("Vite-style node_modules", () => {
23
+ expect(normalizeModulePath("/node_modules/foo.js", "/Users/name/code/my-app")).toBe("/node_modules/foo.js");
24
+ });
25
+ });
26
+ describe("3. Real absolute paths inside project", () => {
27
+ it("Real abs path (inside)", () => {
28
+ expect(normalizeModulePath("/Users/name/code/my-app/src/page.tsx", "/Users/name/code/my-app")).toBe("/src/page.tsx");
29
+ });
30
+ it("Real abs path (deep inside)", () => {
31
+ expect(normalizeModulePath("/Users/name/code/my-app/src/features/auth.ts", "/Users/name/code/my-app")).toBe("/src/features/auth.ts");
32
+ });
33
+ });
34
+ describe("4. Real absolute paths outside project", () => {
35
+ it("External shared pkg", () => {
36
+ expect(normalizeModulePath("/Users/name/code/my-monorepo/packages/shared/utils.ts", "/Users/name/code/my-monorepo/packages/app")).toBe("/Users/name/code/my-monorepo/packages/shared/utils.ts");
37
+ });
38
+ it("External node_modules", () => {
39
+ expect(normalizeModulePath("/Users/name/code/my-monorepo/node_modules/foo/index.js", "/Users/name/code/my-monorepo/packages/app")).toBe("/Users/name/code/my-monorepo/node_modules/foo/index.js");
40
+ });
41
+ it("Completely external path", () => {
42
+ expect(normalizeModulePath("/opt/tools/logger.ts", "/Users/name/code/my-app")).toBe("/opt/tools/logger.ts");
43
+ });
44
+ });
45
+ describe("5. Absolute option", () => {
46
+ describe("Project-local paths with absolute option", () => {
47
+ it("Relative file", () => {
48
+ expect(normalizeModulePath("src/page.tsx", "/Users/name/code/my-app", {
49
+ absolute: true,
50
+ })).toBe("/Users/name/code/my-app/src/page.tsx");
51
+ });
52
+ it("Relative file in subdir", () => {
53
+ expect(normalizeModulePath("src/utils/index.ts", "/Users/name/code/my-app", {
54
+ absolute: true,
55
+ })).toBe("/Users/name/code/my-app/src/utils/index.ts");
56
+ });
57
+ it("Relative file with ./", () => {
58
+ expect(normalizeModulePath("./src/page.tsx", "/Users/name/code/my-app", {
59
+ absolute: true,
60
+ })).toBe("/Users/name/code/my-app/src/page.tsx");
61
+ });
62
+ it("Relative file with ../ (external)", () => {
63
+ expect(normalizeModulePath("../shared/foo.ts", "/Users/name/code/my-app", {
64
+ absolute: true,
65
+ })).toBe("/Users/name/code/shared/foo.ts");
66
+ });
67
+ });
68
+ describe("Vite-style absolute paths with absolute option", () => {
69
+ it("Vite-style root import", () => {
70
+ expect(normalizeModulePath("/src/page.tsx", "/Users/name/code/my-app", {
71
+ absolute: true,
72
+ })).toBe("/Users/name/code/my-app/src/page.tsx");
73
+ });
74
+ it("Vite-style node_modules", () => {
75
+ expect(normalizeModulePath("/node_modules/foo.js", "/Users/name/code/my-app", { absolute: true })).toBe("/Users/name/code/my-app/node_modules/foo.js");
76
+ });
77
+ });
78
+ describe("Real absolute paths with absolute option", () => {
79
+ it("Real abs path (inside)", () => {
80
+ expect(normalizeModulePath("/Users/name/code/my-app/src/page.tsx", "/Users/name/code/my-app", { absolute: true })).toBe("/Users/name/code/my-app/src/page.tsx");
81
+ });
82
+ it("Real abs path (deep inside)", () => {
83
+ expect(normalizeModulePath("/Users/name/code/my-app/src/features/auth.ts", "/Users/name/code/my-app", { absolute: true })).toBe("/Users/name/code/my-app/src/features/auth.ts");
84
+ });
85
+ it("External shared pkg", () => {
86
+ expect(normalizeModulePath("/Users/name/code/my-monorepo/packages/shared/utils.ts", "/Users/name/code/my-monorepo/packages/app", { absolute: true })).toBe("/Users/name/code/my-monorepo/packages/shared/utils.ts");
87
+ });
88
+ it("External node_modules", () => {
89
+ expect(normalizeModulePath("/Users/name/code/my-monorepo/node_modules/foo/index.js", "/Users/name/code/my-monorepo/packages/app", { absolute: true })).toBe("/Users/name/code/my-monorepo/node_modules/foo/index.js");
90
+ });
91
+ it("Completely external path", () => {
92
+ expect(normalizeModulePath("/opt/tools/logger.ts", "/Users/name/code/my-app", {
93
+ absolute: true,
94
+ isViteStyle: false,
95
+ })).toBe("/opt/tools/logger.ts");
96
+ });
97
+ });
98
+ describe("Edge cases with absolute option", () => {
99
+ it("Empty string", () => {
100
+ expect(normalizeModulePath("", "/Users/name/code/my-app", {
101
+ absolute: true,
102
+ })).toBe("/Users/name/code/my-app");
103
+ });
104
+ it("Dot current dir", () => {
105
+ expect(normalizeModulePath(".", "/Users/name/code/my-app", {
106
+ absolute: true,
107
+ })).toBe("/Users/name/code/my-app");
108
+ });
109
+ it("Dot parent dir", () => {
110
+ expect(normalizeModulePath("..", "/Users/name/code/my-app", {
111
+ absolute: true,
112
+ })).toBe("/Users/name/code");
113
+ });
114
+ it("Trailing slash", () => {
115
+ expect(normalizeModulePath("src/", "/Users/name/code/my-app", {
116
+ absolute: true,
117
+ })).toBe("/Users/name/code/my-app/src");
118
+ });
119
+ it("Leading and trailing slashes", () => {
120
+ expect(normalizeModulePath("/src/", "/Users/name/code/my-app", {
121
+ absolute: true,
122
+ })).toBe("/Users/name/code/my-app/src");
123
+ });
124
+ });
125
+ describe("Project root is / with absolute option", () => {
126
+ it("Root-based path", () => {
127
+ expect(normalizeModulePath("/src/index.ts", "/", { absolute: true })).toBe("/src/index.ts");
128
+ });
129
+ it("System path", () => {
130
+ expect(normalizeModulePath("/etc/hosts", "/", { absolute: true })).toBe("/etc/hosts");
131
+ });
132
+ });
133
+ });
134
+ describe("6. Edge and weird cases", () => {
135
+ it("Empty string", () => {
136
+ expect(normalizeModulePath("", "/Users/name/code/my-app")).toBe("/");
137
+ });
138
+ it("Dot current dir", () => {
139
+ expect(normalizeModulePath(".", "/Users/name/code/my-app")).toBe("/");
140
+ });
141
+ it("Dot parent dir", () => {
142
+ expect(normalizeModulePath("..", "/Users/name/code/my-app")).toBe("/Users/name/code");
143
+ });
144
+ it("Trailing slash", () => {
145
+ expect(normalizeModulePath("src/", "/Users/name/code/my-app")).toBe("/src");
146
+ });
147
+ it("Leading and trailing slashes", () => {
148
+ expect(normalizeModulePath("/src/", "/Users/name/code/my-app")).toBe("/src");
149
+ });
150
+ });
151
+ describe("7. Project root is /", () => {
152
+ it("Root-based path", () => {
153
+ expect(normalizeModulePath("/src/index.ts", "/")).toBe("/src/index.ts");
154
+ });
155
+ it("System path", () => {
156
+ expect(normalizeModulePath("/etc/hosts", "/")).toBe("/etc/hosts");
157
+ });
158
+ });
159
+ describe("8. isViteStyle option", () => {
160
+ describe("isViteStyle: false (treat as external)", () => {
161
+ it("System path that would normally be Vite-style", () => {
162
+ expect(normalizeModulePath("/opt/tools/logger.ts", "/Users/name/code/my-app", {
163
+ isViteStyle: false,
164
+ })).toBe("/opt/tools/logger.ts");
165
+ });
166
+ it("Src path with isViteStyle: false", () => {
167
+ expect(normalizeModulePath("/src/page.tsx", "/Users/name/code/my-app", {
168
+ isViteStyle: false,
169
+ })).toBe("/src/page.tsx");
170
+ });
171
+ it("With absolute option", () => {
172
+ expect(normalizeModulePath("/opt/tools/logger.ts", "/Users/name/code/my-app", {
173
+ absolute: true,
174
+ isViteStyle: false,
175
+ })).toBe("/opt/tools/logger.ts");
176
+ });
177
+ });
178
+ describe("isViteStyle: true (force Vite-style)", () => {
179
+ it("System path forced to Vite-style", () => {
180
+ expect(normalizeModulePath("/opt/tools/logger.ts", "/Users/name/code/my-app", {
181
+ isViteStyle: true,
182
+ })).toBe("/opt/tools/logger.ts");
183
+ });
184
+ it("Src path with isViteStyle: true", () => {
185
+ expect(normalizeModulePath("/src/page.tsx", "/Users/name/code/my-app", {
186
+ isViteStyle: true,
187
+ })).toBe("/src/page.tsx");
188
+ });
189
+ it("With absolute option", () => {
190
+ expect(normalizeModulePath("/src/page.tsx", "/Users/name/code/my-app", {
191
+ absolute: true,
192
+ isViteStyle: true,
193
+ })).toBe("/Users/name/code/my-app/src/page.tsx");
194
+ });
195
+ it("System path forced to Vite-style with absolute option", () => {
196
+ expect(normalizeModulePath("/opt/tools/logger.ts", "/Users/name/code/my-app", {
197
+ absolute: true,
198
+ isViteStyle: true,
199
+ })).toBe("/Users/name/code/my-app/opt/tools/logger.ts");
200
+ });
201
+ });
202
+ });
203
+ });
@@ -6,12 +6,14 @@ export type ActionResponse<Result> = {
6
6
  };
7
7
  type TransportContext = {
8
8
  setRscPayload: <Result>(v: Promise<ActionResponse<Result>>) => void;
9
+ handleResponse?: (response: Response) => boolean;
9
10
  };
10
11
  export type Transport = (context: TransportContext) => CallServerCallback;
11
12
  export type CreateCallServer = (context: TransportContext) => <Result>(id: null | string, args: null | unknown[]) => Promise<Result>;
12
13
  export declare const fetchTransport: Transport;
13
- export declare const initClient: ({ transport, hydrateRootOptions, }?: {
14
+ export declare const initClient: ({ transport, hydrateRootOptions, handleResponse, }?: {
14
15
  transport?: Transport;
15
16
  hydrateRootOptions?: HydrationOptions;
17
+ handleResponse?: (response: Response) => boolean;
16
18
  }) => Promise<void>;
17
19
  export {};
@@ -11,21 +11,41 @@ export const fetchTransport = (transportContext) => {
11
11
  if (id != null) {
12
12
  url.searchParams.set("__rsc_action_id", id);
13
13
  }
14
- const streamData = createFromFetch(fetch(url, {
14
+ const fetchPromise = fetch(url, {
15
15
  method: "POST",
16
16
  body: args != null ? await encodeReply(args) : null,
17
- }), { callServer: fetchCallServer });
17
+ });
18
+ // If there's a response handler, check the response first
19
+ if (transportContext.handleResponse) {
20
+ const response = await fetchPromise;
21
+ const shouldContinue = transportContext.handleResponse(response);
22
+ if (!shouldContinue) {
23
+ return;
24
+ }
25
+ // Continue with the response if handler returned true
26
+ const streamData = createFromFetch(Promise.resolve(response), {
27
+ callServer: fetchCallServer,
28
+ });
29
+ transportContext.setRscPayload(streamData);
30
+ const result = await streamData;
31
+ return result.actionResult;
32
+ }
33
+ // Original behavior when no handler is present
34
+ const streamData = createFromFetch(fetchPromise, {
35
+ callServer: fetchCallServer,
36
+ });
18
37
  transportContext.setRscPayload(streamData);
19
38
  const result = await streamData;
20
39
  return result.actionResult;
21
40
  };
22
41
  return fetchCallServer;
23
42
  };
24
- export const initClient = async ({ transport = fetchTransport, hydrateRootOptions, } = {}) => {
43
+ export const initClient = async ({ transport = fetchTransport, hydrateRootOptions, handleResponse, } = {}) => {
25
44
  const React = await import("react");
26
45
  const { hydrateRoot } = await import("react-dom/client");
27
46
  const transportContext = {
28
47
  setRscPayload: () => { },
48
+ handleResponse,
29
49
  };
30
50
  let transportCallServer = transport(transportContext);
31
51
  const callServer = (id, args) => transportCallServer(id, args);
@@ -1,4 +1,6 @@
1
1
  export declare function validateClickEvent(event: MouseEvent, target: HTMLElement): boolean;
2
2
  export declare function initClientNavigation(opts?: {
3
3
  onNavigate: () => void;
4
- }): void;
4
+ }): {
5
+ handleResponse: (response: Response) => boolean;
6
+ };
@@ -50,4 +50,15 @@ export function initClientNavigation(opts = {
50
50
  window.addEventListener("popstate", async function handlePopState() {
51
51
  await opts.onNavigate();
52
52
  });
53
+ // Return a handleResponse function for use with initClient
54
+ return {
55
+ handleResponse: function handleResponse(response) {
56
+ if (!response.ok) {
57
+ // Redirect to the current page (window.location) to show the error
58
+ window.location.href = window.location.href;
59
+ return false;
60
+ }
61
+ return true;
62
+ },
63
+ };
53
64
  }
@@ -1,7 +1,9 @@
1
1
  import { type Transport } from "../../client";
2
- export declare const initRealtimeClient: ({ key, }?: {
2
+ export declare const initRealtimeClient: ({ key, handleResponse, }?: {
3
3
  key?: string;
4
+ handleResponse?: (response: Response) => boolean;
4
5
  }) => Promise<void>;
5
- export declare const realtimeTransport: ({ key }: {
6
+ export declare const realtimeTransport: ({ key, handleResponse, }: {
6
7
  key?: string;
8
+ handleResponse?: (response: Response) => boolean;
7
9
  }) => Transport;