/// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones import yargs from 'yargs/yargs'; import ts from 'typescript'; import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; import { compile } from '@syndicate-lang/compiler'; import { SpanIndex, Token } from '@syndicate-lang/compiler/lib/syntax'; export type CommandLineArguments = { verbose: boolean; intermediateDirectory?: string; watch: boolean; clear: boolean; }; interface SyndicateInfo { originalSource: string; languageVersion: ts.ScriptTarget; targetToSourceMap: SpanIndex; sourceToTargetMap: SpanIndex; } export function main(argv: string[]) { const options: CommandLineArguments = yargs(argv) .option('verbose', { type: 'boolean', default: false, description: "Enable verbose solution builder output", }) .option('intermediate-directory', { type: 'string', description: "Save intermediate expanded Syndicate source code to this directory", }) .option('watch', { alias: 'w', type: 'boolean', description: "Enable watch mode", default: false, }) .option('clear', { type: 'boolean', description: "Clear screen before each build in watch mode", default: true, }) .argv; if (options.watch) { function run() { const toWatch = new ToWatch(); console.log((options.clear ? '\x1b[2J\x1b[H' : '\n') + (new Date()) + ': Running build'); runBuildOnce(options, toWatch); const watchers: Array = []; let rebuildTriggered = false; const cb = () => { if (!rebuildTriggered) { rebuildTriggered = true; watchers.forEach(w => w.close()); queueMicrotask(run); } }; toWatch.files.forEach(f => { const w = ts.sys.watchFile?.(f, cb); if (w) watchers.push(w); }); toWatch.directories.forEach(d => { const w = ts.sys.watchDirectory?.(d, cb, true); if (w) watchers.push(w); }); console.log('\n' + (new Date()) + ': Waiting for changes to input files'); } run(); } else { ts.sys.exit(runBuildOnce(options) ? 0 : 1); } } function finalSlash(s: string): string { if (s[s.length - 1] !== '/') s = s + '/'; return s; } const formatDiagnosticsHost: ts.FormatDiagnosticsHost = { getCurrentDirectory: () => ts.sys.getCurrentDirectory(), getNewLine: () => ts.sys.newLine, getCanonicalFileName: f => f, }; class ToWatch { files: Set = new Set(); directories: Set = new Set(); } function runBuildOnce(options: CommandLineArguments, toWatch = new ToWatch()) { let problemCount = 0; let hasErrors = false; const syndicateInfo: Map = new Map(); function createProgram(commandLineOptions: CommandLineArguments): ts.CreateProgram { return function (rootNames: readonly string[] | undefined, options: ts.CompilerOptions | undefined, host?: ts.CompilerHost, oldProgram?: ts.EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly ts.Diagnostic[], projectReferences?: readonly ts.ProjectReference[]) : ts.EmitAndSemanticDiagnosticsBuilderProgram { if (host === void 0) { throw new Error("CompilerHost not present - cannot continue"); } if (rootNames === void 0) { console.warn("No Syndicate source files to compile"); } const rootDir = finalSlash(options?.rootDir ?? path.resolve('.')); function writeIntermediate(fileName: string, expandedText: string) { if ('intermediateDirectory' in commandLineOptions && commandLineOptions.intermediateDirectory) { const intermediateDirectory = commandLineOptions.intermediateDirectory; if (fileName.startsWith(rootDir)) { const intermediateFileName = path.join(intermediateDirectory, fileName.substr(rootDir.length)); fs.mkdirSync(path.dirname(intermediateFileName), { recursive: true }); fs.writeFileSync(intermediateFileName, expandedText, 'utf-8'); } } } const oldGetSourceFile = host.getSourceFile; host.getSourceFile = (fileName: string, languageVersion: ts.ScriptTarget, onError?: ((message: string) => void), shouldCreateNewSourceFile?: boolean): ts.SourceFile | undefined => { toWatch.files.add(fileName); if ((rootNames?.indexOf(fileName) ?? -1) !== -1) { try { const inputText = host.readFile(fileName); if (inputText === void 0) { onError?.(`Could not read input file ${fileName}`); return undefined; } const { text: expandedText, targetToSourceMap, sourceToTargetMap } = compile({ source: inputText, name: fileName, typescript: true, }); writeIntermediate(fileName, expandedText); syndicateInfo.set(fileName, { originalSource: inputText, languageVersion, targetToSourceMap, sourceToTargetMap, }); const sf = ts.createSourceFile(fileName, expandedText, languageVersion, true); (sf as any).version = crypto.createHash('sha256').update(expandedText).digest('hex'); return sf; } catch (e) { console.error(e); onError?.(e.message); return undefined; } } else { return oldGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile); } }; const program = ts.createEmitAndSemanticDiagnosticsBuilderProgram(rootNames, options, host, oldProgram, configFileParsingDiagnostics, projectReferences); return program; }; } function fixSourceMap(_ctx: ts.TransformationContext): ts.Transformer { return sf => { const fileName = sf.fileName; const info = syndicateInfo.get(fileName); if (info === void 0) throw new Error("No Syndicate info available for " + fileName); const targetToSourceMap = info.targetToSourceMap; const syndicateSource = ts.createSourceMapSource(fileName, info.originalSource); function adjustSourceMap(n: ts.Node) { const ps = targetToSourceMap.get(n.pos); const pe = targetToSourceMap.get(n.end); if (ps !== null && pe !== null) { ts.setSourceMapRange(n, { pos: ps.firstItem.start.pos + ps.offset, end: pe.lastItem.start.pos + pe.offset, source: syndicateSource, }); } ts.forEachChild(n, adjustSourceMap); } adjustSourceMap(sf); return sf; }; } const syntheticSourceFiles = new Map(); function fixupDiagnostic(d: ts.Diagnostic) { if (d.file !== void 0 && d.start !== void 0) { const info = syndicateInfo.get(d.file.fileName); if (info === void 0) return; if (!syntheticSourceFiles.has(d.file.fileName)) { syntheticSourceFiles.set( d.file.fileName, ts.createSourceFile(d.file.fileName, info.originalSource, info.languageVersion, false, ts.ScriptKind.Unknown)); } d.file = syntheticSourceFiles.get(d.file.fileName); const p = info.targetToSourceMap.get(d.start)!; d.start = p.firstItem.start.pos + p.offset; } } function reportDiagnostic(d: ts.Diagnostic) { if (d.category === ts.DiagnosticCategory.Error) problemCount++; fixupDiagnostic(d); console.log(ts.formatDiagnosticsWithColorAndContext([d], formatDiagnosticsHost).trimEnd()); } function reportErrorSummary(n: number) { if (n > 0) { console.error(`\n - ${n} errors reported`); hasErrors = true; } } const sbh = ts.createSolutionBuilderHost(ts.sys, createProgram(options), reportDiagnostic, reportDiagnostic, reportErrorSummary); const sb = ts.createSolutionBuilder(sbh, ['.'], { verbose: options.verbose, }); let project = sb.getNextInvalidatedProject(); // Sneakily get into secret members of the ts.SolutionBuilder and // ts.ParsedCommandline objects to prime our set of watched // files/directories, in case all the projects are up-to-date and // our createProgram function is never called. // (((sb as any).getAllParsedConfigs?.() ?? []) as Array).forEach(c => { const f = (c.options as any).configFilePath; if (f) toWatch.files.add(f); c.fileNames.forEach(f => toWatch.files.add(f)); Object.keys(c.wildcardDirectories ?? {}).forEach(d => toWatch.directories.add(d)); }); while (project !== void 0) { project.done(void 0, void 0, { before: [fixSourceMap] }); project = sb.getNextInvalidatedProject(); } return (problemCount === 0) && !hasErrors; }