249 lines
10 KiB
TypeScript
249 lines
10 KiB
TypeScript
/// SPDX-License-Identifier: GPL-3.0-or-later
|
|
/// SPDX-FileCopyrightText: Copyright © 2016-2023 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, Syntax } 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) {
|
|
if (options.clear) clearScreen(options);
|
|
runBuildOnce(options);
|
|
} else {
|
|
ts.sys.exit(runBuildOnce(options) ? 0 : 1);
|
|
}
|
|
}
|
|
|
|
function clearScreen(options: CommandLineArguments) {
|
|
process.stdout.write(options.clear ? '\x1b[2J\x1b[H' : '\n');
|
|
}
|
|
|
|
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,
|
|
};
|
|
|
|
function runBuildOnce(options: CommandLineArguments): boolean {
|
|
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 => {
|
|
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,
|
|
emitError: (m, start, end) => {
|
|
console.error(`${Syntax.formatPos(start)}-${Syntax.formatPos(end)}: ${m}`);
|
|
onError?.(m);
|
|
},
|
|
});
|
|
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 as any).message ?? '<no error message available>');
|
|
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)) {
|
|
const sf = ts.createSourceFile(d.file.fileName,
|
|
info.originalSource,
|
|
info.languageVersion,
|
|
false,
|
|
ts.ScriptKind.Unknown);
|
|
(sf as any).resolvedPath = d.file.fileName;
|
|
syntheticSourceFiles.set(d.file.fileName, sf);
|
|
}
|
|
d.file = syntheticSourceFiles.get(d.file.fileName);
|
|
}
|
|
}
|
|
|
|
function reportDiagnostic(d: ts.Diagnostic) {
|
|
fixupDiagnostic(d);
|
|
// Sneakily access ts.screenStartingMessageCodes, internally used to decide whether to
|
|
// clear the screen or not.
|
|
const shouldClear = ((ts as any).screenStartingMessageCodes ?? []).indexOf(d.code) !== -1;
|
|
if (shouldClear) clearScreen(options);
|
|
console.log(ts.formatDiagnosticsWithColorAndContext([d], formatDiagnosticsHost).trimEnd());
|
|
}
|
|
|
|
const getCustomTransformers = (_project: string) => ({ before: [fixSourceMap] });
|
|
|
|
const sbh = ts.createSolutionBuilderWithWatchHost(ts.sys,
|
|
createProgram(options),
|
|
reportDiagnostic,
|
|
reportDiagnostic,
|
|
reportDiagnostic);
|
|
if (sbh.getCustomTransformers) {
|
|
console.warn('SolutionBuilderHost already has getCustomTransformers');
|
|
}
|
|
sbh.getCustomTransformers = getCustomTransformers;
|
|
|
|
const sb = ts.createSolutionBuilderWithWatch(sbh, ['.'], {
|
|
verbose: options.verbose,
|
|
});
|
|
|
|
const exitStatus = sb.build(void 0, void 0, void 0, getCustomTransformers);
|
|
|
|
switch (exitStatus) {
|
|
case ts.ExitStatus.Success:
|
|
case ts.ExitStatus.DiagnosticsPresent_OutputsGenerated:
|
|
return true;
|
|
|
|
case ts.ExitStatus.DiagnosticsPresent_OutputsSkipped:
|
|
case ts.ExitStatus.InvalidProject_OutputsSkipped:
|
|
case ts.ExitStatus.ProjectReferenceCycle_OutputsSkipped:
|
|
return false;
|
|
|
|
default:
|
|
((_: never) => { throw new Error("Unexpected ts.ExitStatus: " + _) })(exitStatus);
|
|
}
|
|
}
|