rwsdk 0.3.0 → 0.3.2

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.
@@ -10,14 +10,14 @@ const log = debug("rwsdk:vite:build-app");
10
10
  * @see docs/architecture/productionBuildProcess.md
11
11
  */
12
12
  export async function buildApp({ builder, clientEntryPoints, clientFiles, serverFiles, projectRootDir, }) {
13
+ const workerEnv = builder.environments.worker;
13
14
  await runDirectivesScan({
14
15
  rootConfig: builder.config,
15
- envName: "worker",
16
+ environment: workerEnv,
16
17
  clientFiles,
17
18
  serverFiles,
18
19
  });
19
20
  console.log("Building worker to discover used client components...");
20
- const workerEnv = builder.environments.worker;
21
21
  process.env.RWSDK_BUILD_PASS = "worker";
22
22
  await builder.build(workerEnv);
23
23
  log("Used client files after worker build & filtering: %O", Array.from(clientFiles));
@@ -1,6 +1,4 @@
1
1
  import { Plugin } from "vite";
2
- export declare const cloudflareBuiltInModules: string[];
3
- export declare const externalModules: string[];
4
2
  export declare const configPlugin: ({ silent, projectRootDir, workerEntryPathname, clientFiles, serverFiles, clientEntryPoints, }: {
5
3
  silent: boolean;
6
4
  projectRootDir: string;
@@ -1,23 +1,10 @@
1
1
  import path, { resolve } from "node:path";
2
- import { builtinModules } from "node:module";
3
2
  import enhancedResolve from "enhanced-resolve";
4
3
  import debug from "debug";
5
4
  import { INTERMEDIATE_SSR_BRIDGE_PATH } from "../lib/constants.mjs";
6
5
  import { buildApp } from "./buildApp.mjs";
6
+ import { externalModules } from "./constants.mjs";
7
7
  const log = debug("rwsdk:vite:config");
8
- // port(justinvdm, 09 Jun 2025):
9
- // https://github.com/cloudflare/workers-sdk/blob/d533f5ee7da69c205d8d5e2a5f264d2370fc612b/packages/vite-plugin-cloudflare/src/cloudflare-environment.ts#L123-L128
10
- export const cloudflareBuiltInModules = [
11
- "cloudflare:email",
12
- "cloudflare:sockets",
13
- "cloudflare:workers",
14
- "cloudflare:workflows",
15
- ];
16
- export const externalModules = [
17
- ...cloudflareBuiltInModules,
18
- ...builtinModules,
19
- ...builtinModules.map((m) => `node:${m}`),
20
- ];
21
8
  export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clientFiles, serverFiles, clientEntryPoints, }) => ({
22
9
  name: "rwsdk:config",
23
10
  config: async (_) => {
@@ -0,0 +1,2 @@
1
+ export declare const cloudflareBuiltInModules: string[];
2
+ export declare const externalModules: string[];
@@ -0,0 +1,12 @@
1
+ import { builtinModules } from "node:module";
2
+ export const cloudflareBuiltInModules = [
3
+ "cloudflare:email",
4
+ "cloudflare:sockets",
5
+ "cloudflare:workers",
6
+ "cloudflare:workflows",
7
+ ];
8
+ export const externalModules = [
9
+ ...cloudflareBuiltInModules,
10
+ ...builtinModules,
11
+ ...builtinModules.map((m) => `node:${m}`),
12
+ ];
@@ -70,6 +70,10 @@ export const createDirectiveLookupPlugin = async ({ projectRootDir, files, confi
70
70
  }
71
71
  },
72
72
  resolveId(source) {
73
+ // Skip during directive scanning to avoid performance issues
74
+ if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
75
+ return;
76
+ }
73
77
  if (source !== `${config.virtualModuleName}.js`) {
74
78
  return null;
75
79
  }
@@ -108,6 +112,10 @@ export const createDirectiveLookupPlugin = async ({ projectRootDir, files, confi
108
112
  return source;
109
113
  },
110
114
  async load(id) {
115
+ // Skip during directive scanning to avoid performance issues
116
+ if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
117
+ return;
118
+ }
111
119
  if (id === config.virtualModuleName + ".js") {
112
120
  log("Loading %s module with %d files", config.virtualModuleName, files.size);
113
121
  const environment = this.environment?.name || "client";
@@ -0,0 +1,4 @@
1
+ import resolve, { ResolveOptions } from "enhanced-resolve";
2
+ import { ResolvedConfig } from "vite";
3
+ export declare const mapViteResolveToEnhancedResolveOptions: (viteConfig: ResolvedConfig, envName: string) => ResolveOptions;
4
+ export declare const createViteAwareResolver: (viteConfig: ResolvedConfig, envName: string, environment?: any) => resolve.ResolveFunctionAsync;
@@ -0,0 +1,257 @@
1
+ import resolve from "enhanced-resolve";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import createDebug from "debug";
5
+ import { normalizeModulePath } from "../lib/normalizeModulePath.mjs";
6
+ const debug = createDebug("rwsdk:vite:enhanced-resolve-plugin");
7
+ // Enhanced-resolve plugin that wraps Vite plugin resolution
8
+ class VitePluginResolverPlugin {
9
+ constructor(environment, source = "resolve", target = "resolved") {
10
+ this.environment = environment;
11
+ this.source = source;
12
+ this.target = target;
13
+ // Create an enhanced-resolve instance for the plugin context
14
+ const baseOptions = mapViteResolveToEnhancedResolveOptions(this.environment.config, this.environment.name);
15
+ this.enhancedResolver = resolve.create(baseOptions);
16
+ }
17
+ apply(resolver) {
18
+ const target = resolver.ensureHook(this.target);
19
+ resolver
20
+ .getHook(this.source)
21
+ .tapAsync("VitePluginResolverPlugin", (request, resolveContext, callback) => {
22
+ const plugins = this.environment?.plugins;
23
+ if (!plugins) {
24
+ return callback();
25
+ }
26
+ // Create a plugin context with enhanced-resolve-based resolve method
27
+ const pluginContext = {
28
+ environment: this.environment,
29
+ resolve: async (id, importer) => {
30
+ return new Promise((resolve) => {
31
+ this.enhancedResolver({}, importer || this.environment.config.root, id, {}, (err, result) => {
32
+ if (!err && result) {
33
+ debug("Context resolve: %s -> %s", id, result);
34
+ resolve({ id: result });
35
+ }
36
+ else {
37
+ debug("Context resolve failed for %s: %s", id, err?.message || "not found");
38
+ resolve(null);
39
+ }
40
+ });
41
+ });
42
+ },
43
+ };
44
+ debug("Trying to resolve %s from %s", request.request, request.path);
45
+ // This function encapsulates the logic to process Vite plugins for a given request.
46
+ const runPluginProcessing = async (currentRequest) => {
47
+ debug("Available plugins:", plugins.map((p) => p.name));
48
+ for (const plugin of plugins) {
49
+ const resolveIdHandler = plugin.resolveId;
50
+ if (!resolveIdHandler)
51
+ continue;
52
+ let handlerFn;
53
+ let shouldApplyFilter = false;
54
+ let filter = null;
55
+ if (typeof resolveIdHandler === "function") {
56
+ handlerFn = resolveIdHandler;
57
+ }
58
+ else if (typeof resolveIdHandler === "object" &&
59
+ typeof resolveIdHandler.handler === "function") {
60
+ handlerFn = resolveIdHandler.handler;
61
+ shouldApplyFilter = true;
62
+ filter = resolveIdHandler.filter;
63
+ }
64
+ if (!handlerFn)
65
+ continue;
66
+ if (shouldApplyFilter && filter?.id) {
67
+ const idFilter = filter.id;
68
+ let shouldSkip = false;
69
+ if (idFilter instanceof RegExp) {
70
+ shouldSkip = !idFilter.test(currentRequest.request);
71
+ }
72
+ else if (Array.isArray(idFilter)) {
73
+ // Handle array of filters - matches if ANY filter matches
74
+ shouldSkip = !idFilter.some((f) => f instanceof RegExp
75
+ ? f.test(currentRequest.request)
76
+ : f === currentRequest.request);
77
+ }
78
+ else if (typeof idFilter === "string") {
79
+ shouldSkip = idFilter !== currentRequest.request;
80
+ }
81
+ else if (typeof idFilter === "object" && idFilter !== null) {
82
+ // Handle include/exclude object pattern
83
+ const { include, exclude } = idFilter;
84
+ let matches = true;
85
+ // Check include patterns (if any)
86
+ if (include) {
87
+ const includePatterns = Array.isArray(include)
88
+ ? include
89
+ : [include];
90
+ matches = includePatterns.some((pattern) => pattern instanceof RegExp
91
+ ? pattern.test(currentRequest.request)
92
+ : pattern === currentRequest.request);
93
+ }
94
+ // Check exclude patterns (if any) - exclude overrides include
95
+ if (matches && exclude) {
96
+ const excludePatterns = Array.isArray(exclude)
97
+ ? exclude
98
+ : [exclude];
99
+ const isExcluded = excludePatterns.some((pattern) => pattern instanceof RegExp
100
+ ? pattern.test(currentRequest.request)
101
+ : pattern === currentRequest.request);
102
+ matches = !isExcluded;
103
+ }
104
+ shouldSkip = !matches;
105
+ }
106
+ if (shouldSkip) {
107
+ debug("Skipping plugin '%s' due to filter mismatch for '%s'", plugin.name, currentRequest.request);
108
+ continue;
109
+ }
110
+ }
111
+ try {
112
+ debug("Calling plugin '%s' for '%s'", plugin.name, currentRequest.request);
113
+ const result = await handlerFn.call(pluginContext, currentRequest.request, currentRequest.path, { scan: true });
114
+ debug("Plugin '%s' returned:", plugin.name, result);
115
+ if (!result)
116
+ continue;
117
+ const resolvedId = typeof result === "string" ? result : result.id;
118
+ if (resolvedId && resolvedId !== currentRequest.request) {
119
+ debug("Plugin '%s' resolved '%s' -> '%s'", plugin.name, currentRequest.request, resolvedId);
120
+ return callback(null, {
121
+ ...currentRequest,
122
+ path: resolvedId,
123
+ });
124
+ }
125
+ else if (resolvedId === currentRequest.request) {
126
+ debug("Plugin '%s' returned unchanged ID, continuing to next plugin", plugin.name);
127
+ }
128
+ }
129
+ catch (e) {
130
+ debug("Plugin '%s' failed while resolving '%s': %s", plugin.name, currentRequest.request, e.message);
131
+ }
132
+ }
133
+ // If no plugin resolves, fall back to enhanced-resolve's default behavior
134
+ debug("No Vite plugin resolved '%s', falling back.", currentRequest.request);
135
+ // For absolute paths, check if the file exists
136
+ if (path.isAbsolute(currentRequest.request)) {
137
+ try {
138
+ if (fs.existsSync(currentRequest.request)) {
139
+ debug("File exists, resolving to: %s", currentRequest.request);
140
+ return callback(null, {
141
+ ...currentRequest,
142
+ path: currentRequest.request,
143
+ });
144
+ }
145
+ }
146
+ catch (e) {
147
+ debug("Error checking file existence: %s", e.message);
148
+ }
149
+ }
150
+ callback();
151
+ };
152
+ // For relative imports, normalize them to absolute paths first
153
+ if (request.request.startsWith("../") ||
154
+ request.request.startsWith("./")) {
155
+ try {
156
+ // Use path.dirname to get the directory of the importer file
157
+ const importerDir = path.dirname(request.path);
158
+ const absolutePath = normalizeModulePath(request.request, importerDir, { absolute: true });
159
+ debug("Absolutified %s -> %s", request.request, absolutePath);
160
+ const absoluteRequest = { ...request, request: absolutePath };
161
+ runPluginProcessing(absoluteRequest).catch((e) => {
162
+ debug("Error in plugin processing: %s", e.message);
163
+ callback();
164
+ });
165
+ }
166
+ catch (e) {
167
+ debug("Failed to absolutify %s: %s", request.request, e.message);
168
+ callback();
169
+ }
170
+ }
171
+ else {
172
+ // For non-relative imports, process them directly
173
+ runPluginProcessing(request).catch((e) => {
174
+ debug("Error in plugin processing: %s", e.message);
175
+ callback();
176
+ });
177
+ }
178
+ });
179
+ }
180
+ }
181
+ const mapAlias = (alias) => {
182
+ const mappedAlias = {};
183
+ if (Array.isArray(alias)) {
184
+ // Handle array format: { find: string | RegExp, replacement: string }
185
+ for (const { find, replacement } of alias) {
186
+ if (find instanceof RegExp) {
187
+ // For RegExp, use the source as-is
188
+ mappedAlias[find.source] = replacement;
189
+ }
190
+ else {
191
+ // For string aliases, use them as-is without modification
192
+ mappedAlias[find] = replacement;
193
+ }
194
+ }
195
+ }
196
+ else {
197
+ // Handle object format: { [find: string]: replacement }
198
+ for (const [find, replacement] of Object.entries(alias)) {
199
+ // Use the alias key as-is without modification
200
+ mappedAlias[find] = replacement;
201
+ }
202
+ }
203
+ return mappedAlias;
204
+ };
205
+ export const mapViteResolveToEnhancedResolveOptions = (viteConfig, envName) => {
206
+ const env = viteConfig.environments[envName];
207
+ if (!env) {
208
+ throw new Error(`Could not find environment configuration for "${envName}".`);
209
+ }
210
+ const envResolveOptions = (env.resolve || {});
211
+ // Merge root config aliases with environment-specific aliases
212
+ const mergedAlias = {
213
+ ...(viteConfig.resolve?.alias ? mapAlias(viteConfig.resolve.alias) : {}),
214
+ ...(envResolveOptions.alias ? mapAlias(envResolveOptions.alias) : {}),
215
+ };
216
+ // Use comprehensive extensions list similar to Vite's defaults
217
+ const extensions = envResolveOptions.extensions || [
218
+ ".mjs",
219
+ ".js",
220
+ ".mts",
221
+ ".ts",
222
+ ".jsx",
223
+ ".tsx",
224
+ ".json",
225
+ ];
226
+ const baseOptions = {
227
+ // File system is required by enhanced-resolve.
228
+ fileSystem: fs,
229
+ // Map Vite's resolve options to enhanced-resolve's options.
230
+ alias: Object.keys(mergedAlias).length > 0 ? mergedAlias : undefined,
231
+ conditionNames: envResolveOptions.conditions,
232
+ mainFields: envResolveOptions.mainFields,
233
+ extensions,
234
+ symlinks: envResolveOptions.preserveSymlinks,
235
+ // Add default node modules resolution.
236
+ modules: ["node_modules"],
237
+ roots: [viteConfig.root],
238
+ };
239
+ return baseOptions;
240
+ };
241
+ export const createViteAwareResolver = (viteConfig, envName, environment) => {
242
+ const baseOptions = mapViteResolveToEnhancedResolveOptions(viteConfig, envName);
243
+ // Add Vite plugin resolver if environment is provided
244
+ const plugins = environment
245
+ ? [new VitePluginResolverPlugin(environment)]
246
+ : [];
247
+ const enhancedResolveOptions = {
248
+ ...baseOptions,
249
+ plugins,
250
+ };
251
+ debug("Creating enhanced-resolve with options:", {
252
+ extensions: enhancedResolveOptions.extensions,
253
+ alias: enhancedResolveOptions.alias,
254
+ roots: enhancedResolveOptions.roots,
255
+ });
256
+ return resolve.create(enhancedResolveOptions);
257
+ };
@@ -1,9 +1,8 @@
1
- import path from "node:path";
1
+ import path from "path";
2
2
  import { writeFileSync, mkdirSync } from "node:fs";
3
3
  import { normalizeModulePath } from "../lib/normalizeModulePath.mjs";
4
+ import { CLIENT_BARREL_PATH, SERVER_BARREL_PATH } from "../lib/constants.mjs";
4
5
  import { runDirectivesScan } from "./runDirectivesScan.mjs";
5
- import { CLIENT_BARREL_PATH } from "../lib/constants.mjs";
6
- import { SERVER_BARREL_PATH } from "../lib/constants.mjs";
7
6
  export const VIRTUAL_CLIENT_BARREL_ID = "virtual:rwsdk:client-module-barrel";
8
7
  export const VIRTUAL_SERVER_BARREL_ID = "virtual:rwsdk:server-module-barrel";
9
8
  const CLIENT_BARREL_EXPORT_PATH = "rwsdk/__client_barrel";
@@ -24,38 +23,64 @@ const generateBarrelContent = (files, projectRootDir) => {
24
23
  return `${imports}\n\n${exports}`;
25
24
  };
26
25
  export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRootDir, }) => {
26
+ let scanPromise = null;
27
27
  return {
28
28
  name: "rwsdk:directive-modules-dev",
29
- enforce: "pre",
30
- async configResolved(config) {
31
- if (config.command !== "serve") {
29
+ configureServer(server) {
30
+ if (!process.env.VITE_IS_DEV_SERVER || process.env.RWSDK_WORKER_RUN) {
32
31
  return;
33
32
  }
34
- const workerEnv = config.environments["worker"];
35
- if (workerEnv) {
36
- await runDirectivesScan({
37
- rootConfig: config,
38
- envName: "worker",
39
- clientFiles,
40
- serverFiles,
41
- });
33
+ // Start the directive scan as soon as the server is configured.
34
+ // We don't await it here, allowing Vite to continue its startup.
35
+ scanPromise = runDirectivesScan({
36
+ rootConfig: server.config,
37
+ environment: server.environments.worker,
38
+ clientFiles,
39
+ serverFiles,
40
+ }).then(() => {
41
+ // After the scan is complete, write the barrel files.
42
+ const clientBarrelContent = generateBarrelContent(clientFiles, projectRootDir);
43
+ writeFileSync(CLIENT_BARREL_PATH, clientBarrelContent);
44
+ const serverBarrelContent = generateBarrelContent(serverFiles, projectRootDir);
45
+ writeFileSync(SERVER_BARREL_PATH, serverBarrelContent);
46
+ });
47
+ // context(justinvdm, 4 Sep 2025): Add middleware to block incoming
48
+ // requests until the scan is complete. This gives us a single hook for
49
+ // preventing app code being processed by vite until the scan is complete.
50
+ // This improves perceived startup time by not blocking Vite's optimizer.
51
+ server.middlewares.use(async (_req, _res, next) => {
52
+ if (scanPromise) {
53
+ await scanPromise;
54
+ }
55
+ next();
56
+ });
57
+ },
58
+ configResolved(config) {
59
+ if (config.command !== "serve") {
60
+ return;
42
61
  }
43
- // Generate the barrel content and write it to the dummy files.
44
- // We can do this now because our scan is complete.
45
- const clientBarrelContent = generateBarrelContent(clientFiles, projectRootDir);
46
- const serverBarrelContent = generateBarrelContent(serverFiles, projectRootDir);
62
+ // Create dummy files to give esbuild a real path to resolve.
47
63
  mkdirSync(path.dirname(CLIENT_BARREL_PATH), { recursive: true });
48
- writeFileSync(CLIENT_BARREL_PATH, clientBarrelContent);
64
+ writeFileSync(CLIENT_BARREL_PATH, "");
49
65
  mkdirSync(path.dirname(SERVER_BARREL_PATH), { recursive: true });
50
- writeFileSync(SERVER_BARREL_PATH, serverBarrelContent);
51
- for (const [envName, env] of Object.entries(config.environments)) {
52
- if (envName === "client" || envName === "ssr") {
53
- env.optimizeDeps.include = [
54
- ...(env.optimizeDeps.include || []),
55
- CLIENT_BARREL_EXPORT_PATH,
56
- SERVER_BARREL_EXPORT_PATH,
57
- ];
58
- }
66
+ writeFileSync(SERVER_BARREL_PATH, "");
67
+ for (const [envName, env] of Object.entries(config.environments || {})) {
68
+ env.optimizeDeps ??= {};
69
+ env.optimizeDeps.include ??= [];
70
+ env.optimizeDeps.include.push(CLIENT_BARREL_EXPORT_PATH, SERVER_BARREL_EXPORT_PATH);
71
+ env.optimizeDeps.esbuildOptions ??= {};
72
+ env.optimizeDeps.esbuildOptions.plugins ??= [];
73
+ env.optimizeDeps.esbuildOptions.plugins.push({
74
+ name: "rwsdk:block-optimizer-for-scan",
75
+ setup(build) {
76
+ build.onStart(async () => {
77
+ // context(justinvdm, 4 Sep 2025): We await the scan promise
78
+ // here because we want to block the optimizer until the scan is
79
+ // complete.
80
+ await scanPromise;
81
+ });
82
+ },
83
+ });
59
84
  }
60
85
  },
61
86
  };
@@ -52,6 +52,10 @@ export const directivesPlugin = ({ projectRootDir, clientFiles, serverFiles, })
52
52
  });
53
53
  },
54
54
  async transform(code, id) {
55
+ // Skip during directive scanning to avoid performance issues
56
+ if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
57
+ return;
58
+ }
55
59
  if (isBuild &&
56
60
  this.environment?.name === "worker" &&
57
61
  process.env.RWSDK_BUILD_PASS !== "worker") {
@@ -4,6 +4,10 @@ export const injectVitePreamble = ({ clientEntryPoints, projectRootDir, }) => ({
4
4
  name: "rwsdk:inject-vite-preamble",
5
5
  apply: "serve",
6
6
  transform(code, id) {
7
+ // Skip during directive scanning to avoid performance issues
8
+ if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
9
+ return;
10
+ }
7
11
  if (this.environment.name !== "client") {
8
12
  return;
9
13
  }
@@ -11,11 +11,19 @@ export const manifestPlugin = ({ projectRootDir, }) => {
11
11
  isBuild = config.command === "build";
12
12
  },
13
13
  resolveId(id) {
14
+ // Skip during directive scanning to avoid performance issues
15
+ if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
16
+ return;
17
+ }
14
18
  if (id === virtualModuleId) {
15
19
  return resolvedVirtualModuleId;
16
20
  }
17
21
  },
18
22
  async load(id) {
23
+ // Skip during directive scanning to avoid performance issues
24
+ if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
25
+ return;
26
+ }
19
27
  if (id === resolvedVirtualModuleId) {
20
28
  if (isBuild) {
21
29
  // context(justinvdm, 28 Aug 2025): During the build, we don't have
@@ -169,6 +169,10 @@ export const reactConditionsResolverPlugin = ({ projectRootDir, }) => {
169
169
  name: "rwsdk:react-conditions-resolver:resolveId",
170
170
  enforce: "pre",
171
171
  async resolveId(id, importer) {
172
+ // Skip during directive scanning to avoid performance issues
173
+ if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
174
+ return;
175
+ }
172
176
  if (!isBuild) {
173
177
  return;
174
178
  }
@@ -63,12 +63,12 @@ export const redwoodPlugin = async (options = {}) => {
63
63
  }
64
64
  return [
65
65
  devServerTimingPlugin(),
66
+ devServerConstantPlugin(),
66
67
  directiveModulesDevPlugin({
67
68
  clientFiles,
68
69
  serverFiles,
69
70
  projectRootDir,
70
71
  }),
71
- devServerConstantPlugin(),
72
72
  configPlugin({
73
73
  silent: options.silent ?? false,
74
74
  projectRootDir,
@@ -1,7 +1,7 @@
1
- import { ResolvedConfig } from "vite";
2
- export declare function runDirectivesScan({ rootConfig, envName, clientFiles, serverFiles, }: {
1
+ import { Environment, ResolvedConfig } from "vite";
2
+ export declare const runDirectivesScan: ({ rootConfig, environment, clientFiles, serverFiles, }: {
3
3
  rootConfig: ResolvedConfig;
4
- envName: string;
4
+ environment: Environment;
5
5
  clientFiles: Set<string>;
6
6
  serverFiles: Set<string>;
7
- }): Promise<any>;
7
+ }) => Promise<void>;
@@ -2,9 +2,10 @@ import fsp from "node:fs/promises";
2
2
  import { hasDirective } from "./hasDirective.mjs";
3
3
  import path from "node:path";
4
4
  import debug from "debug";
5
- import { ensureAliasArray } from "./ensureAliasArray.mjs";
6
5
  import { getViteEsbuild } from "./getViteEsbuild.mjs";
7
6
  import { normalizeModulePath } from "../lib/normalizeModulePath.mjs";
7
+ import { externalModules } from "./constants.mjs";
8
+ import { createViteAwareResolver } from "./createViteAwareResolver.mjs";
8
9
  const log = debug("rwsdk:vite:run-directives-scan");
9
10
  // Copied from Vite's source code.
10
11
  // https://github.com/vitejs/vite/blob/main/packages/vite/src/shared/utils.ts
@@ -13,140 +14,143 @@ const isObject = (value) => Object.prototype.toString.call(value) === "[object O
13
14
  // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/utils.ts
14
15
  const externalRE = /^(https?:)?\/\//;
15
16
  const isExternalUrl = (url) => externalRE.test(url);
16
- function createEsbuildScanPlugin({ clientFiles, serverFiles, aliases, projectRootDir, }) {
17
- return {
18
- name: "rwsdk:esbuild-scan-plugin",
19
- setup(build) {
20
- // Match Vite's behavior by externalizing assets and special queries.
21
- // This prevents esbuild from trying to bundle them, which would fail.
22
- const scriptFilter = /\.(c|m)?[jt]sx?$/;
23
- const specialQueryFilter = /[?&](?:url|raw|worker|sharedworker|inline)\b/;
24
- // This regex is used to identify if a path has any file extension.
25
- const hasExtensionRegex = /\.[^/]+$/;
26
- build.onResolve({ filter: specialQueryFilter }, (args) => {
27
- log("Externalizing special query:", args.path);
28
- return { external: true };
17
+ export const runDirectivesScan = async ({ rootConfig, environment, clientFiles, serverFiles, }) => {
18
+ console.log("\n🔍 Scanning for 'use client' and 'use server' directives...");
19
+ // Set environment variable to indicate scanning is in progress
20
+ process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE = "true";
21
+ try {
22
+ const esbuild = await getViteEsbuild(rootConfig.root);
23
+ const input = environment.config.build.rollupOptions?.input;
24
+ let entries;
25
+ if (Array.isArray(input)) {
26
+ entries = input;
27
+ }
28
+ else if (typeof input === "string") {
29
+ entries = [input];
30
+ }
31
+ else if (isObject(input)) {
32
+ entries = Object.values(input);
33
+ }
34
+ else {
35
+ entries = [];
36
+ }
37
+ if (entries.length === 0) {
38
+ log("No entries found for directives scan in environment '%s', skipping.", environment.name);
39
+ return;
40
+ }
41
+ const absoluteEntries = entries.map((entry) => path.resolve(rootConfig.root, entry));
42
+ log("Starting directives scan for environment '%s' with entries:", environment.name, absoluteEntries);
43
+ // Use enhanced-resolve with Vite plugin integration for full compatibility
44
+ const resolver = createViteAwareResolver(rootConfig, environment.name, environment);
45
+ const resolveId = async (id, importer) => {
46
+ return new Promise((resolve) => {
47
+ resolver({}, importer || rootConfig.root, id, {}, (err, result) => {
48
+ if (!err && result) {
49
+ resolve({ id: result });
50
+ }
51
+ else {
52
+ if (err) {
53
+ // Handle specific enhanced-resolve errors gracefully
54
+ const errorMessage = err.message || String(err);
55
+ if (errorMessage.includes("Package path . is not exported")) {
56
+ log("Package exports error for %s, marking as external", id);
57
+ }
58
+ else {
59
+ log("Resolution failed for %s: %s", id, errorMessage);
60
+ }
61
+ }
62
+ resolve(null);
63
+ }
64
+ });
29
65
  });
30
- build.onResolve({ filter: /.*/, namespace: "file" }, (args) => {
31
- // Externalize if the path has an extension AND that extension is not a
32
- // script extension. Extensionless paths are assumed to be scripts and
33
- // are allowed to pass through for resolution.
34
- if (hasExtensionRegex.test(args.path) &&
35
- !scriptFilter.test(args.path)) {
36
- log("Externalizing non-script import:", args.path);
66
+ };
67
+ const esbuildScanPlugin = {
68
+ name: "rwsdk:esbuild-scan-plugin",
69
+ setup(build) {
70
+ // Match Vite's behavior by externalizing assets and special queries.
71
+ // This prevents esbuild from trying to bundle them, which would fail.
72
+ const scriptFilter = /\.(c|m)?[jt]sx?$/;
73
+ const specialQueryFilter = /[?&](?:url|raw|worker|sharedworker|inline)\b/;
74
+ // This regex is used to identify if a path has any file extension.
75
+ const hasExtensionRegex = /\.[^/]+$/;
76
+ build.onResolve({ filter: specialQueryFilter }, (args) => {
77
+ log("Externalizing special query:", args.path);
37
78
  return { external: true };
38
- }
39
- });
40
- build.onResolve({ filter: /.*/ }, async (args) => {
41
- // Prevent infinite recursion.
42
- if (args.pluginData?.rwsdkScanResolver) {
43
- return null;
44
- }
45
- // 1. First, try to resolve aliases.
46
- for (const { find, replacement } of aliases) {
47
- const findPattern = find instanceof RegExp ? find : new RegExp(`^${find}(\\/.*)?$`);
48
- if (findPattern.test(args.path)) {
49
- const newPath = args.path.replace(findPattern, (_match, rest) => {
50
- // `rest` is the captured group `(\\/.*)?` from the regex.
51
- return replacement + (rest || "");
52
- });
53
- const resolved = await build.resolve(newPath, {
54
- importer: args.importer,
55
- resolveDir: args.resolveDir,
56
- kind: args.kind,
57
- pluginData: { rwsdkScanResolver: true },
58
- });
59
- if (resolved.errors.length === 0) {
60
- return resolved;
61
- }
62
- log("Could not resolve aliased path '%s' (from '%s'). Marking as external. Errors: %s", newPath, args.path, resolved.errors.map((e) => e.text).join(", "));
79
+ });
80
+ build.onResolve({ filter: /.*/, namespace: "file" }, (args) => {
81
+ // Externalize if the path has an extension AND that extension is not a
82
+ // script extension. Extensionless paths are assumed to be scripts and
83
+ // are allowed to pass through for resolution.
84
+ if (hasExtensionRegex.test(args.path) &&
85
+ !scriptFilter.test(args.path)) {
86
+ log("Externalizing non-script import:", args.path);
63
87
  return { external: true };
64
88
  }
65
- }
66
- // 2. If no alias matches, try esbuild's default resolver.
67
- const resolved = await build.resolve(args.path, {
68
- importer: args.importer,
69
- resolveDir: args.resolveDir,
70
- kind: args.kind,
71
- pluginData: { rwsdkScanResolver: true },
72
89
  });
73
- // If it fails, mark as external but don't crash.
74
- if (resolved.errors.length > 0) {
75
- log("Could not resolve '%s'. Marking as external. Errors: %s", args.path, resolved.errors.map((e) => e.text).join(", "));
90
+ build.onResolve({ filter: /.*/ }, async (args) => {
91
+ if (externalModules.includes(args.path)) {
92
+ return { external: true };
93
+ }
94
+ log("onResolve called for:", args.path, "from:", args.importer);
95
+ const resolved = await resolveId(args.path, args.importer);
96
+ log("Resolution result:", resolved);
97
+ const resolvedPath = resolved?.id;
98
+ if (resolvedPath && path.isAbsolute(resolvedPath)) {
99
+ // Normalize the path for esbuild compatibility
100
+ const normalizedPath = normalizeModulePath(resolvedPath, rootConfig.root, { absolute: true });
101
+ log("Normalized path:", normalizedPath);
102
+ return { path: normalizedPath };
103
+ }
104
+ log("Marking as external:", args.path, "resolved to:", resolvedPath);
76
105
  return { external: true };
77
- }
78
- return resolved;
79
- });
80
- build.onLoad({ filter: /\.(m|c)?[jt]sx?$/ }, async (args) => {
81
- if (!args.path.startsWith("/") ||
82
- args.path.includes("virtual:") ||
83
- isExternalUrl(args.path)) {
84
- return null;
85
- }
86
- try {
87
- const contents = await fsp.readFile(args.path, "utf-8");
88
- if (hasDirective(contents, "use client")) {
89
- log("Discovered 'use client' in:", args.path);
90
- clientFiles.add(normalizeModulePath(args.path, projectRootDir));
106
+ });
107
+ build.onLoad({ filter: /\.(m|c)?[jt]sx?$/ }, async (args) => {
108
+ log("onLoad called for:", args.path);
109
+ if (!args.path.startsWith("/") ||
110
+ args.path.includes("virtual:") ||
111
+ isExternalUrl(args.path)) {
112
+ log("Skipping file due to filter:", args.path, {
113
+ startsWithSlash: args.path.startsWith("/"),
114
+ hasVirtual: args.path.includes("virtual:"),
115
+ isExternal: isExternalUrl(args.path),
116
+ });
117
+ return null;
91
118
  }
92
- if (hasDirective(contents, "use server")) {
93
- log("Discovered 'use server' in:", args.path);
94
- serverFiles.add(normalizeModulePath(args.path, projectRootDir));
119
+ try {
120
+ const contents = await fsp.readFile(args.path, "utf-8");
121
+ if (hasDirective(contents, "use client")) {
122
+ log("Discovered 'use client' in:", args.path);
123
+ clientFiles.add(normalizeModulePath(args.path, rootConfig.root));
124
+ }
125
+ if (hasDirective(contents, "use server")) {
126
+ log("Discovered 'use server' in:", args.path);
127
+ serverFiles.add(normalizeModulePath(args.path, rootConfig.root));
128
+ }
129
+ return { contents, loader: "default" };
95
130
  }
96
- return { contents, loader: "default" };
97
- }
98
- catch (e) {
99
- log("Could not read file during scan, skipping:", args.path, e);
100
- return null;
101
- }
102
- });
103
- },
104
- };
105
- }
106
- export async function runDirectivesScan({ rootConfig, envName, clientFiles, serverFiles, }) {
107
- const esbuild = await getViteEsbuild(rootConfig.root);
108
- const env = rootConfig.environments[envName];
109
- const input = env.build.rollupOptions?.input;
110
- let entries;
111
- if (Array.isArray(input)) {
112
- entries = input;
113
- }
114
- else if (typeof input === "string") {
115
- entries = [input];
116
- }
117
- else if (isObject(input)) {
118
- entries = Object.values(input);
119
- }
120
- else {
121
- entries = [];
122
- }
123
- if (entries.length === 0) {
124
- log("No entries found for directives scan in environment '%s', skipping.", envName);
125
- return;
126
- }
127
- const absoluteEntries = entries.map((entry) => path.resolve(rootConfig.root, entry));
128
- log("Starting directives scan for environment '%s' with entries:", envName, absoluteEntries);
129
- try {
130
- const result = await esbuild.build({
131
+ catch (e) {
132
+ log("Could not read file during scan, skipping:", args.path, e);
133
+ return null;
134
+ }
135
+ });
136
+ },
137
+ };
138
+ await esbuild.build({
131
139
  entryPoints: absoluteEntries,
132
140
  bundle: true,
133
141
  write: false,
134
142
  platform: "node",
135
143
  format: "esm",
136
144
  logLevel: "silent",
137
- metafile: true,
138
- plugins: [
139
- createEsbuildScanPlugin({
140
- clientFiles,
141
- serverFiles,
142
- aliases: ensureAliasArray(env),
143
- projectRootDir: rootConfig.root,
144
- }),
145
- ],
145
+ plugins: [esbuildScanPlugin],
146
146
  });
147
- return result.metafile;
148
147
  }
149
148
  catch (e) {
150
- throw new Error(`RWSDK directive scan failed:\n${e.message}`);
149
+ throw new Error(`RWSDK directive scan failed:\n${e.stack}`);
150
+ }
151
+ finally {
152
+ // Always clear the scanning flag when done
153
+ delete process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE;
154
+ console.log("✅ Scan complete.");
151
155
  }
152
- }
156
+ };
@@ -48,6 +48,10 @@ export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
48
48
  }
49
49
  },
50
50
  async resolveId(id) {
51
+ // Skip during directive scanning to avoid performance issues
52
+ if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
53
+ return;
54
+ }
51
55
  if (isDev) {
52
56
  // context(justinvdm, 27 May 2025): In dev, we need to dynamically load
53
57
  // SSR modules, so we return the virtual id so that the dynamic loading
@@ -89,6 +93,10 @@ export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
89
93
  }
90
94
  },
91
95
  async load(id) {
96
+ // Skip during directive scanning to avoid performance issues
97
+ if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
98
+ return;
99
+ }
92
100
  if (id.startsWith(VIRTUAL_SSR_PREFIX) &&
93
101
  this.environment.name === "worker") {
94
102
  const realId = id.slice(VIRTUAL_SSR_PREFIX.length);
@@ -319,6 +319,10 @@ export const transformJsxScriptTagsPlugin = ({ clientEntryPoints, projectRootDir
319
319
  isBuild = config.command === "build";
320
320
  },
321
321
  async transform(code, id) {
322
+ // Skip during directive scanning to avoid performance issues
323
+ if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
324
+ return;
325
+ }
322
326
  if (isBuild &&
323
327
  this.environment?.name === "worker" &&
324
328
  process.env.RWSDK_BUILD_PASS !== "worker") {
@@ -4,12 +4,20 @@ export const virtualPlugin = (name, load) => {
4
4
  return {
5
5
  name: `rwsdk:virtual-${name}`,
6
6
  resolveId(source, _importer, _options) {
7
+ // Skip during directive scanning to avoid performance issues
8
+ if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
9
+ return;
10
+ }
7
11
  if (source === name || source.startsWith(`${name}?`)) {
8
12
  return `\0${source}`;
9
13
  }
10
14
  return;
11
15
  },
12
16
  load(id, options) {
17
+ // Skip during directive scanning to avoid performance issues
18
+ if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) {
19
+ return;
20
+ }
13
21
  if (id === `\0${name}` || id.startsWith(`\0${name}?`)) {
14
22
  return load.apply(this, [id, options]);
15
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {