rwsdk 1.5.6 → 1.5.7

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,9 +1,11 @@
1
1
  import { Plugin } from "vite";
2
- export declare function generateLookupMap({ files, isDev, kind, exportName, }: {
2
+ export declare function generateLookupMap({ files, isDev, kind, exportName, optimizeDepsExclude, projectRootDir, }: {
3
3
  files: Set<string>;
4
4
  isDev: boolean;
5
5
  kind: "client" | "server";
6
6
  exportName: string;
7
+ optimizeDepsExclude?: string[];
8
+ projectRootDir?: string;
7
9
  }): {
8
10
  code: string;
9
11
  map: import("magic-string").SourceMap;
@@ -1,14 +1,17 @@
1
1
  import debug from "debug";
2
2
  import MagicString from "magic-string";
3
3
  import path from "path";
4
- import { addOptimizeDepsPlugin } from "./addOptimizeDepsPlugin.mjs";
5
4
  import { VENDOR_CLIENT_BARREL_EXPORT_PATH, VENDOR_SERVER_BARREL_EXPORT_PATH, } from "../lib/constants.mjs";
6
- export function generateLookupMap({ files, isDev, kind, exportName, }) {
5
+ import { addOptimizeDepsPlugin } from "./addOptimizeDepsPlugin.mjs";
6
+ import { getOptimizeDepsExcludePatternsByEnv, isExcludedFromOptimization, resolveOptimizeDepsExcludesByEnv, } from "./resolveOptimizeDepsExcludes.mjs";
7
+ export function generateLookupMap({ files, isDev, kind, exportName, optimizeDepsExclude, projectRootDir, }) {
8
+ const excludedRoots = optimizeDepsExclude ?? [];
7
9
  const s = new MagicString(`
8
10
  export const ${exportName} = {
9
11
  ${Array.from(files)
10
12
  .map((file) => {
11
- if (file.includes("node_modules") && isDev) {
13
+ const excluded = isExcludedFromOptimization(file, excludedRoots, projectRootDir);
14
+ if (file.includes("node_modules") && isDev && !excluded) {
12
15
  const barrelPath = kind === "client"
13
16
  ? VENDOR_CLIENT_BARREL_EXPORT_PATH
14
17
  : VENDOR_SERVER_BARREL_EXPORT_PATH;
@@ -35,11 +38,17 @@ export const createDirectiveLookupPlugin = async ({ projectRootDir, files, confi
35
38
  const log = debug(debugNamespace);
36
39
  let isDev = false;
37
40
  let devServer;
41
+ let optimizeDepsExclude = [];
42
+ let excludedRootsByEnv = {};
38
43
  log("Initializing %s plugin with projectRootDir=%s", config.pluginName, projectRootDir);
39
44
  return {
40
45
  name: `rwsdk:${config.pluginName}`,
41
- config(_, { command, isPreview }) {
46
+ config(config, { command, isPreview }) {
42
47
  isDev = !isPreview && command === "serve";
48
+ optimizeDepsExclude = [
49
+ ...new Set(Object.values(getOptimizeDepsExcludePatternsByEnv(config)).flat()),
50
+ ];
51
+ excludedRootsByEnv = resolveOptimizeDepsExcludesByEnv(getOptimizeDepsExcludePatternsByEnv(config), projectRootDir);
43
52
  log("Development mode: %s", isDev);
44
53
  },
45
54
  configureServer(server) {
@@ -147,11 +156,21 @@ export const createDirectiveLookupPlugin = async ({ projectRootDir, files, confi
147
156
  log("Loading %s module with %d files", config.virtualModuleName, files.size);
148
157
  const environment = this.environment?.name || "client";
149
158
  log("Current environment: %s, isDev: %s", environment, isDev);
159
+ const lookupExcludedRoots = config.kind === "server"
160
+ ? [...(excludedRootsByEnv.worker ?? [])]
161
+ : [
162
+ ...new Set([
163
+ ...(excludedRootsByEnv.client ?? []),
164
+ ...(excludedRootsByEnv.ssr ?? []),
165
+ ]),
166
+ ];
150
167
  return generateLookupMap({
151
168
  files,
152
169
  isDev,
153
170
  kind: config.kind,
154
171
  exportName: config.exportName,
172
+ optimizeDepsExclude: lookupExcludedRoots,
173
+ projectRootDir,
155
174
  });
156
175
  }
157
176
  },
@@ -37,4 +37,16 @@ describe("generateLookupMap", () => {
37
37
  });
38
38
  expect(result.code).toContain(`"node_modules/lib-a/index.js": () => import("node_modules/lib-a/index.js")`);
39
39
  });
40
+ it("should source-serve files under optimizeDeps.exclude roots in dev", () => {
41
+ const result = generateLookupMap({
42
+ files,
43
+ isDev: true,
44
+ kind: "client",
45
+ exportName: "clientLookup",
46
+ optimizeDepsExclude: ["/project/node_modules/lib-a"],
47
+ projectRootDir: "/project",
48
+ });
49
+ expect(result.code).toContain(`"node_modules/lib-a/index.js": () => import("node_modules/lib-a/index.js")`);
50
+ expect(result.code).not.toContain(`"node_modules/lib-a/index.js": () => import("${VENDOR_CLIENT_BARREL_EXPORT_PATH}")`);
51
+ });
40
52
  });
@@ -1,7 +1,7 @@
1
1
  import { Plugin } from "vite";
2
2
  import { ConfigurableEsbuildOptions } from "./runDirectivesScan.mjs";
3
- export declare const generateVendorBarrelContent: (files: Set<string>, projectRootDir: string) => string;
4
- export declare const generateAppBarrelContent: (files: Set<string>, projectRootDir: string) => string;
3
+ export declare const generateVendorBarrelContent: (files: Set<string>, projectRootDir: string, excludedRoots?: string[]) => string;
4
+ export declare const generateAppBarrelContent: (files: Set<string>, projectRootDir: string, excludedRoots?: string[]) => string;
5
5
  export declare const directiveModulesDevPlugin: ({ clientFiles, serverFiles, projectRootDir, workerEntryPathname, esbuildOptions, }: {
6
6
  clientFiles: Set<string>;
7
7
  serverFiles: Set<string>;
@@ -4,25 +4,29 @@ import path from "path";
4
4
  import { VENDOR_CLIENT_BARREL_PATH as SDK_VENDOR_CLIENT_BARREL_PATH, VENDOR_SERVER_BARREL_PATH as SDK_VENDOR_SERVER_BARREL_PATH, VENDOR_CLIENT_BARREL_EXPORT_PATH, VENDOR_SERVER_BARREL_EXPORT_PATH, } from "../lib/constants.mjs";
5
5
  import { normalizeModulePath } from "../lib/normalizeModulePath.mjs";
6
6
  import { setVendorBarrelPaths } from "./barrelPaths.mjs";
7
+ import { getOptimizeDepsExcludePatternsByEnv, isExcludedFromOptimization, resolveOptimizeDepsExcludesByEnv, } from "./resolveOptimizeDepsExcludes.mjs";
7
8
  import { runDirectivesScan, } from "./runDirectivesScan.mjs";
8
- export const generateVendorBarrelContent = (files, projectRootDir) => {
9
+ export const generateVendorBarrelContent = (files, projectRootDir, excludedRoots = []) => {
9
10
  const imports = [...files]
10
- .filter((file) => file.includes("node_modules"))
11
+ .filter((file) => file.includes("node_modules") &&
12
+ !isExcludedFromOptimization(file, excludedRoots, projectRootDir))
11
13
  .map((file, i) => `import * as M${i} from '${normalizeModulePath(file, projectRootDir, {
12
14
  absolute: true,
13
15
  })}';`)
14
16
  .join("\n");
15
17
  const exports = "export default {\n" +
16
18
  [...files]
17
- .filter((file) => file.includes("node_modules"))
19
+ .filter((file) => file.includes("node_modules") &&
20
+ !isExcludedFromOptimization(file, excludedRoots, projectRootDir))
18
21
  .map((file, i) => ` '${normalizeModulePath(file, projectRootDir)}': M${i},`)
19
22
  .join("\n") +
20
23
  "\n};";
21
24
  return `${imports}\n\n${exports}`;
22
25
  };
23
- export const generateAppBarrelContent = (files, projectRootDir) => {
26
+ export const generateAppBarrelContent = (files, projectRootDir, excludedRoots = []) => {
24
27
  return [...files]
25
- .filter((file) => !file.includes("node_modules"))
28
+ .filter((file) => !file.includes("node_modules") ||
29
+ isExcludedFromOptimization(file, excludedRoots, projectRootDir))
26
30
  .map((file) => {
27
31
  const resolvedPath = normalizeModulePath(file, projectRootDir, {
28
32
  absolute: true,
@@ -34,6 +38,9 @@ export const generateAppBarrelContent = (files, projectRootDir) => {
34
38
  export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRootDir, workerEntryPathname, esbuildOptions, }) => {
35
39
  const { promise: scanPromise, resolve: resolveScanPromise, reject: rejectScanPromise, } = Promise.withResolvers();
36
40
  const tempDir = mkdtempSync(path.join(realpathSync(os.tmpdir()), "rwsdk-"));
41
+ let excludedRootsByEnv = {};
42
+ let clientBarrelExcludedRoots = [];
43
+ let serverBarrelExcludedRoots = [];
37
44
  const APP_CLIENT_BARREL_PATH = path.join(tempDir, "app-client-barrel.js");
38
45
  const APP_SERVER_BARREL_PATH = path.join(tempDir, "app-server-barrel.js");
39
46
  const VENDOR_CLIENT_BARREL_PATH = path.join(tempDir, "vendor-client-barrel.js");
@@ -49,7 +56,7 @@ export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRoo
49
56
  const escapeRegExp = (s) => s.replace(/[\\^$*+?.()|[\]{}]/g, "\\$&");
50
57
  const appBarrelFilter = new RegExp(`(${appBarrelPaths.map(escapeRegExp).join("|")})$`);
51
58
  const BARREL_PREFIX = "\0rwsdk-app-barrel:";
52
- const createAppBarrelBlockerPlugin = () => ({
59
+ const createAppBarrelBlockerPlugin = (clientExcludedRoots, serverExcludedRoots) => ({
53
60
  name: "rwsdk:app-barrel-blocker",
54
61
  async resolveId(id) {
55
62
  await scanPromise;
@@ -91,14 +98,14 @@ export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRoo
91
98
  id === VENDOR_SERVER_BARREL_PATH) {
92
99
  const isServerBarrel = id.includes("server-barrel");
93
100
  const files = isServerBarrel ? serverFiles : clientFiles;
94
- return generateVendorBarrelContent(files, projectRootDir);
101
+ return generateVendorBarrelContent(files, projectRootDir, isServerBarrel ? serverExcludedRoots : clientExcludedRoots);
95
102
  }
96
103
  // Handle app barrels
97
104
  if (id.startsWith(BARREL_PREFIX)) {
98
105
  const barrelPath = id.slice(BARREL_PREFIX.length);
99
106
  const isServerBarrel = barrelPath.includes("app-server-barrel");
100
107
  const files = isServerBarrel ? serverFiles : clientFiles;
101
- return generateAppBarrelContent(files, projectRootDir);
108
+ return generateAppBarrelContent(files, projectRootDir, isServerBarrel ? serverExcludedRoots : clientExcludedRoots);
102
109
  }
103
110
  },
104
111
  });
@@ -107,7 +114,7 @@ export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRoo
107
114
  items.push(value);
108
115
  }
109
116
  };
110
- const configureOptimizeDeps = (envName, env) => {
117
+ const configureOptimizeDeps = (envName, env, clientExcludedRoots, serverExcludedRoots) => {
111
118
  env.optimizeDeps ??= {};
112
119
  env.optimizeDeps.include ??= [];
113
120
  addUnique(env.optimizeDeps.include, VENDOR_CLIENT_BARREL_EXPORT_PATH);
@@ -124,7 +131,7 @@ export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRoo
124
131
  env.optimizeDeps.rolldownOptions ??= {};
125
132
  env.optimizeDeps.rolldownOptions.plugins ??= [];
126
133
  if (!env.optimizeDeps.rolldownOptions.plugins.some((plugin) => plugin.name === "rwsdk:app-barrel-blocker")) {
127
- env.optimizeDeps.rolldownOptions.plugins.unshift(createAppBarrelBlockerPlugin());
134
+ env.optimizeDeps.rolldownOptions.plugins.unshift(createAppBarrelBlockerPlugin(clientExcludedRoots, serverExcludedRoots));
128
135
  }
129
136
  };
130
137
  return {
@@ -136,10 +143,10 @@ export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRoo
136
143
  const isServerBarrel = id === VENDOR_SERVER_BARREL_EXPORT_PATH ||
137
144
  id === SDK_VENDOR_SERVER_BARREL_PATH;
138
145
  if (isClientBarrel) {
139
- return generateVendorBarrelContent(clientFiles, projectRootDir);
146
+ return generateVendorBarrelContent(clientFiles, projectRootDir, clientBarrelExcludedRoots);
140
147
  }
141
148
  if (isServerBarrel) {
142
- return generateVendorBarrelContent(serverFiles, projectRootDir);
149
+ return generateVendorBarrelContent(serverFiles, projectRootDir, serverBarrelExcludedRoots);
143
150
  }
144
151
  return null;
145
152
  },
@@ -164,8 +171,8 @@ export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRoo
164
171
  esbuildOptions,
165
172
  })
166
173
  .then(() => {
167
- writeFileSync(VENDOR_CLIENT_BARREL_PATH, generateVendorBarrelContent(clientFiles, projectRootDir));
168
- writeFileSync(VENDOR_SERVER_BARREL_PATH, generateVendorBarrelContent(serverFiles, projectRootDir));
174
+ writeFileSync(VENDOR_CLIENT_BARREL_PATH, generateVendorBarrelContent(clientFiles, projectRootDir, clientBarrelExcludedRoots));
175
+ writeFileSync(VENDOR_SERVER_BARREL_PATH, generateVendorBarrelContent(serverFiles, projectRootDir, serverBarrelExcludedRoots));
169
176
  resolveScanPromise();
170
177
  })
171
178
  .catch((error) => {
@@ -177,6 +184,20 @@ export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRoo
177
184
  });
178
185
  },
179
186
  configResolved(config) {
187
+ // context(chrisvdm, 2026-07-02): This hook must stay synchronous. Vite 7
188
+ // does not await async configResolved hooks before running later plugins,
189
+ // including the SDK's Vite 7 compat shim that translates
190
+ // optimizeDeps.rolldownOptions.plugins into esbuild plugins. If we add
191
+ // our app-barrel-blocker plugin after that shim has run, the optimizer
192
+ // never sees it and the dev vendor barrel stays empty.
193
+ excludedRootsByEnv = resolveOptimizeDepsExcludesByEnv(getOptimizeDepsExcludePatternsByEnv(config), projectRootDir);
194
+ clientBarrelExcludedRoots = [
195
+ ...new Set([
196
+ ...(excludedRootsByEnv.client ?? []),
197
+ ...(excludedRootsByEnv.ssr ?? []),
198
+ ]),
199
+ ];
200
+ serverBarrelExcludedRoots = [...(excludedRootsByEnv.worker ?? [])];
180
201
  if (config.command !== "serve") {
181
202
  resolveScanPromise();
182
203
  return;
@@ -190,7 +211,7 @@ export const directiveModulesDevPlugin = ({ clientFiles, serverFiles, projectRoo
190
211
  mkdirSync(path.dirname(VENDOR_SERVER_BARREL_PATH), { recursive: true });
191
212
  writeFileSync(VENDOR_SERVER_BARREL_PATH, "");
192
213
  for (const [envName, env] of Object.entries(config.environments || {})) {
193
- configureOptimizeDeps(envName, env);
214
+ configureOptimizeDeps(envName, env, clientBarrelExcludedRoots, serverBarrelExcludedRoots);
194
215
  }
195
216
  },
196
217
  };
@@ -29,6 +29,21 @@ export default {
29
29
  const content = generateVendorBarrelContent(files, projectRootDir);
30
30
  expect(content).toEqual("\n\nexport default {\n\n};");
31
31
  });
32
+ it("should exclude files under optimizeDeps.exclude roots", () => {
33
+ const files = new Set([
34
+ "node_modules/lib-a/index.js",
35
+ "node_modules/lib-b/component.tsx",
36
+ ]);
37
+ const excludedRoots = [`${projectRootDir}/node_modules/lib-a`];
38
+ const content = generateVendorBarrelContent(files, projectRootDir, excludedRoots);
39
+ const expected = `import * as M0 from '${projectRootDir}/node_modules/lib-b/component.tsx';
40
+
41
+ export default {
42
+ '/node_modules/lib-b/component.tsx': M0,
43
+ };`;
44
+ expect(content).toEqual(expected);
45
+ expect(content).not.toContain("lib-a");
46
+ });
32
47
  });
33
48
  describe("generateAppBarrelContent", () => {
34
49
  it("should generate correct content for app files", () => {
@@ -55,5 +70,44 @@ import "${projectRootDir}/src/component.tsx";`;
55
70
  const content = generateAppBarrelContent(files, projectRootDir);
56
71
  expect(content).toEqual("");
57
72
  });
73
+ it("should include excluded node_modules files in the app barrel", () => {
74
+ const files = new Set([
75
+ "src/app.js",
76
+ "node_modules/lib-a/index.js",
77
+ "src/component.tsx",
78
+ ]);
79
+ const excludedRoots = [`${projectRootDir}/node_modules/lib-a`];
80
+ const content = generateAppBarrelContent(files, projectRootDir, excludedRoots);
81
+ const expected = `import "${projectRootDir}/src/app.js";
82
+ import "${projectRootDir}/node_modules/lib-a/index.js";
83
+ import "${projectRootDir}/src/component.tsx";`;
84
+ expect(content).toEqual(expected);
85
+ });
86
+ it("should include excluded Vite-style /node_modules files in the app barrel", () => {
87
+ const files = new Set([
88
+ "/src/app.js",
89
+ "/node_modules/lib-a/index.js",
90
+ "/src/component.tsx",
91
+ ]);
92
+ const excludedRoots = [`${projectRootDir}/node_modules/lib-a`];
93
+ const content = generateAppBarrelContent(files, projectRootDir, excludedRoots);
94
+ const expected = `import "${projectRootDir}/src/app.js";
95
+ import "${projectRootDir}/node_modules/lib-a/index.js";
96
+ import "${projectRootDir}/src/component.tsx";`;
97
+ expect(content).toEqual(expected);
98
+ });
99
+ it("should keep transitive node_modules files in the vendor barrel unless they are also excluded", () => {
100
+ const files = new Set([
101
+ "/node_modules/lib-a/index.js",
102
+ "/node_modules/lib-a-utils/index.js",
103
+ ]);
104
+ const excludedRoots = [`${projectRootDir}/node_modules/lib-a`];
105
+ const vendorContent = generateVendorBarrelContent(files, projectRootDir, excludedRoots);
106
+ expect(vendorContent).toContain("lib-a-utils");
107
+ expect(vendorContent).not.toContain("lib-a/index");
108
+ const appContent = generateAppBarrelContent(files, projectRootDir, excludedRoots);
109
+ expect(appContent).toContain("lib-a/index");
110
+ expect(appContent).not.toContain("lib-a-utils");
111
+ });
58
112
  });
59
113
  });
@@ -0,0 +1,53 @@
1
+ type OptimizeDepsConfig = {
2
+ optimizeDeps?: {
3
+ exclude?: string[];
4
+ };
5
+ environments?: Record<string, {
6
+ optimizeDeps?: {
7
+ exclude?: string[];
8
+ };
9
+ }>;
10
+ };
11
+ /**
12
+ * Collect `optimizeDeps.exclude` patterns from the root config and from every
13
+ * environment config into a single, deduplicated list.
14
+ */
15
+ export declare function getOptimizeDepsExcludePatterns(config: OptimizeDepsConfig): string[];
16
+ /**
17
+ * Collect `optimizeDeps.exclude` patterns grouped by Vite environment.
18
+ *
19
+ * Root-level excludes apply to every environment. Per-environment excludes
20
+ * apply only to that environment. When no environments are configured (e.g.
21
+ * Vite 7), the known RedwoodSDK environments (`client`, `ssr`, `worker`) are
22
+ * populated with the root-level patterns.
23
+ */
24
+ export declare function getOptimizeDepsExcludePatternsByEnv(config: OptimizeDepsConfig): Record<string, string[]>;
25
+ /**
26
+ * Resolve per-environment `optimizeDeps.exclude` patterns into absolute
27
+ * filesystem roots. Each environment is resolved with the environment-aware
28
+ * resolver that matches its execution context.
29
+ */
30
+ export declare function resolveOptimizeDepsExcludesByEnv(patternsByEnv: Record<string, string[]>, projectRootDir: string): Record<string, string[]>;
31
+ /**
32
+ * Resolve entries from Vite's `optimizeDeps.exclude` into absolute filesystem
33
+ * roots that can be matched against discovered directive files.
34
+ *
35
+ * Supports:
36
+ * - Bare package names (`my-ui-lib`)
37
+ * - Scoped packages (`@scope/pkg`)
38
+ * - Package subpaths (`my-ui-lib/components`)
39
+ * - Relative paths (`./packages/my-ui-lib`)
40
+ * - Absolute paths
41
+ *
42
+ * Symlinked packages (e.g. `file:./packages/my-ui-lib`) are resolved to their
43
+ * real location on disk, so source files are matched even though the import
44
+ * specifier goes through `node_modules`.
45
+ *
46
+ * Each pattern is resolved through RedwoodSDK's environment-aware resolvers so
47
+ * that packages with environment-specific exports are located correctly for the
48
+ * client, SSR, and worker environments. The resolved roots from all
49
+ * environments are unioned together.
50
+ */
51
+ export declare function resolveOptimizeDepsExcludes(excludes: string[], projectRootDir: string): string[];
52
+ export declare function isExcludedFromOptimization(file: string, excludedRoots: string[], projectRootDir?: string): boolean;
53
+ export {};
@@ -0,0 +1,201 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { normalizePath } from "vite";
4
+ import { ENV_RESOLVERS, maybeResolveEnvImport } from "./envResolvers.mjs";
5
+ const BARE_SPECIFIER_RE = /^(?:@[^/]+\/)?[^/]+/;
6
+ function normalizePathSeparators(p) {
7
+ return normalizePath(p.replace(/\\/g, "/"));
8
+ }
9
+ function getResolverForEnv(envName) {
10
+ return envName in ENV_RESOLVERS ? envName : "client";
11
+ }
12
+ function resolveExcludeRoot(entry, projectRootDir, envName) {
13
+ projectRootDir = normalizePathSeparators(path.resolve(projectRootDir));
14
+ entry = normalizePathSeparators(entry);
15
+ // Absolute filesystem path: keep it but resolve symlinks.
16
+ if (path.isAbsolute(entry)) {
17
+ try {
18
+ return normalizePathSeparators(fs.realpathSync(entry));
19
+ }
20
+ catch {
21
+ return entry;
22
+ }
23
+ }
24
+ // Relative path: resolve from the project root.
25
+ if (entry.startsWith("./") || entry.startsWith("../")) {
26
+ const resolved = normalizePathSeparators(path.resolve(projectRootDir, entry));
27
+ try {
28
+ return normalizePathSeparators(fs.realpathSync(resolved));
29
+ }
30
+ catch {
31
+ return resolved;
32
+ }
33
+ }
34
+ // Bare specifier (package, scoped package, or package subpath).
35
+ const match = entry.match(BARE_SPECIFIER_RE);
36
+ if (match) {
37
+ const pkg = match[0];
38
+ const subpath = entry.slice(pkg.length);
39
+ const pkgJsonPath = maybeResolveEnvImport({
40
+ id: `${pkg}/package.json`,
41
+ envName,
42
+ projectRootDir,
43
+ });
44
+ if (!pkgJsonPath) {
45
+ return undefined;
46
+ }
47
+ const pkgRoot = normalizePathSeparators(path.dirname(pkgJsonPath));
48
+ const resolvedRoot = subpath
49
+ ? normalizePathSeparators(path.join(pkgRoot, subpath))
50
+ : pkgRoot;
51
+ try {
52
+ return normalizePathSeparators(fs.realpathSync(resolvedRoot));
53
+ }
54
+ catch {
55
+ return resolvedRoot;
56
+ }
57
+ }
58
+ // Anything else is treated as root-relative.
59
+ return normalizePathSeparators(path.join(projectRootDir, entry));
60
+ }
61
+ /**
62
+ * Collect `optimizeDeps.exclude` patterns from the root config and from every
63
+ * environment config into a single, deduplicated list.
64
+ */
65
+ export function getOptimizeDepsExcludePatterns(config) {
66
+ const patterns = new Set();
67
+ for (const entry of config.optimizeDeps?.exclude ?? []) {
68
+ if (entry) {
69
+ patterns.add(entry);
70
+ }
71
+ }
72
+ for (const env of Object.values(config.environments ?? {})) {
73
+ for (const entry of env?.optimizeDeps?.exclude ?? []) {
74
+ if (entry) {
75
+ patterns.add(entry);
76
+ }
77
+ }
78
+ }
79
+ return [...patterns];
80
+ }
81
+ /**
82
+ * Collect `optimizeDeps.exclude` patterns grouped by Vite environment.
83
+ *
84
+ * Root-level excludes apply to every environment. Per-environment excludes
85
+ * apply only to that environment. When no environments are configured (e.g.
86
+ * Vite 7), the known RedwoodSDK environments (`client`, `ssr`, `worker`) are
87
+ * populated with the root-level patterns.
88
+ */
89
+ export function getOptimizeDepsExcludePatternsByEnv(config) {
90
+ const rootPatterns = config.optimizeDeps?.exclude ?? [];
91
+ const environments = config.environments ?? {};
92
+ const patternsByEnv = {};
93
+ for (const envName of Object.keys(environments)) {
94
+ patternsByEnv[envName] = [...rootPatterns];
95
+ }
96
+ // Fallback for non-environmental configs.
97
+ if (Object.keys(patternsByEnv).length === 0) {
98
+ for (const envName of Object.keys(ENV_RESOLVERS)) {
99
+ patternsByEnv[envName] = [...rootPatterns];
100
+ }
101
+ }
102
+ for (const [envName, env] of Object.entries(environments)) {
103
+ for (const entry of env?.optimizeDeps?.exclude ?? []) {
104
+ if (entry) {
105
+ patternsByEnv[envName].push(entry);
106
+ }
107
+ }
108
+ }
109
+ return patternsByEnv;
110
+ }
111
+ function resolveOptimizeDepsExcludesForEnv(excludes, projectRootDir, envName) {
112
+ const roots = new Set();
113
+ for (const entry of excludes) {
114
+ if (!entry) {
115
+ continue;
116
+ }
117
+ const root = resolveExcludeRoot(entry, projectRootDir, envName);
118
+ if (root) {
119
+ roots.add(root);
120
+ continue;
121
+ }
122
+ // If the environment resolver couldn't locate the package, fall back to a
123
+ // node_modules path so the exclusion still has a chance to match.
124
+ const match = entry.match(BARE_SPECIFIER_RE);
125
+ if (match) {
126
+ roots.add(normalizePathSeparators(path.join(projectRootDir, "node_modules", entry)));
127
+ }
128
+ }
129
+ return [...roots];
130
+ }
131
+ /**
132
+ * Resolve per-environment `optimizeDeps.exclude` patterns into absolute
133
+ * filesystem roots. Each environment is resolved with the environment-aware
134
+ * resolver that matches its execution context.
135
+ */
136
+ export function resolveOptimizeDepsExcludesByEnv(patternsByEnv, projectRootDir) {
137
+ const rootsByEnv = {};
138
+ for (const [envName, patterns] of Object.entries(patternsByEnv)) {
139
+ rootsByEnv[envName] = resolveOptimizeDepsExcludesForEnv(patterns, projectRootDir, getResolverForEnv(envName));
140
+ }
141
+ return rootsByEnv;
142
+ }
143
+ /**
144
+ * Resolve entries from Vite's `optimizeDeps.exclude` into absolute filesystem
145
+ * roots that can be matched against discovered directive files.
146
+ *
147
+ * Supports:
148
+ * - Bare package names (`my-ui-lib`)
149
+ * - Scoped packages (`@scope/pkg`)
150
+ * - Package subpaths (`my-ui-lib/components`)
151
+ * - Relative paths (`./packages/my-ui-lib`)
152
+ * - Absolute paths
153
+ *
154
+ * Symlinked packages (e.g. `file:./packages/my-ui-lib`) are resolved to their
155
+ * real location on disk, so source files are matched even though the import
156
+ * specifier goes through `node_modules`.
157
+ *
158
+ * Each pattern is resolved through RedwoodSDK's environment-aware resolvers so
159
+ * that packages with environment-specific exports are located correctly for the
160
+ * client, SSR, and worker environments. The resolved roots from all
161
+ * environments are unioned together.
162
+ */
163
+ export function resolveOptimizeDepsExcludes(excludes, projectRootDir) {
164
+ const envNames = Object.keys(ENV_RESOLVERS);
165
+ const roots = new Set();
166
+ for (const envName of envNames) {
167
+ for (const root of resolveOptimizeDepsExcludesForEnv(excludes, projectRootDir, envName)) {
168
+ roots.add(root);
169
+ }
170
+ }
171
+ return [...roots];
172
+ }
173
+ export function isExcludedFromOptimization(file, excludedRoots, projectRootDir) {
174
+ const normalizedFile = normalizePathSeparators(file);
175
+ const candidates = new Set();
176
+ candidates.add(normalizedFile);
177
+ if (projectRootDir) {
178
+ const root = normalizePathSeparators(path.resolve(projectRootDir));
179
+ // If the file isn't already an absolute path inside the project root, also
180
+ // try resolving it from the project root. This covers both relative paths
181
+ // and Vite-style project-relative paths such as `/node_modules/foo/index.js`,
182
+ // which is how files inside the project root are represented after
183
+ // `normalizeModulePath`. We keep the original candidate too so external
184
+ // absolute paths (e.g. symlinked monorepo packages) still match their own
185
+ // excluded roots even if resolving from the project root produces a bogus
186
+ // path.
187
+ if (normalizedFile !== root && !normalizedFile.startsWith(root + "/")) {
188
+ const relativePart = normalizedFile.startsWith("/")
189
+ ? normalizedFile.slice(1)
190
+ : normalizedFile;
191
+ candidates.add(normalizePathSeparators(path.resolve(root, relativePart)));
192
+ }
193
+ }
194
+ return excludedRoots.some((excludedRoot) => {
195
+ const normalizedExcludedRoot = normalizePathSeparators(excludedRoot);
196
+ const prefix = normalizedExcludedRoot.endsWith("/")
197
+ ? normalizedExcludedRoot
198
+ : normalizedExcludedRoot + "/";
199
+ return Array.from(candidates).some((candidate) => candidate === normalizedExcludedRoot || candidate.startsWith(prefix));
200
+ });
201
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,120 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getOptimizeDepsExcludePatterns, getOptimizeDepsExcludePatternsByEnv, isExcludedFromOptimization, resolveOptimizeDepsExcludes, resolveOptimizeDepsExcludesByEnv, } from "./resolveOptimizeDepsExcludes.mjs";
3
+ describe("getOptimizeDepsExcludePatterns", () => {
4
+ it("should collect root-level excludes", () => {
5
+ const patterns = getOptimizeDepsExcludePatterns({
6
+ optimizeDeps: { exclude: ["foo", "bar"] },
7
+ });
8
+ expect(patterns).toEqual(["foo", "bar"]);
9
+ });
10
+ it("should collect per-environment excludes", () => {
11
+ const patterns = getOptimizeDepsExcludePatterns({
12
+ optimizeDeps: { exclude: ["foo"] },
13
+ environments: {
14
+ client: { optimizeDeps: { exclude: ["bar"] } },
15
+ worker: { optimizeDeps: { exclude: ["baz"] } },
16
+ },
17
+ });
18
+ expect(patterns).toEqual(expect.arrayContaining(["foo", "bar", "baz"]));
19
+ expect(patterns).toHaveLength(3);
20
+ });
21
+ it("should deduplicate excludes across root and environments", () => {
22
+ const patterns = getOptimizeDepsExcludePatterns({
23
+ optimizeDeps: { exclude: ["foo", "bar"] },
24
+ environments: {
25
+ client: { optimizeDeps: { exclude: ["bar", "baz"] } },
26
+ },
27
+ });
28
+ expect(patterns).toEqual(expect.arrayContaining(["foo", "bar", "baz"]));
29
+ expect(patterns).toHaveLength(3);
30
+ });
31
+ });
32
+ describe("getOptimizeDepsExcludePatternsByEnv", () => {
33
+ it("should apply root-level excludes to every environment", () => {
34
+ const patterns = getOptimizeDepsExcludePatternsByEnv({
35
+ optimizeDeps: { exclude: ["foo"] },
36
+ environments: {
37
+ client: { optimizeDeps: { exclude: [] } },
38
+ worker: { optimizeDeps: { exclude: ["bar"] } },
39
+ },
40
+ });
41
+ expect(patterns.client).toEqual(["foo"]);
42
+ expect(patterns.worker).toEqual(["foo", "bar"]);
43
+ });
44
+ it("should fall back to known environments when none are configured", () => {
45
+ const patterns = getOptimizeDepsExcludePatternsByEnv({
46
+ optimizeDeps: { exclude: ["foo"] },
47
+ });
48
+ expect(patterns.client).toEqual(["foo"]);
49
+ expect(patterns.ssr).toEqual(["foo"]);
50
+ expect(patterns.worker).toEqual(["foo"]);
51
+ });
52
+ });
53
+ describe("resolveOptimizeDepsExcludes", () => {
54
+ it("should resolve an installed package to its root", () => {
55
+ const roots = resolveOptimizeDepsExcludes(["glob"], process.cwd());
56
+ expect(roots.length).toBe(1);
57
+ expect(roots[0]).toMatch(/node_modules[\/\\]glob$/);
58
+ });
59
+ it("should resolve a scoped package to its root", () => {
60
+ const roots = resolveOptimizeDepsExcludes(["@cloudflare/vite-plugin"], process.cwd());
61
+ expect(roots.length).toBe(1);
62
+ expect(roots[0]).toMatch(/node_modules[\/\\]@cloudflare[\/\\]vite-plugin$/);
63
+ });
64
+ it("should resolve a package subpath to the subpath directory", () => {
65
+ const roots = resolveOptimizeDepsExcludes(["glob/dist"], process.cwd());
66
+ expect(roots.length).toBe(1);
67
+ expect(roots[0]).toMatch(/node_modules[\/\\]glob[\/\\]dist$/);
68
+ });
69
+ it("should resolve a relative path from the project root", () => {
70
+ const roots = resolveOptimizeDepsExcludes(["./sdk/src/vite"], process.cwd());
71
+ expect(roots.length).toBe(1);
72
+ expect(roots[0]).toMatch(/sdk[\/\\]src[\/\\]vite$/);
73
+ });
74
+ it("should fall back to node_modules path for unresolvable patterns", () => {
75
+ const roots = resolveOptimizeDepsExcludes(["this-package-does-not-exist"], process.cwd());
76
+ expect(roots.length).toBe(1);
77
+ expect(roots[0]).toMatch(/node_modules[\/\\]this-package-does-not-exist$/);
78
+ });
79
+ });
80
+ describe("resolveOptimizeDepsExcludesByEnv", () => {
81
+ it("should resolve per environment", () => {
82
+ const roots = resolveOptimizeDepsExcludesByEnv({
83
+ client: ["glob"],
84
+ worker: ["this-package-does-not-exist"],
85
+ }, process.cwd());
86
+ expect(roots.client).toHaveLength(1);
87
+ expect(roots.client[0]).toMatch(/node_modules[\/\\]glob$/);
88
+ expect(roots.worker).toHaveLength(1);
89
+ expect(roots.worker[0]).toMatch(/node_modules[\/\\]this-package-does-not-exist$/);
90
+ });
91
+ });
92
+ describe("isExcludedFromOptimization", () => {
93
+ it("should match files under an excluded root", () => {
94
+ expect(isExcludedFromOptimization("/project/node_modules/foo/index.js", [
95
+ "/project/node_modules/foo",
96
+ ])).toBe(true);
97
+ });
98
+ it("should not match files outside excluded roots", () => {
99
+ expect(isExcludedFromOptimization("/project/node_modules/bar/index.js", [
100
+ "/project/node_modules/foo",
101
+ ])).toBe(false);
102
+ });
103
+ it("should handle roots without trailing separators", () => {
104
+ expect(isExcludedFromOptimization("/project/node_modules/foo/index.js", [
105
+ "/project/node_modules/foo/",
106
+ ])).toBe(true);
107
+ });
108
+ it("should resolve root-relative files against the project root", () => {
109
+ expect(isExcludedFromOptimization("node_modules/foo/index.js", ["/project/node_modules/foo"], "/project")).toBe(true);
110
+ });
111
+ it("should match Vite-style project-relative paths", () => {
112
+ expect(isExcludedFromOptimization("/node_modules/foo/index.js", ["/project/node_modules/foo"], "/project")).toBe(true);
113
+ });
114
+ it("should still match external absolute paths that share no common root", () => {
115
+ expect(isExcludedFromOptimization("/Users/chris/other/lib/index.js", ["/Users/chris/other/lib"], "/Users/chris/project")).toBe(true);
116
+ });
117
+ it("should not match unrelated Vite-style paths", () => {
118
+ expect(isExcludedFromOptimization("/node_modules/bar/index.js", ["/project/node_modules/foo"], "/project")).toBe(false);
119
+ });
120
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "1.5.6",
3
+ "version": "1.5.7",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {