-
-
Notifications
You must be signed in to change notification settings - Fork 238
/
cli.js
executable file
·198 lines (177 loc) · 7.6 KB
/
cli.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#!/usr/bin/env node
const path = require('path');
const os = require('os');
const program = require('commander');
const xfs = require('fs.extra');
const { DiagnosticSeverity } = require('@asyncapi/parser/cjs');
const packageInfo = require('./package.json');
const Generator = require('./lib/generator');
const Watcher = require('./lib/watcher');
const { isLocalTemplate, isFilePath } = require('./lib/utils');
const red = text => `\x1b[31m${text}\x1b[0m`;
const magenta = text => `\x1b[35m${text}\x1b[0m`;
const yellow = text => `\x1b[33m${text}\x1b[0m`;
const green = text => `\x1b[32m${text}\x1b[0m`;
let asyncapiDocPath;
let template;
const params = {};
const noOverwriteGlobs = [];
const disabledHooks = {};
const mapBaseUrlToFolder = {};
const parseOutput = dir => path.resolve(dir);
const paramParser = v => {
if (!v.includes('=')) throw new Error(`Invalid param ${v}. It must be in the format of --param name=value.`);
const [paramName, paramValue] = v.split(/=(.+)/, 2);
params[paramName] = paramValue;
return v;
};
const noOverwriteParser = v => noOverwriteGlobs.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>,...');
if (hookNames) {
disabledHooks[hookType] = hookNames.split(/,/);
} else {
disabledHooks[hookType] = true;
}
};
const mapBaseUrlParser = v => {
// Example value for regular expression: https://schema.example.com/crm/:./test/docs/
// it splits on last occurrence of : into the groups all, url and folder
const re = /(.*):(.*)/g;
let mapping = [];
if ((mapping = re.exec(v))===null || mapping.length!==3) {
throw new Error('Invalid --map-base-url flag. A mapping <url>:<folder> with delimiter : expected.');
}
// Folder is without trailing slash, so make sure that url has also no trailing slash:
mapBaseUrlToFolder.url = mapping[1].replace(/\/$/, '');
mapBaseUrlToFolder.folder = path.resolve(mapping[2]);
const isURL = /^https?:/;
if (!isURL.test(mapBaseUrlToFolder.url.toLowerCase())) {
throw new Error('Invalid --map-base-url flag. The mapping <url>:<folder> requires a valid http/https url and valid folder with delimiter `:`.');
}
};
const showError = err => {
console.error(red('Something went wrong:'));
console.error(red(err.stack || err.message));
if (err.diagnostics) {
const errorDiagnostics = err.diagnostics.filter(diagnostic => diagnostic.severity === DiagnosticSeverity.Error);
console.error(red(`Errors:\n${JSON.stringify(errorDiagnostics, undefined, 2)}`));
}
};
const showErrorAndExit = err => {
showError(err);
process.exit(1);
};
program
.version(packageInfo.version)
.arguments('<asyncapi> <template>')
.action((fileLoc, tmpl) => {
asyncapiDocPath = fileLoc;
template = tmpl;
})
.option('-d, --disable-hook [hooks...]', 'disable a specific hook type or hooks from given hook type', disableHooksParser)
.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('-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)')
.option('--watch-template', 'watches the template directory and the AsyncAPI document, and re-generate the files when changes occur. Ignores the output directory. This flag should be used only for template development.')
.option('--map-base-url <url:folder>','maps all schema references from base url to local folder',mapBaseUrlParser)
.parse(process.argv);
if (!asyncapiDocPath) {
console.error(red('> Path or URL to AsyncAPI file not provided.'));
program.help(); // This exits the process
}
const isAsyncapiDocLocal = isFilePath(asyncapiDocPath);
xfs.mkdirp(program.output, async err => {
if (err) return showErrorAndExit(err);
try {
await generate(program.output);
} catch (e) {
return showErrorAndExit(e);
}
// If we want to watch for changes do that
if (program.watchTemplate) {
let watcher;
const watchDir = path.resolve(template);
const outputPath = path.resolve(watchDir, program.output);
const transpiledTemplatePath = path.resolve(watchDir, Generator.TRANSPILED_TEMPLATE_LOCATION);
const ignorePaths = [outputPath, transpiledTemplatePath];
// Template name is needed as it is not always a part of the cli commad
// There is a use case that you run generator from a root of the template with `./` path
const templateName = require(path.resolve(watchDir,'package.json')).name;
if (isAsyncapiDocLocal) {
console.log(`[WATCHER] Watching for changes in the template directory ${magenta(watchDir)} and in the AsyncAPI file ${magenta(asyncapiDocPath)}`);
watcher = new Watcher([asyncapiDocPath, watchDir], ignorePaths);
} else {
console.log(`[WATCHER] Watching for changes in the template directory ${magenta(watchDir)}`);
watcher = new Watcher(watchDir, ignorePaths);
}
// Must check template in its installation path in generator to use isLocalTemplate function
if (!await isLocalTemplate(path.resolve(Generator.DEFAULT_TEMPLATES_DIR, templateName))) {
console.warn(`WARNING: ${template} is a remote template. Changes may be lost on subsequent installations.`);
}
await watcher.watch(watcherHandler, (paths) => {
showErrorAndExit({ message: `[WATCHER] Could not find the file path ${paths}, are you sure it still exists? If it has been deleted or moved please rerun the generator.` });
});
}
});
/**
* Generates the files based on the template.
* @param {*} targetDir The path to the target directory.
*/
function generate(targetDir) {
return new Promise(async (resolve, reject) => {
try {
const generator = new Generator(template, targetDir || path.resolve(os.tmpdir(), 'asyncapi-generator'), {
templateParams: params,
noOverwriteGlobs,
disabledHooks,
forceWrite: program.forceWrite,
install: program.install,
debug: program.debug,
mapBaseUrlToFolder
});
if (isAsyncapiDocLocal) {
await generator.generateFromFile(path.resolve(asyncapiDocPath));
} else {
await generator.generateFromURL(asyncapiDocPath);
}
console.log(green('\n\nDone! ✨'));
console.log(`${yellow('Check out your shiny new generated files at ') + magenta(program.output) + yellow('.')}\n`);
resolve();
} catch (e) {
reject(e);
}
});
}
async function watcherHandler(changedFiles) {
console.clear();
console.log('[WATCHER] Change detected');
for (const [, value] of Object.entries(changedFiles)) {
let eventText;
switch (value.eventType) {
case 'changed':
eventText = green(value.eventType);
break;
case 'removed':
eventText = red(value.eventType);
break;
case 'renamed':
eventText = yellow(value.eventType);
break;
default:
eventText = yellow(value.eventType);
}
console.log(`\t${magenta(value.path)} was ${eventText}`);
}
console.log('Generating files');
try {
await generate(program.output);
} catch (e) {
showError(e);
}
}
process.on('unhandledRejection', showErrorAndExit);