2021-04-01 19:12:11 +00:00
|
|
|
import { compile } from '../index';
|
2021-03-09 18:29:31 +00:00
|
|
|
import fs from 'fs';
|
2021-03-11 09:56:49 +00:00
|
|
|
import path from 'path';
|
2021-03-11 13:43:06 +00:00
|
|
|
import minimatch from 'minimatch';
|
2021-04-01 19:57:49 +00:00
|
|
|
import { Command } from 'commander';
|
2021-03-11 09:56:49 +00:00
|
|
|
import * as M from '../meta';
|
2021-03-11 13:43:06 +00:00
|
|
|
import chalk from 'chalk';
|
2021-12-13 11:17:12 +00:00
|
|
|
import { is, Position } from '@preserves/core';
|
2021-03-11 22:02:18 +00:00
|
|
|
import chokidar from 'chokidar';
|
2022-05-03 15:12:13 +00:00
|
|
|
import { changeExt, Diagnostic, inputToInputGlob, expandInputGlob, formatFailures } from './cli-utils';
|
2021-03-11 09:56:49 +00:00
|
|
|
|
|
|
|
export type CommandLineArguments = {
|
2021-04-01 19:57:49 +00:00
|
|
|
inputs: string[];
|
2021-12-13 11:17:12 +00:00
|
|
|
xrefs: string[];
|
2022-01-22 22:20:35 +00:00
|
|
|
output: string | undefined;
|
2021-03-17 15:14:45 +00:00
|
|
|
stdout: boolean;
|
2021-03-11 09:56:49 +00:00
|
|
|
core: string;
|
2021-03-11 13:43:06 +00:00
|
|
|
watch: boolean;
|
2021-03-11 16:59:40 +00:00
|
|
|
traceback: boolean;
|
|
|
|
module: string[];
|
|
|
|
};
|
|
|
|
|
2021-03-11 13:43:06 +00:00
|
|
|
export type CompilationResult = {
|
|
|
|
options: CommandLineArguments,
|
2021-12-13 11:17:12 +00:00
|
|
|
inputFiles: Array<TranslatedFile>,
|
2021-03-11 16:59:40 +00:00
|
|
|
failures: Array<Diagnostic>,
|
2021-12-13 11:52:24 +00:00
|
|
|
baseDirs: Array<string>,
|
2021-03-11 13:43:06 +00:00
|
|
|
};
|
|
|
|
|
2021-12-13 11:17:12 +00:00
|
|
|
export type TranslatedFile = {
|
2021-12-13 11:52:24 +00:00
|
|
|
inputBaseDir: string,
|
2021-03-11 13:43:06 +00:00
|
|
|
inputFilePath: string,
|
|
|
|
outputFilePath: string,
|
|
|
|
schemaPath: M.ModulePath,
|
|
|
|
schema: M.Schema,
|
2021-03-11 09:56:49 +00:00
|
|
|
};
|
|
|
|
|
2021-03-11 16:59:40 +00:00
|
|
|
function failureCount(type: 'warn' | 'error', r: CompilationResult): number {
|
|
|
|
return r.failures.filter(f => f.type === type).length;
|
|
|
|
}
|
|
|
|
|
2021-03-11 13:43:06 +00:00
|
|
|
export function run(options: CommandLineArguments): void {
|
|
|
|
if (!options.watch) {
|
2021-03-11 16:59:40 +00:00
|
|
|
if (failureCount('error', runOnce(options)) > 0) {
|
2021-03-11 13:43:06 +00:00
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
function runWatch() {
|
|
|
|
console.clear();
|
2021-03-11 16:59:40 +00:00
|
|
|
console.log(chalk.gray(new Date().toISOString()) +
|
2021-03-11 13:43:06 +00:00
|
|
|
' Compiling Schemas in watch mode...\n');
|
|
|
|
const r = runOnce(options);
|
2021-03-11 16:59:40 +00:00
|
|
|
const warningCount = failureCount('warn', r);
|
|
|
|
const errorCount = failureCount('error', r);
|
|
|
|
const wMsg = (warningCount > 0) && chalk.yellowBright(`${warningCount} warning(s)`);
|
|
|
|
const eMsg = (errorCount > 0) && chalk.redBright(`${errorCount} error(s)`);
|
|
|
|
const errorSummary =
|
|
|
|
(wMsg && eMsg) ? `with ${eMsg} and ${wMsg}` :
|
|
|
|
(wMsg) ? `with ${wMsg}` :
|
|
|
|
(eMsg) ? `with ${eMsg}` :
|
|
|
|
chalk.greenBright('successfully');
|
|
|
|
console.log(chalk.gray(new Date().toISOString()) +
|
|
|
|
` Processed ${r.inputFiles.length} file(s) ${errorSummary}. Waiting for changes.`);
|
2021-12-13 11:52:24 +00:00
|
|
|
let triggered = false;
|
|
|
|
const watchers = r.baseDirs.map(base => chokidar.watch(base, {
|
2021-03-11 22:02:18 +00:00
|
|
|
ignoreInitial: true,
|
|
|
|
}).on('all', (_event, filename) => {
|
2022-05-03 15:12:13 +00:00
|
|
|
const relevant = options.inputs.some(i => {
|
|
|
|
const { baseDir, glob } = inputToInputGlob(i);
|
|
|
|
return minimatch(filename, baseDir + glob);
|
|
|
|
});
|
|
|
|
if (relevant) {
|
2021-12-13 11:52:24 +00:00
|
|
|
if (!triggered) {
|
|
|
|
triggered = true;
|
|
|
|
watchers.map(w => w.close());
|
|
|
|
runWatch();
|
|
|
|
}
|
2021-03-11 13:43:06 +00:00
|
|
|
}
|
2021-12-13 11:52:24 +00:00
|
|
|
}));
|
2021-03-11 13:43:06 +00:00
|
|
|
}
|
|
|
|
runWatch();
|
|
|
|
}
|
|
|
|
}
|
2021-03-11 09:56:49 +00:00
|
|
|
|
2021-10-11 10:56:53 +00:00
|
|
|
export function modulePathTo(file1: string, file2: string): string {
|
2021-03-11 13:43:06 +00:00
|
|
|
let naive = path.relative(path.dirname(file1), file2);
|
|
|
|
if (naive[0] !== '.' && naive[0] !== '/') naive = './' + naive;
|
|
|
|
return changeExt(naive, '');
|
|
|
|
}
|
2021-03-11 09:56:49 +00:00
|
|
|
|
2021-12-12 12:49:50 +00:00
|
|
|
function isAbsoluteOrExplicitlyRelative(p: string) {
|
|
|
|
return p[0] === '.' || p[0] === '/';
|
|
|
|
}
|
|
|
|
|
2021-03-11 13:43:06 +00:00
|
|
|
export function runOnce(options: CommandLineArguments): CompilationResult {
|
2021-12-13 11:52:24 +00:00
|
|
|
const { baseDirs, failures, inputFiles: inputFiles0, xrefFiles: xrefFiles0 } =
|
|
|
|
expandInputGlob(options.inputs, options.xrefs);
|
2021-03-11 13:43:06 +00:00
|
|
|
|
2021-12-13 11:17:12 +00:00
|
|
|
let xrefFiles = xrefFiles0; // filtered during construction of extensionEnv
|
|
|
|
|
2021-03-11 16:59:40 +00:00
|
|
|
const extensionEnv: M.Environment = options.module.map(arg => {
|
|
|
|
const i = arg.indexOf('=');
|
2021-12-12 21:58:00 +00:00
|
|
|
if (i === -1) throw new Error(`--module argument must be Namespace=path or Namespace=path:expr; got ${arg}`);
|
2021-03-11 16:59:40 +00:00
|
|
|
const ns = arg.slice(0, i);
|
2021-12-12 21:58:00 +00:00
|
|
|
const pathAndExpr = arg.slice(i + 1);
|
|
|
|
const j = pathAndExpr.lastIndexOf(':');
|
|
|
|
const path = (j === -1) ? pathAndExpr : pathAndExpr.slice(0, j);
|
2021-12-13 11:17:12 +00:00
|
|
|
const modulePath = ns.split('.').map(Symbol.for);
|
2021-12-12 21:58:00 +00:00
|
|
|
const expr = (j === -1) ? null : pathAndExpr.slice(j + 1);
|
2021-12-13 11:17:12 +00:00
|
|
|
const e = xrefFiles.find(x => is(x.modulePath, modulePath));
|
|
|
|
if (e) xrefFiles = xrefFiles.filter(e0 => !Object.is(e0, e));
|
2021-03-11 16:59:40 +00:00
|
|
|
return {
|
2021-12-13 11:17:12 +00:00
|
|
|
schema: e ? e.schema : null,
|
|
|
|
schemaModulePath: modulePath,
|
2021-03-11 16:59:40 +00:00
|
|
|
typescriptModulePath: path,
|
2021-12-12 21:58:00 +00:00
|
|
|
typescriptModuleExpr: expr,
|
2021-03-11 16:59:40 +00:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2021-12-13 11:17:12 +00:00
|
|
|
const inputFiles: Array<TranslatedFile> = inputFiles0.map(i => {
|
2021-12-13 11:52:24 +00:00
|
|
|
const { inputBaseDir, inputFilePath, baseRelPath, modulePath, schema } = i;
|
2022-01-22 22:20:35 +00:00
|
|
|
const outputFilePath = path.join(options.output ?? '.', changeExt(baseRelPath, '.ts'));
|
2021-12-13 11:52:24 +00:00
|
|
|
return { inputBaseDir, inputFilePath, outputFilePath, schemaPath: modulePath, schema };
|
2021-03-11 13:43:06 +00:00
|
|
|
});
|
2021-03-11 09:56:49 +00:00
|
|
|
|
2021-03-11 13:43:06 +00:00
|
|
|
inputFiles.forEach(c => {
|
2021-03-11 16:59:40 +00:00
|
|
|
const env: M.Environment = [
|
|
|
|
... extensionEnv.flatMap(e => {
|
2021-12-12 12:49:50 +00:00
|
|
|
const p = isAbsoluteOrExplicitlyRelative(e.typescriptModulePath)
|
|
|
|
? modulePathTo(c.outputFilePath, e.typescriptModulePath)
|
|
|
|
: e.typescriptModulePath /* assuming it names something in node_modules */;
|
2021-03-11 16:59:40 +00:00
|
|
|
if (p === null) return [];
|
|
|
|
return [{... e, typescriptModulePath: p}];
|
|
|
|
}),
|
2021-12-13 11:17:12 +00:00
|
|
|
... xrefFiles.map(x => ({
|
|
|
|
schema: x.schema,
|
|
|
|
schemaModulePath: x.modulePath,
|
|
|
|
typescriptModulePath: 'xref-typescript-modules-not-available',
|
|
|
|
typescriptModuleExpr: null,
|
|
|
|
})),
|
2021-03-11 16:59:40 +00:00
|
|
|
... inputFiles.map(cc => ({
|
|
|
|
schema: cc.schema,
|
|
|
|
schemaModulePath: cc.schemaPath,
|
|
|
|
typescriptModulePath: modulePathTo(c.outputFilePath, cc.outputFilePath),
|
2021-12-12 21:58:00 +00:00
|
|
|
typescriptModuleExpr: null,
|
2021-03-11 16:59:40 +00:00
|
|
|
})),
|
|
|
|
];
|
2021-03-11 13:43:06 +00:00
|
|
|
fs.mkdirSync(path.dirname(c.outputFilePath), { recursive: true });
|
2021-03-17 15:14:45 +00:00
|
|
|
let compiledModule;
|
2021-03-11 13:43:06 +00:00
|
|
|
try {
|
2021-10-11 10:56:53 +00:00
|
|
|
compiledModule = compile(env, c.schemaPath, c.schema, {
|
2021-03-11 16:59:40 +00:00
|
|
|
preservesModule: options.core,
|
|
|
|
warn: (message: string, pos: Position | null) =>
|
|
|
|
failures.push({ type: 'warn', file: c.inputFilePath, detail: { message, pos } }),
|
2021-03-17 15:14:45 +00:00
|
|
|
});
|
2021-03-11 13:43:06 +00:00
|
|
|
} catch (e) {
|
2021-12-03 13:42:25 +00:00
|
|
|
failures.push({ type: 'error', file: c.inputFilePath, detail: e as Error });
|
2021-03-11 13:43:06 +00:00
|
|
|
}
|
2021-03-17 15:14:45 +00:00
|
|
|
if (compiledModule !== void 0) {
|
|
|
|
if (options.stdout) {
|
|
|
|
console.log('////------------------------------------------------------------');
|
|
|
|
console.log('//// ' + c.outputFilePath);
|
|
|
|
console.log();
|
|
|
|
console.log(compiledModule);
|
|
|
|
} else {
|
|
|
|
fs.writeFileSync(c.outputFilePath, compiledModule, 'utf-8');
|
|
|
|
}
|
|
|
|
}
|
2021-03-11 09:56:49 +00:00
|
|
|
});
|
2021-03-11 13:43:06 +00:00
|
|
|
|
2021-04-01 19:12:11 +00:00
|
|
|
formatFailures(failures, options.traceback);
|
2021-03-11 13:43:06 +00:00
|
|
|
|
2021-12-13 11:52:24 +00:00
|
|
|
return { options, inputFiles, failures, baseDirs };
|
2021-03-11 09:56:49 +00:00
|
|
|
}
|
2021-03-09 18:29:31 +00:00
|
|
|
|
|
|
|
export function main(argv: Array<string>) {
|
2021-04-01 19:57:49 +00:00
|
|
|
new Command()
|
|
|
|
.arguments('[input...]')
|
|
|
|
.description('Compile Preserves schema definitions to TypeScript', {
|
2022-01-22 22:20:35 +00:00
|
|
|
input: 'Input directory, optionally with :<glob> on the end',
|
2021-04-01 19:57:49 +00:00
|
|
|
})
|
2021-12-13 11:17:12 +00:00
|
|
|
.option('--xref <glob>', 'Cross-reference other textual Preserves schema definitions',
|
|
|
|
(glob, prev: string[]) => [... prev, glob], [])
|
2022-01-22 22:20:35 +00:00
|
|
|
.option('--output <directory>', 'Output directory for modules')
|
2021-04-01 19:57:49 +00:00
|
|
|
.option('--stdout', 'Prints each module to stdout one after the other instead ' +
|
|
|
|
'of writing them to files in the `--output` directory')
|
|
|
|
.option('--core <path>', 'Import path for @preserves/core', '@preserves/core')
|
|
|
|
.option('--watch', 'Watch base directory for changes')
|
|
|
|
.option('--traceback', 'Include stack traces in compiler errors')
|
|
|
|
.option('--module <namespace=path>', 'Additional Namespace=path import',
|
|
|
|
(nsPath: string, previous: string[]): string[] => [... previous, nsPath],
|
|
|
|
[])
|
|
|
|
.action((inputs: string[], rawOptions) => {
|
2022-01-22 22:20:35 +00:00
|
|
|
if ((rawOptions.output === void 0 && !rawOptions.stdout) ||
|
|
|
|
(rawOptions.output !== void 0 && rawOptions.stdout))
|
|
|
|
{
|
|
|
|
throw new Error("Either --output or --stdout (but not both) must be supplied.");
|
|
|
|
}
|
2021-04-01 19:57:49 +00:00
|
|
|
const options: CommandLineArguments = {
|
|
|
|
inputs: inputs.map(i => path.normalize(i)),
|
2021-12-13 11:17:12 +00:00
|
|
|
xrefs: rawOptions.xref.map((x: string) => path.normalize(x)),
|
2021-04-01 19:57:49 +00:00
|
|
|
output: rawOptions.output,
|
|
|
|
stdout: rawOptions.stdout,
|
|
|
|
core: rawOptions.core,
|
|
|
|
watch: rawOptions.watch,
|
|
|
|
traceback: rawOptions.traceback,
|
|
|
|
module: rawOptions.module,
|
|
|
|
};
|
|
|
|
Error.stackTraceLimit = Infinity;
|
|
|
|
run(options);
|
|
|
|
})
|
|
|
|
.parse(argv, { from: 'user' });
|
2021-03-09 18:29:31 +00:00
|
|
|
}
|