Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/scripts/mailchimp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions apps/generator/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ let asyncapiDocPath;
let template;
const params = {};
const noOverwriteGlobs = [];
const generateOnly = [];
const disabledHooks = {};
const mapBaseUrlToFolder = {};

Expand All @@ -33,6 +34,8 @@ const paramParser = v => {

const noOverwriteParser = v => noOverwriteGlobs.push(v);

const generateOnlyParser = v => generateOnly.push(v);

const disableHooksParser = v => {
const [hookType, hookNames] = v.split(/=/);
if (!hookType) throw new Error('Invalid --disable-hook flag. It must be in the format of: --disable-hook <hookType> or --disable-hook <hookType>=<hookName1>,<hookName2>,...');
Expand Down Expand Up @@ -86,6 +89,7 @@ program
.option('--debug', 'enable more specific errors in the console')
.option('-i, --install', 'installs the template and its dependencies (defaults to false)')
.option('-n, --no-overwrite <glob>', 'glob or path of the file(s) to skip when regenerating', noOverwriteParser)
.option('-g, --generate-only <glob>', 'glob or path of the file(s) to generate. Only files matching patterns will be generated', generateOnlyParser)
.option('-o, --output <outputDir>', 'directory where to put the generated files (defaults to current directory)', parseOutput, process.cwd())
.option('-p, --param <name=value>', 'additional param to pass to templates', paramParser)
.option('--force-write', 'force writing of the generated files to given directory even if it is a git repo with unstaged files or not empty dir (defaults to false)')
Expand Down Expand Up @@ -153,6 +157,7 @@ function generate(targetDir) {
const generator = new Generator(template, targetDir || path.resolve(os.tmpdir(), 'asyncapi-generator'), {
templateParams: params,
noOverwriteGlobs,
generateOnly,
disabledHooks,
forceWrite: program.forceWrite,
install: program.install,
Expand Down
45 changes: 38 additions & 7 deletions apps/generator/lib/generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ const {
readFile,
readDir,
writeFile,
writeFileWithFiltering,
copyFile,
copyFileWithFiltering,
exists,
fetchSpec,
isReactTemplate,
Expand All @@ -46,7 +48,7 @@ const DEFAULT_TEMPLATES_DIR = path.resolve(ROOT_DIR, 'node_modules');

const TRANSPILED_TEMPLATE_LOCATION = '__transpiled';
const TEMPLATE_CONTENT_DIRNAME = 'template';
const GENERATOR_OPTIONS = ['debug', 'disabledHooks', 'entrypoint', 'forceWrite', 'install', 'noOverwriteGlobs', 'output', 'templateParams', 'mapBaseUrlToFolder', 'url', 'auth', 'token', 'registry', 'compile'];
const GENERATOR_OPTIONS = new Set(['debug', 'disabledHooks', 'entrypoint', 'forceWrite', 'install', 'noOverwriteGlobs', 'generateOnly', 'output', 'templateParams', 'mapBaseUrlToFolder', 'url', 'auth', 'token', 'registry', 'compile']);
const logMessage = require('./logMessages');

const shouldIgnoreFile = filePath =>
Expand Down Expand Up @@ -78,6 +80,7 @@ class Generator {
* @param {Object<string, string>} [options.templateParams] Optional parameters to pass to the template. Each template define their own params.
* @param {String} [options.entrypoint] Name of the file to use as the entry point for the rendering process. Use in case you want to use only a specific template file. Note: this potentially avoids rendering every file in the template.
* @param {String[]} [options.noOverwriteGlobs] List of globs to skip when regenerating the template.
* @param {String[]} [options.generateOnly] List of globs to specify which files should be generated. Only files matching these patterns will be generated.
* @param {Object<String, Boolean | String | String[]>} [options.disabledHooks] Object with hooks to disable. The key is a hook type. If key has "true" value, then the generator skips all hooks from the given type. If the value associated with a key is a string with the name of a single hook, then the generator skips only this single hook name. If the value associated with a key is an array of strings, then the generator skips only hooks from the array.
* @param {String} [options.output='fs'] Type of output. Can be either 'fs' (default) or 'string'. Only available when entrypoint is set.
* @param {Boolean} [options.forceWrite=false] Force writing of the generated files to given directory even if it is a git repo with unstaged files or not empty dir. Default is set to false.
Expand All @@ -91,7 +94,7 @@ class Generator {
* @param {String} [options.registry.token] Optional parameter to pass npm registry auth token that you can grab from .npmrc file
*/

constructor(templateName, targetDir, { templateParams = {}, entrypoint, noOverwriteGlobs, disabledHooks, output = 'fs', forceWrite = false, install = false, debug = false, mapBaseUrlToFolder = {}, registry = {}, compile = true } = {}) {
constructor(templateName, targetDir, { templateParams = {}, entrypoint, noOverwriteGlobs, generateOnly, disabledHooks, output = 'fs', forceWrite = false, install = false, debug = false, mapBaseUrlToFolder = {}, registry = {}, compile = true } = {}) {
const options = arguments[arguments.length - 1];
this.verifyoptions(options);
if (!templateName) throw new Error('No template name has been specified.');
Expand All @@ -109,6 +112,8 @@ class Generator {
this.entrypoint = entrypoint;
/** @type {String[]} List of globs to skip when regenerating the template. */
this.noOverwriteGlobs = noOverwriteGlobs || [];
/** @type {String[]} List of globs to specify which files should be generated. */
this.generateOnly = generateOnly || [];
/** @type {Object<String, Boolean | String | String[]>} Object with hooks to disable. The key is a hook type. If key has "true" value, then the generator skips all hooks from the given type. If the value associated with a key is a string with the name of a single hook, then the generator skips only this single hook name. If the value associated with a key is an array of strings, then the generator skips only hooks from the array. */
this.disabledHooks = disabledHooks || {};
/** @type {String} Type of output. Can be either 'fs' (default) or 'string'. Only available when entrypoint is set. */
Expand All @@ -125,6 +130,8 @@ class Generator {
this.hooks = {};
/** @type {Object} Maps schema URL to folder. */
this.mapBaseUrlToFolder = mapBaseUrlToFolder;
/** @type {number} Counter for successfully generated files. */
this.generatedFilesCount = 0;

// Load template configuration
/** @type {Object} The template parameters. The structure for this object is based on each individual template. */
Expand Down Expand Up @@ -153,7 +160,7 @@ class Generator {

verifyoptions(Options) {
if (typeof Options !== 'object') return [];
const invalidOptions = Object.keys(Options).filter(param => !GENERATOR_OPTIONS.includes(param));
const invalidOptions = Object.keys(Options).filter(param => !GENERATOR_OPTIONS.has(param));

if (invalidOptions.length > 0) {
throw new Error(`These options are not supported by the generator: ${invalidOptions.join(', ')}`);
Expand Down Expand Up @@ -192,13 +199,15 @@ class Generator {
*/
async generate(asyncapiDocument, parseOptions = {}) {
this.validateAsyncAPIDocument(asyncapiDocument);
this.generatedFilesCount = 0;
await this.setupOutput();
this.setLogLevel();

await this.installAndSetupTemplate();
await this.configureTemplateWorkflow(parseOptions);
await this.handleEntrypoint();
await this.executeAfterHook();
this.warnIfNoGenerateOnlyMatches();
}

/**
Expand Down Expand Up @@ -877,9 +886,13 @@ class Generator {
if (renderContent === undefined) {
return;
} else if (isReactTemplate(this.templateConfig)) {
await saveRenderedReactContent(renderContent, outputpath, this.noOverwriteGlobs);
const writtenCount = await saveRenderedReactContent(renderContent, outputpath, this.noOverwriteGlobs, this.generateOnly, this.targetDir);
this.generatedFilesCount += writtenCount;
} else {
await writeFile(outputpath, renderContent);
const written = await writeFileWithFiltering(outputpath, renderContent, {}, this.targetDir, this.noOverwriteGlobs, this.generateOnly);
if (written) {
this.generatedFilesCount += 1;
}
}
}

Expand Down Expand Up @@ -946,8 +959,14 @@ class Generator {

return log.debug(logMessage.conditionalGenerationMatched(conditionalPath));
}

if (this.isNonRenderableFile(relativeSourceFile)) return await copyFile(sourceFile, targetFile);

if (this.isNonRenderableFile(relativeSourceFile)) {
const copied = await copyFileWithFiltering(sourceFile, targetFile, this.targetDir, this.noOverwriteGlobs, this.generateOnly);
if (copied) {
this.generatedFilesCount += 1;
}
return;
}
await this.renderAndWriteToFile(asyncapiDocument, sourceFile, targetFile);
log.debug(`Successfully rendered template and wrote file ${relativeSourceFile} to location: ${targetFile}`);
}
Expand Down Expand Up @@ -1017,6 +1036,18 @@ class Generator {
return !this.noOverwriteGlobs.some(globExp => minimatch(filePath, globExp));
}

/**
* warn when generateOnly is set but no files were generated.
*
* @private
*/
warnIfNoGenerateOnlyMatches() {
if (this.output !== 'fs') return;
if (Array.isArray(this.generateOnly) && this.generateOnly.length > 0 && this.generatedFilesCount === 0) {
log.warn(logMessage.generateOnlyNoMatches(this.generateOnly));
}
}

/**
* Launches all the hooks registered at a given hook point/name.
*
Expand Down
12 changes: 11 additions & 1 deletion apps/generator/lib/logMessages.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ function skipOverwrite(testFilePath) {
return `Skipping overwrite for: ${testFilePath}`;
}

function skipGenerateOnly(filePath) {
return `Skipping file "${filePath}" because it does not match any generateOnly patterns.`;
}

function generateOnlyNoMatches(patterns) {
return `No files matched the generateOnly patterns: ${patterns.join(', ')}`;
}

function conditionalGenerationMatched(conditionalPath) {
return `${conditionalPath} was not generated because condition specified for this location in template configuration in conditionalGeneration matched.`;
}
Expand Down Expand Up @@ -69,5 +77,7 @@ module.exports = {
conditionalGenerationMatched,
conditionalFilesMatched,
compileEnabled,
skipOverwrite
skipOverwrite,
skipGenerateOnly,
generateOnlyNoMatches
};
36 changes: 18 additions & 18 deletions apps/generator/lib/renderer/react.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
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
writeFileWithFiltering
} = require('../utils');

const reactExport = module.exports;
Expand Down Expand Up @@ -62,8 +61,9 @@ reactExport.renderReact = async (asyncapiDocument, filePath, extraTemplateData,
* @private
* @param {TemplateRenderResult} renderedContent the react content rendered
* @param {String} outputPath Path to the file being rendered.
* @param {String} targetDir Target directory for relative path calculations.
*/
const saveContentToFile = async (renderedContent, outputPath, noOverwriteGlobs = []) => {
const saveContentToFile = async (renderedContent, outputPath, noOverwriteGlobs = [], generateOnly = [], targetDir = process.cwd()) => {
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.
let permissions = 0o666;
Expand All @@ -83,19 +83,16 @@ const saveContentToFile = async (renderedContent, outputPath, noOverwriteGlobs =
}
}

// 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));
const written = await writeFileWithFiltering(
filePath,
content,
{ mode: permissions },
targetDir,
noOverwriteGlobs,
generateOnly
);

// 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));
}
return written ? 1 : 0;
};

/**
Expand All @@ -105,10 +102,13 @@ const saveContentToFile = async (renderedContent, outputPath, noOverwriteGlobs =
* @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.
* @param generateOnly array of globs to specify which files should be generated.
* @param targetDir target directory for relative path calculations.
*/
reactExport.saveRenderedReactContent = async (renderedContent, outputPath, noOverwriteGlobs = []) => {
reactExport.saveRenderedReactContent = async (renderedContent, outputPath, noOverwriteGlobs = [], generateOnly = [], targetDir = process.cwd()) => {
if (Array.isArray(renderedContent)) {
return Promise.all(renderedContent.map(content => saveContentToFile(content, outputPath, noOverwriteGlobs)));
const results = await Promise.all(renderedContent.map(content => saveContentToFile(content, outputPath, noOverwriteGlobs, generateOnly, targetDir)));
return results.reduce((acc, val) => acc + (val || 0), 0);
}
return await saveContentToFile(renderedContent, outputPath, noOverwriteGlobs);
return await saveContentToFile(renderedContent, outputPath, noOverwriteGlobs, generateOnly, targetDir);
};
97 changes: 97 additions & 0 deletions apps/generator/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const resolvePkg = require('resolve-pkg');
const resolveFrom = require('resolve-from');
const globalDirs = require('global-dirs');
const log = require('loglevel');
const minimatch = require('minimatch');

const packageJson = require('../package.json');

Expand All @@ -32,6 +33,102 @@ utils.exists = async (path) => {
}
};

/**
* determine if a file should be generated based on generateOnly globs.
* @param {String} relativeFilePath Target-relative file path.
* @param {String[]} generateOnly Globs whitelist.
* @returns {Boolean} True if generation is allowed.
*/
function isAllowedByGenerateOnly(relativeFilePath, generateOnly = []) {
if (!Array.isArray(generateOnly) || generateOnly.length === 0) return true;

let allowed = false;
let excluded = false;

for (const globExp of generateOnly) {
if (typeof globExp !== 'string') continue;
const isNegation = globExp.startsWith('!');
const pattern = isNegation ? globExp.slice(1) : globExp;

if (minimatch(relativeFilePath, pattern)) {
if (isNegation) excluded = true;
else allowed = true;
}
}

return allowed && !excluded;
}

/**
* determine if an existing file should be skipped based on noOverwriteGlobs.
* @param {String} relativeFilePath Target-relative file path.
* @param {Boolean} fileExists Whether the target file already exists.
* @param {String[]} noOverwriteGlobs Globs blacklist for overwriting.
* @returns {Boolean} True if overwrite should be skipped.
*/
function shouldSkipOverwrite(relativeFilePath, fileExists, noOverwriteGlobs = []) {
if (!fileExists) return false;
if (!Array.isArray(noOverwriteGlobs) || noOverwriteGlobs.length === 0) return false;
return noOverwriteGlobs.some(globExp => minimatch(relativeFilePath, globExp));
}

/**
* writes a file with generateOnly and noOverwriteGlobs filtering
*
* @param {String} filePath Absolute path to the file to write.
* @param {String|Buffer} content Content to write.
* @param {Object} options Options to pass to fs.writeFile (e.g., { mode: 0o666 }).
* @param {String} targetDir Target directory for calculating relative paths.
* @param {String[]} noOverwriteGlobs Array of glob patterns for files to skip overwriting.
* @param {String[]} generateOnly Array of glob patterns for files to generate (whitelist).
* @returns {Promise<Boolean>} True if file was written, false if skipped.
*/
utils.writeFileWithFiltering = async (filePath, content, options, targetDir, noOverwriteGlobs = [], generateOnly = []) => {
const relativeFilePath = path.relative(targetDir, filePath);

if (!isAllowedByGenerateOnly(relativeFilePath, generateOnly)) {
log.debug(logMessage.skipGenerateOnly(filePath));
return false;
}

const fileExists = await utils.exists(filePath);
if (shouldSkipOverwrite(relativeFilePath, fileExists, noOverwriteGlobs)) {
log.debug(logMessage.skipOverwrite(filePath));
return false;
}

await utils.writeFile(filePath, content, options);
return true;
};

/**
* copies a file with generateOnly and noOverwriteGlobs filtering.
*
* @param {String} sourcePath Absolute path to the source file.
* @param {String} targetPath Absolute path to the destination file.
* @param {String} targetDir Target directory for calculating relative paths.
* @param {String[]} noOverwriteGlobs Array of glob patterns for files to skip overwriting.
* @param {String[]} generateOnly Array of glob patterns for files to generate (whitelist).
* @returns {Promise<Boolean>} True if file was copied, false if skipped.
*/
utils.copyFileWithFiltering = async (sourcePath, targetPath, targetDir, noOverwriteGlobs = [], generateOnly = []) => {
const relativeFilePath = path.relative(targetDir, targetPath);

if (!isAllowedByGenerateOnly(relativeFilePath, generateOnly)) {
log.debug(logMessage.skipGenerateOnly(targetPath));
return false;
}

const fileExists = await utils.exists(targetPath);
if (shouldSkipOverwrite(relativeFilePath, fileExists, noOverwriteGlobs)) {
log.debug(logMessage.skipOverwrite(targetPath));
return false;
}

await utils.copyFile(sourcePath, targetPath);
return true;
};

/**
* Checks if a string is a filesystem path.
* @private
Expand Down
Loading
Loading