import { compile, readSchema } from '../index'; import fs from 'fs'; import path from 'path'; import { glob } from 'glob'; import minimatch from 'minimatch'; import yargs from 'yargs/yargs'; import * as M from '../meta'; import { SchemaSyntaxError } from '../error'; import chalk from 'chalk'; import { formatPosition } from '@preserves/core'; export type CommandLineArguments = { input: string; base: string | undefined; output: string | undefined; core: string; watch: boolean; }; export type CompilationResult = { options: CommandLineArguments, inputFiles: Array, failures: Array<[string, Error]>, base: string, output: string, }; export type InputFile = { inputFilePath: string, outputFilePath: string, schemaPath: M.ModulePath, schema: M.Schema, }; export function computeBase(paths: string[]): string { if (paths.length === 0) { return ''; } else if (paths.length === 1) { return path.dirname(paths[0]) + '/'; } else { let i = 0; while (true) { let ch: string | null = null for (const p of paths) { if (i >= p.length) return p.slice(0, i); if (ch === null) ch = p[i]; if (p[i] !== ch) return p.slice(0, i); } i++; } } } export function run(options: CommandLineArguments): void { if (!options.watch) { if (runOnce(options).failures.length !== 0) { process.exit(1); } } else { function runWatch() { console.clear(); console.log(chalk.yellow(new Date().toISOString()) + ' Compiling Schemas in watch mode...\n'); const r = runOnce(options); const hasErrors = r.failures.length > 0; const errorSummary = (hasErrors ? chalk.redBright : chalk.greenBright)( `${r.failures.length} error(s)`); console.log(chalk.yellow(new Date().toISOString()) + ` Processed ${r.inputFiles.length} file(s) with ${errorSummary}. Waiting for changes.`); const watcher = fs.watch(r.base, { recursive: true }, (_event, filename) => { filename = path.join(r.base, filename); if (minimatch(filename, options.input)) { watcher.close(); runWatch(); } }); } runWatch(); } } function changeExt(p: string, newext: string): string { return p.slice(0, -path.extname(p).length) + newext; } export function modulePathTo(file1: string, file2: string): string | null { if (file1 === file2) return null; let naive = path.relative(path.dirname(file1), file2); if (naive[0] !== '.' && naive[0] !== '/') naive = './' + naive; return changeExt(naive, ''); } export function runOnce(options: CommandLineArguments): CompilationResult { const matches = glob.sync(options.input); const failures: Array<[string, Error]> = []; const base = options.base ?? computeBase(matches); const output = options.output ?? base; const inputFiles: Array = matches.flatMap(inputFilePath => { if (!inputFilePath.startsWith(base)) { throw new Error(`Input filename ${inputFilePath} falls outside base ${base}`); } const relPath = inputFilePath.slice(base.length); const outputFilePath = path.join(output, changeExt(relPath, '.ts')); try { const src = fs.readFileSync(inputFilePath, 'utf-8'); const schema = readSchema(src, { name: inputFilePath }); const schemaPath = relPath.split('/').map(p => p.split('.')[0]).map(Symbol.for); return [{ inputFilePath, outputFilePath, schemaPath, schema }]; } catch (e) { failures.push([inputFilePath, e]); return []; } }); inputFiles.forEach(c => { const env: M.Environment = inputFiles.map(cc => ({ schema: cc.schema, schemaModulePath: cc.schemaPath, typescriptModulePath: modulePathTo(c.outputFilePath, cc.outputFilePath), })); fs.mkdirSync(path.dirname(c.outputFilePath), { recursive: true }); try { fs.writeFileSync(c.outputFilePath, compile(env, c.schema, options.core), 'utf-8'); } catch (e) { failures.push([c.inputFilePath, e]); } }); for (const [inputFile, err] of failures) { if (err instanceof SchemaSyntaxError) { console.log(chalk.blueBright(formatPosition(err.pos ?? inputFile)) + ': ' + err.message); } else { console.log(chalk.blueBright(inputFile) + ': ' + err.message); } } if (failures.length > 0) { console.log(); } return { options, inputFiles, failures, base, output }; } export function main(argv: Array) { const options: CommandLineArguments = yargs(argv) .command('$0 ', 'Compile Preserves schema definitions to TypeScript', yargs => yargs .positional('input', { type: 'string', description: 'Input filename or glob', demandOption: true, }) .option('output', { type: 'string', description: 'Output directory for sources (default: next to sources)', }) .option('base', { type: 'string', description: 'Base directory for sources (default: common prefix)', }) .option('core', { type: 'string', description: 'Import path for @preserves/core', default: '@preserves/core', }) .option('watch', { type: 'boolean', descripion: 'Watch base directory for changes', default: false, }), argv => argv) .argv; options.input = path.normalize(options.input); run(options); }