@multiplatform.one/config 7.6.3 → 7.7.1

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/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { t as createViteConfig } from "./vite-W9hIeg2_.js";
2
- import { t as createStorybookViteConfig } from "./storybook-BnfZmzIL.js";
1
+ import { t as createViteConfig } from "./vite-uwqTTN8M.js";
2
+ import { t as createStorybookViteConfig } from "./storybook-BuO1e8Sb.js";
3
3
  import { n as tamaguiWorkspacePathsFile, t as ensureTamaguiWorkspacePaths } from "./tamaguiWorkspacePaths-ZIcqybtn.js";
4
4
  import { createVitestConfig } from "./vitest.js";
5
5
 
@@ -4,6 +4,100 @@ import { createRequire } from "node:module";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
 
7
+ //#region src/singletonDepAliases.ts
8
+ /**
9
+ * Packages that create a React context at module scope and must therefore
10
+ * resolve to exactly ONE module record across the whole bundle.
11
+ *
12
+ * MPO-215. `react-cookie` runs `React.createContext(null)` when its entry
13
+ * evaluates. `@multiplatform.one/storybook` renders `<CookiesProvider>` in the
14
+ * framework decorator and `@multiplatform.one/theme`'s `useTheme` reads it via
15
+ * `useCookies`, so provider and consumer live in two different workspace
16
+ * packages. When those two packages resolve `react-cookie` to two different
17
+ * realpaths — a hoisted `node_modules/react-cookie` for the importer that does
18
+ * not declare it, the pnpm store copy for the one that does — the entry
19
+ * evaluates twice, there are two contexts, and `useCookies` throws
20
+ * `Missing <CookiesProvider>` with the provider sitting two lines above it in
21
+ * the same tree.
22
+ *
23
+ * Nothing catches that today. The dev server pre-bundles bare imports by
24
+ * package NAME, which collapses both realpaths onto one `deps/react-cookie.js`
25
+ * and renders fine; the production build has no optimizer, so both paths
26
+ * survive, every story renders Storybook's error panel, and
27
+ * `storybook build` still exits 0.
28
+ *
29
+ * `resolve.dedupe` does not fix it: measured against Vite 8 / Rolldown, a
30
+ * build with `dedupe: ["react-cookie"]` emitted a byte-identical iframe chunk
31
+ * and the story still threw. An alias to one absolute directory does, which is
32
+ * the same instrument the storybook config already uses to pin react,
33
+ * react-dom and react-is.
34
+ *
35
+ * MPO-217. `@tamagui/web` is the same shape. It owns the theme context, the
36
+ * style registry and the config singleton; `@tamagui/core` re-exports it and
37
+ * `tamagui` re-exports that, so every tamagui component in the tree reads the
38
+ * theme through whichever `@tamagui/web` record its own import chain reached.
39
+ *
40
+ * Measured on the build's module graph at e19a0fda3: `tamagui` resolved to
41
+ * two realpaths. 562 importers (public/components, public/backoffice,
42
+ * public/storybook and the rest) took the pnpm store copy, whose chain is
43
+ * store `tamagui` -> store `@tamagui/core` -> store `@tamagui/web`; five took
44
+ * the hoisted `node_modules/tamagui`, which has no nested node_modules and so
45
+ * walks up to the hoisted `@tamagui/core` and `@tamagui/web`. Those five were
46
+ * public/frappe's story files — the only modules in the repo that import
47
+ * `tamagui` from a package with no copy of its own — and their 14 stories were
48
+ * the 14 that threw `Missing theme.` on storybook-static while rendering on
49
+ * dev. The theme provider is mounted from the store record; their `Button`,
50
+ * `Text` and `YStack` read the hoisted record's context, which no provider
51
+ * ever wrote to.
52
+ *
53
+ * `tamagui` itself is deliberately NOT pinned. A directory alias resolves
54
+ * `tamagui/linear-gradient` to `node_modules/tamagui/linear-gradient/`, a
55
+ * metro-compat stub that `require`s the CJS build, instead of the ESM target
56
+ * the package's exports map picks for the browser. Pinning core and web is
57
+ * enough: both `tamagui` records import `@tamagui/core` by bare specifier, so
58
+ * they funnel into one web record and one theme context.
59
+ */
60
+ const CONTEXT_SINGLETONS = [
61
+ "react-cookie",
62
+ "@tamagui/core",
63
+ "@tamagui/web"
64
+ ];
65
+ /**
66
+ * Vite `resolve.alias` entries pinning each context singleton to one directory.
67
+ *
68
+ * Alias keys match the whole specifier or a `key + "/"` prefix, so subpath
69
+ * imports follow the same copy instead of resolving independently. Entries are
70
+ * omitted when the package is not installed, so a consumer that does not use
71
+ * cookies is unaffected.
72
+ *
73
+ * @param root Directory whose `node_modules` the packages resolve from.
74
+ */
75
+ function singletonDepAliases(root = process.cwd()) {
76
+ const aliases = {};
77
+ const requireFrom = createRequire(path.join(root, "package.json"));
78
+ for (const pkgName of CONTEXT_SINGLETONS) {
79
+ const pkgDir = resolvePackageDir$1(pkgName, root, requireFrom);
80
+ if (pkgDir) aliases[pkgName] = pkgDir;
81
+ }
82
+ return aliases;
83
+ }
84
+ function resolvePackageDir$1(pkgName, root, requireFrom) {
85
+ try {
86
+ return path.dirname(requireFrom.resolve(`${pkgName}/package.json`));
87
+ } catch {
88
+ try {
89
+ let dir = path.dirname(requireFrom.resolve(pkgName));
90
+ while (dir !== path.dirname(dir)) {
91
+ if (fs.existsSync(path.join(dir, "package.json"))) return dir;
92
+ dir = path.dirname(dir);
93
+ }
94
+ } catch {}
95
+ const hoisted = path.join(root, "node_modules", ...pkgName.split("/"));
96
+ return fs.existsSync(path.join(hoisted, "package.json")) ? hoisted : void 0;
97
+ }
98
+ }
99
+
100
+ //#endregion
7
101
  //#region src/unexportedDepAliases.ts
8
102
  /**
9
103
  * Package subtrees that consumers deep-import but the publisher never declared
@@ -95,7 +189,8 @@ function createStorybookViteConfig(options = {}) {
95
189
  const aliases = {
96
190
  ...discoverPackageAliases(path.join(workspaceRoot, "packages")),
97
191
  ...discoverPublicPackageViteAliases(workspaceRoot),
98
- ...unexportedDepAliases(workspaceRoot)
192
+ ...unexportedDepAliases(workspaceRoot),
193
+ ...singletonDepAliases(workspaceRoot)
99
194
  };
100
195
  if (options.aliases) Object.assign(aliases, options.aliases);
101
196
  const sortedAliases = {};
@@ -157,6 +252,8 @@ function createStorybookViteConfig(options = {}) {
157
252
  "@tamagui/themes",
158
253
  "@tamagui/toast",
159
254
  "@tamagui/use-presence",
255
+ "react-cookie",
256
+ "tamagui/linear-gradient",
160
257
  "@mdx-js/react",
161
258
  "i18next",
162
259
  "react-i18next",
package/lib/storybook.js CHANGED
@@ -1,3 +1,3 @@
1
- import { t as createStorybookViteConfig } from "./storybook-BnfZmzIL.js";
1
+ import { t as createStorybookViteConfig } from "./storybook-BuO1e8Sb.js";
2
2
 
3
3
  export { createStorybookViteConfig };
@@ -92,6 +92,7 @@ function createViteConfig(options = {}) {
92
92
  vitePlugins.push(ssrReactNativeAliasPlugin());
93
93
  vitePlugins.push(clientBrokenEsmPlugin());
94
94
  registerNativeEngineIoBrowserTransports();
95
+ registerNativeSinglePackageInstances();
95
96
  registerNativeWorkspaceEntries();
96
97
  registerNativePhosphorIcons();
97
98
  if (nativeFixes?.protoShadow !== false) registerNativeProtoShadowFix();
@@ -189,7 +190,13 @@ function createViteConfig(options = {}) {
189
190
  return {
190
191
  css: { modules: { localsConvention: "camelCase" } },
191
192
  build: { chunkSizeWarningLimit: 600 },
192
- define: { "process.env.VITE_FRAPPE_ENABLED": JSON.stringify(process.env.VITE_FRAPPE_ENABLED === "true" ? "true" : "false") },
193
+ define: {
194
+ "process.env.VITE_FRAPPE_ENABLED": JSON.stringify(process.env.VITE_FRAPPE_ENABLED === "true" ? "true" : "false"),
195
+ ...publicConfigKeys.length > 0 ? {
196
+ "process.env.VITE_MP_CONFIG": JSON.stringify(process.env.VITE_MP_CONFIG ?? ""),
197
+ "process.env.VITE_MP_PUBLIC_CONFIG_KEYS": JSON.stringify(process.env.VITE_MP_PUBLIC_CONFIG_KEYS ?? "")
198
+ } : {}
199
+ },
193
200
  ssr: ssr || defaultSsr,
194
201
  resolve: {
195
202
  alias: {
@@ -276,6 +283,107 @@ function clientBrokenEsmPlugin() {
276
283
  * This re-asserts the browser redirect with no node_modules patch. Web is
277
284
  * unaffected (engine.io is SSR-externalized and the web client already maps `browser`).
278
285
  */
286
+ /**
287
+ * Collapse duplicate PHYSICAL copies of the same package@version to one path
288
+ * in the NATIVE graph, so React (and every other stateful singleton) exists
289
+ * exactly once in the Hermes bundle.
290
+ *
291
+ * The workspace's node_modules is a MIXED install. `.npmrc` says
292
+ * `node-linker=hoisted`, so the root `node_modules/<pkg>` entries are real
293
+ * directories; but an earlier isolated install left a `node_modules/.pnpm`
294
+ * virtual store plus per-package symlink farms behind — e.g. each package's
295
+ * own `node_modules/react` still points at
296
+ * `../../../node_modules/.pnpm/react@19.2.5/node_modules/react`.
297
+ * Both trees are fully populated with byte-identical copies, and which one an
298
+ * importer reaches depends only on where that importer sits on disk:
299
+ *
300
+ * apps/one/app -> node_modules/.pnpm/react@19.2.5/node_modules/react
301
+ * public/forms/src -> node_modules/react (no local symlink)
302
+ *
303
+ * Rolldown resolves symlinks, so the two land as two DIFFERENT absolute paths
304
+ * and it bundles both. React 19's `useContext` is
305
+ * `ReactSharedInternals.H.useContext(Context)` — `H` is the dispatcher the
306
+ * renderer installs during render, and it is only ever set on the copy the
307
+ * renderer imported. A component from the other copy therefore reads `H` as
308
+ * null and the app dies with "Cannot read property 'useContext' of null".
309
+ * That is the whole bug; the same split had also duplicated react-native (427
310
+ * modules each), @tamagui/web, @react-navigation/core, one, scheduler and 58
311
+ * more packages.
312
+ *
313
+ * The rule: an id that resolves inside the virtual store is rewritten to the
314
+ * hoisted root copy when the root copy exists, carries the SAME version, and
315
+ * actually has the file. Version equality is the safety guard — genuine
316
+ * multi-version installs (two @babel/runtime, two viem) keep both copies.
317
+ *
318
+ * On a correctly installed tree this is a no-op by construction: `hoisted`
319
+ * leaves no `.pnpm` store to match, and `isolated` makes the root entry a
320
+ * symlink INTO the store, which realpaths back to the same directory (checked
321
+ * explicitly). It only fires on the mixed tree, and it is cheap because only
322
+ * BARE specifiers are inspected — once a package's entry is canonical, its own
323
+ * relative imports resolve inside the canonical copy for free.
324
+ */
325
+ function registerNativeSinglePackageInstances() {
326
+ const g = globalThis;
327
+ const name = "vxrn-single-package-instances";
328
+ g.__vxrnAddNativePlugins = g.__vxrnAddNativePlugins ?? [];
329
+ if (g.__vxrnAddNativePlugins.some((p) => p?.name === name)) return;
330
+ const rootModules = path.join(findProjectRoot(), "node_modules");
331
+ const storePath = /[\\/]node_modules[\\/]\.pnpm[\\/][^\\/]+[\\/]node_modules[\\/]((?:@[^\\/]+[\\/])?[^\\/]+)(?:[\\/](.*))?$/;
332
+ function versionOf(dir) {
333
+ try {
334
+ const raw = fs.readFileSync(path.join(dir, "package.json"), "utf8");
335
+ return JSON.parse(raw).version ?? null;
336
+ } catch {
337
+ return null;
338
+ }
339
+ }
340
+ const hoisted = /* @__PURE__ */ new Map();
341
+ function hoistedTwin(pkg, storeDir) {
342
+ if (!hoisted.has(pkg)) {
343
+ const rootDir = path.join(rootModules, pkg);
344
+ let twin = null;
345
+ if (fs.existsSync(rootDir)) {
346
+ const rootVersion = versionOf(rootDir);
347
+ const storeVersion = versionOf(storeDir);
348
+ if (rootVersion && rootVersion === storeVersion) try {
349
+ if (fs.realpathSync(rootDir) !== fs.realpathSync(storeDir)) twin = rootDir;
350
+ } catch {
351
+ twin = null;
352
+ }
353
+ }
354
+ hoisted.set(pkg, twin);
355
+ }
356
+ return hoisted.get(pkg) ?? null;
357
+ }
358
+ const seen = /* @__PURE__ */ new Map();
359
+ g.__vxrnAddNativePlugins.push({
360
+ name,
361
+ async resolveId(id, importer) {
362
+ if (!id || id.startsWith(".") || id.startsWith("/") || id.startsWith("\0")) return null;
363
+ const key = `${id} ${importer ?? ""}`;
364
+ const cached = seen.get(key);
365
+ if (cached !== void 0) return cached;
366
+ let out = null;
367
+ try {
368
+ const resolved = await this.resolve(id, importer, { skipSelf: true });
369
+ const match = resolved?.external ? null : resolved?.id?.match(storePath);
370
+ if (resolved && match) {
371
+ const pkg = match[1].split(path.sep).join("/");
372
+ const rest = match[2] ?? "";
373
+ const twin = hoistedTwin(pkg, resolved.id.slice(0, resolved.id.length - (rest ? rest.length + 1 : 0)));
374
+ if (twin) {
375
+ const candidate = rest ? path.join(twin, rest) : twin;
376
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) out = candidate;
377
+ }
378
+ }
379
+ } catch {
380
+ out = null;
381
+ }
382
+ seen.set(key, out);
383
+ return out;
384
+ }
385
+ });
386
+ }
279
387
  function registerNativeEngineIoBrowserTransports() {
280
388
  const g = globalThis;
281
389
  const name = "vxrn-engineio-browser-transports";
@@ -322,7 +430,12 @@ function registerNativeWorkspaceEntries() {
322
430
  const root = roots.get(id);
323
431
  if (root) {
324
432
  const nativeEntry = path.join(root, "dist/esm/index.native.js");
325
- return fs.existsSync(nativeEntry) ? nativeEntry : null;
433
+ if (fs.existsSync(nativeEntry)) return nativeEntry;
434
+ const srcTs = path.join(root, "src/index.ts");
435
+ if (fs.existsSync(srcTs)) return srcTs;
436
+ const srcTsx = path.join(root, "src/index.tsx");
437
+ if (fs.existsSync(srcTsx)) return srcTsx;
438
+ return null;
326
439
  }
327
440
  if (id.endsWith("/dist/esm/index.mjs")) {
328
441
  const nativeEntry = `${id.slice(0, -4)}.native.js`;
package/lib/vite.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as createViteConfig } from "./vite-W9hIeg2_.js";
1
+ import { t as createViteConfig } from "./vite-uwqTTN8M.js";
2
2
  import { n as publicPackageViteSourceAliases, t as discoverPublicPackageRoots } from "./workspacePublicPackages-COicQSj4.js";
3
3
 
4
4
  export { createViteConfig, discoverPublicPackageRoots, publicPackageViteSourceAliases };