UNPKG

@baseplate-dev/sync

Version:

Library for syncing Baseplate descriptions

231 lines 11.1 kB
import { enhanceErrorWithContext } from '@baseplate-dev/utils'; import { groupBy, uniq } from 'es-toolkit'; import { loadIgnorePatterns } from '#src/utils/ignore-patterns.js'; import { readTemplateInfoFiles } from '../metadata/read-template-info-files.js'; import { templateConfigSchema } from './configs/extractor-config.schema.js'; import { TemplateExtractorConfigLookup } from './configs/template-extractor-config-lookup.js'; import { tryCreateExtractorJson } from './configs/try-create-extractor-json.js'; import { initializeTemplateExtractorPlugins } from './runner/initialize-template-extractor-plugins.js'; import { TemplateExtractorApi } from './runner/template-extractor-api.js'; import { TemplateExtractorContext } from './runner/template-extractor-context.js'; import { TemplateExtractorFileContainer } from './runner/template-extractor-file-container.js'; import { cleanupOrphanedTemplates } from './utils/cleanup-orphaned-templates.js'; import { cleanupUnusedTemplateFiles } from './utils/cleanup-unused-template-files.js'; import { mergeExtractorTemplateEntries } from './utils/merge-extractor-template-entries.js'; import { writeExtractorTemplateJsons } from './utils/write-extractor-template-jsons.js'; /** * Run the template file extractors on a target output directory * * @param extractors - The template file extractors to run * @param outputDirectories - The output directories to run the extractors on * @param generatorPackageMap - The map of package names with generators to package paths * @param logger - The logger to use * @param options - The options to use */ export async function runTemplateFileExtractors(templateFileExtractors, outputDirectory, generatorPackageMap, logger, options) { const ignorePatterns = await loadIgnorePatterns(outputDirectory); const { entries: templateMetadataFiles, orphanedEntries } = await readTemplateInfoFiles(outputDirectory, ignorePatterns); const configLookup = new TemplateExtractorConfigLookup(generatorPackageMap); await configLookup.initialize(); // Clean up orphaned templates (file deleted but metadata remains) let orphanedGenerators = []; if (orphanedEntries.length > 0) { logger.info(`Cleaning up ${orphanedEntries.length} orphaned template(s)...`); orphanedGenerators = await cleanupOrphanedTemplates(orphanedEntries, configLookup, logger); } if (options?.autoGenerateExtractor) { const generatorNames = templateMetadataFiles.map((m) => m.templateInfo.generator); const missingGeneratorNames = generatorNames.filter((name) => !configLookup.getExtractorConfig(name)); if (missingGeneratorNames.length > 0) { logger.info(`Auto-generating extractor.json files for ${missingGeneratorNames.length} generators: ${missingGeneratorNames.join(', ')}`); for (const generatorName of missingGeneratorNames) { await tryCreateExtractorJson({ packageMap: generatorPackageMap, generatorName, }); } // Re-initialize the config lookup to pick up the new extractor.json files await configLookup.initialize(); } } // Initialize plugins const fileContainer = new TemplateExtractorFileContainer([ ...generatorPackageMap.values(), ]); const initializerContext = new TemplateExtractorContext({ configLookup, logger, outputDirectory, plugins: new Map(), fileContainer, }); const { hooks, pluginMap } = await initializeTemplateExtractorPlugins({ templateExtractors: templateFileExtractors, context: initializerContext, }); async function runHooks(hook) { for (const hookFn of hooks[hook].toReversed()) { await hookFn(); } } // Create the context for the extractors const context = new TemplateExtractorContext({ configLookup, logger, outputDirectory, plugins: pluginMap, fileContainer, }); // Group files by type (need to look up type from template definition) const filesWithTypeAndMetadata = templateMetadataFiles.map((file) => { const templateConfig = configLookup.getTemplateConfigOrThrow(file.templateInfo.generator, file.templateInfo.template); return { ...file, templateType: templateConfig.type, metadata: templateConfig, }; }); const filesByType = groupBy(filesWithTypeAndMetadata, (f) => f.templateType); // Get the metadata entries for each file const metadataEntries = []; for (const [type, files] of Object.entries(filesByType)) { const extractor = templateFileExtractors.find((e) => e.name === type); if (!extractor) { throw new Error(`No extractor found for template type: ${type}`); } const parsedFiles = files // Only files with instanceData are extractable .filter((f) => f.templateInfo.instanceData !== undefined) .map((f) => { const { absolutePath: path, templateInfo, metadata, modifiedTime } = f; try { return { absolutePath: path, templateName: templateInfo.template, generatorName: templateInfo.generator, existingMetadata: metadata, instanceData: extractor.templateInstanceDataSchema ? extractor.templateInstanceDataSchema.parse(templateInfo.instanceData) : {}, modifiedTime, }; } catch (err) { throw enhanceErrorWithContext(err, `Error parsing instance data for ${path}`); } }); const api = new TemplateExtractorApi(context, type); const newEntries = await extractor.extractTemplateMetadataEntries(parsedFiles, context, api); metadataEntries.push(...newEntries); } await runHooks('afterExtract'); // Merge template entries into extractor configurations mergeExtractorTemplateEntries(metadataEntries, context); // Group metadata entries by type const metadataEntriesByType = groupBy(metadataEntries, (e) => e.metadata.type); for (const [type, entries] of Object.entries(metadataEntriesByType)) { const extractor = templateFileExtractors.find((e) => e.name === type); if (!extractor) { throw new Error(`No extractor found for template type: ${type}`); } const api = new TemplateExtractorApi(context, type); await extractor.writeTemplateFiles(entries, context, api, templateMetadataFiles); const generatorNames = uniq(entries.map((e) => e.generator)); await extractor.writeGeneratedFiles(generatorNames, context, api); } // Write extractor.json files before afterWrite hook so writeTemplateFiles can update extractor config // Include generators modified by orphan cleanup to ensure their configs are saved const generatorNames = uniq([ ...metadataEntries.map((e) => e.generator), ...orphanedGenerators, ]); await writeExtractorTemplateJsons(generatorNames, context); await runHooks('afterWrite'); // Commit the file changes once all the extractors and plugins have written their files await fileContainer.commit(); if (!options?.skipClean) { await cleanupUnusedTemplateFiles(generatorNames, context); } } /** * Generate template files from existing extractor.json configurations without running extraction * * @param templateFileExtractors - The template file extractors to use for generation * @param outputDirectory - The output directory (not used for generation but needed for context) * @param generatorPackageMap - The map of package names with generators to package paths * @param logger - The logger to use * @param options - The options to use */ export async function generateTemplateFiles(templateFileExtractors, generatorPackageMap, logger, options) { // Initialize config lookup from existing extractor.json files const configLookup = new TemplateExtractorConfigLookup(generatorPackageMap); await configLookup.initialize(); // Initialize plugins const fileContainer = new TemplateExtractorFileContainer([ ...generatorPackageMap.values(), ]); const initializerContext = new TemplateExtractorContext({ configLookup, logger, plugins: new Map(), fileContainer, }); const { hooks, pluginMap } = await initializeTemplateExtractorPlugins({ templateExtractors: templateFileExtractors, context: initializerContext, }); async function runHooks(hook) { for (const hookFn of hooks[hook].toReversed()) { await hookFn(); } } // Create the context for the extractors const context = new TemplateExtractorContext({ configLookup, logger, plugins: pluginMap, fileContainer, }); // Get all generator configurations and group them by template type const allGeneratorNames = []; const generatorsByType = new Map(); // Get all unique template types from all extractors const allTemplateTypes = new Set(); for (const extractor of templateFileExtractors) { allTemplateTypes.add(extractor.name); } // For each template type, get all generators that have templates of that type for (const templateType of allTemplateTypes) { const generatorConfigs = configLookup.getGeneratorConfigsForExtractorType(templateType, templateConfigSchema); const generatorNames = generatorConfigs .filter((config) => Object.keys(config.templates).length > 0) .map((config) => config.generatorName); if (generatorNames.length > 0) { generatorsByType.set(templateType, generatorNames); } // Add generator names to the complete list for (const name of generatorNames) { if (!allGeneratorNames.includes(name)) { allGeneratorNames.push(name); } } } // Generate files for each extractor type for (const [templateType, generatorNames] of generatorsByType) { const extractor = templateFileExtractors.find((e) => e.name === templateType); if (!extractor) { logger.warn(`No extractor found for template type: ${templateType}, skipping...`); continue; } const api = new TemplateExtractorApi(context, templateType); await extractor.writeGeneratedFiles(generatorNames, context, api); } await runHooks('afterWrite'); // Commit the file changes once all the extractors and plugins have written their files await fileContainer.commit(); if (!options?.skipClean) { await cleanupUnusedTemplateFiles(allGeneratorNames, context); } logger.info('Template file generation completed'); } //# sourceMappingURL=run-template-file-extractors.js.map