preserves/implementations/javascript/packages/schema/src/bin/cli-utils.ts

128 lines
4.0 KiB
TypeScript

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';
import * as M from '../meta';
export interface Diagnostic {
type: 'warn' | 'error';
file: string | null;
detail: Error | { message: string, pos: Position | null };
};
export type InputFile = {
inputFilePath: string,
text: string,
baseRelPath: string,
modulePath: M.ModulePath,
schema: M.Schema,
};
export type XrefFile = {
xrefFilePath: string,
text: string,
modulePath: M.ModulePath,
schema: M.Schema,
};
export type Expanded = {
base: string,
inputFiles: Array<InputFile>,
xrefFiles: Array<XrefFile>,
failures: Array<Diagnostic>,
};
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[],
xrefs: string[],
base0: string | undefined,
): Expanded {
const matches = input.flatMap(i => glob.sync(i));
const failures: Array<Diagnostic> = [];
function processInputFile(base: string, filePath: string, xref: true): XrefFile[];
function processInputFile(base: string, filePath: string, xref: false): InputFile[];
function processInputFile(base: string, filePath: string, xref: boolean): (InputFile | XrefFile)[] {
if (!xref && !filePath.startsWith(base)) {
throw new Error(`Input filename ${filePath} falls outside base ${base}`);
}
try {
const text = fs.readFileSync(filePath, 'utf-8');
const baseRelPath = filePath.slice(base.length);
const modulePath = baseRelPath.split('/').map(p => p.split('.')[0]).map(Symbol.for);
const schema = readSchema(text, {
name: filePath,
readInclude(includePath: string): string {
return fs.readFileSync(
path.resolve(path.dirname(filePath), includePath),
'utf-8');
},
});
if (xref) {
return [{ xrefFilePath: filePath, text, modulePath, schema }];
} else {
return [{ inputFilePath: filePath, text, baseRelPath, modulePath, schema }];
}
} catch (e) {
failures.push({ type: 'error', file: filePath, detail: e as Error });
return [];
}
}
const base = base0 ?? computeBase(matches);
return {
base,
inputFiles: matches.flatMap(f => processInputFile(base, f, false)),
xrefFiles: xrefs.flatMap(i => {
const names = glob.sync(i);
const base = computeBase(names);
return names.flatMap(f => processInputFile(base, f, true));
}),
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.error(
(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.error();
}
}