UNPKG

nav-new-publication-cli

Version:

Add new publication to the navigator web project

505 lines (464 loc) 18.4 kB
import * as path from 'path'; import * as fs from 'fs/promises'; import { resolve } from 'path'; import { readFile, writeFile, existsSync } from 'fs-extra'; import { logError, logSuccess } from './utils'; import { PublicationData } from './readWritePublications'; // Configuration for each file that needs to be updated/created interface FileConfig { path: string; // Path in web repo mode: 'append' | 'create' | 'update'; // How to handle the file // Function to find insertion points in the file findInsertionPoints?: (lines: string[], publication: PublicationData) => { [key: string]: number | string[]; }; // Function to generate content for the file generateContent: (publication: PublicationData) => Promise<{ [key: string]: string }> | { [key: string]: string }; } // Configuration for all files that need to be managed const fileConfigs: { [key: string]: FileConfig } = { storybookPreview: { path: '.storybook/preview.js', mode: 'update', findInsertionPoints: (lines: string[], _publication: PublicationData) => { const points: { [key: string]: number } = {}; // Find the last import statement for each section for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('import') && line.includes('css-loader!./themes/') && line.endsWith('.css\';')) { if (line.includes('lazyStyleTag')) { points.lazyImport = i; } else { points.regularImport = i; } } // Find cssList section if (line.includes('const cssList = {')) { points.cssList = i; // Find the closing brace of cssList for (let j = i + 1; j < lines.length; j++) { if (lines[j].trim().startsWith('}')) { points.cssListEnd = j; break; } } } // Find cssVariables.files section if (line.includes('cssVariables: {')) { for (let j = i + 1; j < lines.length; j++) { if (lines[j].includes('files: {')) { points.cssVarFiles = j; // Find the closing brace of files for (let k = j + 1; k < lines.length; k++) { if (lines[k].trim() === '},') { points.cssVarFilesEnd = k; break; } } break; } } } } return points; }, generateContent: async (publication: PublicationData) => { const publicationName = publication.stackAlias || publication.title.toLowerCase().replace(/\s/g, ''); return { lazyImport: `import ${publicationName} from '!!style-loader?injectType=lazyStyleTag!css-loader!./themes/${publicationName}.css';`, regularImport: `import ${publicationName}Css from '!css-loader!./themes/${publicationName}.css';`, cssList: ` ${publicationName}: ${publicationName}Css,`, cssVarFiles: ` ${publicationName}: ${publicationName},` }; } }, localProxy: { path: 'configs/localProxy.config.js', mode: 'update', findInsertionPoints: (lines: string[], _publication: PublicationData) => { const points: { [key: string]: number } = {}; // Find the domainName object for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('const domainName = {')) { // Find the closing brace of domainName for (let j = i + 1; j < lines.length; j++) { if (lines[j].trim() === '};') { points.domainMapEnd = j; break; } } break; } } return points; }, generateContent: async (publication: PublicationData) => { const publicationName = publication.stackAlias || publication.title.toLowerCase().replace(/\s/g, ''); return { domainMapEnd: ` ${publicationName}: '${publication.domain}',` }; } }, publicationJson: { path: 'drone/publication.json', mode: 'update', findInsertionPoints: (lines: string[], publication: PublicationData) => { const points: { [key: string]: number } = {}; try { // Match: "<platform>": [ const keyRe = new RegExp(`^"${publication.platform}"\\s*:\\s*\\[$`); for (let i = 0; i < lines.length; i++) { const trimmed = lines[i].trim(); if (keyRe.test(trimmed)) { // IMPORTANT: insert immediately AFTER the '[' line, // so we appear BEFORE the original first '{' points.platformArray = i; // engine inserts AFTER this line break; } } return points; } catch (error) { logError('Error parsing publication.json', error); return points; } }, generateContent: async (publication: PublicationData) => { interface PublicationEntry { title: string; export: { vr: number; functional: number; }; domain: string; stackAlias?: string; } const newPublication: PublicationEntry = { title: publication.title, export: { vr: publication.vr, functional: publication.func, }, domain: publication.domain, }; if (publication.stackAlias) { newPublication.stackAlias = publication.stackAlias; } // Pretty-print and indent to match array items (4 spaces) const body = JSON.stringify(newPublication, null, 2) .split('\n') .map(l => ' ' + l) .join('\n'); // Insert on its own line, with a trailing comma for separation return { platformArray: `\n${body},\n` }; } }, infrastructureConfig: { path: 'infrastructure/configs/publications/@PUBLICATION@.ts', mode: 'create', generateContent: async (publication: PublicationData) => { const templatePath = path.join(__dirname, '../template/infrastucture.txt'); const template = await fs.readFile(templatePath, 'utf8'); const publicationName = publication.stackAlias || (publication.title ? publication.title.toLowerCase().replace(/\s/g, '') : 'default'); const upperName = publicationName.toUpperCase(); // Replace placeholders with appropriate casing let modifiedTemplate = template; modifiedTemplate = modifiedTemplate .replace(/@PUBLICATION_UPPER@/g, upperName) .replace(/@PUBLICATION_LOWER@/g, publicationName); modifiedTemplate = modifiedTemplate.replace(/@DOMAIN@/g, publication.domain || ''); modifiedTemplate = modifiedTemplate.replace(/@TITLE@/g, publication.title || ''); modifiedTemplate = modifiedTemplate.replace(/@STACKALIAS@/g, publicationName); // Ensure ARNs are properly quoted modifiedTemplate = modifiedTemplate.replace(/@DEVARN@/g, `'${publication.arn?.dev || ''}'`); modifiedTemplate = modifiedTemplate.replace(/@QAVARN@/g, `'${publication.arn?.qa || ''}'`); modifiedTemplate = modifiedTemplate.replace(/@PREPRODARN@/g, `'${publication.arn?.preprod || ''}'`); modifiedTemplate = modifiedTemplate.replace(/@PRODARN@/g, `'${publication.arn?.prod || ''}'`); modifiedTemplate = modifiedTemplate.replace(/@MINCAPACITY@/g, (publication.autoScaling?.min ?? 5).toString()); modifiedTemplate = modifiedTemplate.replace(/@MAXCAPACITY@/g, (publication.autoScaling?.max ?? 20).toString()); return { content: modifiedTemplate, path: `infrastructure/configs/publications/${publicationName}.ts` }; } }, webFargate: { path: 'infrastructure/configs/web-fargate.ts', mode: 'update', findInsertionPoints: (lines: string[], _publication: PublicationData) => { const points: { [key: string]: number } = {}; // Find the first import statement for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('import')) { points.imports = i; break; } } // Find the webConfigData spread for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('const webConfigData = {')) { // Find the closing brace for (let j = i + 1; j < lines.length; j++) { if (lines[j].trim().startsWith('} as const;')) { points.configSpread = j; break; } } break; } } return points; }, generateContent: (publication: PublicationData) => { const publicationName = publication.stackAlias || publication.title.toLowerCase().replace(/\s/g, ''); const upperName = publicationName.toUpperCase(); return { imports: `import ${upperName} from './publications/${publicationName}';`, configSpread: ` ...${upperName},` }; } }, constants: { path: 'src/constants/config.ts', mode: 'update', findInsertionPoints: (lines: string[], _publication: PublicationData) => { const points: { [key: string]: number } = {}; // Find PUBLICATIONS array for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('export const PUBLICATIONS = [')) { points.publications = i; // Find the closing bracket for (let j = i + 1; j < lines.length; j++) { if (lines[j].trim().startsWith('] as const;')) { points.publicationsEnd = j; break; } } } // Find ENVIRONMENTS object if (line.startsWith('export const ENVIRONMENTS = {')) { points.environments = i; // Find the closing brace for (let j = i + 1; j < lines.length; j++) { if (lines[j].trim().startsWith('} as const;')) { points.environmentsEnd = j; break; } } } } return points; }, generateContent: (publication: PublicationData) => { const publicationName = publication.stackAlias || publication.title.toLowerCase().replace(/\s/g, ''); const upperName = publicationName.toUpperCase(); // For publications array, add to the end before closing bracket const publicationsContent = ` '${publicationName}',`; // For environments object, add all environments before closing brace const environmentsContent = [ ` DEV_${upperName}: 'dev_${publicationName}',`, ` QA_${upperName}: 'qa_${publicationName}',`, ` PREPROD_${upperName}: 'preprod_${publicationName}',`, ` PROD_${upperName}: 'prod_${publicationName}',` ].join('\n'); return { publications: publicationsContent, environments: environmentsContent }; } }, publications: { path: 'src/constants/publications.ts', mode: 'update', findInsertionPoints: (lines: string[], _publication: PublicationData) => { const points: { [key: string]: number } = {}; // Find the interface definition for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('interface PublicationConstants {')) { points.interface = i; // Find the closing brace of interface for (let j = i + 1; j < lines.length; j++) { if (lines[j].trim() === '}') { points.interfaceEnd = j; break; } } } // Find PUBLICATIONS constant if (line.startsWith('const PUBLICATIONS: Readonly<PublicationConstants> = {')) { points.publications = i; // Find the closing brace for (let j = i + 1; j < lines.length; j++) { if (lines[j].trim() === '};') { points.publicationsEnd = j; break; } } } } return points; }, generateContent: (publication: PublicationData) => { const publicationName = publication.stackAlias || publication.title.toLowerCase().replace(/\s/g, ''); const upperName = publicationName.toUpperCase(); return { interface: ` ${upperName}: string;`, publications: ` ${upperName}: '${publicationName}',` }; } }, publicationMap: { path: 'src/helpers/utils/publicationMap.ts', mode: 'update', findInsertionPoints: (lines: string[], _publication: PublicationData) => { const points: { [key: string]: number } = {}; // Find the last publicationMap.set line for (let i = lines.length - 1; i >= 0; i--) { const line = lines[i].trim(); if (line.startsWith('publicationMap.set(')) { points.mapSet = i; break; } } return points; }, generateContent: (publication: PublicationData) => { const publicationName = publication.stackAlias || publication.title.toLowerCase().replace(/\s/g, ''); return { mapSet: `publicationMap.set('${publicationName}', '${publication.domain}');` }; } }, publicationsTest: { path: 'tests/unit/constants/publications.test.ts', mode: 'update', findInsertionPoints: (lines: string[], _publication: PublicationData) => { const points: { [key: string]: number } = {}; // Find the expected object entries for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line === 'const expected = {') { // Find all publication entries const entries: string[] = []; for (let j = i + 1; j < lines.length; j++) { const entryLine = lines[j].trim(); if (entryLine === '};') break; if (entryLine.includes(':')) { entries.push(entryLine); } } // Point to insert before the last entry points.insertBefore = i + entries.length; break; } } return points; }, generateContent: (publication: PublicationData) => { const publicationName = publication.stackAlias || publication.title.toLowerCase().replace(/\s/g, ''); const upperName = publicationName.toUpperCase(); return { insertBefore: ` ${upperName}: '${publicationName}',` }; } }, platformsMap: { path: 'src/helpers/utils/platforms.ts', mode: 'update', findInsertionPoints: (lines: string[], publication: PublicationData) => { const points: { [key: string]: number } = {}; // Find the last publicationToPlatform.set line for (let i = lines.length - 1; i >= 0; i--) { if (lines[i].trim().startsWith('publicationToPlatform.set(')) { points.mapSet = i; break; } } return points; }, generateContent: (publication: PublicationData) => { const publicationName = (publication.stackAlias || publication.title.toLowerCase().replace(/\s/g, '')); return { mapSet: `publicationToPlatform.set('${publicationName}', '${publication.platform}');` }; } }, }; export async function updateFile(config: FileConfig, publication: PublicationData) { const currentDir = process.cwd(); const webRepoPath = currentDir.includes('web') ? currentDir : null; if (!webRepoPath) { throw new Error('This script must be run inside the web repository.'); } const publicationName = publication.stackAlias || (publication.title ? publication.title.toLowerCase().replace(/\s/g, '') : 'default'); const resolvedPath = config.path.replace('@PUBLICATION@', publicationName); const fullPath = resolve(webRepoPath, resolvedPath); if (config.mode !== 'create' && !existsSync(fullPath)) { throw new Error(`Required file not found: ${fullPath}`); } try { if (config.mode === 'create') { // Handle file creation const generated = await config.generateContent(publication); const filePath = generated.path || config.path; const finalPath = resolve(webRepoPath, filePath); const contentToWrite = typeof generated.content === 'string' ? generated.content : Object.values(generated).join('\n'); await writeFile(finalPath, contentToWrite); logSuccess(`Created ${filePath}`); return; } const content = await readFile(fullPath, 'utf-8'); let lines = content.split('\n'); if (config.mode === 'append') { const newContent = await config.generateContent(publication); lines.push(...Object.values(newContent)); } else if (config.mode === 'update' && config.findInsertionPoints) { const points = config.findInsertionPoints(lines, publication); const newContent = await config.generateContent(publication); Object.entries(points).forEach(([key, index]) => { if (typeof index !== 'number') return; if (key === 'domainMapEnd') { lines.splice(index, 0, newContent[key]); return; } if (key === 'publications' && points.publicationsEnd) { lines.splice(points.publicationsEnd as number, 0, newContent[key]); return; } if (key === 'environments' && points.environmentsEnd) { lines.splice(points.environmentsEnd as number, 0, newContent[key]); return; } if (key.endsWith('End')) return; if (key === 'cssList' && points.cssListEnd) { const cssListEnd = points.cssListEnd as number; lines.splice(cssListEnd, 0, newContent[key]); } else if (key === 'cssVarFiles' && points.cssVarFilesEnd) { const cssVarFilesEnd = points.cssVarFilesEnd as number; lines.splice(cssVarFilesEnd, 0, newContent[key]); } else { lines.splice(index + 1, 0, newContent[key]); } }); } await writeFile(fullPath, lines.join('\n')); logSuccess(`Updated ${config.path}`); } catch (error) { logError(`Failed to update ${config.path}`, error); throw error; } } export async function updateFiles(publication: PublicationData) { for (const [name, config] of Object.entries(fileConfigs)) { await updateFile(config, publication); } }