@multiplatform.one/config 7.4.0 → 7.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/vitest.ts CHANGED
@@ -1,8 +1,61 @@
1
+ import fs from "node:fs";
2
+ import Module from "node:module";
1
3
  import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
2
5
  import { tamaguiPlugin } from "@tamagui/vite-plugin";
3
6
  import react from "@vitejs/plugin-react";
4
7
  import type { UserConfig } from "vite";
5
8
 
9
+ const pinReactRequireSetup = path.resolve(
10
+ path.dirname(fileURLToPath(import.meta.url)),
11
+ "../pin-react-require.cjs",
12
+ );
13
+
14
+ /**
15
+ * CJS `require('react')` does not go through Vite aliases. `use-sync-external-store`
16
+ * is a valid Node import, so Vitest leaves it external even when we ask to inline
17
+ * it, and Node then loads the hoisted root copy — a different inode than the
18
+ * `.pnpm` copy Vite binds. Patching Module._resolveFilename in the vitest
19
+ * process (config eval + workers that re-load this factory) makes that
20
+ * require land on the same physical copy as the aliases.
21
+ */
22
+ function pinNodeReactRequire(reactDir: string, reactDomDir: string, schedulerDir: string): void {
23
+ const ctor = Module as typeof Module & {
24
+ _resolveFilename: (
25
+ request: string,
26
+ parent: unknown,
27
+ isMain: boolean,
28
+ options?: unknown,
29
+ ) => string;
30
+ _mpoOneReact?: boolean;
31
+ };
32
+ if (ctor._mpoOneReact) return;
33
+ ctor._mpoOneReact = true;
34
+ const orig = ctor._resolveFilename;
35
+ ctor._resolveFilename = function pinned(request, parent, isMain, options) {
36
+ const next =
37
+ request === "react"
38
+ ? reactDir
39
+ : request.startsWith("react/")
40
+ ? path.join(reactDir, request.slice("react/".length))
41
+ : request === "react-dom"
42
+ ? reactDomDir
43
+ : request.startsWith("react-dom/")
44
+ ? path.join(reactDomDir, request.slice("react-dom/".length))
45
+ : request === "scheduler"
46
+ ? schedulerDir
47
+ : null;
48
+ if (next) {
49
+ try {
50
+ return orig.call(this, next, parent, isMain, options);
51
+ } catch {
52
+ // package layout didn't match — keep Node's original lookup
53
+ }
54
+ }
55
+ return orig.call(this, request, parent, isMain, options);
56
+ };
57
+ }
58
+
6
59
  export interface CreateVitestConfigOptions {
7
60
  /**
8
61
  * Path to the Tamagui config file for test environments.
@@ -168,22 +221,54 @@ export function createVitestConfig(options: CreateVitestConfigOptions = {}): Use
168
221
  resolveAliases["react-native-svg"] = "@tamagui/react-native-svg";
169
222
  }
170
223
  if (dedupeReact) {
171
- resolveAliases.react = path.resolve(rootNodeModules, "react");
172
- resolveAliases["react-dom"] = path.resolve(rootNodeModules, "react-dom");
173
- resolveAliases["react-dom/client"] = path.resolve(rootNodeModules, "react-dom/client");
174
- resolveAliases["react-dom/server"] = path.resolve(rootNodeModules, "react-dom/server");
175
- resolveAliases["react/jsx-runtime"] = path.resolve(rootNodeModules, "react/jsx-runtime");
176
- resolveAliases["react/jsx-dev-runtime"] = path.resolve(
177
- rootNodeModules,
178
- "react/jsx-dev-runtime",
179
- );
180
- resolveAliases.scheduler = path.resolve(rootNodeModules, "scheduler");
224
+ // MPO-37. `.npmrc` sets `node-linker=hoisted`, so the workspace ROOT gets
225
+ // a real `node_modules/react` directory while each package's own
226
+ // `node_modules/react` is a symlink into `.pnpm/react@<v>/...`. Those are
227
+ // two PHYSICALLY distinct copies of the same version, and Vite resolves
228
+ // symlinks to their real path — so aliasing at the root while a dependency
229
+ // (say @tanstack/react-query, whose source Vite transforms) resolves
230
+ // through the package symlink puts two Reacts in one module graph.
231
+ //
232
+ // react-dom then installs its dispatcher into one copy while the component
233
+ // tree reads the other, and every render dies on
234
+ // `Cannot read properties of null (reading 'useEffect')` — the whole
235
+ // components render suite, and the render half of forms.
236
+ //
237
+ // So resolve each package to ONE physical copy, preferring the one the
238
+ // package under test actually resolves. react and react-dom live in
239
+ // DIFFERENT `.pnpm` directories (`react@19.2.5` and
240
+ // `react-dom@19.2.5_react@19.2.5`), so this is per-package: a shared
241
+ // parent directory does not exist and assuming one loses react-dom.
242
+ const onePhysicalCopy = (pkg: string): string => {
243
+ for (const base of [path.resolve(process.cwd(), "node_modules"), rootNodeModules]) {
244
+ try {
245
+ return fs.realpathSync(path.resolve(base, pkg));
246
+ } catch {
247
+ // not installed at this level — fall through to the next base
248
+ }
249
+ }
250
+ return path.resolve(rootNodeModules, pkg);
251
+ };
252
+ const reactDir = onePhysicalCopy("react");
253
+ const reactDomDir = onePhysicalCopy("react-dom");
254
+ resolveAliases.react = reactDir;
255
+ resolveAliases["react-dom"] = reactDomDir;
256
+ resolveAliases["react-dom/client"] = path.join(reactDomDir, "client");
257
+ resolveAliases["react-dom/server"] = path.join(reactDomDir, "server");
258
+ resolveAliases["react/jsx-runtime"] = path.join(reactDir, "jsx-runtime");
259
+ resolveAliases["react/jsx-dev-runtime"] = path.join(reactDir, "jsx-dev-runtime");
260
+ const schedulerDir = onePhysicalCopy("scheduler");
261
+ resolveAliases.scheduler = schedulerDir;
262
+ process.env.MPO_VITEST_REACT = reactDir;
263
+ process.env.MPO_VITEST_REACT_DOM = reactDomDir;
264
+ process.env.MPO_VITEST_SCHEDULER = schedulerDir;
265
+ pinNodeReactRequire(reactDir, reactDomDir, schedulerDir);
181
266
  }
182
267
 
183
268
  const testConfig: Record<string, unknown> = {
184
269
  environment,
185
270
  globals: true,
186
- setupFiles,
271
+ setupFiles: dedupeReact ? [pinReactRequireSetup, ...setupFiles] : setupFiles,
187
272
  testTimeout,
188
273
  coverage: {
189
274
  provider: "v8" as const,
@@ -203,8 +288,25 @@ export function createVitestConfig(options: CreateVitestConfigOptions = {}): Use
203
288
  },
204
289
  server: {
205
290
  deps: {
206
- inline: ["react-native-web", "one", /@tamagui\//, "tamagui", ...inlineDeps],
207
- ...(externalDeps.length > 0 ? { external: externalDeps } : {}),
291
+ // `@tanstack/*` ships `.ts` source; Vite already transforms it, but its
292
+ // CJS child `use-sync-external-store` is a valid Node import and stays
293
+ // external unless named here. Node then `require('react')`s the hoisted
294
+ // root directory — a different inode than the `.pnpm` copy the aliases
295
+ // bind. Measured Gantt stack after rebase onto main (forms Button now
296
+ // calls `@tanstack/react-store`): useCallback from node_modules/react,
297
+ // useSelector from @tanstack/react-store/src, react-dom from .pnpm.
298
+ inline: [
299
+ "react-native-web",
300
+ "one",
301
+ /@tamagui\//,
302
+ "tamagui",
303
+ /@tanstack\//,
304
+ "use-sync-external-store",
305
+ ...inlineDeps,
306
+ ],
307
+ ...(externalDeps.length > 0
308
+ ? { external: [pinReactRequireSetup, ...externalDeps] }
309
+ : { external: [pinReactRequireSetup] }),
208
310
  interopDefault: true,
209
311
  },
210
312
  },
@@ -217,33 +319,43 @@ export function createVitestConfig(options: CreateVitestConfigOptions = {}): Use
217
319
  plugins,
218
320
  test: testConfig,
219
321
  resolve: {
220
- extensions: [
221
- ".test.ts",
222
- ".test.tsx",
223
- ".web.ts",
224
- ".web.tsx",
225
- ".web.js",
226
- ".web.jsx",
227
- ".ts",
228
- ".tsx",
229
- ".js",
230
- ".jsx",
231
- ],
322
+ // Never list .test.* here. Vite tries these suffixes in order for an
323
+ // extensionless specifier, so a leading ".test.ts" makes `import "./foo"`
324
+ // resolve to foo.test.ts instead of foo.ts whenever both exist. That drags
325
+ // the test file (and whatever it imports, e.g. node:test) into the module
326
+ // graph and vitest dies with "Cannot bundle Node.js built-in node:test".
327
+ // Test files are collected by test.include globs, not by resolution.
328
+ extensions: [".web.ts", ".web.tsx", ".web.js", ".web.jsx", ".ts", ".tsx", ".js", ".jsx"],
232
329
  mainFields: ["browser", "module", "main"],
233
330
  alias: {
234
331
  ...resolveAliases,
235
332
  ...aliases,
236
333
  },
237
334
  conditions,
238
- dedupe: dedupeReact ? ["react", "react-dom", "scheduler"] : [],
335
+ // Do not also `dedupe` react. Vite's dedupe always picks the project-root
336
+ // copy, which under `node-linker=hoisted` is a different inode than the
337
+ // `.pnpm` copy the aliases bind. That split is the MPO-37 crash: the
338
+ // alias sends react-dom to `.pnpm`, dedupe sends @tanstack/react-store's
339
+ // `import 'react'` to the hoisted root, dispatcher is null.
340
+ dedupe: [],
239
341
  },
240
342
  define: {
241
343
  __DEV__: true,
242
344
  global: "globalThis",
345
+ // This replaces the `process` identifier in transformed modules. Keep the
346
+ // MPO_VITEST_* paths on that stub so pin-react-require.cjs still sees
347
+ // them if Vite ever transforms the setup file.
243
348
  process: JSON.stringify({
244
349
  env: {
245
350
  NODE_ENV: "test",
246
351
  NODE_DEBUG: false,
352
+ ...(dedupeReact
353
+ ? {
354
+ MPO_VITEST_REACT: process.env.MPO_VITEST_REACT,
355
+ MPO_VITEST_REACT_DOM: process.env.MPO_VITEST_REACT_DOM,
356
+ MPO_VITEST_SCHEDULER: process.env.MPO_VITEST_SCHEDULER,
357
+ }
358
+ : {}),
247
359
  },
248
360
  platform: process.platform,
249
361
  version: process.version,
@@ -1 +1 @@
1
- {"version":3,"file":"vitest.d.ts","sourceRoot":"","sources":["../src/vitest.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAEvC,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAE/B;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,WAAW,GAAG,MAAM,CAAC;IAE7C;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAExB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC;;OAEG;IACH,QAAQ,CAAC,EAAE;QACT,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IAEF;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,GAAG,UAAU,CAmJtF"}
1
+ {"version":3,"file":"vitest.d.ts","sourceRoot":"","sources":["../src/vitest.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAoDvC,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAE/B;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,WAAW,GAAG,MAAM,CAAC;IAE7C;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAExB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC;;OAEG;IACH,QAAQ,CAAC,EAAE;QACT,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IAEF;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,GAAG,UAAU,CA8MtF"}