@baseplate-dev/react-generators
Version:
React Generators for Baseplate
182 lines (181 loc) • 10.4 kB
JavaScript
import { createNodePackagesTask, eslintConfigProvider, extractPackageVersions, packageScope, pathRootsProvider, prettierProvider, tsCodeFragment, TsCodeUtils, tsImportBuilder, tsTemplate, tsTemplateWithImports, } from '@baseplate-dev/core-generators';
import { createConfigProviderTask, createGenerator, createGeneratorTask, createProviderTask, createProviderType, } from '@baseplate-dev/sync';
import { z } from 'zod';
import { REACT_PACKAGES } from '#src/constants/react-packages.js';
import { reactRoutesProvider } from '#src/providers/routes.js';
import { reactAppConfigProvider } from '../react-app/index.js';
import { reactErrorImportsProvider } from '../react-error/index.js';
import { reactBaseConfigProvider } from '../react/react.generator.js';
import { CORE_REACT_ROUTER_GENERATED } from './generated/index.js';
const descriptorSchema = z.object({
renderPlaceholderIndex: z.boolean().default(false),
});
const [setupTask, reactRouterConfigProvider, reactRouterConfigValuesProvider] = createConfigProviderTask((t) => ({
/* The code to set up the component that will be placed in the render function of the router component (no conditional logic, hooks only) */
routerSetupFragments: t.map(),
/* The code to set up the component that will be placed in the body of the router component (conditional logic, no hooks) */
routerBodyFragments: t.map(),
/* The component that contains the root layout, e.g. <RootLayout /> */
rootLayoutComponent: t.scalar(),
/* The fields in the root route context */
rootContextFields: t.namedArray(),
/* Any fragments in the ErrorComponent header */
errorComponentHeaderFragments: t.map(),
/* Any fragments in the ErrorComponent body */
errorComponentBodyFragments: t.map(),
}), {
prefix: 'react-router',
configScope: packageScope,
});
export { reactRouterConfigProvider };
export const reactRouterProvider = createProviderType('react-router');
export const reactRouterGenerator = createGenerator({
name: 'core/react-router',
generatorFileUrl: import.meta.url,
descriptorSchema,
buildTasks: ({ renderPlaceholderIndex }) => ({
setup: setupTask,
nodePackages: createNodePackagesTask({
prod: extractPackageVersions(REACT_PACKAGES, ['@tanstack/react-router']),
dev: extractPackageVersions(REACT_PACKAGES, ['@tanstack/router-plugin']),
}),
paths: CORE_REACT_ROUTER_GENERATED.paths.task,
imports: CORE_REACT_ROUTER_GENERATED.imports.task,
renderers: CORE_REACT_ROUTER_GENERATED.renderers.task,
vite: createProviderTask(reactBaseConfigProvider, (reactBaseConfig) => {
reactBaseConfig.vitePlugins.set('@tanstack/router-plugin', tsTemplate `${TsCodeUtils.importFragment('tanstackRouter', '@tanstack/router-plugin/vite')}({
target: 'react',
autoCodeSplitting: true,
generatedRouteTree: './src/route-tree.gen.ts',
quoteStyle: 'single',
})`);
}),
reactAppConfig: createGeneratorTask({
dependencies: {
reactAppConfig: reactAppConfigProvider,
paths: CORE_REACT_ROUTER_GENERATED.paths.provider,
},
run({ reactAppConfig, paths }) {
reactAppConfig.renderRoot.set(tsCodeFragment('<AppRoutes />', tsImportBuilder(['AppRoutes']).from(paths.router)));
},
}),
prettier: createProviderTask(prettierProvider, (prettier) => {
prettier.addPrettierIgnore('/src/route-tree.gen.ts');
}),
eslint: createProviderTask(eslintConfigProvider, (eslint) => {
eslint.eslintIgnore.push('src/route-tree.gen.ts');
}),
routes: createGeneratorTask({
dependencies: {
pathRoots: pathRootsProvider,
},
exports: {
reactRoutes: reactRoutesProvider.export(packageScope),
reactRouter: reactRouterProvider.export(packageScope),
},
run({ pathRoots }) {
const directoryBase = `@/src/routes`;
pathRoots.registerPathRoot('routes-root', directoryBase);
return {
providers: {
reactRoutes: {
getOutputRelativePath: () => directoryBase,
getRoutePrefix: () => '',
getRouteFilePath: () => '',
},
reactRouter: {
getRootRouteDirectory: () => directoryBase,
},
},
};
},
}),
main: createGeneratorTask({
dependencies: {
reactRouterConfigValues: reactRouterConfigValuesProvider,
renderers: CORE_REACT_ROUTER_GENERATED.renderers.provider,
reactErrorImports: reactErrorImportsProvider,
},
run({ reactRouterConfigValues: { routerSetupFragments, routerBodyFragments, rootLayoutComponent, rootContextFields, errorComponentHeaderFragments, errorComponentBodyFragments, }, renderers, reactErrorImports, }) {
const fieldMissingInitializer = rootContextFields.filter((field) => !field.createRouteInitializer &&
!field.routerProviderInitializer &&
!field.optional);
if (fieldMissingInitializer.length > 0) {
throw new Error(`The following route root context fields are missing an initializer: ${fieldMissingInitializer.map((field) => field.name).join(', ')}`);
}
const sortedRootContextFields = rootContextFields.toSorted((a, b) => a.name.localeCompare(b.name));
return {
build: async (builder) => {
const routerProvider = TsCodeUtils.importFragment('RouterProvider', '@tanstack/react-router');
const routeProviderInitializers = new Map(sortedRootContextFields
.filter((f) => f.routerProviderInitializer?.code)
.map((field) => [
field.name,
field.routerProviderInitializer?.code,
]));
const routerContext = routeProviderInitializers.size > 0
? tsTemplateWithImports([
reactErrorImports.logError.declaration(),
tsImportBuilder(['useMemo', 'useEffect', 'useRef']).from('react'),
]) `
const routerContext = useMemo(() => (${TsCodeUtils.mergeFragmentsAsObject(routeProviderInitializers)}), [${[...sortedRootContextFields]
.flatMap((v) => v.routerProviderInitializer?.dependencies ?? [])
.join(', ')}])
// Ensure we always have the latest context in the router
const previousContext = useRef<typeof routerContext>(undefined);
useEffect(() => {
if (previousContext.current && previousContext.current !== routerContext) {
router.invalidate().catch(logError);
}
previousContext.current = routerContext;
}, [routerContext])
`
: '';
await builder.apply(renderers.router.render({
variables: {
TPL_ERROR_COMPONENT_HEADER: TsCodeUtils.mergeFragments(errorComponentHeaderFragments, '\n\n'),
TPL_ERROR_COMPONENT_BODY: TsCodeUtils.mergeFragments(errorComponentBodyFragments, '\n\n'),
TPL_ADDITIONAL_ROUTER_OPTIONS: rootContextFields.length > 0
? tsTemplate `
context: {
${TsCodeUtils.mergeFragmentsPresorted(sortedRootContextFields
.filter((f) => !f.optional || f.createRouteInitializer)
.map((field) => field.createRouteInitializer
? tsTemplate `${field.name}: ${field.createRouteInitializer},`
: `// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- context instantiated in the RouteProvider
${field.name}: undefined!,`))}
}
`
: '',
TPL_COMPONENT_SETUP: TsCodeUtils.mergeFragments(routerSetupFragments, '\n\n'),
TPL_COMPONENT_BODY: TsCodeUtils.mergeFragments(routerBodyFragments, '\n\n'),
TPL_ROUTER_CONTEXT: routerContext,
TPL_ROUTER_PROVIDER: tsTemplate `<${routerProvider} router={router} ${routeProviderInitializers.size > 0
? tsTemplate `context={routerContext}`
: ''} />`,
},
}));
await builder.apply(renderers.rootRoute.render({
variables: {
TPL_ROOT_ROUTE_CONTEXT: rootContextFields.length > 0
? TsCodeUtils.mergeFragmentsAsInterfaceContent(new Map(sortedRootContextFields.map((field) => [
field.optional ? `${field.name}?` : field.name,
field.type,
])))
: 'placeholder?: string',
TPL_ROOT_ROUTE_OPTIONS: TsCodeUtils.mergeFragmentsAsObject({
component: rootLayoutComponent,
}),
},
}));
if (renderPlaceholderIndex) {
await builder.apply(renderers.placeholderIndex.render({}));
}
await builder.apply(renderers.routeTree.render({}));
},
};
},
}),
}),
});
//# sourceMappingURL=react-router.generator.js.map