diff --git a/implementations/javascript/packages/schema/package.json b/implementations/javascript/packages/schema/package.json index 9f68485..8fb6936 100644 --- a/implementations/javascript/packages/schema/package.json +++ b/implementations/javascript/packages/schema/package.json @@ -31,6 +31,7 @@ "@types/minimatch": "^3.0.3", "@types/yargs": "^16.0.0", "chalk": "^4.1.0", + "chokidar": "^3.5.1", "glob": "^7.1.6", "minimatch": "^3.0.4", "yargs": "^16.2.0" diff --git a/implementations/javascript/packages/schema/rollup.config.js b/implementations/javascript/packages/schema/rollup.config.js index 4c931f9..b970462 100644 --- a/implementations/javascript/packages/schema/rollup.config.js +++ b/implementations/javascript/packages/schema/rollup.config.js @@ -43,6 +43,7 @@ export default [{ external: [ '@preserves/core', 'chalk', + 'chokidar', 'fs', 'glob', 'minimatch', 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 71fff8d..88bf4ba 100644 --- a/implementations/javascript/packages/schema/src/bin/preserves-schema-ts.ts +++ b/implementations/javascript/packages/schema/src/bin/preserves-schema-ts.ts @@ -7,6 +7,7 @@ import yargs from 'yargs/yargs'; import * as M from '../meta'; import chalk from 'chalk'; import { formatPosition, Position } from '@preserves/core'; +import chokidar from 'chokidar'; export type CommandLineArguments = { input: string; @@ -84,8 +85,9 @@ export function run(options: CommandLineArguments): void { chalk.greenBright('successfully'); console.log(chalk.gray(new Date().toISOString()) + ` Processed ${r.inputFiles.length} file(s) ${errorSummary}. Waiting for changes.`); - const watcher = fs.watch(r.base, { recursive: true }, (_event, filename) => { - filename = path.join(r.base, filename); + const watcher = chokidar.watch(r.base, { + ignoreInitial: true, + }).on('all', (_event, filename) => { if (minimatch(filename, options.input)) { watcher.close(); runWatch(); @@ -134,7 +136,14 @@ export function runOnce(options: CommandLineArguments): CompilationResult { const outputFilePath = path.join(output, changeExt(relPath, '.ts')); try { const src = fs.readFileSync(inputFilePath, 'utf-8'); - const schema = readSchema(src, { name: inputFilePath }); + 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) { diff --git a/implementations/javascript/packages/schema/src/compiler.ts b/implementations/javascript/packages/schema/src/compiler.ts index 00257f4..14e759f 100644 --- a/implementations/javascript/packages/schema/src/compiler.ts +++ b/implementations/javascript/packages/schema/src/compiler.ts @@ -90,9 +90,9 @@ export function compile(env: Environment, schema: Schema, options: CompilerOptio seq('... Array<', typeFor(unname(p[1])), '>')); } case M.$setof: - return seq('_.KeyedSet<', typeFor(p[0]), '>'); + return seq('_.KeyedSet', anglebrackets(typeFor(p[0]), '_ptr')); case M.$dictof: - return seq('_.KeyedDictionary', anglebrackets(typeFor(p[0]), typeFor(p[1]))); + return seq('_.KeyedDictionary', anglebrackets(typeFor(p[0]), typeFor(p[1]), '_ptr')); case M.$dict: return parens(seq( block( diff --git a/implementations/javascript/packages/schema/src/meta.ts b/implementations/javascript/packages/schema/src/meta.ts index 939e9e8..40679ce 100644 --- a/implementations/javascript/packages/schema/src/meta.ts +++ b/implementations/javascript/packages/schema/src/meta.ts @@ -19,6 +19,7 @@ export const ANDSYM = Symbol.for('&'); export const DOT = Symbol.for('.'); export const DOTDOTDOT = Symbol.for('...'); export const EQUALS = Symbol.for('='); +export const INCLUDE = Symbol.for('include'); export const ORSYM = Symbol.for('/'); export type SchemaEnvEntry = { schemaModulePath: ModulePath } & ( diff --git a/implementations/javascript/packages/schema/src/reader.ts b/implementations/javascript/packages/schema/src/reader.ts index f139c32..bdfd213 100644 --- a/implementations/javascript/packages/schema/src/reader.ts +++ b/implementations/javascript/packages/schema/src/reader.ts @@ -44,47 +44,77 @@ function invalidPattern(name: string, item: Input, pos: Position | null): never throw new SchemaSyntaxError(`Invalid pattern in ${name}: ${stringify(item)}`, pos); } -export function readSchema(source: string, options?: ReaderOptions): Schema { - const toplevelTokens = new Reader(source, { ... options ?? {}, includeAnnotations: true }).readToEnd(); - return parseSchema(toplevelTokens); +export type SchemaReaderOptions = { + readInclude?(includePath: string): string; +}; + +function _readSchema(source: string, options?: ReaderOptions): Array { + return new Reader(source, { + ... options ?? {}, + includeAnnotations: true + }).readToEnd(); } -export function parseSchema(toplevelTokens: Array): Schema { - const toplevelClauses = splitBy(peel(toplevelTokens) as Array, M.DOT); +export function readSchema(source: string, + options?: ReaderOptions & SchemaReaderOptions): Schema +{ + return parseSchema(_readSchema(source, options), options ?? {}); +} + +export function parseSchema(toplevelTokens: Array, + options: ReaderOptions & SchemaReaderOptions): Schema +{ let version: M.Version | undefined = void 0; let pointer: M.PointerName = false; let definitions = new Dictionary(); - for (const clause of toplevelClauses) { - if (!Array.isArray(clause)) { - invalidClause(clause); - } else if (clause.length >= 2 && is(clause[1], M.EQUALS)) { - const name = peel(clause[0]); - if (typeof name !== 'symbol') invalidClause(clause); - if (!M.isValidToken(name.description!)) { - throw new SchemaSyntaxError(preserves`Invalid definition name: ${name}`, - position(clause[0])); + + function process(toplevelTokens: Array): void { + const toplevelClauses = splitBy(peel(toplevelTokens) as Array, M.DOT); + for (const clause of toplevelClauses) { + if (!Array.isArray(clause)) { + invalidClause(clause); + } else if (clause.length >= 2 && is(clause[1], M.EQUALS)) { + const pos = position(clause[0]); + const name = peel(clause[0]); + if (typeof name !== 'symbol') invalidClause(clause); + if (!M.isValidToken(name.description!)) { + throw new SchemaSyntaxError(preserves`Invalid definition name: ${name}`, pos); + } + if (definitions.has(name)) { + throw new SchemaSyntaxError(preserves`Duplicate definition: ${clause}`, pos); + } + definitions.set(name, parseDefinition(name, clause.slice(2))); + } else if (clause.length === 2 && is(clause[0], M.$version)) { + version = M.asVersion(peel(clause[1])); + } else if (clause.length === 2 && is(clause[0], M.$pointer)) { + const pos = position(clause[1]); + const stx = peel(clause[1]); + const quasiName = 'pointer name specification'; + pointer = M.asPointerName((stx === false) ? stx + : (typeof stx === 'symbol') ? parseRef(quasiName, pos, stx) + : invalidPattern(quasiName, stx, pos)); + } else if (clause.length === 2 && is(clause[0], M.INCLUDE)) { + const pos = position(clause[1]); + const path = peel(clause[1]); + if (typeof path !== 'string') { + throw new SchemaSyntaxError(preserves`Invalid include: ${clause}`, pos); + } + if (options.readInclude === void 0) { + throw new SchemaSyntaxError(preserves`Cannot include files in schema`, pos); + } + process(_readSchema(options.readInclude(path), options)); + } else { + invalidClause(clause); } - if (definitions.has(name)) { - throw new SchemaSyntaxError(preserves`Duplicate definition: ${clause}`, - position(clause[0])); - } - definitions.set(name, parseDefinition(name, clause.slice(2))); - } else if (clause.length === 2 && is(clause[0], M.$version)) { - version = M.asVersion(peel(clause[1])); - } else if (clause.length === 2 && is(clause[0], M.$pointer)) { - const pos = position(clause[1]); - const stx = peel(clause[1]); - const quasiName = 'pointer name specification'; - pointer = M.asPointerName((stx === false) ? stx - : (typeof stx === 'symbol') ? parseRef(quasiName, pos, stx) - : invalidPattern(quasiName, stx, pos)); - } else { - invalidClause(clause); } } + + process(toplevelTokens); + if (version === void 0) { throw new SchemaSyntaxError("Schema: missing version declaration.", null); } + return M.asSchema(Record(M.$schema, [new Dictionary([ [M.$version, version], [M.$pointer, pointer],