syndicate-js/packages/tsc/src/tsc.ts

279 lines
11 KiB
TypeScript

/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
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<Token>;
sourceToTargetMap: SpanIndex<number>;
}
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<ts.FileWatcher> = [];
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<string> = new Set();
directories: Set<string> = new Set();
}
function runBuildOnce(options: CommandLineArguments, toWatch = new ToWatch()) {
let problemCount = 0;
let hasErrors = false;
const syndicateInfo: Map<string, SyndicateInfo> = new Map();
function createProgram(commandLineOptions: CommandLineArguments): ts.CreateProgram<any>
{
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<ts.SourceFile> {
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<string, ts.SourceFile>();
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<ts.ParsedCommandLine>).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;
}