UNPKG

@tanstack/router-plugin

Version:

Modern and scalable routing for React applications

274 lines (227 loc) 7.39 kB
/** * It is important to familiarize yourself with how the code-splitting works in this plugin. * https://github.com/TanStack/router/pull/3355 */ import { isAbsolute, join, normalize } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' import { logDiff } from '@tanstack/router-utils' import { getConfig, splitGroupingsSchema } from './config' import { compileCodeSplitReferenceRoute, compileCodeSplitVirtualRoute, detectCodeSplitGroupingsFromRoute, } from './code-splitter/compilers' import { defaultCodeSplitGroupings, splitRouteIdentNodes, tsrSplit, } from './constants' import { decodeIdentifier } from './code-splitter/path-ids' import type { CodeSplitGroupings, SplitRouteIdentNodes } from './constants' import type { Config } from './config' import type { UnpluginContextMeta, UnpluginFactory, TransformResult as UnpluginTransformResult, } from 'unplugin' const debug = process.env.TSR_VITE_DEBUG && ['true', 'router-plugin'].includes(process.env.TSR_VITE_DEBUG) function capitalizeFirst(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1) } function fileIsInRoutesDirectory( filePath: string, routesDirectory: string, ): boolean { const routesDirectoryPath = isAbsolute(routesDirectory) ? routesDirectory : join(process.cwd(), routesDirectory) const path = normalize(filePath) return path.startsWith(routesDirectoryPath) } type BannedBeforeExternalPlugin = { identifier: string pkg: string usage: string frameworks: Array<UnpluginContextMeta['framework']> } const bannedBeforeExternalPlugins: Array<BannedBeforeExternalPlugin> = [ { identifier: '@react-refresh', pkg: '@vitejs/plugin-react', usage: 'viteReact()', frameworks: ['vite'], }, ] class FoundPluginInBeforeCode extends Error { constructor(externalPlugin: BannedBeforeExternalPlugin, framework: string) { super(`We detected that the '${externalPlugin.pkg}' was passed before '@tanstack/router-plugin'. Please make sure that '@tanstack/router-plugin' is passed before '${externalPlugin.pkg}' and try again: e.g. plugins: [ TanStackRouter${capitalizeFirst(framework)}(), // Place this before ${externalPlugin.usage} ${externalPlugin.usage}, ] `) } } const PLUGIN_NAME = 'unplugin:router-code-splitter' export const unpluginRouterCodeSplitterFactory: UnpluginFactory< Partial<Config> | undefined > = (options = {}, { framework }) => { let ROOT: string = process.cwd() let userConfig = options as Config const isProduction = process.env.NODE_ENV === 'production' const getGlobalCodeSplitGroupings = () => { return ( userConfig.codeSplittingOptions?.defaultBehavior || defaultCodeSplitGroupings ) } const getShouldSplitFn = () => { return userConfig.codeSplittingOptions?.splitBehavior } const handleCompilingReferenceFile = ( code: string, id: string, ): UnpluginTransformResult => { if (debug) console.info('Compiling Route: ', id) const fromCode = detectCodeSplitGroupingsFromRoute({ code, root: ROOT, filename: id, }) if (fromCode.groupings) { const res = splitGroupingsSchema.safeParse(fromCode.groupings) if (!res.success) { const message = res.error.errors.map((e) => e.message).join('. ') throw new Error( `The groupings for the route "${id}" are invalid.\n${message}`, ) } } const userShouldSplitFn = getShouldSplitFn() const pluginSplitBehavior = userShouldSplitFn?.({ routeId: fromCode.routeId, }) as CodeSplitGroupings | undefined if (pluginSplitBehavior) { const res = splitGroupingsSchema.safeParse(pluginSplitBehavior) if (!res.success) { const message = res.error.errors.map((e) => e.message).join('. ') throw new Error( `The groupings returned when using \`splitBehavior\` for the route "${id}" are invalid.\n${message}`, ) } } const splitGroupings: CodeSplitGroupings = fromCode.groupings || pluginSplitBehavior || getGlobalCodeSplitGroupings() const compiledReferenceRoute = compileCodeSplitReferenceRoute({ code, root: ROOT, filename: id, runtimeEnv: isProduction ? 'prod' : 'dev', codeSplitGroupings: splitGroupings, targetFramework: userConfig.target, }) if (debug) { logDiff(code, compiledReferenceRoute.code) console.log('Output:\n', compiledReferenceRoute.code + '\n\n') } return compiledReferenceRoute } const handleCompilingVirtualFile = ( code: string, id: string, ): UnpluginTransformResult => { if (debug) console.info('Splitting Route: ', id) const [_, ...pathnameParts] = id.split('?') const searchParams = new URLSearchParams(pathnameParts.join('?')) const splitValue = searchParams.get(tsrSplit) if (!splitValue) { throw new Error( `The split value for the virtual route "${id}" was not found.`, ) } const rawGrouping = decodeIdentifier(splitValue) const grouping = [...new Set(rawGrouping)].filter((p) => splitRouteIdentNodes.includes(p as any), ) as Array<SplitRouteIdentNodes> const result = compileCodeSplitVirtualRoute({ code, root: ROOT, filename: id, splitTargets: grouping, }) if (debug) { logDiff(code, result.code) console.log('Output:\n', result.code + '\n\n') } return result } return { name: 'router-code-splitter-plugin', enforce: 'pre', transform(code, id) { if (!userConfig.autoCodeSplitting) { return null } const url = pathToFileURL(id) url.searchParams.delete('v') id = fileURLToPath(url).replace(/\\/g, '/') if (id.includes(tsrSplit)) { return handleCompilingVirtualFile(code, id) } else if ( fileIsInRoutesDirectory(id, userConfig.routesDirectory) && (code.includes('createRoute(') || code.includes('createFileRoute(')) ) { for (const externalPlugin of bannedBeforeExternalPlugins) { if (!externalPlugin.frameworks.includes(framework)) { continue } if (code.includes(externalPlugin.identifier)) { throw new FoundPluginInBeforeCode(externalPlugin, framework) } } return handleCompilingReferenceFile(code, id) } return null }, transformInclude(id) { if (!userConfig.autoCodeSplitting) { return undefined } if ( fileIsInRoutesDirectory(id, userConfig.routesDirectory) || id.includes(tsrSplit) ) { return true } return false }, vite: { configResolved(config) { ROOT = config.root userConfig = getConfig(options, ROOT) }, }, rspack(_compiler) { ROOT = process.cwd() userConfig = getConfig(options, ROOT) }, webpack(compiler) { ROOT = process.cwd() userConfig = getConfig(options, ROOT) if ( userConfig.autoCodeSplitting && compiler.options.mode === 'production' ) { compiler.hooks.done.tap(PLUGIN_NAME, () => { console.info('✅ ' + PLUGIN_NAME + ': code-splitting done!') setTimeout(() => { process.exit(0) }) }) } }, } }