UNPKG

one

Version:

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

201 lines (194 loc) 8.47 kB
import { resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { configuration } from "@vxrn/compiler"; import micromatch from "micromatch"; import { isNativeEnvironment } from "vxrn"; import { API_ROUTE_GLOB_PATTERN, ROUTE_GLOB_PATTERN, ROUTE_NATIVE_EXCLUSION_GLOB_PATTERNS, ROUTE_WEB_EXCLUSION_GLOB_PATTERNS } from "../../router/glob-patterns.mjs"; import { matchDirectoryRenderMode, matchFileRenderMode } from "../../router/matchers.mjs"; import { resolvedVirtualEntryId, resolvedVirtualEntryIdNative, virtualEntryId, virtualEntryIdNative } from "./virtualEntryConstants.mjs"; const escapeRe = s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const VIRTUAL_ENTRY_SOURCE_RE = new RegExp(`^(?:${escapeRe(virtualEntryId)}|${escapeRe(virtualEntryIdNative)})$`); const VIRTUAL_ENTRY_RESOLVED_RE = new RegExp(`^(?:${escapeRe(resolvedVirtualEntryId)}|${escapeRe(resolvedVirtualEntryIdNative)})$`); function normalizeSetupFile(setupFile) { if (!setupFile) return {}; if (typeof setupFile === "string") { return { client: setupFile, server: setupFile, ios: setupFile, android: setupFile }; } if ("native" in setupFile) { return { client: setupFile.client, server: setupFile.server, ios: setupFile.native, android: setupFile.native }; } const sf = setupFile; return { client: sf.client, server: sf.server, ios: sf.ios, android: sf.android }; } function getSetupFileImport(environmentName, setupFiles, useStaticImport, root) { const envMap = { client: "client", ssr: "server", ios: "ios", android: "android" }; const key = envMap[environmentName]; if (!key) return { importStatement: "", promiseDeclaration: "", promiseVarName: "" }; const setupFile = setupFiles[key]; if (!setupFile) return { importStatement: "", promiseDeclaration: "", promiseVarName: "" }; const resolvedSetupFile = root ? pathToFileURL(resolve(root, setupFile)).href : setupFile; if (useStaticImport) { return { importStatement: `import ${JSON.stringify(resolvedSetupFile)}`, promiseDeclaration: "", promiseVarName: "" }; } return { importStatement: "", promiseDeclaration: `const __oneGetSetupPromise = () => import(${JSON.stringify(resolvedSetupFile)})`, promiseVarName: "__oneGetSetupPromise" }; } function createVirtualEntry(options) { const routeGlobs = [`/${options.root}/${ROUTE_GLOB_PATTERN}`, ...(options.router?.ignoredRouteFiles?.map(pattern => `!/${options.root}/${pattern}`) || [])]; const apiRouteGlobs = `/${options.root}/${API_ROUTE_GLOB_PATTERN}`; const setupFiles = normalizeSetupFile(options.setupFile); let viteRoot = ""; let isDevMode = false; return { name: "one-virtual-entry", enforce: "pre", configResolved(config) { viteRoot = config.root; isDevMode = config.command === "serve"; }, // only the two virtual entry ids reach js; every other import in the graph // is rejected rust-side resolveId: { filter: { id: VIRTUAL_ENTRY_SOURCE_RE }, handler(id) { if (id === virtualEntryId) { return resolvedVirtualEntryId; } if (id === virtualEntryIdNative) { return resolvedVirtualEntryIdNative; } } }, load: { filter: { id: VIRTUAL_ENTRY_RESOLVED_RE }, handler(id) { if (id === resolvedVirtualEntryId) { const isNative = isNativeEnvironment(this.environment); const isSSR = this.environment.name === "ssr"; const serverSpaRouteFiles = isSSR ? options.routeIndex.getPaths().map(file => file.replace(/^\.\//, "")).filter(file => /\.tsx?$/.test(file)).filter(file => !micromatch.isMatch(file, ROUTE_WEB_EXCLUSION_GLOB_PATTERNS)).filter(file => { const parts = file.split("/"); const filename = parts.at(-1) || ""; if (filename.startsWith("_layout") || filename.startsWith("_middleware")) { return false; } const fileMode = matchFileRenderMode(filename); if (fileMode) return fileMode === "spa"; let parentMode; for (const directory of parts.slice(0, -1)) { parentMode = matchDirectoryRenderMode(directory)?.renderMode ?? parentMode; } return parentMode === "spa"; }) : []; const serverSpaRouteStubs = serverSpaRouteFiles.map(file => `${JSON.stringify(`/${options.root}/${file}`)}: () => Promise.resolve({})`).join(",\n "); const serverSpaBuildGlobs = serverSpaRouteFiles.map(file => `/${options.root}/${file}`); const serverSpaBuildRoutes = serverSpaBuildGlobs.length ? `{ loader: import.meta.glob(${JSON.stringify(serverSpaBuildGlobs)}, { exhaustive: true, import: 'loader', query: '?one-spa-build' }), generateStaticParams: import.meta.glob(${JSON.stringify(serverSpaBuildGlobs)}, { exhaustive: true, import: 'generateStaticParams', query: '?one-spa-build' }), sitemap: import.meta.glob(${JSON.stringify(serverSpaBuildGlobs)}, { exhaustive: true, import: 'sitemap', query: '?one-spa-build' }) }` : "{}"; const webRouteGlobs = [...routeGlobs, ...ROUTE_WEB_EXCLUSION_GLOB_PATTERNS.map(pattern => `!${pattern}`), ...serverSpaRouteFiles.map(file => `!/${options.root}/${file}`)]; const useStaticImport = isNative || isSSR && isDevMode; const setupResult = getSetupFileImport(this.environment.name, setupFiles, useStaticImport, viteRoot); const nativewindImport = configuration.enableNativewind ? `import 'react-native-css-interop/dist/runtime/components'` : ""; const setupPromiseArg = setupResult.promiseVarName ? `getSetupPromise: ${setupResult.promiseVarName},` : ""; const linkingArg = options.router?.linking ? `linking: ${JSON.stringify(options.router.linking)},` : ""; const isDevClient = this.environment.mode === "dev" && this.environment.name === "client"; const refreshPreambleImport = isDevClient ? `import { injectIntoGlobalHook as __oneInjectRefresh } from '/@react-refresh'` : ""; const refreshPreambleSetup = isDevClient ? `if (typeof window !== 'undefined' && !window.$RefreshReg$) { __oneInjectRefresh(window) window.$RefreshReg$ = () => {} window.$RefreshSig$ = () => (type) => type }` : ""; return ` ${setupResult.promiseDeclaration} ${nativewindImport} ${refreshPreambleImport} import { createApp, registerPreloadedRoute as _registerPreloadedRoute } from 'one' ${setupResult.importStatement} ${refreshPreambleSetup} // Export registerPreloadedRoute so preload files can import it from this bundle // Named export that wraps the original function export function registerPreloadedRoute(key, module) { return _registerPreloadedRoute(key, module) } // Also expose on window for debugging and to prevent tree-shaking if (typeof window !== 'undefined') { window.__oneRegisterPreloadedRoute = registerPreloadedRoute } // build-only spa exports generate route artifacts without exposing the page // component to the runtime server route map. export const oneBuildOnlySpaRoutes = ${serverSpaBuildRoutes} // globbing ${JSON.stringify(webRouteGlobs)} export default createApp({ ${setupPromiseArg} routes: { ...import.meta.glob(${JSON.stringify(webRouteGlobs)}, { exhaustive: true }), ${serverSpaRouteStubs} }, routerRoot: ${JSON.stringify(options.root)}, flags: ${JSON.stringify(options.flags)}, ${linkingArg} }) `; } if (id === resolvedVirtualEntryIdNative) { const isNative = isNativeEnvironment(this.environment); const setupResult = getSetupFileImport(this.environment.name, setupFiles, isNative, viteRoot); const linkingArg = options.router?.linking ? `linking: ${JSON.stringify(options.router.linking)},` : ""; return ` import { createApp } from 'one' ${setupResult.importStatement} // globbing ${JSON.stringify(routeGlobs)} export default createApp({ routes: import.meta.glob(${JSON.stringify([...routeGlobs, ...ROUTE_NATIVE_EXCLUSION_GLOB_PATTERNS.map(p => `!${p}`), `!${apiRouteGlobs}`])}, { exhaustive: true }), routerRoot: ${JSON.stringify(options.root)}, flags: ${JSON.stringify(options.flags)}, ${linkingArg} }) `; } } } }; } export { createVirtualEntry }; //# sourceMappingURL=virtualEntryPlugin.mjs.map