diff --git a/.changeset/enable_noOverwriteGlobs.md b/.changeset/enable_noOverwriteGlobs.md new file mode 100644 index 000000000..b9e40ab44 --- /dev/null +++ b/.changeset/enable_noOverwriteGlobs.md @@ -0,0 +1,5 @@ +--- +"@asyncapi/generator": minor +--- + +Enable `noOverwriteGlobs` option for templates based on react rendering engine. diff --git a/.gitignore b/.gitignore index ecb69486e..a53c05bd1 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ coverage # Turbo .turbo +/.idea diff --git a/apps/generator/lib/generator.js b/apps/generator/lib/generator.js index 0e66e6f8f..a8fb6c102 100644 --- a/apps/generator/lib/generator.js +++ b/apps/generator/lib/generator.js @@ -852,7 +852,7 @@ class Generator { if (renderContent === undefined) { return; } else if (isReactTemplate(this.templateConfig)) { - await saveRenderedReactContent(renderContent, outputpath); + await saveRenderedReactContent(renderContent, outputpath, this.noOverwriteGlobs); } else { await writeFile(outputpath, renderContent); } diff --git a/apps/generator/lib/logMessages.js b/apps/generator/lib/logMessages.js index 40a0e77ce..2d6e855fa 100644 --- a/apps/generator/lib/logMessages.js +++ b/apps/generator/lib/logMessages.js @@ -6,22 +6,22 @@ const NODE_MODULES_INSTALL ='Remember that your local template must have its own const NPM_INSTALL_TRIGGER = 'Installation of template located on disk technically means symlink creation betweed node_modules of the generator and template sources. Your local template must have its own node_modules, "npm install" is not triggered.'; -function templateVersion(ver) { +function templateVersion(ver) { return `Version of used template is ${ver}.`; -} +} function templateSource(localHtmlTemplate) { return `Template sources taken from ${localHtmlTemplate}.`; -} +} function templateNotFound(templateName) { return `${templateName} not found in local dependencies but found it installed as a global package.`; -} +} function packageNotAvailable(packageDetails) { if (packageDetails && packageDetails.pkgPath) { return `Unable to resolve template location at ${packageDetails.pkgPath}. Package is not available locally.`; - } + } return `Template is not available locally and expected location is undefined. Known details are: ${JSON.stringify(packageDetails, null, 2)}`; } @@ -38,6 +38,10 @@ function relativeSourceFileNotGenerated(relativeSourceFile , subject) { return `${relativeSourceFile} was not generated because ${subject} specified in template configuration in conditionalFiles was not found in provided AsyncAPI specification file.`; } +function skipOverwrite(testFilePath) { + return `Skipping overwrite for: ${testFilePath}`; +} + function conditionalFilesMatched(relativeSourceFile) { return `${relativeSourceFile} was not generated because condition specified for this file in template configuration in conditionalFiles matched.`; } @@ -54,6 +58,6 @@ module.exports = { installationDebugMessage, templateSuccessfullyInstalled, relativeSourceFileNotGenerated, - conditionalFilesMatched - -}; \ No newline at end of file + conditionalFilesMatched, + skipOverwrite +}; diff --git a/apps/generator/lib/renderer/react.js b/apps/generator/lib/renderer/react.js index ab139254a..1a21c203c 100644 --- a/apps/generator/lib/renderer/react.js +++ b/apps/generator/lib/renderer/react.js @@ -1,5 +1,8 @@ const path = require('path'); const AsyncReactSDK = require('@asyncapi/generator-react-sdk'); +const minimatch = require('minimatch'); +const logMessage = require('../logMessages.js'); +const log = require('loglevel'); const { writeFile } = require('../utils'); @@ -8,7 +11,7 @@ const reactExport = module.exports; /** * Configures React templating system, this handles all the transpilation work. - * + * * @private * @param {string} templateLocation located for thetemplate * @param {string} templateContentDir where the template content are located @@ -23,9 +26,9 @@ reactExport.configureReact = async (templateLocation, templateContentDir, transp /** * Renders the template with react and returns the content and meta data for the file. - * + * * @private - * @param {AsyncAPIDocument} asyncapiDocument + * @param {AsyncAPIDocument} asyncapiDocument * @param {string} filePath path to the template file * @param {Object} extraTemplateData Extra data to pass to the template. * @param {string} templateLocation located for thetemplate @@ -33,34 +36,34 @@ reactExport.configureReact = async (templateLocation, templateContentDir, transp * @param {string} transpiledTemplateLocation folder for the transpiled code * @param {Object} templateParams provided template parameters * @param {boolean} debug flag - * @param {string} originalAsyncAPI + * @param {string} originalAsyncAPI * @return {Promise} */ reactExport.renderReact = async (asyncapiDocument, filePath, extraTemplateData, templateLocation, templateContentDir, transpiledTemplateLocation, templateParams, debug, originalAsyncAPI) => { extraTemplateData = extraTemplateData || {}; filePath = filePath.replace(templateContentDir, path.resolve(templateLocation, transpiledTemplateLocation)); return await AsyncReactSDK.renderTemplate( - filePath, + filePath, { asyncapi: asyncapiDocument, params: templateParams, originalAsyncAPI, ...extraTemplateData - }, + }, debug ); }; /** * Save the single rendered react content based on the meta data available. - * + * * @private * @param {TemplateRenderResult} renderedContent the react content rendered * @param {String} outputPath Path to the file being rendered. */ -const saveContentToFile = async (renderedContent, outputPath) => { +const saveContentToFile = async (renderedContent, outputPath, noOverwriteGlobs = []) => { let filePath = outputPath; - // Might be the same as in the `fs` package, but is an active choice for our default file permission for any rendered files. + // Might be the same as in the `fs` package, but is an active choice for our default file permission for any rendered files. let permissions = 0o666; const content = renderedContent.content; @@ -78,21 +81,32 @@ const saveContentToFile = async (renderedContent, outputPath) => { } } - await writeFile(filePath, content, { - mode: permissions - }); + // get the final file name of the file + const finalFileName = path.basename(filePath); + // check whether the filename should be ignored based on user's inputs + const shouldOverwrite = !noOverwriteGlobs.some(globExp => minimatch(finalFileName, globExp)); + + // Write the file only if it should not be skipped + if (shouldOverwrite) { + await writeFile(filePath, content, { + mode: permissions + }); + } else { + await log.debug(logMessage.skipOverwrite(filePath)); + } }; /** * Save the rendered react content based on the meta data available. - * + * * @private * @param {TemplateRenderResult[] | TemplateRenderResult} renderedContent the react content rendered * @param {String} outputPath Path to the file being rendered. + * @param noOverwriteGlobs Array of globs to skip overwriting files. */ -reactExport.saveRenderedReactContent = async (renderedContent, outputPath) => { +reactExport.saveRenderedReactContent = async (renderedContent, outputPath, noOverwriteGlobs = []) => { if (Array.isArray(renderedContent)) { - return Promise.all(renderedContent.map(content => saveContentToFile(content, outputPath))); + return Promise.all(renderedContent.map(content => saveContentToFile(content, outputPath, noOverwriteGlobs))); } - return saveContentToFile(renderedContent, outputPath); + return await saveContentToFile(renderedContent, outputPath, noOverwriteGlobs); }; diff --git a/apps/generator/package.json b/apps/generator/package.json index dd0f8fc5d..284c2d9a8 100644 --- a/apps/generator/package.json +++ b/apps/generator/package.json @@ -15,9 +15,8 @@ "test": "npm run test:unit && npm run test:integration && npm run test:cli", "test:unit": "jest --coverage --testPathIgnorePatterns=integration --testPathIgnorePatterns=test-project", "test:dev": "npm run test:unit -- --watchAll", - "test:integration": "npm run test:cleanup && jest --testPathPattern=integration --modulePathIgnorePatterns='./__mocks__'", - "test:integration:update": "jest --updateSnapshot --testPathPattern=integration --modulePathIgnorePatterns='./__mocks__'", - "test:cli": "node cli.js ./test/docs/dummy.yml ./test/test-templates/react-template -o test/output --force-write --debug && test -e test/output/test-file.md", + "test:integration": "npm run test:cleanup && jest --testPathPattern=integration --modulePathIgnorePatterns='./__mocks__(?!\\/loglevel\\.js$)'", + "test:integration:update": "jest --updateSnapshot --testPathPattern=integration --modulePathIgnorePatterns='./__mocks__(?!\\/loglevel\\.js$)'", "test:cli": "node cli.js ./test/docs/dummy.yml ./test/test-templates/react-template -o test/output --force-write --debug && test -e test/output/test-file.md", "test:cleanup": "rimraf \"test/temp\"", "docs": "jsdoc2md --partial docs/jsdoc2md-handlebars/custom-sig-name.hbs docs/jsdoc2md-handlebars/main.hbs docs/jsdoc2md-handlebars/docs.hbs docs/jsdoc2md-handlebars/header.hbs docs/jsdoc2md-handlebars/defaultvalue.hbs docs/jsdoc2md-handlebars/link.hbs docs/jsdoc2md-handlebars/params-table.hbs --files lib/generator.js > docs/api.md", "docker:build": "docker build -t asyncapi/generator:latest .", diff --git a/apps/generator/test/integration.test.js b/apps/generator/test/integration.test.js index 81c9f4a67..d627ca9e7 100644 --- a/apps/generator/test/integration.test.js +++ b/apps/generator/test/integration.test.js @@ -2,7 +2,7 @@ * @jest-environment node */ -const { readFile } = require('fs').promises; +const { mkdir, writeFile, readFile } = require('fs').promises; const path = require('path'); const Generator = require('../lib/generator'); const dummySpecPath = path.resolve(__dirname, './docs/dummy.yml'); @@ -24,7 +24,7 @@ describe('Integration testing generateFromFile() to make sure the result of the it('generated using Nunjucks template', async () => { const outputDir = generateFolderName(); - const generator = new Generator(nunjucksTemplate, outputDir, { + const generator = new Generator(nunjucksTemplate, outputDir, { forceWrite: true, templateParams: { version: 'v1', mode: 'production' } }); @@ -35,7 +35,7 @@ describe('Integration testing generateFromFile() to make sure the result of the it('generate using React template', async () => { const outputDir = generateFolderName(); - const generator = new Generator(reactTemplate, outputDir, { + const generator = new Generator(reactTemplate, outputDir, { forceWrite: true , templateParams: { version: 'v1', mode: 'production' } }); @@ -55,4 +55,33 @@ describe('Integration testing generateFromFile() to make sure the result of the const file = await readFile(path.join(outputDir, testOutputFile), 'utf8'); expect(file).toMatchSnapshot(); }); + + it('should ignore specified files with noOverwriteGlobs', async () => { + const outputDir = generateFolderName(); + // Manually create a file to test if it's not overwritten + await mkdir(outputDir, { recursive: true }); + // Create a variable to store the file content + const testContent = ''; + // eslint-disable-next-line sonarjs/no-duplicate-string + const testFilePath = path.normalize(path.resolve(outputDir, testOutputFile)); + await writeFile(testFilePath, testContent); + + // Manually create an output first, before generation, with additional custom file to validate if later it is still there, not overwritten + const generator = new Generator(reactTemplate, outputDir, { + forceWrite: true, + noOverwriteGlobs: [`**/${testOutputFile}`], + debug: true, + }); + + await generator.generateFromFile(dummySpecPath); + + // Read the file to confirm it was not overwritten + const fileContent = await readFile(testFilePath, 'utf8'); + // Check if the files have been overwritten + expect(fileContent).toBe(testContent); + // Check if the log debug message was printed + /*TODO: + Include log message test in the future to ensure that the log.debug for skipping overwrite is called + */ + }); });