UNPKG

one

Version:

One is a new React Framework that makes Vite serve both native and web.

662 lines (661 loc) 24.2 kB
import { configureVXRNCompilerPlugin } from "@vxrn/compiler"; import { resolvePath } from "@vxrn/resolve"; import { getPlatformEnvDefine } from "@vxrn/vite-plugin-metro"; import events from "node:events"; import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; import { autoDepOptimizePlugin, getOptionsFilled, loadEnv } from "vxrn"; import vxrnVitePlugin from "vxrn/vite-plugin"; import { CACHE_KEY } from "../constants.mjs"; import { getViteMetroPluginOptions } from "../metro-config/getViteMetroPluginOptions.mjs"; import { setServerGlobals } from "../server/setServerGlobals.mjs"; import { getRouterRootFromOneOptions } from "../utils/getRouterRootFromOneOptions.mjs"; import { ensureTSConfig } from "./ensureTsConfig.mjs"; import { setOneOptions } from "./loadConfig.mjs"; import { clientTreeShakePlugin } from "./plugins/clientTreeShakePlugin.mjs"; import { createDevtoolsPlugin } from "./plugins/devtoolsPlugin.mjs"; import { createFileSystemRouterPlugin } from "./plugins/fileSystemRouterPlugin.mjs"; import { fixDependenciesPlugin } from "./plugins/fixDependenciesPlugin.mjs"; import { generateFileSystemRouteTypesPlugin } from "./plugins/generateFileSystemRouteTypesPlugin.mjs"; import { criticalCSSPlugin } from "./plugins/criticalCSSPlugin.mjs"; import { imageDataPlugin } from "./plugins/imageDataPlugin.mjs"; import { sourceInspectorPlugin } from "./plugins/sourceInspectorPlugin.mjs"; import { SSRCSSPlugin } from "./plugins/SSRCSSPlugin.mjs"; import { virtualEntryId } from "./plugins/virtualEntryConstants.mjs"; import { createVirtualEntry } from "./plugins/virtualEntryPlugin.mjs"; import { environmentGuardPlugin } from "./plugins/environmentGuardPlugin.mjs"; events.setMaxListeners(1e3); globalThis.__vxrnEnableNativeEnv = true; function one(options = {}) { setServerGlobals(); const routerRoot = getRouterRootFromOneOptions(options); const nativeDisabled = options.native === false; const nativeOptions = options.native === false ? void 0 : options.native; if (nativeDisabled) { globalThis.__vxrnEnableNativeEnv = false; } const metroOptions = (() => { if (nativeDisabled) return null; if (nativeOptions?.bundler !== "metro" && !process.env.ONE_METRO_MODE) return null; if (process.env.ONE_METRO_MODE) { console.info("ONE_METRO_MODE environment variable is set, enabling Metro mode"); } const routerRoot2 = getRouterRootFromOneOptions(options); const defaultMetroOptions = getViteMetroPluginOptions({ projectRoot: nativeOptions?.bundlerOptions?.argv?.projectRoot || process.cwd(), relativeRouterRoot: routerRoot2, ignoredRouteFiles: options.router?.ignoredRouteFiles, userDefaultConfigOverrides: nativeOptions?.bundlerOptions?.defaultConfigOverrides, setupFile: options.setupFile }); const userMetroOptions = nativeOptions?.bundlerOptions; const babelConfig = { ...defaultMetroOptions?.babelConfig, ...userMetroOptions?.babelConfig }; return { ...defaultMetroOptions, ...userMetroOptions, defaultConfigOverrides: defaultMetroOptions?.defaultConfigOverrides, // defaultConfigOverrides is merged by getViteMetroPluginOptions, so we need to set it here again. argv: { ...defaultMetroOptions?.argv, ...userMetroOptions?.argv }, babelConfig: { ...babelConfig, plugins: [...(babelConfig.plugins || []), ...(options.react?.compiler === true || options.react?.compiler === "native" ? ["babel-plugin-react-compiler"] : [])] }, mainModuleName: "one/metro-entry", // So users won't need to write `"main": "one/metro-entry"` in their `package.json` like ordinary Expo apps. // allow env var to enable lazy startup startup: process.env.ONE_METRO_LAZY ? "lazy" : userMetroOptions?.startup }; })(); const vxrnPlugins = []; if (!process.env.IS_VXRN_CLI) { console.warn("Experimental: running VxRN as a Vite plugin. This is not yet stable."); vxrnPlugins.push(vxrnVitePlugin({ metro: metroOptions, disableNative: nativeDisabled })); } else { if (!globalThis.__oneOptions) { setOneOptions(options); globalThis["__vxrnPluginConfig__"] = options; globalThis["__vxrnMetroOptions__"] = metroOptions; return []; } } if (options.config?.ensureTSConfig !== false) { void ensureTSConfig(); } const vxrnOptions = getOptionsFilled(); const root = vxrnOptions?.root || process.cwd(); const compiler = options.react?.compiler; if (compiler) { configureVXRNCompilerPlugin({ enableCompiler: // pass through object config, regex, or function directly typeof compiler === "object" || typeof compiler === "function" ? compiler : compiler === "native" ? ["ios", "android"] : compiler === "web" ? ["ssr", "client"] : true }); } const autoDepsOptions = options.ssr?.autoDepsOptimization; const dedupeSymlinks = options.ssr?.dedupeSymlinkedModules ?? false; let ssrDedup_optimizedPackages = null; let ssrDedup_projectRoot = ""; const ssrSymlinkDedupPlugin = { name: "one:ssr-symlink-dedup", enforce: "pre", configResolved(config) { if (!dedupeSymlinks) return; ssrDedup_projectRoot = config.root || process.cwd(); const ssrInclude = config.ssr?.optimizeDeps?.include; if (!ssrInclude?.length) return; ssrDedup_optimizedPackages = /* @__PURE__ */new Set(); for (const entry of ssrInclude) { if (entry.startsWith("@")) { const parts = entry.split("/"); ssrDedup_optimizedPackages.add(`${parts[0]}/${parts[1]}`); } else { ssrDedup_optimizedPackages.add(entry.split("/")[0]); } } }, async resolveId(source, importer, options2) { if (!dedupeSymlinks) return; if (source[0] === "." || source[0] === "/") return; let pkgName; let subpath = null; if (source.startsWith("@")) { const parts = source.split("/"); pkgName = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : source; if (parts.length > 2) subpath = `./${parts.slice(2).join("/")}`; } else { const parts = source.split("/"); pkgName = parts[0]; if (parts.length > 1) subpath = `./${parts.slice(1).join("/")}`; } const resolved = await this.resolve(source, importer, { ...options2, skipSelf: true }); if (!resolved?.id) return; if (resolved.id.includes("/node_modules/")) return; const path2 = await import("node:path"); const fs = await import("node:fs"); const { join, dirname } = path2; const { realpathSync, existsSync: existsSync2, readFileSync: readFileSync2 } = fs; let dir = ssrDedup_projectRoot; while (dir !== dirname(dir)) { const nmPkgDir = join(dir, "node_modules", pkgName); if (existsSync2(nmPkgDir)) { if (subpath) { try { const pkg = JSON.parse(readFileSync2(join(nmPkgDir, "package.json"), "utf8")); const exportEntry = pkg.exports?.[subpath]; if (exportEntry && typeof exportEntry === "object") { const target = exportEntry.import || exportEntry.module || exportEntry.default; if (target) { const fullPath = join(nmPkgDir, target); if (existsSync2(fullPath)) return { id: fullPath, external: resolved.external }; } } } catch {} } const realPkgDir = realpathSync(nmPkgDir); if (resolved.id.startsWith(realPkgDir)) { const relativePart = resolved.id.slice(realPkgDir.length); return { id: nmPkgDir + relativePart, external: resolved.external }; } break; } dir = dirname(dir); } } }; const devAndProdPlugins = [{ name: "one:config", __get: options }, { name: "one:env-prefix", config(userConfig) { if (userConfig.envPrefix) return; return { envPrefix: ["VITE_", "EXPO_PUBLIC_"] }; } }, environmentGuardPlugin(options.environmentGuards), criticalCSSPlugin(), imageDataPlugin(), { name: "one-define-client-env", async config(userConfig) { const clientEnvDefine = options.skipEnv ? {} : (await loadEnv(vxrnOptions?.mode ?? userConfig?.mode ?? "development", process.cwd(), userConfig?.envPrefix)).clientEnvDefine; return { define: { ...clientEnvDefine, ...(process.env.ONE_DEBUG_ROUTER && { "process.env.ONE_DEBUG_ROUTER": JSON.stringify(process.env.ONE_DEBUG_ROUTER) }) } }; } }, ...(autoDepsOptions === false ? [] : [autoDepOptimizePlugin({ onScannedDeps({ hasReanimated, hasNativewind }) { configureVXRNCompilerPlugin({ enableReanimated: hasReanimated, enableNativeCSS: nativeOptions?.css ?? hasNativewind, enableNativewind: hasNativewind }); }, root, include: /node_modules/, ...(autoDepsOptions === true ? {} : autoDepsOptions) })]), ...(options.config?.tsConfigPaths === false ? [] : [/* @__PURE__ */(() => { let mappings = []; function loadMappings(resolvedRoot) { try { const configPath = path.resolve(resolvedRoot, "tsconfig.json"); if (!existsSync(configPath)) return; const raw = readFileSync(configPath, "utf-8"); const stripped = raw.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m); const config = JSON.parse(stripped); const paths = config?.compilerOptions?.paths; const baseUrl = config?.compilerOptions?.baseUrl || "."; if (!paths) return; for (const [pattern, targets] of Object.entries(paths)) { const target = targets[0]; if (!target) continue; if (pattern.endsWith("/*")) { const resolved = path.resolve(resolvedRoot, baseUrl, target.slice(0, -1)); mappings.push({ prefix: pattern.slice(0, -1), replacement: resolved.endsWith("/") ? resolved : resolved + "/", wildcard: true }); } else { mappings.push({ prefix: pattern, replacement: path.resolve(resolvedRoot, baseUrl, target), wildcard: false }); } } } catch {} } return { name: "one:tsconfig-paths", enforce: "pre", config() { return { resolve: { tsconfigPaths: true } }; }, configResolved(config) { if (mappings.length === 0) { loadMappings(config.root); } }, resolveId(source) { const jsExts = [".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cjs", ".cts"]; for (const m of mappings) { let candidate; if (m.wildcard) { if (source.startsWith(m.prefix)) { candidate = m.replacement + source.slice(m.prefix.length); } } else if (source === m.prefix) { candidate = m.replacement; } if (!candidate) continue; if (jsExts.includes(path.extname(candidate))) return candidate; for (const e of jsExts) { if (existsSync(candidate + e)) return candidate + e; } for (const e of jsExts) { if (existsSync(candidate + "/index" + e)) return candidate + "/index" + e; } return candidate; } } }; })()]), // resolveId-based aliases that work during both vite transforms AND // rolldown dep pre-bundling (where resolve.alias is not applied) ...(options.alias ? [(() => { const resolveMap = map => { if (!map) return null; const out = {}; for (const [key, value] of Object.entries(map)) { try { out[key] = path.isAbsolute(value) ? value : resolvePath(value); } catch { out[key] = value; } } return out; }; const a = options.alias; const resolved = { web: resolveMap(a.web), native: resolveMap(a.native), client: resolveMap(a.client), ssr: resolveMap(a.ssr), ios: resolveMap(a.ios), android: resolveMap(a.android) }; return { name: "one:alias", enforce: "pre", resolveId(source) { const env = this.environment?.name; const specific = env ? resolved[env] : null; if (specific && source in specific) { return { id: specific[source], external: false }; } const isWeb = !env || env === "client" || env === "ssr"; const general = isWeb ? resolved.web : resolved.native; if (general && source in general) { return { id: general[source], external: false }; } } }; })()] : []), { // rolldown fails on deep react-native/Libraries/* imports during dep pre-bundling. // these are native-only paths that don't exist in react-native-web. name: "one:redirect-rn-deep-imports", enforce: "pre", resolveId(source) { if (this.environment?.name === "client" || this.environment?.name === "ssr") { if (source.startsWith("react-native/Libraries/") || /react-native-web(-lite)?\/.*\/Libraries\//.test(source)) { return "\0rn-empty-module"; } } }, load(id) { if (id === "\0rn-empty-module") { return "export default {}; export {};"; } } }, { name: "one-aliases", enforce: "pre", config() { let tslibLitePath = ""; try { tslibLitePath = resolvePath("@vxrn/tslib-lite", process.cwd()); } catch (err) { console.info(`Can't find tslib-lite, falling back to tslib`); if (process.env.DEBUG) { console.error(err); } } return { resolve: { alias: { // testing getting transition between routes working // 'use-sync-external-store/with-selector': resolvePath( // 'use-sync-external-store/shim/with-selector' // ), ...(tslibLitePath && { tslib: tslibLitePath }) } } }; } }, { name: "one:init-config", config() { const setupFileDefines = (() => { if (!options.setupFile) return {}; let setupFiles; if (typeof options.setupFile === "string") { setupFiles = { client: options.setupFile, server: options.setupFile, ios: options.setupFile, android: options.setupFile }; } else if ("native" in options.setupFile) { setupFiles = { client: options.setupFile.client, server: options.setupFile.server, ios: options.setupFile.native, android: options.setupFile.native }; } else { setupFiles = options.setupFile; } return { ...(setupFiles.client && { "process.env.ONE_SETUP_FILE_CLIENT": JSON.stringify(setupFiles.client) }), ...(setupFiles.server && { "process.env.ONE_SETUP_FILE_SERVER": JSON.stringify(setupFiles.server) }), ...(setupFiles.ios && { "process.env.ONE_SETUP_FILE_IOS": JSON.stringify(setupFiles.ios) }), ...(setupFiles.android && { "process.env.ONE_SETUP_FILE_ANDROID": JSON.stringify(setupFiles.android) }) }; })(); const serverURL = process.env.ONE_SERVER_URL || vxrnOptions?.server.url; return { // Platform env defined at root level for client (workaround for Vite bug with environment.client.define) define: { ...getPlatformEnvDefine("client"), ...setupFileDefines, ...(options.web?.defaultRenderMode && { "process.env.ONE_DEFAULT_RENDER_MODE": JSON.stringify(options.web.defaultRenderMode), "import.meta.env.ONE_DEFAULT_RENDER_MODE": JSON.stringify(options.web.defaultRenderMode) }), ...(process.env.NODE_ENV !== "production" && serverURL && { "process.env.ONE_SERVER_URL": JSON.stringify(serverURL), "import.meta.env.ONE_SERVER_URL": JSON.stringify(serverURL) }), ...(options.web?.linkPrefetch && { "process.env.ONE_LINK_PREFETCH": JSON.stringify(options.web.linkPrefetch) }), ...(options.web?.skewProtection !== void 0 && { "process.env.ONE_SKEW_PROTECTION": JSON.stringify(options.web.skewProtection === true ? "true" : options.web.skewProtection === false ? "false" : options.web.skewProtection // 'proactive' ) }), ...(options.web?.suspendRoutes !== void 0 && { "process.env.ONE_SUSPEND_ROUTES": JSON.stringify(options.web.suspendRoutes ? "1" : "0") }) }, environments: { ssr: { define: getPlatformEnvDefine("ssr") }, ios: { define: { ...getPlatformEnvDefine("ios"), ...(nativeOptions?.suspendRoutes !== void 0 && { "process.env.ONE_SUSPEND_ROUTES_NATIVE": JSON.stringify(nativeOptions.suspendRoutes ? "1" : "0") }) } }, android: { define: { ...getPlatformEnvDefine("android"), ...(nativeOptions?.suspendRoutes !== void 0 && { "process.env.ONE_SUSPEND_ROUTES_NATIVE": JSON.stringify(nativeOptions.suspendRoutes ? "1" : "0") }) } } }, ssr: { // ensure server-only/client-only go through vite so our environmentGuardPlugin can handle them noExternal: ["server-only", "client-only"] } }; } }, { name: "one:tamagui", config() { return { define: { // safe to set because it only affects web in tamagui, and one is always react 19 "process.env.TAMAGUI_REACT_19": '"1"' }, environments: { ssr: { define: { "process.env.TAMAGUI_IS_SERVER": '"1"', "process.env.TAMAGUI_KEEP_THEMES": '"1"' } }, ios: { define: { "process.env.TAMAGUI_KEEP_THEMES": '"1"' } }, android: { define: { "process.env.TAMAGUI_KEEP_THEMES": '"1"' } } } }; } }, { name: "route-module-hmr-fix", hotUpdate({ server, modules, file }) { const envName = this.environment?.name; const fileRelativePath = path.relative(server.config.root, file); const fileRootDir = fileRelativePath.split(path.sep)[0]; const isAppFile = fileRootDir === "app"; if (envName === "ssr" && isAppFile) { return []; } let hasRouteUpdate = false; const result = modules.map(m => { const { id } = m; if (!id) return m; const relativePath = path.relative(server.config.root, id); const rootDir = relativePath.split(path.sep)[0]; if (rootDir === "app") { m.acceptedHmrExports = /* @__PURE__ */new Set(); const isRootLayout = relativePath === path.join("app", "_layout.tsx") || /^app[\\/]\([^)]+\)[\\/]_layout\.tsx$/.test(relativePath); if (isRootLayout) { hasRouteUpdate = true; } } return m; }); if (hasRouteUpdate) { server.hot.send({ type: "custom", event: "one:route-update", data: { file: fileRelativePath } }); } return result; } }, // Plugins may transform the source code and add imports of `react/jsx-dev-runtime`, which won't be discovered by Vite's initial `scanImports` since the implementation is using ESbuild where such plugins are not executed. // Thus, if the project has a valid `react/jsx-dev-runtime` import, we tell Vite to optimize it, so Vite won't only discover it on the next page load and trigger a full reload. { name: "one:optimize-dev-deps", config(_, env) { if (env.mode === "development") { return { optimizeDeps: { include: ["react/jsx-dev-runtime", "react/compiler-runtime"] } }; } } }, { name: "one:remove-server-from-client", enforce: "pre", transform(code, id) { if (this.environment.name === "client") { if (id.includes(`one-server-only`)) { return code.replace(`import { AsyncLocalStorage } from "node:async_hooks"`, `class AsyncLocalStorage {}`); } } } }, // packages in resolve.dedupe must also be pre-bundled for SSR to prevent // duplicate module instances (e.g. symlinked monorepo packages resolving // to different paths) { name: "one:ssr-dedupe-prebundle", config(config) { if (!dedupeSymlinks) return; const dedupeList = config.resolve?.dedupe; if (!Array.isArray(dedupeList) || dedupeList.length === 0) return; return { ssr: { optimizeDeps: { include: [...dedupeList] }, noExternal: [...dedupeList] } }; } }, // fix: vite's ssr dep optimizer registers pre-bundled deps by their // node_modules path, but symlinks cause imports to resolve to the real // (source) path. the optimizer doesn't recognize the real path, so it // loads from source — creating a duplicate instance. // this plugin forces optimized SSR deps to resolve via node_modules. ssrSymlinkDedupPlugin]; const nativeWebDevAndProdPlugsin = [clientTreeShakePlugin()]; if (!nativeDisabled) { globalThis.__vxrnAddNativePlugins = [clientTreeShakePlugin({ runtime: "rolldown" })]; } globalThis.__vxrnAddWebPluginsProd = devAndProdPlugins; const flags = {}; if (!nativeDisabled) { globalThis.__vxrnNativeEntryConfig = { routerRoot, ignoredRouteFiles: options.router?.ignoredRouteFiles, setupFile: options.setupFile, flags }; } const inspectorPlugins = (() => { const devtools = options.devtools ?? true; const inspector = devtools === true || devtools !== false && (devtools.inspector ?? true); const editor = devtools !== true && devtools !== false ? devtools.editor : void 0; return inspector ? sourceInspectorPlugin({ editor }) : []; })(); return [...vxrnPlugins, ...devAndProdPlugins, ...inspectorPlugins, ...nativeWebDevAndProdPlugsin, /** * This is really the meat of one, where it handles requests: */ createFileSystemRouterPlugin(options), generateFileSystemRouteTypesPlugin(options), fixDependenciesPlugin(options.patches), createVirtualEntry({ ...options, flags, root: routerRoot }), { name: "one-define-environment", config() { return { define: { ...(nativeOptions?.key && { "process.env.ONE_APP_NAME": JSON.stringify(nativeOptions.key), "import.meta.env.ONE_APP_NAME": JSON.stringify(nativeOptions.key) }), "process.env.ONE_CACHE_KEY": JSON.stringify(CACHE_KEY), "import.meta.env.ONE_CACHE_KEY": JSON.stringify(CACHE_KEY) } }; } }, SSRCSSPlugin({ entries: [virtualEntryId] }), // devtools (always includes refresh preamble for HMR, optionally includes UI) ...(() => { const devtools = options.devtools ?? true; const includeUI = devtools !== false; return [ // always include devtools plugin for refresh preamble (required for HMR) createDevtoolsPlugin({ includeUI })]; })()]; } export { one }; //# sourceMappingURL=one.mjs.map