nav-new-publication-cli
Version:
Add new publication to the navigator web project
505 lines (464 loc) • 18.4 kB
text/typescript
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);
}
}