import fs from 'fs'; import path from 'path'; import { glob } from 'glob'; import { IdentitySet, 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 = { inputBaseDir: string, 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 = { baseDirs: Array, inputFiles: Array, xrefFiles: Array, failures: Array, }; 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 inputToInputGlob(userInput: string): { baseDir: string, glob: string } { const colonPos = userInput.lastIndexOf(':'); let [base, g] = (colonPos === -1) ? [userInput, '**/*.prs'] : [userInput.slice(0, colonPos), userInput.slice(colonPos + 1)]; if (base[base.length - 1] !== '/') { base = base + '/'; } return { baseDir: base, glob: g }; } export function findSchemas(userInput: string): ({ names: string[], base: string, }) { const { baseDir: base, glob: g } = inputToInputGlob(userInput); const names = glob.sync(base + g); return { names, base }; } export function expandInputGlob(input: string[], xrefs: string[]): Expanded { const failures: Array = []; const baseDirs = new IdentitySet(); 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 (!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 [{ inputBaseDir: base, inputFilePath: filePath, text, baseRelPath, modulePath, schema }]; } } catch (e) { failures.push({ type: 'error', file: filePath, detail: e as Error }); return []; } } const inputFiles = input.flatMap(userInput => { const { names, base } = findSchemas(userInput); baseDirs.add(base); return names.flatMap(f => processInputFile(base, f, false)); }); const xrefFiles = xrefs.flatMap(userInput => { const { names, base } = findSchemas(userInput); baseDirs.add(base); return names.flatMap(f => processInputFile(base, f, true)); }); return { baseDirs: Array.from(baseDirs), inputFiles, xrefFiles, 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.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(); } }