@tanstack/router-plugin
Version:
Modern and scalable routing for React applications
150 lines (128 loc) • 4.25 kB
text/typescript
import { isAbsolute, join, normalize } from 'node:path'
import { Generator, resolveConfigPath } from '@tanstack/router-generator'
import { getConfig } from './config'
import type { GeneratorEvent } from '@tanstack/router-generator'
import type { FSWatcher } from 'chokidar'
import type { UnpluginFactory } from 'unplugin'
import type { Config } from './config'
const PLUGIN_NAME = 'unplugin:router-generator'
export const unpluginRouterGeneratorFactory: UnpluginFactory<
Partial<Config> | undefined
> = (options = {}) => {
const ROOT: string = process.cwd()
let userConfig = options as Config
let generator: Generator
const routeGenerationDisabled = () =>
userConfig.enableRouteGeneration === false
const getRoutesDirectoryPath = () => {
return isAbsolute(userConfig.routesDirectory)
? userConfig.routesDirectory
: join(ROOT, userConfig.routesDirectory)
}
const initConfigAndGenerator = () => {
userConfig = getConfig(options, ROOT)
generator = new Generator({
config: userConfig,
root: ROOT,
})
}
const generate = async (opts?: {
file: string
event: 'create' | 'update' | 'delete'
}) => {
if (routeGenerationDisabled()) {
return
}
let generatorEvent: GeneratorEvent | undefined = undefined
if (opts) {
const filePath = normalize(opts.file)
if (filePath === resolveConfigPath({ configDirectory: ROOT })) {
initConfigAndGenerator()
return
}
generatorEvent = { path: filePath, type: opts.event }
}
try {
await generator.run(generatorEvent)
globalThis.TSR_ROUTES_BY_ID_MAP = generator.getRoutesByFileMap()
} catch (e) {
console.error(e)
}
}
return {
name: 'tanstack:router-generator',
enforce: 'pre',
async watchChange(id, { event }) {
await generate({
file: id,
event,
})
},
async buildStart() {
await generate()
},
vite: {
configResolved() {
initConfigAndGenerator()
},
async buildStart() {
// to support vite 5, we need to optionally chain the access to the environment
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (this.environment?.config?.consumer === 'server') {
// When building in environment mode, we only need to generate routes
// for the client environment
return
}
await generate()
},
sharedDuringBuild: true,
},
rspack(compiler) {
initConfigAndGenerator()
let handle: FSWatcher | null = null
compiler.hooks.beforeRun.tapPromise(PLUGIN_NAME, () => generate())
compiler.hooks.watchRun.tapPromise(PLUGIN_NAME, async () => {
if (handle) {
return
}
// rspack watcher doesn't register newly created files
const routesDirectoryPath = getRoutesDirectoryPath()
const chokidar = await import('chokidar')
handle = chokidar
.watch(routesDirectoryPath, { ignoreInitial: true })
.on('add', (file) => generate({ file, event: 'create' }))
await generate()
})
compiler.hooks.watchClose.tap(PLUGIN_NAME, async () => {
if (handle) {
await handle.close()
}
})
},
webpack(compiler) {
initConfigAndGenerator()
let handle: FSWatcher | null = null
compiler.hooks.beforeRun.tapPromise(PLUGIN_NAME, () => generate())
compiler.hooks.watchRun.tapPromise(PLUGIN_NAME, async () => {
if (handle) {
return
}
// webpack watcher doesn't register newly created files
const routesDirectoryPath = getRoutesDirectoryPath()
const chokidar = await import('chokidar')
handle = chokidar
.watch(routesDirectoryPath, { ignoreInitial: true })
.on('add', (file) => generate({ file, event: 'create' }))
await generate()
})
compiler.hooks.watchClose.tap(PLUGIN_NAME, async () => {
if (handle) {
await handle.close()
}
})
compiler.hooks.done.tap(PLUGIN_NAME, () => {
console.info('✅ ' + PLUGIN_NAME + ': route-tree generation done')
})
},
}
}