UNPKG

@baseplate-dev/core-generators

Version:

Core generators for Baseplate

142 lines 7.32 kB
import { createTemplateExtractorPlugin, toCanonicalPath, } from '@baseplate-dev/sync'; import { handleFileNotFoundError, posixJoin, readJsonWithSchema, } from '@baseplate-dev/utils/node'; import { camelCase } from 'change-case'; import path from 'node:path'; import { z } from 'zod'; import { normalizeTsPathToJsPath } from '#src/utils/ts-paths.js'; import { templateExtractorBarrelExportPlugin } from '../barrel-export.js'; import { GENERATED_PATHS_FILE_NAME, getPathsFileExportNames, writePathMapFile, } from './paths-file.js'; // Key is the canonical path and value is the path root name const templatePathRootSchema = z.object({ canonicalPath: z.string(), pathRootName: z.string(), }); function getPathsRootExportName(generatorName) { const fileExportNames = getPathsFileExportNames(generatorName); return fileExportNames.rootExportName; } export const TEMPLATE_PATHS_METADATA_FILE = '.paths-metadata.json'; const templatePathsPluginConfigSchema = z.object({ /** * Whether to skip generating the paths task. */ skipTaskGeneration: z.boolean().default(false), }); /** * The template paths plugin is used to enable templates to get assigned a path * relative to their nearest path root, e.g. the feature folder or package root. * * To support such behavior, it is necessary to use the path-metadata generator * which writes a .paths-metadata.json to the output directory. */ export const templatePathsPlugin = createTemplateExtractorPlugin({ name: 'template-paths', pluginDependencies: [templateExtractorBarrelExportPlugin], getInstance: async ({ context, api }) => { const barrelExportPlugin = context.getPlugin(templateExtractorBarrelExportPlugin.name); const templatePathRoots = context.outputDirectory ? await discoverTemplatePathRoots(context.outputDirectory) : undefined; // Key: Generator name, Value: Map of template name to path root relative path const pathMapByGenerator = new Map(); function registerTemplatePathEntry(generatorName, templateName, pathRootRelativePath) { const pathMap = pathMapByGenerator.get(generatorName) ?? new Map(); pathMap.set(camelCase(templateName), pathRootRelativePath); pathMapByGenerator.set(generatorName, pathMap); } /** * Gets the path of a singleton template file relative to the closest file path root. * * @param absolutePath - The absolute path of the template file. * @returns The relative path of the template file to the closest file path root. */ function getPathRootRelativePath(absolutePath) { if (!context.outputDirectory || !templatePathRoots) { throw new Error('Template paths are not supported when running with metadata-only extraction'); } const outputRelativePath = path.relative(context.outputDirectory, absolutePath); return getTemplatePathFromRelativePath(outputRelativePath, templatePathRoots); } /** * Resolves template paths for a given file based on its file options. * * @param fileOptions - The file options containing template path configuration. * @param absolutePath - The absolute path of the template file. * @param templateName - The name of the template (for error messages). * @param generatorName - The name of the generator (for error messages). * @returns An object containing the pathRootRelativePath and generatorTemplatePath. */ function resolveTemplatePaths(fileOptions, absolutePath, templateName, generatorName) { const pathRootRelativePath = fileOptions.kind === 'singleton' ? (fileOptions.pathRootRelativePath ?? getPathRootRelativePath(absolutePath)) : undefined; // By default, singleton templates have the path like `feature-root/services/[file].ts` const generatorTemplatePath = fileOptions.generatorTemplatePath ?? (pathRootRelativePath && getTemplatePathFromPathRootRelativePath(pathRootRelativePath)); if (!generatorTemplatePath) { throw new Error(`Template path is required for ${templateName} in ${generatorName}`); } return { pathRootRelativePath, generatorTemplatePath }; } api.registerHook('afterWrite', async () => { for (const [generatorName, pathMap] of pathMapByGenerator) { const config = context.configLookup.getPluginConfigForGenerator(generatorName, templatePathsPlugin.name, templatePathsPluginConfigSchema); const { exportName } = await writePathMapFile(generatorName, pathMap, context, { skipTaskGeneration: config?.skipTaskGeneration }); barrelExportPlugin.addGeneratedBarrelExport(generatorName, { moduleSpecifier: `./${normalizeTsPathToJsPath(GENERATED_PATHS_FILE_NAME)}`, namedExport: exportName, name: 'paths', }); } }); return { getPathRootRelativePath, getTemplatePathFromPathRootRelativePath, registerTemplatePathEntry, resolveTemplatePaths, getPathsRootExportName, }; }, }); /** * Gets the template path from a path root relative path by stripping the brackets and -root from the path. * * @param pathRootRelativePath - The path root relative path. (e.g. `{feature-root}/services/test.ts`) * @returns The template path. (e.g. `feature/services/[file].ts`) */ function getTemplatePathFromPathRootRelativePath(pathRootRelativePath) { return pathRootRelativePath .replaceAll(/-root(?=})/g, '') .replaceAll(/[{}]/g, ''); } /** * Discovers the template path roots from the paths-metadata.json file. * * @param outputDirectory - The output directory of the project. * @returns The template path roots from longest to shortest. */ async function discoverTemplatePathRoots(outputDirectory) { const pathsMetadataFile = path.join(outputDirectory, TEMPLATE_PATHS_METADATA_FILE); const pathsMetadataContents = await readJsonWithSchema(pathsMetadataFile, z.array(templatePathRootSchema)).catch(handleFileNotFoundError); if (!pathsMetadataContents) return []; return pathsMetadataContents.sort((a, b) => b.canonicalPath.length - a.canonicalPath.length); } /** * Gets the template path from a relative path. * * @param outputRelativePath - The output relative path. * @param templatePathRoots - The template path roots. * @returns The template path. */ function getTemplatePathFromRelativePath(outputRelativePath, templatePathRoots) { const canonicalPath = toCanonicalPath(outputRelativePath); const templatePathRoot = templatePathRoots.find((templatePathRoot) => `${canonicalPath}/`.startsWith(`${templatePathRoot.canonicalPath}/`)); if (!templatePathRoot) { throw new Error(`Could not find template path root for ${canonicalPath}`); } return posixJoin(`{${templatePathRoot.pathRootName}}`, path.posix.relative(templatePathRoot.canonicalPath, canonicalPath)); } //# sourceMappingURL=template-paths.plugin.js.map