Enhance preserves-schemac to emit bundles

This commit is contained in:
Tony Garnock-Jones 2021-04-01 21:12:11 +02:00
parent 55fab35073
commit 3ad56a5275
4 changed files with 152 additions and 81 deletions

View File

@ -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<Diagnostic> = [];
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<Diagnostic>, 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();
}
}

View File

@ -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<InputFile>,
@ -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<Diagnostic> = [];
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<InputFile> = 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<InputFile> = 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 };
}

View File

@ -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<string>) {
export function main(argv: Array<string>) {
const options: CommandLineArguments = yargs(argv)
.command('$0 <input>',
'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);
}

View File

@ -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: