rwsdk 1.0.0-alpha.7 → 1.0.0-alpha.9

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.
@@ -1,54 +1,70 @@
1
1
  /**
2
2
  * Efficiently checks if a React directive (e.g., "use server", "use client")
3
- * is present in the code. Optimized for performance with a two-step approach:
4
- * 1. Quick string search to see if directive exists anywhere
5
- * 2. Line-by-line check only if the directive might be present
3
+ * is present in the code.
4
+ *
5
+ * This function is optimized for performance by only checking the first few
6
+ * lines of the code, as directives must appear at the very top of a file.
7
+ * It handles comments, whitespace, and any valid directive prologue
8
+ * (e.g., "use strict").
6
9
  */
7
10
  export function hasDirective(code, directive) {
8
- // Quick performance check: if directive doesn't exist anywhere, skip line checking
9
- const singleQuoteDirective = `'${directive}'`;
10
- const doubleQuoteDirective = `"${directive}"`;
11
- if (!code.includes(singleQuoteDirective) &&
12
- !code.includes(doubleQuoteDirective)) {
13
- return false;
14
- }
15
- // Split into lines and check each one
16
- const lines = code.split("\n");
11
+ const lines = code.slice(0, 512).split("\n"); // Check first ~512 chars
17
12
  let inMultiLineComment = false;
13
+ let foundUseClient = false;
14
+ let foundTargetDirective = false;
15
+ const doubleQuoteDirective = `"${directive}"`;
16
+ const singleQuoteDirective = `'${directive}'`;
17
+ const doubleQuoteUseClient = `"use client"`;
18
+ const singleQuoteUseClient = `'use client'`;
18
19
  for (const line of lines) {
19
20
  const trimmedLine = line.trim();
20
- // Skip empty lines
21
21
  if (trimmedLine.length === 0) {
22
22
  continue;
23
23
  }
24
- // Handle multi-line comments
25
- if (trimmedLine.startsWith("/*")) {
26
- inMultiLineComment = true;
27
- // Check if the comment ends on the same line
24
+ if (inMultiLineComment) {
28
25
  if (trimmedLine.includes("*/")) {
29
26
  inMultiLineComment = false;
30
27
  }
31
28
  continue;
32
29
  }
33
- if (inMultiLineComment) {
34
- // Check if this line ends the multi-line comment
35
- if (trimmedLine.includes("*/")) {
36
- inMultiLineComment = false;
30
+ if (trimmedLine.startsWith("/*")) {
31
+ if (!trimmedLine.includes("*/")) {
32
+ inMultiLineComment = true;
37
33
  }
38
34
  continue;
39
35
  }
40
- // Skip single-line comments
41
36
  if (trimmedLine.startsWith("//")) {
42
37
  continue;
43
38
  }
44
- // Check if this line starts with the directive
39
+ const cleanedLine = trimmedLine.endsWith(";")
40
+ ? trimmedLine.slice(0, -1)
41
+ : trimmedLine;
42
+ if (trimmedLine.startsWith(doubleQuoteUseClient) ||
43
+ trimmedLine.startsWith(singleQuoteUseClient)) {
44
+ foundUseClient = true;
45
+ if (directive === "use client") {
46
+ return true;
47
+ }
48
+ }
45
49
  if (trimmedLine.startsWith(doubleQuoteDirective) ||
46
50
  trimmedLine.startsWith(singleQuoteDirective)) {
47
- return true;
51
+ foundTargetDirective = true;
52
+ if (directive !== "use server") {
53
+ return true;
54
+ }
48
55
  }
49
- // If we hit a non-empty, non-comment line that's not a directive, we can stop
50
- // (directives must be at the top of the file/scope, after comments)
56
+ // Any other string literal is part of a valid directive prologue.
57
+ // We can continue searching.
58
+ if (trimmedLine.startsWith('"') || trimmedLine.startsWith("'")) {
59
+ continue;
60
+ }
61
+ // If we encounter any other non-directive, non-comment, non-string-literal
62
+ // line of code, the directive prologue is over. Stop.
51
63
  break;
52
64
  }
53
- return false;
65
+ // If looking for 'use server' and 'use client' was found, return false (client takes priority)
66
+ if (directive === "use server" && foundUseClient) {
67
+ return false;
68
+ }
69
+ return foundTargetDirective;
54
70
  }
@@ -1,109 +1,107 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { hasDirective } from "./hasDirective.mjs";
3
3
  describe("hasDirective", () => {
4
- it('should find "use client" directive with double quotes', () => {
5
- const code = `"use client";
6
-
7
- import React from "react";
8
-
9
- const MyComponent = () => <div>Hello</div>;
10
- export default MyComponent;`;
4
+ it('should find "use client" directive', () => {
5
+ const code = `"use client"; import React from "react";`;
11
6
  expect(hasDirective(code, "use client")).toBe(true);
12
7
  });
13
- it("should find 'use server' directive with single quotes", () => {
14
- const code = `'use server';
15
-
16
- export async function myAction() {
17
- // ...
18
- }`;
8
+ it('should find "use server" directive', () => {
9
+ const code = `'use server'; export async function myAction() {}`;
19
10
  expect(hasDirective(code, "use server")).toBe(true);
20
11
  });
21
- it("should find directive with leading whitespace", () => {
22
- const code = ` "use client";
23
-
24
- const MyComponent = () => <div>Hello</div>;`;
12
+ it("should not find a directive that is not there", () => {
13
+ const code = `import React from "react";`;
14
+ expect(hasDirective(code, "use client")).toBe(false);
15
+ });
16
+ it('should find "use client" directive with single quotes', () => {
17
+ const code = `'use client'; import React from "react";`;
25
18
  expect(hasDirective(code, "use client")).toBe(true);
26
19
  });
27
- it("should find directive after single-line comments", () => {
28
- const code = `// This is a component
29
- "use client";
30
-
31
- const MyComponent = () => <div>Hello</div>;`;
20
+ it("should find directive when preceded by comments and whitespace", () => {
21
+ const code = `
22
+ // This is a client component
23
+ /* And here is another comment */
24
+
25
+ "use client";
26
+ import React from 'react';
27
+ export default () => <div>Hello</div>;
28
+ `;
32
29
  expect(hasDirective(code, "use client")).toBe(true);
33
30
  });
34
- it("should find directive after multi-line comments", () => {
35
- const code = `/* This is a component */
36
- "use client";
37
-
38
- const MyComponent = () => <div>Hello</div>;`;
31
+ it('should find "use client" directive when preceded by "use strict"', () => {
32
+ const code = `
33
+ "use strict";
34
+ "use client";
35
+ import React from 'react';
36
+ export default () => <div>Hello</div>;
37
+ `;
39
38
  expect(hasDirective(code, "use client")).toBe(true);
40
39
  });
41
- it("should find directive after empty lines", () => {
40
+ it('should find "use server" directive when preceded by "use strict" and comments', () => {
42
41
  const code = `
43
-
44
- "use client";
45
-
46
- const MyComponent = () => <div>Hello</div>;`;
42
+ // server stuff
43
+ "use strict";
44
+ /* another comment */
45
+ "use server";
46
+ export async function myAction() {}
47
+ `;
48
+ expect(hasDirective(code, "use server")).toBe(true);
49
+ });
50
+ it("should find directive when preceded by another string literal directive", () => {
51
+ const code = `
52
+ "use awesome"; // Some other directive
53
+ "use client";
54
+ import React from 'react';
55
+ export default () => <div>Hello</div>;
56
+ `;
47
57
  expect(hasDirective(code, "use client")).toBe(true);
48
58
  });
49
59
  it("should return false if no directive is present", () => {
50
60
  const code = `import React from "react";
51
-
52
- const MyComponent = () => <div>Hello</div>;`;
61
+ export default () => <div>Hello</div>;`;
53
62
  expect(hasDirective(code, "use client")).toBe(false);
54
63
  });
55
- it("should return false if directive is not at the top", () => {
56
- const code = `import React from "react";
57
- "use client";
58
-
59
- const MyComponent = () => <div>Hello</div>;`;
60
- expect(hasDirective(code, "use client")).toBe(false);
61
- });
62
- it("should return false if directive is inside a single-line comment", () => {
64
+ it("should return false if the directive is commented out", () => {
63
65
  const code = `// "use client";
64
-
65
- const MyComponent = () => <div>Hello</div>;`;
66
+ import React from "react";
67
+ export default () => <div>Hello</div>;`;
66
68
  expect(hasDirective(code, "use client")).toBe(false);
67
69
  });
68
- it("should return false if directive is inside a multi-line comment", () => {
69
- const code = `/*
70
- * "use client";
71
- */
72
-
73
- const MyComponent = () => <div>Hello</div>;`;
70
+ it("should return false if the directive appears after code", () => {
71
+ const code = `import React from "react";
72
+ "use client";
73
+ export default () => <div>Hello</div>;`;
74
74
  expect(hasDirective(code, "use client")).toBe(false);
75
75
  });
76
- it("should return false if directive is a substring in other code", () => {
77
- const code = `const message = 'please "use client" wisely';`;
76
+ it("should handle multi-line comments correctly", () => {
77
+ const code = `
78
+ /*
79
+ * "use client";
80
+ */
81
+ import React from "react";
82
+ `;
78
83
  expect(hasDirective(code, "use client")).toBe(false);
79
84
  });
80
- it("should handle a file with only the directive", () => {
81
- const code = `"use client"`;
85
+ it("should handle code with no whitespace", () => {
86
+ const code = `"use client";import React from "react";`;
82
87
  expect(hasDirective(code, "use client")).toBe(true);
83
88
  });
84
- it("should handle mixed comments, whitespace, and the directive", () => {
85
- const code = `// A component
86
-
87
- /*
88
- Another comment
89
- */
90
-
91
- 'use client';
92
-
93
- const MyComponent = () => <div>Hello</div>;
94
- `;
95
- expect(hasDirective(code, "use client")).toBe(true);
89
+ it("should handle empty code", () => {
90
+ const code = "";
91
+ expect(hasDirective(code, "use client")).toBe(false);
96
92
  });
97
- it("should handle multi-line comment ending on same line", () => {
98
- const code = `/* "use client" */
99
- const MyComponent = () => <div>Hello</div>;
100
- `;
93
+ it("should handle code with only whitespace", () => {
94
+ const code = " \n\t ";
101
95
  expect(hasDirective(code, "use client")).toBe(false);
102
96
  });
103
- it("should return false for code where directive appears after a valid line of code", () => {
104
- const code = `const a = 1;
105
- "use client";
106
- `;
97
+ it("should handle files with only comments", () => {
98
+ const code = `// comment 1
99
+ /* comment 2 */`;
107
100
  expect(hasDirective(code, "use client")).toBe(false);
108
101
  });
102
+ it("should prioritize 'use client' over 'use server'", () => {
103
+ const code = `'use client';\n'use server';\nconsole.log('hello');`;
104
+ expect(hasDirective(code, "use client")).toBe(true);
105
+ expect(hasDirective(code, "use server")).toBe(false);
106
+ });
109
107
  });
@@ -72,6 +72,7 @@ export const redwoodPlugin = async (options = {}) => {
72
72
  clientFiles,
73
73
  serverFiles,
74
74
  projectRootDir,
75
+ workerEntryPathname,
75
76
  }),
76
77
  configPlugin({
77
78
  silent: options.silent ?? false,
@@ -17,10 +17,11 @@ export declare function classifyModule({ contents, inheritedEnv, }: {
17
17
  isClient: boolean;
18
18
  isServer: boolean;
19
19
  };
20
- export declare const runDirectivesScan: ({ rootConfig, environments, clientFiles, serverFiles, }: {
20
+ export declare const runDirectivesScan: ({ rootConfig, environments, clientFiles, serverFiles, entries: initialEntries, }: {
21
21
  rootConfig: ResolvedConfig;
22
22
  environments: Record<string, Environment>;
23
23
  clientFiles: Set<string>;
24
24
  serverFiles: Set<string>;
25
+ entries: string[];
25
26
  }) => Promise<void>;
26
27
  export {};
@@ -4,8 +4,9 @@ import path from "node:path";
4
4
  import debug from "debug";
5
5
  import { getViteEsbuild } from "./getViteEsbuild.mjs";
6
6
  import { normalizeModulePath } from "../lib/normalizeModulePath.mjs";
7
+ import { INTERMEDIATES_OUTPUT_DIR } from "../lib/constants.mjs";
7
8
  import { externalModules } from "./constants.mjs";
8
- import { createViteAwareResolver, } from "./createViteAwareResolver.mjs";
9
+ import { createViteAwareResolver } from "./createViteAwareResolver.mjs";
9
10
  const log = debug("rwsdk:vite:run-directives-scan");
10
11
  // Copied from Vite's source code.
11
12
  // https://github.com/vitejs/vite/blob/main/packages/vite/src/shared/utils.ts
@@ -48,13 +49,13 @@ export function classifyModule({ contents, inheritedEnv, }) {
48
49
  }
49
50
  return { moduleEnv, isClient, isServer };
50
51
  }
51
- export const runDirectivesScan = async ({ rootConfig, environments, clientFiles, serverFiles, }) => {
52
+ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles, serverFiles, entries: initialEntries, }) => {
52
53
  console.log("\nšŸ” Scanning for 'use client' and 'use server' directives...");
53
54
  // Set environment variable to indicate scanning is in progress
54
55
  process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE = "true";
55
56
  try {
56
57
  const esbuild = await getViteEsbuild(rootConfig.root);
57
- const input = environments.worker.config.build.rollupOptions?.input;
58
+ const input = initialEntries ?? environments.worker.config.build.rollupOptions?.input;
58
59
  let entries;
59
60
  if (Array.isArray(input)) {
60
61
  entries = input;
@@ -72,7 +73,9 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
72
73
  log("No entries found for directives scan in worker environment, skipping.");
73
74
  return;
74
75
  }
75
- const absoluteEntries = entries.map((entry) => path.resolve(rootConfig.root, entry));
76
+ // Filter out virtual modules since they can't be scanned by esbuild
77
+ const realEntries = entries.filter((entry) => !entry.includes("virtual:"));
78
+ const absoluteEntries = realEntries.map((entry) => path.resolve(rootConfig.root, entry));
76
79
  log("Starting directives scan for worker environment with entries:", absoluteEntries);
77
80
  const workerResolver = createViteAwareResolver(rootConfig, environments.worker);
78
81
  const clientResolver = createViteAwareResolver(rootConfig, environments.client);
@@ -203,6 +206,7 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
203
206
  entryPoints: absoluteEntries,
204
207
  bundle: true,
205
208
  write: false,
209
+ outdir: path.join(INTERMEDIATES_OUTPUT_DIR, "directive-scan"),
206
210
  platform: "node",
207
211
  format: "esm",
208
212
  logLevel: "silent",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "1.0.0-alpha.7",
3
+ "version": "1.0.0-alpha.9",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,6 +59,10 @@
59
59
  "types": "./dist/runtime/entries/auth.d.ts",
60
60
  "default": "./dist/runtime/entries/auth.js"
61
61
  },
62
+ "./e2e": {
63
+ "types": "./dist/lib/e2e/index.d.mts",
64
+ "default": "./dist/lib/e2e/index.mjs"
65
+ },
62
66
  "./db": {
63
67
  "types": "./dist/runtime/lib/db/index.d.ts",
64
68
  "default": "./dist/runtime/lib/db/index.js"
@@ -90,10 +94,6 @@
90
94
  "./realtime/durableObject": {
91
95
  "types": "./dist/runtime/lib/realtime/durableObject.d.ts",
92
96
  "default": "./dist/runtime/lib/realtime/durableObject.js"
93
- },
94
- "./e2e": {
95
- "types": "./dist/lib/e2e/index.d.mts",
96
- "default": "./dist/lib/e2e/index.mjs"
97
97
  }
98
98
  },
99
99
  "keywords": [
@@ -163,8 +163,8 @@
163
163
  },
164
164
  "peerDependencies": {
165
165
  "@cloudflare/vite-plugin": "^1.12.4",
166
- "react": "19.2.0-canary-3fb190f7-20250908 <20.0.0",
167
- "react-dom": "19.2.0-canary-3fb190f7-20250908 <20.0.0",
166
+ "react": ">=19.2.0-canary-3fb190f7-20250908 <20.0.0",
167
+ "react-dom": ">=19.2.0-canary-3fb190f7-20250908 <20.0.0",
168
168
  "react-server-dom-webpack": ">=19.2.0-canary-3fb190f7-20250908 <20.0.0",
169
169
  "vite": "^6.2.6 || 7.x",
170
170
  "wrangler": "^4.35.0"