@multiplatform.one/config 6.0.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/LICENSE +201 -0
- package/README.md +57 -0
- package/lib/index.js +5 -0
- package/lib/playwright.js +107 -0
- package/lib/storybook.js +204 -0
- package/lib/vite-BfDEN9iw.js +369 -0
- package/lib/vite.js +4 -0
- package/lib/vitest.js +150 -0
- package/lib/workspacePublicPackages-BPzu1MKc.js +56 -0
- package/package.json +110 -0
- package/src/index.ts +3 -0
- package/src/playwright.ts +239 -0
- package/src/reactNative.config.cjs +5 -0
- package/src/storybook.ts +318 -0
- package/src/vite.ts +623 -0
- package/src/vitest.ts +255 -0
- package/src/workspacePublicPackages.spec.ts +72 -0
- package/src/workspacePublicPackages.ts +59 -0
- package/tsconfig/app.json +22 -0
- package/tsconfig/base.json +39 -0
- package/tsconfig/build.json +24 -0
- package/tsconfig/tamagui_fix.d.ts +29 -0
package/src/vite.ts
ADDED
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { lookupTamaguiModules, resolveConfig } from "@multiplatform.one/utils/dev";
|
|
4
|
+
import { discoverPublicPackageRoots } from "./workspacePublicPackages.js";
|
|
5
|
+
import { tamaguiPlugin } from "@tamagui/vite-plugin";
|
|
6
|
+
import dotenv from "dotenv";
|
|
7
|
+
import type { Plugin, UserConfig } from "vite";
|
|
8
|
+
import createExternal from "vite-plugin-external";
|
|
9
|
+
import i18nextLoader from "vite-plugin-i18next-loader";
|
|
10
|
+
|
|
11
|
+
export interface CreateViteConfigOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Absolute path to the project root (directory containing .env files).
|
|
14
|
+
* Defaults to looking up the git root.
|
|
15
|
+
*/
|
|
16
|
+
projectRoot?: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Public config keys to expose via VITE_MP_CONFIG.
|
|
20
|
+
* These are typically the `public` keys from features/config.json.
|
|
21
|
+
*/
|
|
22
|
+
publicConfigKeys?: string[];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Enable Tamagui plugin. Defaults to true.
|
|
26
|
+
*/
|
|
27
|
+
tamagui?:
|
|
28
|
+
| boolean
|
|
29
|
+
| {
|
|
30
|
+
/**
|
|
31
|
+
* Tamagui components to include.
|
|
32
|
+
* Defaults to looking up tamaguiModules from package.json files.
|
|
33
|
+
*/
|
|
34
|
+
components?: string[];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Path to the Tamagui config file.
|
|
38
|
+
* Defaults to "./config/tamagui.config.ts".
|
|
39
|
+
*/
|
|
40
|
+
config?: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Path to output CSS. Defaults to "./tamagui.css".
|
|
44
|
+
*/
|
|
45
|
+
outputCSS?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Enable i18next loader plugin. Defaults to false.
|
|
50
|
+
*/
|
|
51
|
+
i18n?:
|
|
52
|
+
| boolean
|
|
53
|
+
| {
|
|
54
|
+
/**
|
|
55
|
+
* Paths to scan for translation files.
|
|
56
|
+
* Defaults to ["../../features/i18n"].
|
|
57
|
+
*/
|
|
58
|
+
paths?: string[];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* How to resolve namespaces. Defaults to "basename".
|
|
62
|
+
*/
|
|
63
|
+
namespaceResolution?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Enable One framework plugin. Defaults to false.
|
|
68
|
+
* When true, uses sensible defaults for router, web, react, and native.
|
|
69
|
+
*/
|
|
70
|
+
one?:
|
|
71
|
+
| boolean
|
|
72
|
+
| {
|
|
73
|
+
router?: { root?: string };
|
|
74
|
+
web?: { deploy?: string; defaultRenderMode?: string };
|
|
75
|
+
react?: { compiler?: boolean };
|
|
76
|
+
native?: { key?: string };
|
|
77
|
+
deps?: Record<string, boolean>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* SSR configuration. Defaults to reasonable values when One is enabled.
|
|
82
|
+
*/
|
|
83
|
+
ssr?: UserConfig["ssr"];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Server configuration. Merged with defaults.
|
|
87
|
+
*/
|
|
88
|
+
server?: UserConfig["server"];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Additional Vite plugins to include.
|
|
92
|
+
*/
|
|
93
|
+
plugins?: UserConfig["plugins"];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Externalize react-router-dom. Defaults to true when One is enabled.
|
|
97
|
+
*/
|
|
98
|
+
externalReactRouter?: boolean;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Additional dependency optimizations for SSR.
|
|
102
|
+
*/
|
|
103
|
+
ssrDeps?: {
|
|
104
|
+
include?: string[];
|
|
105
|
+
exclude?: string[];
|
|
106
|
+
external?: string[];
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Entry files for Vite's native SSR dep scanner.
|
|
111
|
+
* Vite follows actual import statements from these files to determine what
|
|
112
|
+
* needs pre-bundling — much more accurate than walking package.json dep trees.
|
|
113
|
+
* Defaults to ["routes/**\/*.{ts,tsx}"] for One framework apps.
|
|
114
|
+
* Only used when `one` is enabled.
|
|
115
|
+
*/
|
|
116
|
+
ssrEntries?: string[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Creates a shared Vite configuration for multiplatform.one apps.
|
|
121
|
+
*
|
|
122
|
+
* This abstracts away the boilerplate that's typically duplicated across
|
|
123
|
+
* app vite.config.ts files, including Tamagui plugin setup, i18n,
|
|
124
|
+
* env variable loading, SSR configuration, and One framework integration.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* // apps/one/vite.config.ts
|
|
129
|
+
* import { createViteConfig } from "@multiplatform.one/config/vite";
|
|
130
|
+
* import { public as publicConfigKeys } from "../../features/config.json";
|
|
131
|
+
*
|
|
132
|
+
* export default createViteConfig({
|
|
133
|
+
* publicConfigKeys,
|
|
134
|
+
* one: { web: { deploy: "node", defaultRenderMode: "ssr" } },
|
|
135
|
+
* tamagui: { config: "./config/tamagui.config.ts" },
|
|
136
|
+
* i18n: { paths: ["../../features/i18n"] },
|
|
137
|
+
* });
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export function createViteConfig(options: CreateViteConfigOptions = {}): UserConfig {
|
|
141
|
+
const {
|
|
142
|
+
projectRoot: _projectRoot,
|
|
143
|
+
publicConfigKeys = [],
|
|
144
|
+
tamagui = true,
|
|
145
|
+
i18n = false,
|
|
146
|
+
one = false,
|
|
147
|
+
ssr,
|
|
148
|
+
server,
|
|
149
|
+
plugins: extraPlugins = [],
|
|
150
|
+
externalReactRouter,
|
|
151
|
+
ssrDeps,
|
|
152
|
+
ssrEntries,
|
|
153
|
+
} = options;
|
|
154
|
+
|
|
155
|
+
const projectRoot = _projectRoot || findProjectRoot();
|
|
156
|
+
|
|
157
|
+
// Load env from project root
|
|
158
|
+
dotenv.config({ path: path.resolve(projectRoot, ".env") });
|
|
159
|
+
if (publicConfigKeys.length > 0) {
|
|
160
|
+
process.env.VITE_MP_CONFIG = JSON.stringify(resolveConfig(publicConfigKeys));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const vitePlugins: UserConfig["plugins"] = [];
|
|
164
|
+
vitePlugins.push(preferBuiltWorkspaceEntryPlugin());
|
|
165
|
+
|
|
166
|
+
if (one) {
|
|
167
|
+
// One plugin must be added by the consumer in their extraPlugins.
|
|
168
|
+
// Alias react-native to react-native-web in SSR so Rolldown never parses
|
|
169
|
+
// react-native/index.js (which contains Flow-only syntax).
|
|
170
|
+
vitePlugins.push(ssrReactNativeAliasPlugin());
|
|
171
|
+
vitePlugins.push(clientBrokenEsmPlugin());
|
|
172
|
+
// Register a native-only resolveId plugin through vxrn's __vxrnAddNativePlugins
|
|
173
|
+
// hook (consumed in vxrn's getNativePlugins). vxrn's native rolldown resolver
|
|
174
|
+
// does NOT honor engine.io-client's `browser` field, so its Node `.node`
|
|
175
|
+
// transports (ws + xmlhttprequest-ssl) leak into Hermes and won't even compile.
|
|
176
|
+
// This re-asserts the package's own browser redirect (-> browser transports that
|
|
177
|
+
// use RN's global WebSocket/XMLHttpRequest) with NO node_modules patch.
|
|
178
|
+
registerNativeEngineIoBrowserTransports();
|
|
179
|
+
// preferBuiltWorkspaceEntryPlugin (web-only) hard-resolves every workspace
|
|
180
|
+
// package to its `dist/esm/index.mjs` (WEB build), and that resolution reaches
|
|
181
|
+
// the native graph too — pinning the whole @multiplatform.one/* chain to web
|
|
182
|
+
// (dragging @tiptap/prosemirror/html5-qrcode/etc into Hermes). This native-only
|
|
183
|
+
// resolveId redirects each workspace web entry to its `dist/esm/index.native.js`
|
|
184
|
+
// sibling so native gets the react-native build (the wrappers' native variants).
|
|
185
|
+
registerNativeWorkspaceEntries();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Tamagui plugin
|
|
189
|
+
if (tamagui) {
|
|
190
|
+
const tamaguiOpts = typeof tamagui === "object" ? tamagui : {};
|
|
191
|
+
vitePlugins.push(
|
|
192
|
+
tamaguiPlugin({
|
|
193
|
+
components: tamaguiOpts.components || lookupTamaguiModules([process.cwd()]),
|
|
194
|
+
config: tamaguiOpts.config || "./config/tamagui.config.ts",
|
|
195
|
+
outputCSS: tamaguiOpts.outputCSS || "./public/tamagui.css",
|
|
196
|
+
// Prevents the config file watcher from re-evaluating the Tamagui
|
|
197
|
+
// config in the main process when the Piscina worker writes changes
|
|
198
|
+
// to .tamagui/. Without this, the watcher triggers bundleConfig →
|
|
199
|
+
// import() → createTamagui() on a raw @tamagui/web instance that
|
|
200
|
+
// conflicts with the SSR module runner's pre-bundled instance.
|
|
201
|
+
// See also: patches/@tamagui__static (forces CJS config bundles in
|
|
202
|
+
// the worker to avoid ESM/CJS dual-instance globalThis conflicts).
|
|
203
|
+
disableWatchTamaguiConfig: true,
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// i18n plugin
|
|
209
|
+
if (i18n) {
|
|
210
|
+
const i18nOpts = typeof i18n === "object" ? i18n : {};
|
|
211
|
+
vitePlugins.push(
|
|
212
|
+
i18nextLoader({
|
|
213
|
+
paths: i18nOpts.paths || ["../../features/i18n"],
|
|
214
|
+
namespaceResolution: (i18nOpts.namespaceResolution as any) || "basename",
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Externalize react-router-dom (needed for One framework)
|
|
220
|
+
if (externalReactRouter !== false && one) {
|
|
221
|
+
vitePlugins.push(
|
|
222
|
+
createExternal({
|
|
223
|
+
externals: {
|
|
224
|
+
"react-router-dom": {} as any,
|
|
225
|
+
},
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Merge extra plugins
|
|
231
|
+
vitePlugins.push(...(extraPlugins as any[]));
|
|
232
|
+
|
|
233
|
+
// SSR defaults for One framework
|
|
234
|
+
const defaultSsr: UserConfig["ssr"] = one
|
|
235
|
+
? ({
|
|
236
|
+
noExternal: true,
|
|
237
|
+
optimizeDeps: {
|
|
238
|
+
entries: ssrEntries ?? ["routes/**/*.{ts,tsx}"],
|
|
239
|
+
include: [
|
|
240
|
+
// @tamagui/core holds a global config singleton. It must be
|
|
241
|
+
// pre-bundled as a single chunk so all SSR code shares one instance.
|
|
242
|
+
// If loaded via two different paths (optimizer + module runner),
|
|
243
|
+
// TamaguiProvider sets config on one instance but components read
|
|
244
|
+
// from the other, triggering the "global config fallback" warning.
|
|
245
|
+
"@tamagui/core",
|
|
246
|
+
"@tamagui/web",
|
|
247
|
+
"tamagui",
|
|
248
|
+
"@tamagui/toast",
|
|
249
|
+
"@tamagui/linear-gradient",
|
|
250
|
+
// react-i18next and i18next must be pre-bundled for SSR so they
|
|
251
|
+
// share the same React instance as react-dom/server. Without
|
|
252
|
+
// explicit include, the SSR module runner loads react-i18next
|
|
253
|
+
// directly from node_modules, which imports a raw
|
|
254
|
+
// node_modules/react/cjs/react.development.js. That raw React
|
|
255
|
+
// has its dispatcher unset (null), causing "Cannot read properties
|
|
256
|
+
// of null (reading 'useSyncExternalStore')" during useTranslation.
|
|
257
|
+
"react-i18next",
|
|
258
|
+
"i18next",
|
|
259
|
+
// @tanstack/react-store must be pre-bundled for SSR so it uses the
|
|
260
|
+
// shared React chunk (react-D-en6o7j.js) instead of loading
|
|
261
|
+
// use-sync-external-store/shim/with-selector as a CJS external which
|
|
262
|
+
// internally does require('react') → raw node_modules React →
|
|
263
|
+
// different instance from react-dom/server → dispatcher null.
|
|
264
|
+
"@tanstack/react-store",
|
|
265
|
+
"@tanstack/store",
|
|
266
|
+
// CJS-only transitive dep of react-i18next (via html-parse-stringify).
|
|
267
|
+
// Rolldown doesn't pull it into the react-i18next pre-bundle chunk, so
|
|
268
|
+
// the SSR module runner gets the raw file and fails with
|
|
269
|
+
// "module is not defined".
|
|
270
|
+
"void-elements",
|
|
271
|
+
// use-sync-external-store must share the same React instance as the
|
|
272
|
+
// SSR renderer. Without bundling it in, the CJS shim does
|
|
273
|
+
// require('react') at runtime → different node_modules React →
|
|
274
|
+
// dispatcher null → "Cannot read properties of null (reading
|
|
275
|
+
// 'useSyncExternalStore')" crash during useTranslation in SSR.
|
|
276
|
+
"use-sync-external-store",
|
|
277
|
+
"use-sync-external-store/shim",
|
|
278
|
+
...(ssrDeps?.include || []),
|
|
279
|
+
],
|
|
280
|
+
exclude: [
|
|
281
|
+
"@react-native/assets-registry",
|
|
282
|
+
"@react-native-community/datetimepicker",
|
|
283
|
+
"socket.io-client",
|
|
284
|
+
"engine.io-client",
|
|
285
|
+
"xmlhttprequest-ssl",
|
|
286
|
+
// Devtools — never needed in SSR, and their nested deps have
|
|
287
|
+
// version mismatches (form-core@1.27.7 needs Derived from store).
|
|
288
|
+
"@tanstack/form-devtools",
|
|
289
|
+
"@tanstack/react-form-devtools",
|
|
290
|
+
...(ssrDeps?.exclude || []),
|
|
291
|
+
],
|
|
292
|
+
rolldownOptions: {
|
|
293
|
+
// Belt-and-suspenders: force Rolldown to treat these packages as
|
|
294
|
+
// external at the bundler level, not just via the exclude plugin.
|
|
295
|
+
// Without this, Rolldown can still inline them as transitive deps
|
|
296
|
+
// of other pre-bundled packages, causing MISSING_EXPORT errors.
|
|
297
|
+
external: [
|
|
298
|
+
"@tanstack/form-devtools",
|
|
299
|
+
"@tanstack/react-form-devtools",
|
|
300
|
+
...(ssrDeps?.external || []),
|
|
301
|
+
],
|
|
302
|
+
// Shim any remaining missing exports instead of hard-failing.
|
|
303
|
+
shimMissingExports: true,
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
external: [
|
|
307
|
+
"loglevel",
|
|
308
|
+
"@react-native/assets-registry",
|
|
309
|
+
"@react-native-community/datetimepicker",
|
|
310
|
+
"socket.io-client",
|
|
311
|
+
"engine.io-client",
|
|
312
|
+
"xmlhttprequest-ssl",
|
|
313
|
+
"@tanstack/form-devtools",
|
|
314
|
+
"@tanstack/react-form-devtools",
|
|
315
|
+
...(ssrDeps?.external || []),
|
|
316
|
+
// better-auth pulls @opentelemetry/* (CJS) which fails in the ESM
|
|
317
|
+
// SSR module runner with "exports is not defined". Externalize so
|
|
318
|
+
// Node's native CJS loader handles them.
|
|
319
|
+
"@opentelemetry/semantic-conventions",
|
|
320
|
+
"@opentelemetry/api",
|
|
321
|
+
"@opentelemetry/sdk-trace-base",
|
|
322
|
+
"@opentelemetry/sdk-trace-node",
|
|
323
|
+
],
|
|
324
|
+
} as any)
|
|
325
|
+
: undefined;
|
|
326
|
+
|
|
327
|
+
// Server defaults
|
|
328
|
+
//
|
|
329
|
+
// When the One app runs behind Frappe's dev proxy (port 8000 → 3000), the
|
|
330
|
+
// browser page origin is :8000 but the vxrn HMR WebSocket lives on :3000.
|
|
331
|
+
// Werkzeug (WSGI) can't proxy WebSocket upgrades, so we tell the HMR client
|
|
332
|
+
// to connect directly to the One dev server port.
|
|
333
|
+
//
|
|
334
|
+
// IMPORTANT: vxrn internally sets `hmr.path = '/__vxrnhmr'`. We must
|
|
335
|
+
// include the path here because config merging replaces the entire `hmr`
|
|
336
|
+
// object (shallow merge at the `server` level). If vxrn ever changes the
|
|
337
|
+
// path, update it here too.
|
|
338
|
+
const onePort = Number(process.env.ONE_PORT) || 3000;
|
|
339
|
+
const defaultServer: UserConfig["server"] = {
|
|
340
|
+
allowedHosts: ["localhost", "127.0.0.1", "app.localhost", "app.test"],
|
|
341
|
+
strictPort: true,
|
|
342
|
+
watch: {
|
|
343
|
+
ignored: ["**/src-tauri/**"],
|
|
344
|
+
},
|
|
345
|
+
...(one
|
|
346
|
+
? {
|
|
347
|
+
hmr: {
|
|
348
|
+
path: "/__vxrnhmr",
|
|
349
|
+
clientPort: onePort,
|
|
350
|
+
},
|
|
351
|
+
}
|
|
352
|
+
: {}),
|
|
353
|
+
...server,
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
css: {
|
|
358
|
+
modules: {
|
|
359
|
+
localsConvention: "camelCase",
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
build: {
|
|
363
|
+
chunkSizeWarningLimit: 600,
|
|
364
|
+
},
|
|
365
|
+
define: {
|
|
366
|
+
// Build-time flag for Frappe translation backend tree-shaking
|
|
367
|
+
"process.env.VITE_FRAPPE_ENABLED": JSON.stringify(
|
|
368
|
+
process.env.VITE_FRAPPE_ENABLED === "true" ? "true" : "false",
|
|
369
|
+
),
|
|
370
|
+
},
|
|
371
|
+
ssr: ssr || defaultSsr,
|
|
372
|
+
resolve: {
|
|
373
|
+
alias: {
|
|
374
|
+
// Fix tamagui-build outputting `from "react.mjs"` instead of `from "react"`
|
|
375
|
+
"react.mjs": "react",
|
|
376
|
+
|
|
377
|
+
// React 19.1+ ships its own compiler runtime. The standalone
|
|
378
|
+
// `react-compiler-runtime` beta package (pulled in by `one`) gets
|
|
379
|
+
// pre-bundled by Vite with its own React copy, causing a duplicate-
|
|
380
|
+
// React dispatcher-is-null crash (`useMemoCache`). Aliasing to
|
|
381
|
+
// the built-in entry point ensures a single React instance.
|
|
382
|
+
"react-compiler-runtime": "react/compiler-runtime",
|
|
383
|
+
|
|
384
|
+
// use-sync-external-store is marked as SSR-external by the
|
|
385
|
+
// ssrExternalFix plugin in vite.config.ts, so Node's CJS loader
|
|
386
|
+
// handles it during SSR (avoiding the "module is not defined" error).
|
|
387
|
+
// Do NOT alias it here — Vite's prefix-match aliases catch subpaths
|
|
388
|
+
// like /shim/with-selector.js and /with-selector, rewriting them to
|
|
389
|
+
// react/with-selector which doesn't exist, crashing dep optimisation.
|
|
390
|
+
},
|
|
391
|
+
// @react-navigation/core ships a nested node_modules/react-is (CJS-only).
|
|
392
|
+
// Deduplication forces the root copy so clientBrokenEsmPlugin's pre-bundle
|
|
393
|
+
// covers it everywhere.
|
|
394
|
+
dedupe: ["react-is"],
|
|
395
|
+
},
|
|
396
|
+
server: defaultServer,
|
|
397
|
+
envPrefix: ["VITE_", "TAURI_ENV_"],
|
|
398
|
+
plugins: vitePlugins,
|
|
399
|
+
} satisfies UserConfig;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export {
|
|
403
|
+
discoverPublicPackageRoots,
|
|
404
|
+
publicPackageViteSourceAliases,
|
|
405
|
+
} from "./workspacePublicPackages.js";
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Aliases react-native to react-native-web in the SSR environment.
|
|
409
|
+
*
|
|
410
|
+
* @react-navigation/native statically imports react-native, which causes
|
|
411
|
+
* Rolldown's dep optimizer to load react-native/index.js — a file that contains
|
|
412
|
+
* Flow-only syntax (`import typeof`, `as Type`) that Rolldown cannot parse.
|
|
413
|
+
* Aliasing to react-native-web redirects those imports to the web-safe shim,
|
|
414
|
+
* so the real react-native package is never touched during SSR bundling.
|
|
415
|
+
*/
|
|
416
|
+
function ssrReactNativeAliasPlugin(): Plugin {
|
|
417
|
+
return {
|
|
418
|
+
name: "multiplatform-ssr-react-native-alias",
|
|
419
|
+
enforce: "post",
|
|
420
|
+
configResolved(config) {
|
|
421
|
+
const ssrEnv = (config as any).environments?.ssr;
|
|
422
|
+
if (!ssrEnv?.resolve) return;
|
|
423
|
+
const alias: Array<{ find: string | RegExp; replacement: string }> =
|
|
424
|
+
ssrEnv.resolve.alias ?? [];
|
|
425
|
+
if (!alias.some((a: any) => a.find === "react-native")) {
|
|
426
|
+
alias.push({ find: "react-native", replacement: "react-native-web" });
|
|
427
|
+
}
|
|
428
|
+
ssrEnv.resolve.alias = alias;
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Adds packages that have broken or missing ESM default exports to the client
|
|
435
|
+
* environments pre-bundle. Rolldown resolves them via their "import" condition
|
|
436
|
+
* and generates a proper ESM chunk with synthetic interop default export.
|
|
437
|
+
*
|
|
438
|
+
* use-latest-callback: type=commonjs, exports.import → esm.mjs, but vxrn's
|
|
439
|
+
* client conditions (["vxrn-web"]) skip "import" and land on "default" →
|
|
440
|
+
* lib/src/index.js (CJS, named-only). Pre-bundling forces the import condition.
|
|
441
|
+
*/
|
|
442
|
+
function clientBrokenEsmPlugin(): Plugin {
|
|
443
|
+
return {
|
|
444
|
+
name: "multiplatform-client-broken-esm",
|
|
445
|
+
configResolved(config) {
|
|
446
|
+
const clientEnv = (config as any).environments?.client;
|
|
447
|
+
if (!clientEnv?.optimizeDeps) return;
|
|
448
|
+
const extras = [
|
|
449
|
+
"use-latest-callback",
|
|
450
|
+
// CJS packages in vxrn's dedupe list that are not auto-added to the
|
|
451
|
+
// client environment optimizer, causing "doesn't provide an export
|
|
452
|
+
// named 'default'" errors when served raw by Vite.
|
|
453
|
+
"escape-string-regexp",
|
|
454
|
+
// Pure CJS with no "import" condition — subpath must be pre-bundled
|
|
455
|
+
// explicitly so named exports (useSyncExternalStoreWithSelector) exist.
|
|
456
|
+
// Root entry is SSR-external (Node handles it); client needs pre-bundle.
|
|
457
|
+
"use-sync-external-store/with-selector",
|
|
458
|
+
// @react-navigation/core ships a nested node_modules/react-is (CJS).
|
|
459
|
+
// resolve.dedupe forces the root copy to be used; pre-bundling it here
|
|
460
|
+
// adds ESM interop so named exports like isValidElementType are available.
|
|
461
|
+
"react-is",
|
|
462
|
+
// CJS-only packages imported by @react-navigation/* TypeScript source.
|
|
463
|
+
// When the client loads @react-navigation/* via the "source" condition
|
|
464
|
+
// (TypeScript), these transitive CJS deps get served raw and lack ESM
|
|
465
|
+
// named exports. Pre-bundling generates the ESM interop wrappers.
|
|
466
|
+
"fast-deep-equal",
|
|
467
|
+
"color",
|
|
468
|
+
// query-string v7 is CJS-only; imported by @react-navigation/core for URL parsing.
|
|
469
|
+
"query-string",
|
|
470
|
+
];
|
|
471
|
+
const include: string[] = clientEnv.optimizeDeps.include ?? [];
|
|
472
|
+
for (const dep of extras) {
|
|
473
|
+
if (!include.includes(dep)) include.push(dep);
|
|
474
|
+
}
|
|
475
|
+
clientEnv.optimizeDeps.include = include;
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Adds engine.io-client's `browser`-field transport redirect to vxrn's NATIVE
|
|
482
|
+
* rolldown pipeline via its `globalThis.__vxrnAddNativePlugins` hook.
|
|
483
|
+
*
|
|
484
|
+
* socket.io-client -> engine.io-client/index.js statically imports its Node
|
|
485
|
+
* transports (`./globals.node.js`, `./transports/{websocket,polling-xhr}.node.js`),
|
|
486
|
+
* which pull `ws` + `xmlhttprequest-ssl` + Node globals. Those `.node` files also
|
|
487
|
+
* contain raw ESM `import` syntax that Hermes can't even COMPILE, so the native
|
|
488
|
+
* bundle dies at boot. engine.io's own `package.json` `browser` field already
|
|
489
|
+
* redirects each `*.node.js` to a browser sibling that uses the global
|
|
490
|
+
* `WebSocket` / `XMLHttpRequest` React Native provides — but vxrn's native
|
|
491
|
+
* resolver omits the `browser` aliasField (and its rolldown binding doesn't even
|
|
492
|
+
* support `aliasFields`). vxrn's native dev engine ignores the Vite config's
|
|
493
|
+
* `resolve.alias`/`resolveId` too — BUT it DOES read `globalThis.__vxrnAddNativePlugins`
|
|
494
|
+
* inside `getNativePlugins`, so a `resolveId` registered there runs on native.
|
|
495
|
+
* This re-asserts the browser redirect with no node_modules patch. Web is
|
|
496
|
+
* unaffected (engine.io is SSR-externalized and the web client already maps `browser`).
|
|
497
|
+
*/
|
|
498
|
+
function registerNativeEngineIoBrowserTransports(): void {
|
|
499
|
+
const g = globalThis as unknown as { __vxrnAddNativePlugins?: unknown[] };
|
|
500
|
+
const name = "vxrn-engineio-browser-transports";
|
|
501
|
+
g.__vxrnAddNativePlugins = g.__vxrnAddNativePlugins ?? [];
|
|
502
|
+
if (g.__vxrnAddNativePlugins.some((p) => (p as { name?: string })?.name === name)) return;
|
|
503
|
+
g.__vxrnAddNativePlugins.push({
|
|
504
|
+
name,
|
|
505
|
+
async resolveId(
|
|
506
|
+
this: { resolve: (id: string, importer: string, opts: { skipSelf: boolean }) => unknown },
|
|
507
|
+
id: string,
|
|
508
|
+
importer?: string,
|
|
509
|
+
) {
|
|
510
|
+
if (
|
|
511
|
+
importer &&
|
|
512
|
+
/engine\.io-client[\\/]build[\\/]/.test(importer) &&
|
|
513
|
+
id.endsWith(".node.js")
|
|
514
|
+
) {
|
|
515
|
+
return this.resolve(`${id.slice(0, -".node.js".length)}.js`, importer, { skipSelf: true });
|
|
516
|
+
}
|
|
517
|
+
return null;
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Routes workspace (`@multiplatform.one/*`) packages to their `dist/esm/index.native.js`
|
|
524
|
+
* build on NATIVE, via vxrn's `globalThis.__vxrnAddNativePlugins` hook.
|
|
525
|
+
*
|
|
526
|
+
* Why this is needed: `preferBuiltWorkspaceEntryPlugin` (a Vite plugin, web/SSR only)
|
|
527
|
+
* hard-resolves every workspace bare specifier to `<root>/dist/esm/index.mjs` — the WEB
|
|
528
|
+
* build — bypassing the package.json `exports` `react-native` condition. That web
|
|
529
|
+
* resolution propagates into vxrn's native graph (the native engine receives no Vite
|
|
530
|
+
* userPlugins, so it cannot be made native-aware there), pinning the entire workspace
|
|
531
|
+
* chain to web on native and dragging web-only deps (@tiptap, prosemirror, html5-qrcode,
|
|
532
|
+
* @mixmark-io/domino, …) into Hermes. Each affected package ships a `dist/esm/index.native.js`
|
|
533
|
+
* (built by tamagui-build, intra-package imports already rewritten to their `.native`
|
|
534
|
+
* siblings). This resolveId matches both forms — the bare specifier and the already-resolved
|
|
535
|
+
* `…/dist/esm/index.mjs` path — and redirects to the native sibling when it exists, so the
|
|
536
|
+
* react-native build wins on native. Web/SSR are unaffected (this plugin runs ONLY on native).
|
|
537
|
+
*/
|
|
538
|
+
function registerNativeWorkspaceEntries(): void {
|
|
539
|
+
const g = globalThis as unknown as { __vxrnAddNativePlugins?: unknown[] };
|
|
540
|
+
const name = "vxrn-workspace-native-entries";
|
|
541
|
+
g.__vxrnAddNativePlugins = g.__vxrnAddNativePlugins ?? [];
|
|
542
|
+
if (g.__vxrnAddNativePlugins.some((p) => (p as { name?: string })?.name === name)) return;
|
|
543
|
+
let roots: Map<string, string>;
|
|
544
|
+
try {
|
|
545
|
+
roots = discoverPublicPackageRoots(findProjectRoot());
|
|
546
|
+
} catch {
|
|
547
|
+
roots = new Map();
|
|
548
|
+
}
|
|
549
|
+
g.__vxrnAddNativePlugins.push({
|
|
550
|
+
name,
|
|
551
|
+
resolveId(id: string): string | null {
|
|
552
|
+
// (a) bare workspace specifier (e.g. "@multiplatform.one/rich-text")
|
|
553
|
+
const root = roots.get(id);
|
|
554
|
+
if (root) {
|
|
555
|
+
const nativeEntry = path.join(root, "dist/esm/index.native.js");
|
|
556
|
+
return fs.existsSync(nativeEntry) ? nativeEntry : null;
|
|
557
|
+
}
|
|
558
|
+
// (b) already-resolved web entry "<...>/dist/esm/index.mjs" -> ".native.js" sibling
|
|
559
|
+
if (id.endsWith("/dist/esm/index.mjs")) {
|
|
560
|
+
const nativeEntry = `${id.slice(0, -".mjs".length)}.native.js`;
|
|
561
|
+
if (fs.existsSync(nativeEntry)) return nativeEntry;
|
|
562
|
+
}
|
|
563
|
+
return null;
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function findProjectRoot(): string {
|
|
569
|
+
try {
|
|
570
|
+
const { execSync } = require("node:child_process");
|
|
571
|
+
return execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
|
|
572
|
+
} catch {
|
|
573
|
+
return process.cwd();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* package.json `exports` cannot branch on whether `dist/` exists. For workspace
|
|
579
|
+
* packages, prefer `dist/esm/index.{mjs,js}` when built; otherwise fall back to
|
|
580
|
+
* `src/index.ts` so local dev works without a build while published tarballs
|
|
581
|
+
* still resolve through `exports` to `dist`.
|
|
582
|
+
*/
|
|
583
|
+
function preferBuiltWorkspaceEntryPlugin(): Plugin {
|
|
584
|
+
const roots = discoverPublicPackageRoots(findProjectRoot());
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
name: "multiplatform-prefer-built-workspace-main",
|
|
588
|
+
enforce: "pre",
|
|
589
|
+
resolveId(id) {
|
|
590
|
+
const root = roots.get(id);
|
|
591
|
+
if (!root) return null;
|
|
592
|
+
const distCandidates = [
|
|
593
|
+
path.join(root, "dist/esm/index.mjs"),
|
|
594
|
+
path.join(root, "dist/esm/index.js"),
|
|
595
|
+
];
|
|
596
|
+
for (const file of distCandidates) {
|
|
597
|
+
if (fs.existsSync(file)) return file;
|
|
598
|
+
}
|
|
599
|
+
const srcTs = path.join(root, "src/index.ts");
|
|
600
|
+
if (fs.existsSync(srcTs)) return srcTs;
|
|
601
|
+
const srcTsx = path.join(root, "src/index.tsx");
|
|
602
|
+
if (fs.existsSync(srcTsx)) return srcTsx;
|
|
603
|
+
|
|
604
|
+
// Vendored packages (e.g. keycloak-js) have no src/ — resolve via
|
|
605
|
+
// package.json exports/main so the dep scanner doesn't fail.
|
|
606
|
+
const pkgJsonPath = path.join(root, "package.json");
|
|
607
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
608
|
+
try {
|
|
609
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
610
|
+
const entry =
|
|
611
|
+
pkg.exports?.["."]?.import || pkg.exports?.["."]?.default || pkg.main || pkg.module;
|
|
612
|
+
if (entry) {
|
|
613
|
+
const resolved = path.resolve(root, entry);
|
|
614
|
+
if (fs.existsSync(resolved)) return resolved;
|
|
615
|
+
}
|
|
616
|
+
} catch {
|
|
617
|
+
/* ignore */
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
}
|