@multiplatform.one/config 7.7.6 → 7.10.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/lib/index.js +2 -2
- package/lib/{storybook-BuO1e8Sb.js → storybook-CGNSgSBS.js} +4 -102
- package/lib/storybook.js +1 -1
- package/lib/{vite-DF5gYMDn.js → vite-D6vMjPwv.js} +3 -3
- package/lib/vite.js +2 -2
- package/lib/vitest.js +1 -0
- package/lib/workspacePublicPackages-B4FGgRYG.js +218 -0
- package/package.json +2 -2
- package/src/oxlint.spec.ts +1 -1
- package/src/singletonDepAliases.spec.ts +124 -0
- package/src/singletonDepAliases.ts +77 -1
- package/src/storybook.ts +10 -19
- package/src/vite.ts +23 -16
- package/src/vitest.spec.ts +2 -1
- package/src/vitest.ts +3 -0
- package/types/singletonDepAliases.d.ts +20 -0
- package/types/singletonDepAliases.d.ts.map +1 -1
- package/types/storybook.d.ts.map +1 -1
- package/types/vite.d.ts.map +1 -1
- package/types/vitest.d.ts.map +1 -1
- package/lib/workspacePublicPackages-COicQSj4.js +0 -67
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as createViteConfig } from "./vite-
|
|
2
|
-
import { t as createStorybookViteConfig } from "./storybook-
|
|
1
|
+
import { t as createViteConfig } from "./vite-D6vMjPwv.js";
|
|
2
|
+
import { t as createStorybookViteConfig } from "./storybook-CGNSgSBS.js";
|
|
3
3
|
import { n as tamaguiWorkspacePathsFile, t as ensureTamaguiWorkspacePaths } from "./tamaguiWorkspacePaths-ZIcqybtn.js";
|
|
4
4
|
import { createVitestConfig } from "./vitest.js";
|
|
5
5
|
|
|
@@ -1,103 +1,9 @@
|
|
|
1
|
-
import { r as resolvePackageMainSource, t as discoverPublicPackageRoots } from "./workspacePublicPackages-
|
|
1
|
+
import { i as withSingletonDepAliases, r as resolvePackageMainSource, t as discoverPublicPackageRoots } from "./workspacePublicPackages-B4FGgRYG.js";
|
|
2
2
|
import { t as ensureTamaguiWorkspacePaths } from "./tamaguiWorkspacePaths-ZIcqybtn.js";
|
|
3
3
|
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
|
|
101
7
|
//#region src/unexportedDepAliases.ts
|
|
102
8
|
/**
|
|
103
9
|
* Package subtrees that consumers deep-import but the publisher never declared
|
|
@@ -186,15 +92,11 @@ function resolvePackageDir(pkgName, root, requireFrom) {
|
|
|
186
92
|
function createStorybookViteConfig(options = {}) {
|
|
187
93
|
ensureTamaguiWorkspacePaths();
|
|
188
94
|
const workspaceRoot = options.workspaceRoot || findWorkspaceRoot();
|
|
189
|
-
const
|
|
95
|
+
const sortedAliases = withSingletonDepAliases({
|
|
190
96
|
...discoverPackageAliases(path.join(workspaceRoot, "packages")),
|
|
191
97
|
...discoverPublicPackageViteAliases(workspaceRoot),
|
|
192
|
-
...unexportedDepAliases(workspaceRoot)
|
|
193
|
-
|
|
194
|
-
};
|
|
195
|
-
if (options.aliases) Object.assign(aliases, options.aliases);
|
|
196
|
-
const sortedAliases = {};
|
|
197
|
-
for (const key of Object.keys(aliases).sort((a, b) => b.length - a.length)) sortedAliases[key] = aliases[key];
|
|
98
|
+
...unexportedDepAliases(workspaceRoot)
|
|
99
|
+
}, workspaceRoot, options.aliases);
|
|
198
100
|
return {
|
|
199
101
|
define: {
|
|
200
102
|
"process.env.VITE_ENVIRONMENT": JSON.stringify("client"),
|
package/lib/storybook.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as discoverPublicPackageRoots } from "./workspacePublicPackages-
|
|
1
|
+
import { i as withSingletonDepAliases, t as discoverPublicPackageRoots } from "./workspacePublicPackages-B4FGgRYG.js";
|
|
2
2
|
import { t as ensureTamaguiWorkspacePaths } from "./tamaguiWorkspacePaths-ZIcqybtn.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import fs from "node:fs";
|
|
@@ -199,10 +199,10 @@ function createViteConfig(options = {}) {
|
|
|
199
199
|
},
|
|
200
200
|
ssr: ssr || defaultSsr,
|
|
201
201
|
resolve: {
|
|
202
|
-
alias: {
|
|
202
|
+
alias: withSingletonDepAliases({
|
|
203
203
|
"react.mjs": "react",
|
|
204
204
|
"react-compiler-runtime": "react/compiler-runtime"
|
|
205
|
-
},
|
|
205
|
+
}, projectRoot),
|
|
206
206
|
dedupe: ["react-is"]
|
|
207
207
|
},
|
|
208
208
|
server: defaultServer,
|
package/lib/vite.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as createViteConfig } from "./vite-
|
|
2
|
-
import { n as publicPackageViteSourceAliases, t as discoverPublicPackageRoots } from "./workspacePublicPackages-
|
|
1
|
+
import { t as createViteConfig } from "./vite-D6vMjPwv.js";
|
|
2
|
+
import { n as publicPackageViteSourceAliases, t as discoverPublicPackageRoots } from "./workspacePublicPackages-B4FGgRYG.js";
|
|
3
3
|
|
|
4
4
|
export { createViteConfig, discoverPublicPackageRoots, publicPackageViteSourceAliases };
|
package/lib/vitest.js
CHANGED
|
@@ -130,6 +130,7 @@ function createVitestConfig(options = {}) {
|
|
|
130
130
|
"tamagui",
|
|
131
131
|
/@tanstack\//,
|
|
132
132
|
"use-sync-external-store",
|
|
133
|
+
"react-i18next",
|
|
133
134
|
...inlineDeps
|
|
134
135
|
],
|
|
135
136
|
...externalDeps.length > 0 ? { external: [pinReactRequireSetup, ...externalDeps] } : { external: [pinReactRequireSetup] },
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
//#region src/singletonDepAliases.ts
|
|
6
|
+
/**
|
|
7
|
+
* Packages that create a React context at module scope and must therefore
|
|
8
|
+
* resolve to exactly ONE module record across the whole bundle.
|
|
9
|
+
*
|
|
10
|
+
* MPO-215. `react-cookie` runs `React.createContext(null)` when its entry
|
|
11
|
+
* evaluates. `@multiplatform.one/storybook` renders `<CookiesProvider>` in the
|
|
12
|
+
* framework decorator and `@multiplatform.one/theme`'s `useTheme` reads it via
|
|
13
|
+
* `useCookies`, so provider and consumer live in two different workspace
|
|
14
|
+
* packages. When those two packages resolve `react-cookie` to two different
|
|
15
|
+
* realpaths — a hoisted `node_modules/react-cookie` for the importer that does
|
|
16
|
+
* not declare it, the pnpm store copy for the one that does — the entry
|
|
17
|
+
* evaluates twice, there are two contexts, and `useCookies` throws
|
|
18
|
+
* `Missing <CookiesProvider>` with the provider sitting two lines above it in
|
|
19
|
+
* the same tree.
|
|
20
|
+
*
|
|
21
|
+
* Nothing catches that today. The dev server pre-bundles bare imports by
|
|
22
|
+
* package NAME, which collapses both realpaths onto one `deps/react-cookie.js`
|
|
23
|
+
* and renders fine; the production build has no optimizer, so both paths
|
|
24
|
+
* survive, every story renders Storybook's error panel, and
|
|
25
|
+
* `storybook build` still exits 0.
|
|
26
|
+
*
|
|
27
|
+
* `resolve.dedupe` does not fix it: measured against Vite 8 / Rolldown, a
|
|
28
|
+
* build with `dedupe: ["react-cookie"]` emitted a byte-identical iframe chunk
|
|
29
|
+
* and the story still threw. An alias to one absolute directory does, which is
|
|
30
|
+
* the same instrument the storybook config already uses to pin react,
|
|
31
|
+
* react-dom and react-is.
|
|
32
|
+
*
|
|
33
|
+
* MPO-217. `@tamagui/web` is the same shape. It owns the theme context, the
|
|
34
|
+
* style registry and the config singleton; `@tamagui/core` re-exports it and
|
|
35
|
+
* `tamagui` re-exports that, so every tamagui component in the tree reads the
|
|
36
|
+
* theme through whichever `@tamagui/web` record its own import chain reached.
|
|
37
|
+
*
|
|
38
|
+
* Measured on the build's module graph at e19a0fda3: `tamagui` resolved to
|
|
39
|
+
* two realpaths. 562 importers (public/components, public/backoffice,
|
|
40
|
+
* public/storybook and the rest) took the pnpm store copy, whose chain is
|
|
41
|
+
* store `tamagui` -> store `@tamagui/core` -> store `@tamagui/web`; five took
|
|
42
|
+
* the hoisted `node_modules/tamagui`, which has no nested node_modules and so
|
|
43
|
+
* walks up to the hoisted `@tamagui/core` and `@tamagui/web`. Those five were
|
|
44
|
+
* public/frappe's story files — the only modules in the repo that import
|
|
45
|
+
* `tamagui` from a package with no copy of its own — and their 14 stories were
|
|
46
|
+
* the 14 that threw `Missing theme.` on storybook-static while rendering on
|
|
47
|
+
* dev. The theme provider is mounted from the store record; their `Button`,
|
|
48
|
+
* `Text` and `YStack` read the hoisted record's context, which no provider
|
|
49
|
+
* ever wrote to.
|
|
50
|
+
*
|
|
51
|
+
* `tamagui` itself is deliberately NOT pinned. A directory alias resolves
|
|
52
|
+
* `tamagui/linear-gradient` to `node_modules/tamagui/linear-gradient/`, a
|
|
53
|
+
* metro-compat stub that `require`s the CJS build, instead of the ESM target
|
|
54
|
+
* the package's exports map picks for the browser. Pinning core and web is
|
|
55
|
+
* enough: both `tamagui` records import `@tamagui/core` by bare specifier, so
|
|
56
|
+
* they funnel into one web record and one theme context.
|
|
57
|
+
*
|
|
58
|
+
* MPO-225. `@tanstack/db` is the same shape one layer down, and it is the one
|
|
59
|
+
* that reaches generated apps. `@multiplatform.one/frappe`'s collections are
|
|
60
|
+
* TanStack DB, and TanStack reaches React through `use-sync-external-store`,
|
|
61
|
+
* so a second module record subscribes against a store nothing armed and the
|
|
62
|
+
* hook reads `useSyncExternalStore` off null. Measured on a fresh
|
|
63
|
+
* `mpo init --universal --frappe --tauri` at cli 7.7.1: the first page
|
|
64
|
+
* mounting a data-bound panel threw `Cannot read properties of null (reading
|
|
65
|
+
* 'useSyncExternalStore')` on every load, and so did every data-bound page
|
|
66
|
+
* after it.
|
|
67
|
+
*
|
|
68
|
+
* MPO-288. THE CRITERION, so the next addition is a decision against a rule
|
|
69
|
+
* rather than a guess. A package belongs here when a second module record
|
|
70
|
+
* would break it: it holds state at module scope, it subscribes an external
|
|
71
|
+
* store, or it provides a React context consumers compare by identity.
|
|
72
|
+
* Grepping the installed package for `useSyncExternalStore` is a cheap test
|
|
73
|
+
* for the second clause, and `singletonDepAliases.spec.ts` re-runs it against
|
|
74
|
+
* the tree so a tanstack upgrade is checked rather than assumed. It is one
|
|
75
|
+
* clause and not the whole rule: `@tanstack/db` references the hook nowhere
|
|
76
|
+
* and is pinned anyway, for the collection registry it holds at module scope.
|
|
77
|
+
*
|
|
78
|
+
* `@tanstack/react-query` references the hook in 25 shipped files and is not
|
|
79
|
+
* listed. That is recorded rather than settled — no split has been observed
|
|
80
|
+
* there, and pinning it reaches further than this measurement does.
|
|
81
|
+
*/
|
|
82
|
+
const CONTEXT_SINGLETONS = [
|
|
83
|
+
"react-cookie",
|
|
84
|
+
"@tamagui/core",
|
|
85
|
+
"@tamagui/web",
|
|
86
|
+
"@tanstack/db",
|
|
87
|
+
"@tanstack/react-db",
|
|
88
|
+
"@tanstack/react-store"
|
|
89
|
+
];
|
|
90
|
+
/**
|
|
91
|
+
* Vite `resolve.alias` entries pinning each context singleton to one directory.
|
|
92
|
+
*
|
|
93
|
+
* Alias keys match the whole specifier or a `key + "/"` prefix, so subpath
|
|
94
|
+
* imports follow the same copy instead of resolving independently. Entries are
|
|
95
|
+
* omitted when the package is not installed, so a consumer that does not use
|
|
96
|
+
* cookies is unaffected.
|
|
97
|
+
*
|
|
98
|
+
* @param root Directory whose `node_modules` the packages resolve from.
|
|
99
|
+
*/
|
|
100
|
+
function singletonDepAliases(root = process.cwd()) {
|
|
101
|
+
const aliases = {};
|
|
102
|
+
const requireFrom = createRequire(path.join(root, "package.json"));
|
|
103
|
+
for (const pkgName of CONTEXT_SINGLETONS) {
|
|
104
|
+
const pkgDir = resolvePackageDir(pkgName, root, requireFrom);
|
|
105
|
+
if (pkgDir) aliases[pkgName] = pkgDir;
|
|
106
|
+
}
|
|
107
|
+
return aliases;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Merges `base` with the singleton pins and orders the result for Vite.
|
|
111
|
+
*
|
|
112
|
+
* Both entry points build `resolve.alias` through here so the list cannot go
|
|
113
|
+
* out of reach again: before MPO-225 only `createStorybookViteConfig` applied
|
|
114
|
+
* `singletonDepAliases`, and a project generated by `mpo init` builds through
|
|
115
|
+
* `createViteConfig` alone, so every package on the list resolved to two
|
|
116
|
+
* records there.
|
|
117
|
+
*
|
|
118
|
+
* MPO-215: singletons are merged last so a discovered workspace or dependency
|
|
119
|
+
* alias can never shadow one. Keys are sorted longest-first because Vite tries
|
|
120
|
+
* alias entries in insertion order and matches a key or its `key + "/"` prefix.
|
|
121
|
+
* `overrides` are applied after the pins: a caller naming a package explicitly
|
|
122
|
+
* means it.
|
|
123
|
+
*
|
|
124
|
+
* @param base Aliases the pins may shadow.
|
|
125
|
+
* @param root Directory whose `node_modules` the packages resolve from.
|
|
126
|
+
* @param overrides Aliases that win over everything, including the pins.
|
|
127
|
+
*/
|
|
128
|
+
function withSingletonDepAliases(base, root = process.cwd(), overrides = {}) {
|
|
129
|
+
const merged = {
|
|
130
|
+
...base,
|
|
131
|
+
...singletonDepAliases(root),
|
|
132
|
+
...overrides
|
|
133
|
+
};
|
|
134
|
+
const sorted = {};
|
|
135
|
+
for (const key of Object.keys(merged).sort((a, b) => b.length - a.length)) sorted[key] = merged[key];
|
|
136
|
+
return sorted;
|
|
137
|
+
}
|
|
138
|
+
function resolvePackageDir(pkgName, root, requireFrom) {
|
|
139
|
+
try {
|
|
140
|
+
return path.dirname(requireFrom.resolve(`${pkgName}/package.json`));
|
|
141
|
+
} catch {
|
|
142
|
+
try {
|
|
143
|
+
let dir = path.dirname(requireFrom.resolve(pkgName));
|
|
144
|
+
while (dir !== path.dirname(dir)) {
|
|
145
|
+
if (fs.existsSync(path.join(dir, "package.json"))) return dir;
|
|
146
|
+
dir = path.dirname(dir);
|
|
147
|
+
}
|
|
148
|
+
} catch {}
|
|
149
|
+
const hoisted = path.join(root, "node_modules", ...pkgName.split("/"));
|
|
150
|
+
return fs.existsSync(path.join(hoisted, "package.json")) ? hoisted : void 0;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/workspacePublicPackages.ts
|
|
156
|
+
/**
|
|
157
|
+
* Every direct child of `public/` with a `package.json` `name` is treated as a
|
|
158
|
+
* publishable workspace package (same rule as Metro in `apps/storybook-expo`).
|
|
159
|
+
*
|
|
160
|
+
* Tamagui’s internal esbuild (bundling `tamagui.config` + `components`) reads
|
|
161
|
+
* `compilerOptions.paths` from the repo `tsconfig.base.json` chain, including
|
|
162
|
+
* `tamagui-workspace-paths.generated.json` (see `scripts/generate-tamagui-workspace-paths.mjs`).
|
|
163
|
+
*/
|
|
164
|
+
function discoverPublicPackageRoots(workspaceRoot) {
|
|
165
|
+
const byName = /* @__PURE__ */ new Map();
|
|
166
|
+
const publicDir = path.join(workspaceRoot, "public");
|
|
167
|
+
if (!fs.existsSync(publicDir)) return byName;
|
|
168
|
+
for (const ent of fs.readdirSync(publicDir, { withFileTypes: true })) {
|
|
169
|
+
if (!ent.isDirectory()) continue;
|
|
170
|
+
const pkgRoot = path.join(publicDir, ent.name);
|
|
171
|
+
const pkgJsonPath = path.join(pkgRoot, "package.json");
|
|
172
|
+
if (!fs.existsSync(pkgJsonPath)) continue;
|
|
173
|
+
try {
|
|
174
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
|
175
|
+
if (typeof pkg.name === "string" && pkg.name.length > 0) byName.set(pkg.name, pkgRoot);
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
return byName;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Main entry file for Vite/Storybook-style “compile from source” aliases.
|
|
182
|
+
*
|
|
183
|
+
* These aliases are only consumed by WEB Vite configs (Storybook, SPA), so
|
|
184
|
+
* `.web.*` entries take priority over the plain (native) ones — otherwise a
|
|
185
|
+
* package like @package/fonts resolves to its native index.ts and its
|
|
186
|
+
* web-only side effects (fontsource @font-face CSS imports) never load.
|
|
187
|
+
*/
|
|
188
|
+
function resolvePackageMainSource(pkgDir) {
|
|
189
|
+
return [
|
|
190
|
+
path.join(pkgDir, "src", "index.storybook.ts"),
|
|
191
|
+
path.join(pkgDir, "src", "index.storybook.tsx"),
|
|
192
|
+
path.join(pkgDir, "src", "index.web.ts"),
|
|
193
|
+
path.join(pkgDir, "src", "index.web.tsx"),
|
|
194
|
+
path.join(pkgDir, "src", "index.ts"),
|
|
195
|
+
path.join(pkgDir, "src", "index.tsx"),
|
|
196
|
+
path.join(pkgDir, "index.storybook.ts"),
|
|
197
|
+
path.join(pkgDir, "index.storybook.tsx"),
|
|
198
|
+
path.join(pkgDir, "index.web.ts"),
|
|
199
|
+
path.join(pkgDir, "index.web.tsx"),
|
|
200
|
+
path.join(pkgDir, "index.ts"),
|
|
201
|
+
path.join(pkgDir, "index.tsx")
|
|
202
|
+
].find((c) => fs.existsSync(c));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Vite `resolve.alias` entries: each public package name → its TypeScript main.
|
|
206
|
+
* For SPA / ad-hoc Vite configs that should not list packages by hand.
|
|
207
|
+
*/
|
|
208
|
+
function publicPackageViteSourceAliases(workspaceRoot) {
|
|
209
|
+
const aliases = {};
|
|
210
|
+
for (const [name, dir] of discoverPublicPackageRoots(workspaceRoot)) {
|
|
211
|
+
const main = resolvePackageMainSource(dir);
|
|
212
|
+
if (main) aliases[name] = main;
|
|
213
|
+
}
|
|
214
|
+
return aliases;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
//#endregion
|
|
218
|
+
export { withSingletonDepAliases as i, publicPackageViteSourceAliases as n, resolvePackageMainSource as r, discoverPublicPackageRoots as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/config",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.10.0",
|
|
4
4
|
"description": "Shared build and test configuration presets for multiplatform.one",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"config",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"vite-plugin-external": "^6.2.2",
|
|
103
103
|
"vite-plugin-i18next-loader": "^3.1.3",
|
|
104
104
|
"vitest": "^4.1.5",
|
|
105
|
-
"@multiplatform.one/utils": "7.
|
|
105
|
+
"@multiplatform.one/utils": "7.10.0"
|
|
106
106
|
},
|
|
107
107
|
"devDependencies": {
|
|
108
108
|
"tsdown": "^0.21.10",
|
package/src/oxlint.spec.ts
CHANGED
|
@@ -290,7 +290,7 @@ describe("no-hex-literals comments and palette (MPO-17 AC-4)", () => {
|
|
|
290
290
|
|
|
291
291
|
it("stays silent in packages/themes/base.ts even for rgba", () => {
|
|
292
292
|
const reports: Report[] = [];
|
|
293
|
-
const rule = plugin.rules?.["no-hex-literals"] as
|
|
293
|
+
const rule = plugin.rules?.["no-hex-literals"] as {
|
|
294
294
|
create: (c: unknown) => { Literal?: (n: unknown) => void };
|
|
295
295
|
};
|
|
296
296
|
const visitors = rule.create({
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
4
|
import { createStorybookViteConfig } from "./storybook";
|
|
5
5
|
import { singletonDepAliases } from "./singletonDepAliases";
|
|
6
|
+
import { createViteConfig } from "./vite";
|
|
6
7
|
|
|
7
8
|
const workspaceRoot = path.resolve(__dirname, "../../..");
|
|
8
9
|
|
|
@@ -42,6 +43,42 @@ describe("singletonDepAliases", () => {
|
|
|
42
43
|
expect(singletonDepAliases(workspaceRoot).tamagui).toBeUndefined();
|
|
43
44
|
});
|
|
44
45
|
|
|
46
|
+
it("pins the tanstack db record to one package directory (MPO-225)", () => {
|
|
47
|
+
// @multiplatform.one/frappe's collections are TanStack DB, and TanStack
|
|
48
|
+
// reaches React through use-sync-external-store. Two realpaths means the
|
|
49
|
+
// second record subscribes against a store nothing armed, and every
|
|
50
|
+
// data-bound page dies on load with "Cannot read properties of null
|
|
51
|
+
// (reading 'useSyncExternalStore')".
|
|
52
|
+
const aliases = singletonDepAliases(workspaceRoot);
|
|
53
|
+
expect(aliases["@tanstack/db"]).toBeTruthy();
|
|
54
|
+
expect(path.isAbsolute(aliases["@tanstack/db"])).toBe(true);
|
|
55
|
+
expect(fs.existsSync(path.join(aliases["@tanstack/db"], "package.json"))).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("pins the react-facing half too, which is the one that calls the hook (MPO-225)", () => {
|
|
59
|
+
// Measured in the installed tree: @tanstack/db has zero references to
|
|
60
|
+
// useSyncExternalStore and @tanstack/react-db has three, useLiveQuery.js
|
|
61
|
+
// among them. public/frappe depends on both and imports react-db from
|
|
62
|
+
// src/useLiveQuery.ts, so pinning the base package alone leaves the
|
|
63
|
+
// reported crash in place. The pair travels together or neither works.
|
|
64
|
+
const aliases = singletonDepAliases(workspaceRoot);
|
|
65
|
+
expect(aliases["@tanstack/react-db"]).toBeTruthy();
|
|
66
|
+
expect(path.isAbsolute(aliases["@tanstack/react-db"])).toBe(true);
|
|
67
|
+
expect(fs.existsSync(path.join(aliases["@tanstack/react-db"], "package.json"))).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("pins the react half of the store pair, which is the one that calls the hook (MPO-288)", () => {
|
|
71
|
+
// The same split as react-db, measured the same way: @tanstack/store has
|
|
72
|
+
// zero references to useSyncExternalStore and @tanstack/react-store has
|
|
73
|
+
// three, all of them useSelector, which is what useStore calls.
|
|
74
|
+
// public/store and public/forms both reach React through it, so a second
|
|
75
|
+
// record subscribes a store nothing armed.
|
|
76
|
+
const aliases = singletonDepAliases(workspaceRoot);
|
|
77
|
+
expect(aliases["@tanstack/react-store"]).toBeTruthy();
|
|
78
|
+
expect(path.isAbsolute(aliases["@tanstack/react-store"])).toBe(true);
|
|
79
|
+
expect(fs.existsSync(path.join(aliases["@tanstack/react-store"], "package.json"))).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
45
82
|
it("omits packages that are not installed", () => {
|
|
46
83
|
// Resolution runs against the given root, so a consumer without the
|
|
47
84
|
// package gets no alias rather than a broken one.
|
|
@@ -81,3 +118,90 @@ describe("createStorybookViteConfig react-cookie singleton", () => {
|
|
|
81
118
|
expect(config.optimizeDeps?.include ?? []).toContain("react-cookie");
|
|
82
119
|
});
|
|
83
120
|
});
|
|
121
|
+
|
|
122
|
+
describe("createViteConfig context singletons (MPO-225)", () => {
|
|
123
|
+
const alias = (createViteConfig({ projectRoot: workspaceRoot }).resolve?.alias ?? {}) as Record<
|
|
124
|
+
string,
|
|
125
|
+
string
|
|
126
|
+
>;
|
|
127
|
+
|
|
128
|
+
it("applies every pin on the consumer path, not only in storybook", () => {
|
|
129
|
+
// This is the defect the ticket names. The list existed and only
|
|
130
|
+
// createStorybookViteConfig read it. A project generated by `mpo init`
|
|
131
|
+
// builds through createViteConfig and nothing else, so it inherited none
|
|
132
|
+
// of it.
|
|
133
|
+
const expected = singletonDepAliases(workspaceRoot);
|
|
134
|
+
expect(Object.keys(expected).length).toBeGreaterThan(0);
|
|
135
|
+
for (const [pkg, dir] of Object.entries(expected)) {
|
|
136
|
+
expect(alias[pkg], `${pkg} must be pinned on the consumer path`).toBe(dir);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("keeps the hand-written aliases createViteConfig already carried", () => {
|
|
141
|
+
expect(alias["react.mjs"]).toBe("react");
|
|
142
|
+
expect(alias["react-compiler-runtime"]).toBe("react/compiler-runtime");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("orders keys longest-first so a pin cannot be swallowed by a shorter one", () => {
|
|
146
|
+
// Vite tries object alias entries in insertion order and matches a key or
|
|
147
|
+
// its `key + "/"` prefix, so a subpath entry must precede its package.
|
|
148
|
+
const lengths = Object.keys(alias).map((key) => key.length);
|
|
149
|
+
expect(lengths).toEqual([...lengths].sort((a, b) => b - a));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("does not pin tamagui itself on the consumer path either", () => {
|
|
153
|
+
expect(alias.tamagui).toBeUndefined();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("CONTEXT_SINGLETONS membership, measured rather than recited (MPO-288)", () => {
|
|
158
|
+
const aliases = singletonDepAliases(workspaceRoot);
|
|
159
|
+
|
|
160
|
+
it("pins every tanstack half that subscribes an external store", () => {
|
|
161
|
+
for (const pkg of ["@tanstack/react-store", "@tanstack/react-db"]) {
|
|
162
|
+
expect(subscribesExternalStore(pkg), `${pkg} must reference the hook`).toBe(true);
|
|
163
|
+
expect(aliases[pkg], `${pkg} subscribes and must be pinned`).toBeTruthy();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("leaves the pacer pair off, which the same measurement confirms", () => {
|
|
168
|
+
for (const pkg of ["@tanstack/pacer", "@tanstack/react-pacer"]) {
|
|
169
|
+
expect(subscribesExternalStore(pkg), `${pkg} must not reference the hook`).toBe(false);
|
|
170
|
+
expect(aliases[pkg], `${pkg} has no measured reason to be pinned`).toBeUndefined();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("does not treat the grep as the whole rule", () => {
|
|
175
|
+
// @tanstack/db is pinned and references the hook nowhere: it holds the
|
|
176
|
+
// collection registry at module scope, which is a different clause of the
|
|
177
|
+
// criterion and one no grep can see. @tanstack/store carries no such
|
|
178
|
+
// registry, so silence there means what it says.
|
|
179
|
+
expect(subscribesExternalStore("@tanstack/db")).toBe(false);
|
|
180
|
+
expect(aliases["@tanstack/db"]).toBeTruthy();
|
|
181
|
+
expect(subscribesExternalStore("@tanstack/store")).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Whether an installed package references `useSyncExternalStore` in anything
|
|
187
|
+
* it ships. The cheap half of the membership rule, re-run against the tree so
|
|
188
|
+
* the next tanstack upgrade is checked rather than assumed.
|
|
189
|
+
*/
|
|
190
|
+
function subscribesExternalStore(pkgName: string): boolean {
|
|
191
|
+
const dir = path.join(workspaceRoot, "node_modules", ...pkgName.split("/"));
|
|
192
|
+
return fs.existsSync(dir) && referencesHook(dir);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function referencesHook(dir: string): boolean {
|
|
196
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
197
|
+
if (entry.name === "node_modules") continue;
|
|
198
|
+
const full = path.join(dir, entry.name);
|
|
199
|
+
if (fs.statSync(full).isDirectory()) {
|
|
200
|
+
if (referencesHook(full)) return true;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (!/\.(?:js|cjs|mjs|ts|tsx)$/.test(entry.name)) continue;
|
|
204
|
+
if (fs.readFileSync(full, "utf8").includes("useSyncExternalStore")) return true;
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
@@ -53,8 +53,52 @@ import path from "node:path";
|
|
|
53
53
|
* the package's exports map picks for the browser. Pinning core and web is
|
|
54
54
|
* enough: both `tamagui` records import `@tamagui/core` by bare specifier, so
|
|
55
55
|
* they funnel into one web record and one theme context.
|
|
56
|
+
*
|
|
57
|
+
* MPO-225. `@tanstack/db` is the same shape one layer down, and it is the one
|
|
58
|
+
* that reaches generated apps. `@multiplatform.one/frappe`'s collections are
|
|
59
|
+
* TanStack DB, and TanStack reaches React through `use-sync-external-store`,
|
|
60
|
+
* so a second module record subscribes against a store nothing armed and the
|
|
61
|
+
* hook reads `useSyncExternalStore` off null. Measured on a fresh
|
|
62
|
+
* `mpo init --universal --frappe --tauri` at cli 7.7.1: the first page
|
|
63
|
+
* mounting a data-bound panel threw `Cannot read properties of null (reading
|
|
64
|
+
* 'useSyncExternalStore')` on every load, and so did every data-bound page
|
|
65
|
+
* after it.
|
|
66
|
+
*
|
|
67
|
+
* MPO-288. THE CRITERION, so the next addition is a decision against a rule
|
|
68
|
+
* rather than a guess. A package belongs here when a second module record
|
|
69
|
+
* would break it: it holds state at module scope, it subscribes an external
|
|
70
|
+
* store, or it provides a React context consumers compare by identity.
|
|
71
|
+
* Grepping the installed package for `useSyncExternalStore` is a cheap test
|
|
72
|
+
* for the second clause, and `singletonDepAliases.spec.ts` re-runs it against
|
|
73
|
+
* the tree so a tanstack upgrade is checked rather than assumed. It is one
|
|
74
|
+
* clause and not the whole rule: `@tanstack/db` references the hook nowhere
|
|
75
|
+
* and is pinned anyway, for the collection registry it holds at module scope.
|
|
76
|
+
*
|
|
77
|
+
* `@tanstack/react-query` references the hook in 25 shipped files and is not
|
|
78
|
+
* listed. That is recorded rather than settled — no split has been observed
|
|
79
|
+
* there, and pinning it reaches further than this measurement does.
|
|
56
80
|
*/
|
|
57
|
-
const CONTEXT_SINGLETONS: readonly string[] = [
|
|
81
|
+
const CONTEXT_SINGLETONS: readonly string[] = [
|
|
82
|
+
"react-cookie",
|
|
83
|
+
"@tamagui/core",
|
|
84
|
+
"@tamagui/web",
|
|
85
|
+
"@tanstack/db",
|
|
86
|
+
// The half that actually reaches React. Measured in the installed tree:
|
|
87
|
+
// @tanstack/db contains ZERO references to useSyncExternalStore and
|
|
88
|
+
// @tanstack/react-db contains three, useLiveQuery.js among them, which is
|
|
89
|
+
// the call in the reported crash. public/frappe depends on both and imports
|
|
90
|
+
// react-db from src/useLiveQuery.ts, so pinning only @tanstack/db would
|
|
91
|
+
// leave "Cannot read properties of null (reading 'useSyncExternalStore')"
|
|
92
|
+
// exactly where it was. MPO-225 named @tanstack/db; the measurement says
|
|
93
|
+
// the pair has to travel together.
|
|
94
|
+
"@tanstack/react-db",
|
|
95
|
+
// Measured at 0.11.0: @tanstack/store references useSyncExternalStore in
|
|
96
|
+
// nothing it ships and @tanstack/react-store in three, all useSelector,
|
|
97
|
+
// which is the call behind useStore. public/store and public/forms both
|
|
98
|
+
// reach React through it, so a second record would subscribe a store
|
|
99
|
+
// nothing armed — the null read MPO-225 reported, one package family over.
|
|
100
|
+
"@tanstack/react-store",
|
|
101
|
+
];
|
|
58
102
|
|
|
59
103
|
/**
|
|
60
104
|
* Vite `resolve.alias` entries pinning each context singleton to one directory.
|
|
@@ -78,6 +122,38 @@ export function singletonDepAliases(root: string = process.cwd()): Record<string
|
|
|
78
122
|
return aliases;
|
|
79
123
|
}
|
|
80
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Merges `base` with the singleton pins and orders the result for Vite.
|
|
127
|
+
*
|
|
128
|
+
* Both entry points build `resolve.alias` through here so the list cannot go
|
|
129
|
+
* out of reach again: before MPO-225 only `createStorybookViteConfig` applied
|
|
130
|
+
* `singletonDepAliases`, and a project generated by `mpo init` builds through
|
|
131
|
+
* `createViteConfig` alone, so every package on the list resolved to two
|
|
132
|
+
* records there.
|
|
133
|
+
*
|
|
134
|
+
* MPO-215: singletons are merged last so a discovered workspace or dependency
|
|
135
|
+
* alias can never shadow one. Keys are sorted longest-first because Vite tries
|
|
136
|
+
* alias entries in insertion order and matches a key or its `key + "/"` prefix.
|
|
137
|
+
* `overrides` are applied after the pins: a caller naming a package explicitly
|
|
138
|
+
* means it.
|
|
139
|
+
*
|
|
140
|
+
* @param base Aliases the pins may shadow.
|
|
141
|
+
* @param root Directory whose `node_modules` the packages resolve from.
|
|
142
|
+
* @param overrides Aliases that win over everything, including the pins.
|
|
143
|
+
*/
|
|
144
|
+
export function withSingletonDepAliases(
|
|
145
|
+
base: Record<string, string>,
|
|
146
|
+
root: string = process.cwd(),
|
|
147
|
+
overrides: Record<string, string> = {},
|
|
148
|
+
): Record<string, string> {
|
|
149
|
+
const merged = { ...base, ...singletonDepAliases(root), ...overrides };
|
|
150
|
+
const sorted: Record<string, string> = {};
|
|
151
|
+
for (const key of Object.keys(merged).sort((a, b) => b.length - a.length)) {
|
|
152
|
+
sorted[key] = merged[key];
|
|
153
|
+
}
|
|
154
|
+
return sorted;
|
|
155
|
+
}
|
|
156
|
+
|
|
81
157
|
function resolvePackageDir(
|
|
82
158
|
pkgName: string,
|
|
83
159
|
root: string,
|
package/src/storybook.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { Plugin, UserConfig } from "vite";
|
|
4
|
-
import {
|
|
4
|
+
import { withSingletonDepAliases } from "./singletonDepAliases.js";
|
|
5
5
|
import { unexportedDepAliases } from "./unexportedDepAliases.js";
|
|
6
6
|
import { discoverPublicPackageRoots, resolvePackageMainSource } from "./workspacePublicPackages.js";
|
|
7
7
|
import { ensureTamaguiWorkspacePaths } from "./tamaguiWorkspacePaths.js";
|
|
@@ -56,24 +56,15 @@ export function createStorybookViteConfig(
|
|
|
56
56
|
const workspaceRoot = options.workspaceRoot || findWorkspaceRoot();
|
|
57
57
|
const packagesDir = path.join(workspaceRoot, "packages");
|
|
58
58
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (options.aliases) {
|
|
69
|
-
Object.assign(aliases, options.aliases);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Sort by key length descending so more-specific aliases match first
|
|
73
|
-
const sortedAliases: Record<string, string> = {};
|
|
74
|
-
for (const key of Object.keys(aliases).sort((a, b) => b.length - a.length)) {
|
|
75
|
-
sortedAliases[key] = aliases[key];
|
|
76
|
-
}
|
|
59
|
+
const sortedAliases = withSingletonDepAliases(
|
|
60
|
+
{
|
|
61
|
+
...discoverPackageAliases(packagesDir),
|
|
62
|
+
...discoverPublicPackageViteAliases(workspaceRoot),
|
|
63
|
+
...unexportedDepAliases(workspaceRoot),
|
|
64
|
+
},
|
|
65
|
+
workspaceRoot,
|
|
66
|
+
options.aliases,
|
|
67
|
+
);
|
|
77
68
|
|
|
78
69
|
return {
|
|
79
70
|
define: {
|
package/src/vite.ts
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { lookupTamaguiModules, resolveConfig } from "@multiplatform.one/utils/dev";
|
|
4
4
|
import { discoverPublicPackageRoots } from "./workspacePublicPackages.js";
|
|
5
|
+
import { withSingletonDepAliases } from "./singletonDepAliases.js";
|
|
5
6
|
import { ensureTamaguiWorkspacePaths } from "./tamaguiWorkspacePaths.js";
|
|
6
7
|
import { tamaguiPlugin } from "@tamagui/vite-plugin";
|
|
7
8
|
import dotenv from "dotenv";
|
|
@@ -461,24 +462,30 @@ export function createViteConfig(options: CreateViteConfigOptions = {}): UserCon
|
|
|
461
462
|
},
|
|
462
463
|
ssr: ssr || defaultSsr,
|
|
463
464
|
resolve: {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
465
|
+
// MPO-225: the context-singleton pins are merged in HERE too, not only
|
|
466
|
+
// in storybook.ts. This is the whole build config a generated app has,
|
|
467
|
+
// so a package that resolves to two module records dies here first.
|
|
468
|
+
alias: withSingletonDepAliases(
|
|
469
|
+
{
|
|
470
|
+
// Fix tamagui-build outputting `from "react.mjs"` instead of `from "react"`
|
|
471
|
+
"react.mjs": "react",
|
|
467
472
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
473
|
+
// React 19.1+ ships its own compiler runtime. The standalone
|
|
474
|
+
// `react-compiler-runtime` beta package (pulled in by `one`) gets
|
|
475
|
+
// pre-bundled by Vite with its own React copy, causing a duplicate-
|
|
476
|
+
// React dispatcher-is-null crash (`useMemoCache`). Aliasing to
|
|
477
|
+
// the built-in entry point ensures a single React instance.
|
|
478
|
+
"react-compiler-runtime": "react/compiler-runtime",
|
|
474
479
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
480
|
+
// use-sync-external-store is marked as SSR-external by the
|
|
481
|
+
// ssrExternalFix plugin in vite.config.ts, so Node's CJS loader
|
|
482
|
+
// handles it during SSR (avoiding the "module is not defined" error).
|
|
483
|
+
// Do NOT alias it here — Vite's prefix-match aliases catch subpaths
|
|
484
|
+
// like /shim/with-selector.js and /with-selector, rewriting them to
|
|
485
|
+
// react/with-selector which doesn't exist, crashing dep optimisation.
|
|
486
|
+
},
|
|
487
|
+
projectRoot,
|
|
488
|
+
),
|
|
482
489
|
// @react-navigation/core ships a nested node_modules/react-is (CJS-only).
|
|
483
490
|
// Deduplication forces the root copy so clientBrokenEsmPlugin's pre-bundle
|
|
484
491
|
// covers it everywhere.
|
package/src/vitest.spec.ts
CHANGED
|
@@ -128,7 +128,7 @@ describe("dedupeReact aliases", () => {
|
|
|
128
128
|
expect(dedupe).not.toContain("react-dom");
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
-
it("inlines
|
|
131
|
+
it("inlines React consumers that Node would otherwise load from the hoisted root", () => {
|
|
132
132
|
const config = createVitestConfig({ tamaguiConfig: false }) as {
|
|
133
133
|
test?: { server?: { deps?: { inline?: unknown[]; external?: unknown[] } } };
|
|
134
134
|
};
|
|
@@ -137,6 +137,7 @@ describe("dedupeReact aliases", () => {
|
|
|
137
137
|
inline.some((entry) => entry instanceof RegExp && String(entry).includes("@tanstack")),
|
|
138
138
|
).toBe(true);
|
|
139
139
|
expect(inline).toContain("use-sync-external-store");
|
|
140
|
+
expect(inline).toContain("react-i18next");
|
|
140
141
|
});
|
|
141
142
|
|
|
142
143
|
it("installs a worker setup that pins CJS require('react') to the same copy", () => {
|
package/src/vitest.ts
CHANGED
|
@@ -306,6 +306,9 @@ export function createVitestConfig(options: CreateVitestConfigOptions = {}): Use
|
|
|
306
306
|
"tamagui",
|
|
307
307
|
/@tanstack\//,
|
|
308
308
|
"use-sync-external-store",
|
|
309
|
+
// Native ESM bypasses both the Vite React alias and the CJS require
|
|
310
|
+
// pin. Transform react-i18next so useTranslation shares the renderer.
|
|
311
|
+
"react-i18next",
|
|
309
312
|
...inlineDeps,
|
|
310
313
|
],
|
|
311
314
|
...(externalDeps.length > 0
|
|
@@ -9,4 +9,24 @@
|
|
|
9
9
|
* @param root Directory whose `node_modules` the packages resolve from.
|
|
10
10
|
*/
|
|
11
11
|
export declare function singletonDepAliases(root?: string): Record<string, string>;
|
|
12
|
+
/**
|
|
13
|
+
* Merges `base` with the singleton pins and orders the result for Vite.
|
|
14
|
+
*
|
|
15
|
+
* Both entry points build `resolve.alias` through here so the list cannot go
|
|
16
|
+
* out of reach again: before MPO-225 only `createStorybookViteConfig` applied
|
|
17
|
+
* `singletonDepAliases`, and a project generated by `mpo init` builds through
|
|
18
|
+
* `createViteConfig` alone, so every package on the list resolved to two
|
|
19
|
+
* records there.
|
|
20
|
+
*
|
|
21
|
+
* MPO-215: singletons are merged last so a discovered workspace or dependency
|
|
22
|
+
* alias can never shadow one. Keys are sorted longest-first because Vite tries
|
|
23
|
+
* alias entries in insertion order and matches a key or its `key + "/"` prefix.
|
|
24
|
+
* `overrides` are applied after the pins: a caller naming a package explicitly
|
|
25
|
+
* means it.
|
|
26
|
+
*
|
|
27
|
+
* @param base Aliases the pins may shadow.
|
|
28
|
+
* @param root Directory whose `node_modules` the packages resolve from.
|
|
29
|
+
* @param overrides Aliases that win over everything, including the pins.
|
|
30
|
+
*/
|
|
31
|
+
export declare function withSingletonDepAliases(base: Record<string, string>, root?: string, overrides?: Record<string, string>): Record<string, string>;
|
|
12
32
|
//# sourceMappingURL=singletonDepAliases.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"singletonDepAliases.d.ts","sourceRoot":"","sources":["../src/singletonDepAliases.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"singletonDepAliases.d.ts","sourceRoot":"","sources":["../src/singletonDepAliases.ts"],"names":[],"mappings":"AAsGA;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,MAAsB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUxF;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,IAAI,GAAE,MAAsB,EAC5B,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GACrC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOxB"}
|
package/types/storybook.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storybook.d.ts","sourceRoot":"","sources":["../src/storybook.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAU,UAAU,EAAE,MAAM,MAAM,CAAC;AAM/C,MAAM,WAAW,gCAAgC;IAC/C;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC;;OAEG;IACH,OAAO,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC;IAEhC;;OAEG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,gCAAqC,GAC7C,UAAU,
|
|
1
|
+
{"version":3,"file":"storybook.d.ts","sourceRoot":"","sources":["../src/storybook.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAU,UAAU,EAAE,MAAM,MAAM,CAAC;AAM/C,MAAM,WAAW,gCAAgC;IAC/C;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC;;OAEG;IACH,OAAO,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC;IAEhC;;OAEG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,gCAAqC,GAC7C,UAAU,CA8KZ"}
|
package/types/vite.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAU,UAAU,EAAE,MAAM,MAAM,CAAC;AAI/C,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE5B;;OAEG;IACH,OAAO,CAAC,EACJ,OAAO,GACP;QACE;;;WAGG;QACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QAEtB;;;WAGG;QACH,MAAM,CAAC,EAAE,MAAM,CAAC;QAEhB;;WAEG;QACH,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IAEN;;OAEG;IACH,IAAI,CAAC,EACD,OAAO,GACP;QACE;;;WAGG;QACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QAEjB;;WAEG;QACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;KAC9B,CAAC;IAEN;;;OAGG;IACH,GAAG,CAAC,EACA,OAAO,GACP;QACE,MAAM,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC3B,GAAG,CAAC,EAAE;YAAE,MAAM,CAAC,EAAE,MAAM,CAAC;YAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACtD,KAAK,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC;QAC/B,MAAM,CAAC,EAAE;YAAE,GAAG,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC1B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAChC,CAAC;IAEN;;OAEG;IACH,GAAG,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;IAExB;;OAEG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;IAE9B;;OAEG;IACH,OAAO,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC;IAEhC;;OAEG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;OAEG;IACH,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;IAEF;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAElC;;;OAGG;IACH,WAAW,CAAC,EAAE;QACZ;;;;;;WAMG;QACH,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB;;;;;;;WAOG;QACH,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,uBAA4B,GAAG,UAAU,CAiUlF;AAED,OAAO,EACL,0BAA0B,EAC1B,8BAA8B,GAC/B,MAAM,8BAA8B,CAAC"}
|
package/types/vitest.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitest.d.ts","sourceRoot":"","sources":["../src/vitest.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAqDvC,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,
|
|
1
|
+
{"version":3,"file":"vitest.d.ts","sourceRoot":"","sources":["../src/vitest.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAqDvC,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,CAoNtF"}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
//#region src/workspacePublicPackages.ts
|
|
5
|
-
/**
|
|
6
|
-
* Every direct child of `public/` with a `package.json` `name` is treated as a
|
|
7
|
-
* publishable workspace package (same rule as Metro in `apps/storybook-expo`).
|
|
8
|
-
*
|
|
9
|
-
* Tamagui’s internal esbuild (bundling `tamagui.config` + `components`) reads
|
|
10
|
-
* `compilerOptions.paths` from the repo `tsconfig.base.json` chain, including
|
|
11
|
-
* `tamagui-workspace-paths.generated.json` (see `scripts/generate-tamagui-workspace-paths.mjs`).
|
|
12
|
-
*/
|
|
13
|
-
function discoverPublicPackageRoots(workspaceRoot) {
|
|
14
|
-
const byName = /* @__PURE__ */ new Map();
|
|
15
|
-
const publicDir = path.join(workspaceRoot, "public");
|
|
16
|
-
if (!fs.existsSync(publicDir)) return byName;
|
|
17
|
-
for (const ent of fs.readdirSync(publicDir, { withFileTypes: true })) {
|
|
18
|
-
if (!ent.isDirectory()) continue;
|
|
19
|
-
const pkgRoot = path.join(publicDir, ent.name);
|
|
20
|
-
const pkgJsonPath = path.join(pkgRoot, "package.json");
|
|
21
|
-
if (!fs.existsSync(pkgJsonPath)) continue;
|
|
22
|
-
try {
|
|
23
|
-
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
|
24
|
-
if (typeof pkg.name === "string" && pkg.name.length > 0) byName.set(pkg.name, pkgRoot);
|
|
25
|
-
} catch {}
|
|
26
|
-
}
|
|
27
|
-
return byName;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Main entry file for Vite/Storybook-style “compile from source” aliases.
|
|
31
|
-
*
|
|
32
|
-
* These aliases are only consumed by WEB Vite configs (Storybook, SPA), so
|
|
33
|
-
* `.web.*` entries take priority over the plain (native) ones — otherwise a
|
|
34
|
-
* package like @package/fonts resolves to its native index.ts and its
|
|
35
|
-
* web-only side effects (fontsource @font-face CSS imports) never load.
|
|
36
|
-
*/
|
|
37
|
-
function resolvePackageMainSource(pkgDir) {
|
|
38
|
-
return [
|
|
39
|
-
path.join(pkgDir, "src", "index.storybook.ts"),
|
|
40
|
-
path.join(pkgDir, "src", "index.storybook.tsx"),
|
|
41
|
-
path.join(pkgDir, "src", "index.web.ts"),
|
|
42
|
-
path.join(pkgDir, "src", "index.web.tsx"),
|
|
43
|
-
path.join(pkgDir, "src", "index.ts"),
|
|
44
|
-
path.join(pkgDir, "src", "index.tsx"),
|
|
45
|
-
path.join(pkgDir, "index.storybook.ts"),
|
|
46
|
-
path.join(pkgDir, "index.storybook.tsx"),
|
|
47
|
-
path.join(pkgDir, "index.web.ts"),
|
|
48
|
-
path.join(pkgDir, "index.web.tsx"),
|
|
49
|
-
path.join(pkgDir, "index.ts"),
|
|
50
|
-
path.join(pkgDir, "index.tsx")
|
|
51
|
-
].find((c) => fs.existsSync(c));
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Vite `resolve.alias` entries: each public package name → its TypeScript main.
|
|
55
|
-
* For SPA / ad-hoc Vite configs that should not list packages by hand.
|
|
56
|
-
*/
|
|
57
|
-
function publicPackageViteSourceAliases(workspaceRoot) {
|
|
58
|
-
const aliases = {};
|
|
59
|
-
for (const [name, dir] of discoverPublicPackageRoots(workspaceRoot)) {
|
|
60
|
-
const main = resolvePackageMainSource(dir);
|
|
61
|
-
if (main) aliases[name] = main;
|
|
62
|
-
}
|
|
63
|
-
return aliases;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
//#endregion
|
|
67
|
-
export { publicPackageViteSourceAliases as n, resolvePackageMainSource as r, discoverPublicPackageRoots as t };
|