UNPKG

one

Version:

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

287 lines (265 loc) 10.1 kB
import { Buffer } from 'node:buffer' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { describe, expect, it, vi } from 'vitest' import { build } from 'vite' import { createRouteIndex } from '../../utils/routeIndex' import { createVirtualEntry } from './virtualEntryPlugin' // mock isNativeEnvironment vi.mock('vxrn', () => ({ isNativeEnvironment: (env: any) => { return env?.name === 'ios' || env?.name === 'android' }, })) vi.mock('@vxrn/compiler', () => ({ configuration: { enableNativewind: false }, })) function loadEntry( plugin: any, envName: string, command: 'serve' | 'build' = 'build', envMode: 'dev' | 'build' = command === 'serve' ? 'dev' : 'build' ) { // simulate configResolved so isDevMode is set plugin.configResolved?.({ root: '', command }) const ctx = { environment: { name: envName, mode: envMode }, } return plugin.load.handler.call(ctx, '\0virtual:one-entry') } describe('virtualEntryPlugin', () => { it('keeps spa route matching without bundling the client module on the server', async () => { const root = mkdtempSync(join(tmpdir(), 'one-spa-server-')) const appDir = join(root, 'app') mkdirSync(appDir) const spaRoute = join(appDir, 'client-only+spa.tsx') writeFileSync( spaRoute, `export const browserPayload = 'must-not-enter-server-graph' export default function ClientOnlyPage() { return null } ` ) try { const result = await build({ root, configFile: false, logLevel: 'silent', plugins: [ { name: 'test-one-entry', enforce: 'pre', resolveId(id) { if (id === 'one') return '\0test-one-entry' }, load(id) { if (id === '\0test-one-entry') { return `export const createApp = (options) => options export const registerPreloadedRoute = () => {}` } }, }, createVirtualEntry({ root: 'app', flags: {}, routeIndex: createRouteIndex({ routerRoot: appDir }), }), ], build: { ssr: true, write: false, rolldownOptions: { input: 'virtual:one-entry', }, }, }) if (Array.isArray(result)) throw new Error('expected one server build output') if (!('output' in result)) throw new Error('expected completed server build') const moduleIds = result.output.flatMap((output) => output.type === 'chunk' ? output.moduleIds : [] ) expect(moduleIds).not.toContain(resolve(spaRoute)) expect( result.output .flatMap((output) => (output.type === 'chunk' ? output.code : '')) .join('\n') ).not.toContain('must-not-enter-server-graph') const entry = result.output.find( (output) => output.type === 'chunk' && output.isEntry ) if (!entry || entry.type !== 'chunk') throw new Error('missing server entry chunk') const built = await import( `data:text/javascript;base64,${Buffer.from(entry.code).toString('base64')}` ) expect(Object.keys(built.default.routes)).toContain('/app/client-only+spa.tsx') await expect(built.default.routes['/app/client-only+spa.tsx']()).resolves.toEqual( {} ) } finally { rmSync(root, { recursive: true, force: true }) } }) describe('setupFile', () => { const base = { root: 'app', flags: {}, routeIndex: { getPaths: () => [], update: () => false, }, } it('server (ssr) uses lazy dynamic import, not static import', () => { const plugin = createVirtualEntry({ ...base, setupFile: { client: './src/setupClient.ts', server: './src/setupServer.ts', }, }) const code = loadEntry(plugin, 'ssr') // must NOT use static import (executes at build time) expect(code).not.toContain('import "./src/setupServer.ts"') // must use lazy function wrapping dynamic import expect(code).toContain('__oneGetSetupPromise = () => import(') expect(code).toContain('./src/setupServer.ts') // must pass it to createApp so it's called at render time expect(code).toContain('getSetupPromise: __oneGetSetupPromise') }) it('client uses dynamic import for setupFile', () => { const plugin = createVirtualEntry({ ...base, setupFile: { client: './src/setupClient.ts', server: './src/setupServer.ts', }, }) const code = loadEntry(plugin, 'client') // should have lazy dynamic import expect(code).toContain('__oneGetSetupPromise') expect(code).toContain('import(') expect(code).toContain('./src/setupClient.ts') expect(code).toContain('getSetupPromise: __oneGetSetupPromise') // should NOT have a static import expect(code).not.toContain('import "./src/setupClient.ts"') }) it('native (ios) uses static import for setupFile', () => { const plugin = createVirtualEntry({ ...base, setupFile: { client: './src/setupClient.ts', native: './src/setupNative.ts', }, }) const code = loadEntry(plugin, 'ios') expect(code).toContain('import "./src/setupNative.ts"') // native should NOT use lazy import or pass getSetupPromise expect(code).not.toContain('__oneGetSetupPromise') expect(code).not.toContain('getSetupPromise') }) it('android uses static import like ios', () => { const plugin = createVirtualEntry({ ...base, setupFile: { native: './src/setupNative.ts', }, }) const code = loadEntry(plugin, 'android') expect(code).toContain('import "./src/setupNative.ts"') expect(code).not.toContain('__oneGetSetupPromise') }) it('server (ssr) in dev mode uses static import for dep discovery', () => { const plugin = createVirtualEntry({ ...base, setupFile: { client: './src/setupClient.ts', server: './src/setupServer.ts', }, }) const code = loadEntry(plugin, 'ssr', 'serve') // dev mode SSR uses static import so Vite crawls deps before first request expect(code).toContain('import "./src/setupServer.ts"') expect(code).not.toContain('__oneGetSetupPromise') }) it('no setupFile produces no import', () => { const plugin = createVirtualEntry(base) const code = loadEntry(plugin, 'ssr') expect(code).not.toContain('setupServer') expect(code).not.toContain('__oneGetSetupPromise') expect(code).not.toContain('getSetupPromise') }) it('string setupFile applies to all environments', () => { const plugin = createVirtualEntry({ ...base, setupFile: './src/setup.ts', }) const ssrCode = loadEntry(plugin, 'ssr') const clientCode = loadEntry(plugin, 'client') const iosCode = loadEntry(plugin, 'ios') // client and ssr both use lazy dynamic import expect(ssrCode).toContain('./src/setup.ts') expect(ssrCode).toContain('__oneGetSetupPromise') expect(ssrCode).not.toContain('import "./src/setup.ts"') expect(clientCode).toContain('./src/setup.ts') expect(clientCode).toContain('__oneGetSetupPromise') expect(clientCode).not.toContain('import "./src/setup.ts"') // native uses static import expect(iosCode).toContain('import "./src/setup.ts"') expect(iosCode).not.toContain('__oneGetSetupPromise') }) it('passes router linking config to createApp', () => { const plugin = createVirtualEntry({ ...base, router: { linking: { scheme: 'threepunchconvo', prefixes: ['threepunchconvo://app'], }, }, }) const code = loadEntry(plugin, 'ios') expect(code).toContain( 'linking: {"scheme":"threepunchconvo","prefixes":["threepunchconvo://app"]}' ) }) }) describe('react-refresh preamble', () => { const base = { root: 'app', flags: {}, routeIndex: { getPaths: () => [], update: () => false, }, } // /@one/dev.js also installs the preamble, but it is a deferred module // script and the client entry is async, so on a large prerendered document // the entry wins and every compiler-wrapped route module throws "React // refresh preamble was not loaded". the entry has to install it itself so // the result does not depend on which script arrives first. it('dev client installs the preamble before route modules load', () => { const code = loadEntry(createVirtualEntry(base), 'client', 'serve') expect(code).toContain(`from '/@react-refresh'`) expect(code).toContain('window.$RefreshReg$ = () => {}') // must be guarded so it is a no-op when /@one/dev.js already won the race expect(code).toContain(`!window.$RefreshReg$`) // and it has to sit in the entry body, ahead of the lazily-globbed routes expect(code.indexOf('__oneInjectRefresh(window)')).toBeLessThan( code.indexOf('createApp(') ) }) it('production build emits no reference to the dev-only refresh runtime', () => { const code = loadEntry(createVirtualEntry(base), 'client', 'build') // /@react-refresh only exists while serving, so a build that imported it // would ship a broken specifier expect(code).not.toContain('/@react-refresh') expect(code).not.toContain('$RefreshReg$') }) it('server and native entries never get the preamble', () => { for (const envName of ['ssr', 'ios', 'android']) { const code = loadEntry(createVirtualEntry(base), envName, 'serve') expect(code).not.toContain('/@react-refresh') expect(code).not.toContain('$RefreshReg$') } }) }) })