UNPKG

@baseplate-dev/core-generators

Version:

Core generators for Baseplate

250 lines 10.8 kB
import { createTemplateExtractorPlugin, parseGeneratorName, TEMPLATE_EXTRACTOR_GENERATED_DIRECTORY, } from '@baseplate-dev/sync'; import { handleFileNotFoundError } from '@baseplate-dev/utils/node'; import fs from 'node:fs/promises'; import path from 'node:path'; import { Project, QuoteKind, VariableDeclarationKind } from 'ts-morph'; import { getGeneratedTemplateConstantName } from '../utils/generated-template-file-names.js'; export function mergeBarrelExports(indexFileContents, barrelExports) { const project = new Project({ useInMemoryFileSystem: true, manipulationSettings: { quoteKind: QuoteKind.Single, }, }); const sourceFile = project.createSourceFile('index.ts', indexFileContents ?? ''); // Collect existing exports to merge with new ones const existingExports = []; for (const exportDecl of sourceFile.getExportDeclarations()) { const moduleSpecifier = exportDecl.getModuleSpecifierValue(); const isTypeOnly = exportDecl.isTypeOnly(); if (!moduleSpecifier) continue; if (moduleSpecifier.startsWith(`./${TEMPLATE_EXTRACTOR_GENERATED_DIRECTORY}`)) { continue; } // Handle star exports if (exportDecl.getNamedExports().length === 0) { existingExports.push({ moduleSpecifier, namedExports: ['*'], isTypeOnly, }); } else { // Handle named exports const namedExports = exportDecl .getNamedExports() .map((ne) => ne.getName()); if (namedExports.length > 0) { existingExports.push({ moduleSpecifier, namedExports, isTypeOnly, }); } } } // Merge existing exports with new ones const allExports = [...existingExports, ...barrelExports]; // Remove all existing export statements for (const decl of sourceFile.getExportDeclarations()) { decl.remove(); } // Group exports by module specifier and type const exportsByModuleAndType = new Map(); for (const barrelExport of allExports) { const key = barrelExport.moduleSpecifier; if (!exportsByModuleAndType.has(key)) { exportsByModuleAndType.set(key, { namedExports: new Map(), }); } const moduleData = exportsByModuleAndType.get(key); if (!moduleData) { throw new Error(`Module data not found for key: ${key}`); } const isTypeOnly = barrelExport.isTypeOnly ?? false; for (const namedExport of barrelExport.namedExports) { if (namedExport === '*') { // Star export takes precedence moduleData.starExport = { isTypeOnly }; } else { moduleData.namedExports.set(namedExport, isTypeOnly); } } } // Sort module specifiers const sortedModuleSpecifiers = [...exportsByModuleAndType.keys()].toSorted(); // Add exports back in sorted order for (const moduleSpecifier of sortedModuleSpecifiers) { const moduleData = exportsByModuleAndType.get(moduleSpecifier); if (!moduleData) continue; // Add star export first if it exists if (moduleData.starExport) { sourceFile.addExportDeclaration({ moduleSpecifier, isTypeOnly: moduleData.starExport.isTypeOnly, }); } else if (moduleData.namedExports.size > 0) { // Group named exports by isTypeOnly const typeOnlyExports = []; const regularExports = []; for (const [name, isTypeOnly] of moduleData.namedExports) { if (isTypeOnly) { typeOnlyExports.push(name); } else { regularExports.push(name); } } // Add type-only exports if (typeOnlyExports.length > 0) { sourceFile.addExportDeclaration({ moduleSpecifier, isTypeOnly: true, namedExports: typeOnlyExports.toSorted().map((name) => ({ name })), }); } // Add regular exports if (regularExports.length > 0) { sourceFile.addExportDeclaration({ moduleSpecifier, isTypeOnly: false, namedExports: regularExports.toSorted().map((name) => ({ name })), }); } } } return sourceFile.getFullText(); } export function mergeGeneratedBarrelExports(generatorName, indexFileContents, generatedBarrelExports) { const project = new Project({ useInMemoryFileSystem: true, manipulationSettings: { quoteKind: QuoteKind.Single, }, }); const sourceFile = project.createSourceFile('index.ts', indexFileContents ?? ''); // Remove all existing import and export statements for (const decl of sourceFile.getImportDeclarations()) decl.remove(); for (const decl of sourceFile.getExportDeclarations()) decl.remove(); for (const decl of sourceFile.getVariableStatements()) decl.remove(); if (generatedBarrelExports.length === 0) { return sourceFile.getFullText(); } // Group exports by module specifier const importsByModuleSpecifier = new Map(); for (const barrelExport of generatedBarrelExports) { let imports = importsByModuleSpecifier.get(barrelExport.moduleSpecifier); if (!imports) { imports = new Set(); importsByModuleSpecifier.set(barrelExport.moduleSpecifier, imports); } imports.add(barrelExport.namedExport); } // Add import statements (sorted by module specifier) const sortedModuleSpecifiers = [ ...importsByModuleSpecifier.keys(), ].toSorted(); for (const moduleSpecifier of sortedModuleSpecifiers) { const imports = importsByModuleSpecifier.get(moduleSpecifier); if (!imports) continue; const namedImports = [...imports].toSorted(); sourceFile.addImportDeclaration({ moduleSpecifier, namedImports, }); } // Create the export constant const constantName = getGeneratedTemplateConstantName(generatorName, 'GENERATED'); // Group exports by their property names const exportProperties = new Map(); for (const barrelExport of generatedBarrelExports) { exportProperties.set(barrelExport.name, barrelExport.namedExport); } // Sort properties by name for consistent output const sortedPropertyNames = [...exportProperties.keys()].toSorted(); const properties = sortedPropertyNames.map((propName) => `${propName}: ${exportProperties.get(propName)}`); sourceFile.addVariableStatement({ isExported: true, declarationKind: VariableDeclarationKind.Const, declarations: [ { name: constantName, initializer: `{\n ${properties.join(',\n ')},\n}`, }, ], }); return sourceFile.getFullText(); } export const templateExtractorBarrelExportPlugin = createTemplateExtractorPlugin({ name: 'barrel-export', getInstance: ({ context, api }) => { const { fileContainer } = context; const barrelExportMap = new Map(); const generatedBarrelExportMap = new Map(); function addBarrelExport(generatorName, barrelExport) { const barrelExports = barrelExportMap.get(generatorName) ?? []; barrelExports.push(barrelExport); barrelExportMap.set(generatorName, barrelExports); } function addGeneratedBarrelExport(generatorName, generatedBarrelExport) { const generatedBarrelExports = generatedBarrelExportMap.get(generatorName) ?? []; generatedBarrelExports.push(generatedBarrelExport); generatedBarrelExportMap.set(generatorName, generatedBarrelExports); } // Merge the barrel exports into the barrel file api.registerHook('afterWrite', async () => { // Process regular barrel exports for (const [generatorName, barrelExports] of barrelExportMap) { const parsedGeneratorName = parseGeneratorName(generatorName); const extractorConfig = context.configLookup.getExtractorConfig(generatorName); if (!extractorConfig) { throw new Error(`Extractor config not found: ${generatorName}`); } const indexFileContents = await fs .readFile(path.join(extractorConfig.generatorDirectory, 'index.ts'), 'utf8') .catch(handleFileNotFoundError); const updatedContents = mergeBarrelExports(indexFileContents, [ ...barrelExports, // always export the generator file { moduleSpecifier: `./${parsedGeneratorName.generatorBasename}.generator.js`, namedExports: ['*'], }, ]); await fileContainer.writeFile(path.join(extractorConfig.generatorDirectory, 'index.ts'), updatedContents); } // Process generated barrel exports for (const [generatorName, generatedBarrelExports,] of generatedBarrelExportMap) { const extractorConfig = context.configLookup.getExtractorConfig(generatorName); if (!extractorConfig) { throw new Error(`Extractor config not found: ${generatorName}`); } if (generatedBarrelExports.length === 0) { continue; } // Ensure generated directory exists const generatedDir = path.join(extractorConfig.generatorDirectory, 'generated'); const generatedIndexPath = path.join(generatedDir, 'index.ts'); const generatedIndexFileContents = await fs .readFile(generatedIndexPath, 'utf8') .catch(handleFileNotFoundError); const updatedGeneratedContents = mergeGeneratedBarrelExports(generatorName, generatedIndexFileContents, generatedBarrelExports); await fileContainer.writeFile(generatedIndexPath, updatedGeneratedContents); } }); return { addBarrelExport, addGeneratedBarrelExport, }; }, }); //# sourceMappingURL=barrel-export.js.map