diff --git a/implementations/javascript/packages/schema/src/bin/cli-utils.ts b/implementations/javascript/packages/schema/src/bin/cli-utils.ts new file mode 100644 index 0000000..a1e9ca3 --- /dev/null +++ b/implementations/javascript/packages/schema/src/bin/cli-utils.ts @@ -0,0 +1,86 @@ +import fs from 'fs'; +import path from 'path'; +import { glob } from 'glob'; +import { formatPosition, Position } from '@preserves/core'; +import { readSchema } from '../reader'; +import chalk from 'chalk'; + +export interface Diagnostic { + type: 'warn' | 'error'; + file: string | null; + detail: Error | { message: string, pos: Position | null }; +}; + +export function computeBase(paths: string[]): string { + if (paths.length === 0) { + return ''; + } else if (paths.length === 1) { + const d = path.dirname(paths[0]); + return (d === '.') ? '' : d + '/'; + } 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 expandInputGlob(input: string, base0: string | undefined) { + const matches = glob.sync(input); + const base = base0 ?? computeBase(matches); + const failures: Array = []; + + return { + base, + inputFiles: matches.flatMap(inputFilePath => { + if (!inputFilePath.startsWith(base)) { + throw new Error(`Input filename ${inputFilePath} falls outside base ${base}`); + } + try { + const text = fs.readFileSync(inputFilePath, 'utf-8'); + const baseRelPath = inputFilePath.slice(base.length); + const modulePath = baseRelPath.split('/').map(p => p.split('.')[0]).map(Symbol.for); + const schema = readSchema(text, { + name: inputFilePath, + readInclude(includePath: string): string { + return fs.readFileSync( + path.resolve(path.dirname(inputFilePath), includePath), + 'utf-8'); + }, + }); + return [{ inputFilePath, text, baseRelPath, modulePath, schema }]; + } catch (e) { + failures.push({ type: 'error', file: inputFilePath, detail: e }); + return []; + } + }), + failures, + }; +} + +export function changeExt(p: string, newext: string): string { + return p.slice(0, -path.extname(p).length) + newext; +} + +export function formatFailures(failures: Array, traceback = false): void { + for (const d of failures) { + console.log( + (d.type === 'error' ? chalk.redBright('[ERROR]') : chalk.yellowBright('[WARNING]')) + + ' ' + + chalk.blueBright(formatPosition((d.detail as any).pos ?? d.file)) + + ': ' + + d.detail.message + + (traceback && (d.detail instanceof Error) + ? '\n' + d.detail.stack + : '')); + } + if (failures.length > 0) { + console.log(); + } +} diff --git a/implementations/javascript/packages/schema/src/bin/preserves-schema-ts.ts b/implementations/javascript/packages/schema/src/bin/preserves-schema-ts.ts index ee59d42..6e3442d 100644 --- a/implementations/javascript/packages/schema/src/bin/preserves-schema-ts.ts +++ b/implementations/javascript/packages/schema/src/bin/preserves-schema-ts.ts @@ -1,13 +1,13 @@ -import { compile, readSchema } from '../index'; +import { compile } 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 chalk from 'chalk'; -import { formatPosition, Position } from '@preserves/core'; +import { Position } from '@preserves/core'; import chokidar from 'chokidar'; +import { changeExt, Diagnostic, expandInputGlob, formatFailures } from './cli-utils'; export type CommandLineArguments = { input: string; @@ -20,12 +20,6 @@ export type CommandLineArguments = { module: string[]; }; -export interface Diagnostic { - type: 'warn' | 'error'; - file: string; - detail: Error | { message: string, pos: Position | null }; -}; - export type CompilationResult = { options: CommandLineArguments, inputFiles: Array, @@ -41,25 +35,6 @@ export type InputFile = { 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++; - } - } -} - function failureCount(type: 'warn' | 'error', r: CompilationResult): number { return r.failures.filter(f => f.type === type).length; } @@ -99,10 +74,6 @@ export function run(options: CommandLineArguments): void { } } -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); @@ -111,10 +82,8 @@ export function modulePathTo(file1: string, file2: string): string | null { } export function runOnce(options: CommandLineArguments): CompilationResult { - const matches = glob.sync(options.input); - const failures: Array = []; - - const base = options.base ?? computeBase(matches); + const { base, failures, inputFiles: inputFiles0 } = + expandInputGlob(options.input, options.base); const output = options.output ?? base; const extensionEnv: M.Environment = options.module.map(arg => { @@ -129,28 +98,10 @@ export function runOnce(options: CommandLineArguments): CompilationResult { }; }); - 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, - readInclude(includePath: string): string { - return fs.readFileSync( - path.resolve(path.dirname(inputFilePath), includePath), - 'utf-8'); - }, - }); - const schemaPath = relPath.split('/').map(p => p.split('.')[0]).map(Symbol.for); - return [{ inputFilePath, outputFilePath, schemaPath, schema }]; - } catch (e) { - failures.push({ type: 'error', file: inputFilePath, detail: e }); - return []; - } + const inputFiles: Array = inputFiles0.map(i => { + const { inputFilePath, baseRelPath, modulePath, schema } = i; + const outputFilePath = path.join(output, changeExt(baseRelPath, '.ts')); + return { inputFilePath, outputFilePath, schemaPath: modulePath, schema }; }); inputFiles.forEach(c => { @@ -190,20 +141,7 @@ export function runOnce(options: CommandLineArguments): CompilationResult { } }); - for (const d of failures) { - console.log( - (d.type === 'error' ? chalk.redBright('[ERROR]') : chalk.yellowBright('[WARNING]')) - + ' ' - + chalk.blueBright(formatPosition((d.detail as any).pos ?? d.file)) - + ': ' - + d.detail.message - + (options.traceback && (d.detail instanceof Error) - ? '\n' + d.detail.stack - : '')); - } - if (failures.length > 0) { - console.log(); - } + formatFailures(failures, options.traceback); return { options, inputFiles, failures, base, output }; } diff --git a/implementations/javascript/packages/schema/src/bin/preserves-schemac.ts b/implementations/javascript/packages/schema/src/bin/preserves-schemac.ts index 5b38d4e..054462f 100644 --- a/implementations/javascript/packages/schema/src/bin/preserves-schemac.ts +++ b/implementations/javascript/packages/schema/src/bin/preserves-schemac.ts @@ -1,14 +1,61 @@ -import { canonicalEncode, underlying } from '@preserves/core'; +import yargs from 'yargs/yargs'; +import { canonicalEncode, KeyedDictionary, underlying } from '@preserves/core'; import fs from 'fs'; -import { readSchema } from '../reader'; -import { fromSchema } from '../meta'; +import path from 'path'; +import { fromSchema, fromBundle } from '../meta'; +import { expandInputGlob, formatFailures } from './cli-utils'; -export function run(): void { - const src = fs.readFileSync('/dev/stdin', 'utf-8'); - fs.writeSync(1, underlying(canonicalEncode(fromSchema(readSchema(src))))) +export type CommandLineArguments = { + input: string; + base: string | undefined; + bundle: boolean; +}; + +export function run(options: CommandLineArguments): void { + const { failures, inputFiles } = expandInputGlob(options.input, options.base); + + if (!options.bundle && inputFiles.length !== 1) { + failures.push({ type: 'error', file: null, detail: { + message: 'Cannot emit non-bundle with anything other than exactly one input file', + pos: null, + }}); + } + + formatFailures(failures); + + if (failures.length === 0) { + if (options.bundle) { + fs.writeSync(1, underlying(canonicalEncode(fromBundle({ + modules: new KeyedDictionary(inputFiles.map(i => [i.modulePath, i.schema])), + })))); + } else { + fs.writeSync(1, underlying(canonicalEncode(fromSchema(inputFiles[0].schema)))); + } + } } -export function main(_argv: Array) { +export function main(argv: Array) { + const options: CommandLineArguments = yargs(argv) + .command('$0 ', + 'Compile textual Preserves schema definitions to binary format', + yargs => yargs + .positional('input', { + type: 'string', + description: 'Input filename or glob', + demandOption: true, + }) + .option('bundle', { + type: 'boolean', + description: 'Determines whether to emit a schema Bundle or a lone Schema', + default: true, + }) + .option('base', { + type: 'string', + description: 'Base directory for sources (default: common prefix)', + }), + argv => argv) + .argv; + options.input = path.normalize(options.input); Error.stackTraceLimit = Infinity; - run(); + run(options); } diff --git a/schema/Makefile b/schema/Makefile index 0606e5d..caa4f11 100644 --- a/schema/Makefile +++ b/schema/Makefile @@ -1,7 +1,7 @@ all: schema.bin schema.bin: schema.prs - ../implementations/javascript/packages/schema/bin/preserves-schemac.js < $< > $@.tmp || (rm -f $@.tmp; false) + ../implementations/javascript/packages/schema/bin/preserves-schemac.js --no-bundle $< > $@.tmp || (rm -f $@.tmp; false) mv $@.tmp $@ clean: