mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
Merge from vscode merge-base (#22769)
* Merge from vscode merge-base * Turn off basic checks * Enable compilation, unit, and integration tests
This commit is contained in:
491
build/lib/tsb/builder.js
Normal file
491
build/lib/tsb/builder.js
Normal file
@@ -0,0 +1,491 @@
|
||||
"use strict";
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createTypeScriptBuilder = exports.CancellationToken = void 0;
|
||||
const fs_1 = require("fs");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const utils = require("./utils");
|
||||
const colors = require("ansi-colors");
|
||||
const ts = require("typescript");
|
||||
const Vinyl = require("vinyl");
|
||||
var CancellationToken;
|
||||
(function (CancellationToken) {
|
||||
CancellationToken.None = {
|
||||
isCancellationRequested() { return false; }
|
||||
};
|
||||
})(CancellationToken = exports.CancellationToken || (exports.CancellationToken = {}));
|
||||
function normalize(path) {
|
||||
return path.replace(/\\/g, '/');
|
||||
}
|
||||
function createTypeScriptBuilder(config, projectFile, cmd) {
|
||||
const _log = config.logFn;
|
||||
const host = new LanguageServiceHost(cmd, projectFile, _log);
|
||||
const service = ts.createLanguageService(host, ts.createDocumentRegistry());
|
||||
const lastBuildVersion = Object.create(null);
|
||||
const lastDtsHash = Object.create(null);
|
||||
const userWantsDeclarations = cmd.options.declaration;
|
||||
let oldErrors = Object.create(null);
|
||||
let headUsed = process.memoryUsage().heapUsed;
|
||||
let emitSourceMapsInStream = true;
|
||||
// always emit declaraction files
|
||||
host.getCompilationSettings().declaration = true;
|
||||
function file(file) {
|
||||
// support gulp-sourcemaps
|
||||
if (file.sourceMap) {
|
||||
emitSourceMapsInStream = false;
|
||||
}
|
||||
if (!file.contents) {
|
||||
host.removeScriptSnapshot(file.path);
|
||||
}
|
||||
else {
|
||||
host.addScriptSnapshot(file.path, new VinylScriptSnapshot(file));
|
||||
}
|
||||
}
|
||||
function baseFor(snapshot) {
|
||||
if (snapshot instanceof VinylScriptSnapshot) {
|
||||
return cmd.options.outDir || snapshot.getBase();
|
||||
}
|
||||
else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
function isExternalModule(sourceFile) {
|
||||
return sourceFile.externalModuleIndicator
|
||||
|| /declare\s+module\s+('|")(.+)\1/.test(sourceFile.getText());
|
||||
}
|
||||
function build(out, onError, token = CancellationToken.None) {
|
||||
function checkSyntaxSoon(fileName) {
|
||||
return new Promise(resolve => {
|
||||
process.nextTick(function () {
|
||||
if (!host.getScriptSnapshot(fileName, false)) {
|
||||
resolve([]); // no script, no problems
|
||||
}
|
||||
else {
|
||||
resolve(service.getSyntacticDiagnostics(fileName));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function checkSemanticsSoon(fileName) {
|
||||
return new Promise(resolve => {
|
||||
process.nextTick(function () {
|
||||
if (!host.getScriptSnapshot(fileName, false)) {
|
||||
resolve([]); // no script, no problems
|
||||
}
|
||||
else {
|
||||
resolve(service.getSemanticDiagnostics(fileName));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function emitSoon(fileName) {
|
||||
return new Promise(resolve => {
|
||||
process.nextTick(function () {
|
||||
if (/\.d\.ts$/.test(fileName)) {
|
||||
// if it's already a d.ts file just emit it signature
|
||||
const snapshot = host.getScriptSnapshot(fileName);
|
||||
const signature = crypto.createHash('md5')
|
||||
.update(snapshot.getText(0, snapshot.getLength()))
|
||||
.digest('base64');
|
||||
return resolve({
|
||||
fileName,
|
||||
signature,
|
||||
files: []
|
||||
});
|
||||
}
|
||||
const output = service.getEmitOutput(fileName);
|
||||
const files = [];
|
||||
let signature;
|
||||
for (const file of output.outputFiles) {
|
||||
if (!emitSourceMapsInStream && /\.js\.map$/.test(file.name)) {
|
||||
continue;
|
||||
}
|
||||
if (/\.d\.ts$/.test(file.name)) {
|
||||
signature = crypto.createHash('md5')
|
||||
.update(file.text)
|
||||
.digest('base64');
|
||||
if (!userWantsDeclarations) {
|
||||
// don't leak .d.ts files if users don't want them
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const vinyl = new Vinyl({
|
||||
path: file.name,
|
||||
contents: Buffer.from(file.text),
|
||||
base: !config._emitWithoutBasePath && baseFor(host.getScriptSnapshot(fileName)) || undefined
|
||||
});
|
||||
if (!emitSourceMapsInStream && /\.js$/.test(file.name)) {
|
||||
const sourcemapFile = output.outputFiles.filter(f => /\.js\.map$/.test(f.name))[0];
|
||||
if (sourcemapFile) {
|
||||
const extname = path.extname(vinyl.relative);
|
||||
const basename = path.basename(vinyl.relative, extname);
|
||||
const dirname = path.dirname(vinyl.relative);
|
||||
const tsname = (dirname === '.' ? '' : dirname + '/') + basename + '.ts';
|
||||
const sourceMap = JSON.parse(sourcemapFile.text);
|
||||
sourceMap.sources[0] = tsname.replace(/\\/g, '/');
|
||||
vinyl.sourceMap = sourceMap;
|
||||
}
|
||||
}
|
||||
files.push(vinyl);
|
||||
}
|
||||
resolve({
|
||||
fileName,
|
||||
signature,
|
||||
files
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
const newErrors = Object.create(null);
|
||||
const t1 = Date.now();
|
||||
const toBeEmitted = [];
|
||||
const toBeCheckedSyntactically = [];
|
||||
const toBeCheckedSemantically = [];
|
||||
const filesWithChangedSignature = [];
|
||||
const dependentFiles = [];
|
||||
const newLastBuildVersion = new Map();
|
||||
for (const fileName of host.getScriptFileNames()) {
|
||||
if (lastBuildVersion[fileName] !== host.getScriptVersion(fileName)) {
|
||||
toBeEmitted.push(fileName);
|
||||
toBeCheckedSyntactically.push(fileName);
|
||||
toBeCheckedSemantically.push(fileName);
|
||||
}
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
const semanticCheckInfo = new Map();
|
||||
const seenAsDependentFile = new Set();
|
||||
function workOnNext() {
|
||||
let promise;
|
||||
// let fileName: string;
|
||||
// someone told us to stop this
|
||||
if (token.isCancellationRequested()) {
|
||||
_log('[CANCEL]', '>>This compile run was cancelled<<');
|
||||
newLastBuildVersion.clear();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
// (1st) emit code
|
||||
else if (toBeEmitted.length) {
|
||||
const fileName = toBeEmitted.pop();
|
||||
promise = emitSoon(fileName).then(value => {
|
||||
for (const file of value.files) {
|
||||
_log('[emit code]', file.path);
|
||||
out(file);
|
||||
}
|
||||
// remember when this was build
|
||||
newLastBuildVersion.set(fileName, host.getScriptVersion(fileName));
|
||||
// remeber the signature
|
||||
if (value.signature && lastDtsHash[fileName] !== value.signature) {
|
||||
lastDtsHash[fileName] = value.signature;
|
||||
filesWithChangedSignature.push(fileName);
|
||||
}
|
||||
}).catch(e => {
|
||||
// can't just skip this or make a result up..
|
||||
host.error(`ERROR emitting ${fileName}`);
|
||||
host.error(e);
|
||||
});
|
||||
}
|
||||
// (2nd) check syntax
|
||||
else if (toBeCheckedSyntactically.length) {
|
||||
const fileName = toBeCheckedSyntactically.pop();
|
||||
_log('[check syntax]', fileName);
|
||||
promise = checkSyntaxSoon(fileName).then(diagnostics => {
|
||||
delete oldErrors[fileName];
|
||||
if (diagnostics.length > 0) {
|
||||
diagnostics.forEach(d => onError(d));
|
||||
newErrors[fileName] = diagnostics;
|
||||
// stop the world when there are syntax errors
|
||||
toBeCheckedSyntactically.length = 0;
|
||||
toBeCheckedSemantically.length = 0;
|
||||
filesWithChangedSignature.length = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
// (3rd) check semantics
|
||||
else if (toBeCheckedSemantically.length) {
|
||||
let fileName = toBeCheckedSemantically.pop();
|
||||
while (fileName && semanticCheckInfo.has(fileName)) {
|
||||
fileName = toBeCheckedSemantically.pop();
|
||||
}
|
||||
if (fileName) {
|
||||
_log('[check semantics]', fileName);
|
||||
promise = checkSemanticsSoon(fileName).then(diagnostics => {
|
||||
delete oldErrors[fileName];
|
||||
semanticCheckInfo.set(fileName, diagnostics.length);
|
||||
if (diagnostics.length > 0) {
|
||||
diagnostics.forEach(d => onError(d));
|
||||
newErrors[fileName] = diagnostics;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// (4th) check dependents
|
||||
else if (filesWithChangedSignature.length) {
|
||||
while (filesWithChangedSignature.length) {
|
||||
const fileName = filesWithChangedSignature.pop();
|
||||
if (!isExternalModule(service.getProgram().getSourceFile(fileName))) {
|
||||
_log('[check semantics*]', fileName + ' is an internal module and it has changed shape -> check whatever hasn\'t been checked yet');
|
||||
toBeCheckedSemantically.push(...host.getScriptFileNames());
|
||||
filesWithChangedSignature.length = 0;
|
||||
dependentFiles.length = 0;
|
||||
break;
|
||||
}
|
||||
host.collectDependents(fileName, dependentFiles);
|
||||
}
|
||||
}
|
||||
// (5th) dependents contd
|
||||
else if (dependentFiles.length) {
|
||||
let fileName = dependentFiles.pop();
|
||||
while (fileName && seenAsDependentFile.has(fileName)) {
|
||||
fileName = dependentFiles.pop();
|
||||
}
|
||||
if (fileName) {
|
||||
seenAsDependentFile.add(fileName);
|
||||
const value = semanticCheckInfo.get(fileName);
|
||||
if (value === 0) {
|
||||
// already validated successfully -> look at dependents next
|
||||
host.collectDependents(fileName, dependentFiles);
|
||||
}
|
||||
else if (typeof value === 'undefined') {
|
||||
// first validate -> look at dependents next
|
||||
dependentFiles.push(fileName);
|
||||
toBeCheckedSemantically.push(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
// (last) done
|
||||
else {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
if (!promise) {
|
||||
promise = Promise.resolve();
|
||||
}
|
||||
promise.then(function () {
|
||||
// change to change
|
||||
process.nextTick(workOnNext);
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
workOnNext();
|
||||
}).then(() => {
|
||||
// store the build versions to not rebuilt the next time
|
||||
newLastBuildVersion.forEach((value, key) => {
|
||||
lastBuildVersion[key] = value;
|
||||
});
|
||||
// print old errors and keep them
|
||||
utils.collections.forEach(oldErrors, entry => {
|
||||
entry.value.forEach(diag => onError(diag));
|
||||
newErrors[entry.key] = entry.value;
|
||||
});
|
||||
oldErrors = newErrors;
|
||||
// print stats
|
||||
const headNow = process.memoryUsage().heapUsed;
|
||||
const MB = 1024 * 1024;
|
||||
_log('[tsb]', `time: ${colors.yellow((Date.now() - t1) + 'ms')} + \nmem: ${colors.cyan(Math.ceil(headNow / MB) + 'MB')} ${colors.bgCyan('delta: ' + Math.ceil((headNow - headUsed) / MB))}`);
|
||||
headUsed = headNow;
|
||||
});
|
||||
}
|
||||
return {
|
||||
file,
|
||||
build,
|
||||
languageService: service
|
||||
};
|
||||
}
|
||||
exports.createTypeScriptBuilder = createTypeScriptBuilder;
|
||||
class ScriptSnapshot {
|
||||
constructor(text, mtime) {
|
||||
this._text = text;
|
||||
this._mtime = mtime;
|
||||
}
|
||||
getVersion() {
|
||||
return this._mtime.toUTCString();
|
||||
}
|
||||
getText(start, end) {
|
||||
return this._text.substring(start, end);
|
||||
}
|
||||
getLength() {
|
||||
return this._text.length;
|
||||
}
|
||||
getChangeRange(_oldSnapshot) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
class VinylScriptSnapshot extends ScriptSnapshot {
|
||||
constructor(file) {
|
||||
super(file.contents.toString(), file.stat.mtime);
|
||||
this._base = file.base;
|
||||
}
|
||||
getBase() {
|
||||
return this._base;
|
||||
}
|
||||
}
|
||||
class LanguageServiceHost {
|
||||
constructor(_cmdLine, _projectPath, _log) {
|
||||
this._cmdLine = _cmdLine;
|
||||
this._projectPath = _projectPath;
|
||||
this._log = _log;
|
||||
this.directoryExists = ts.sys.directoryExists;
|
||||
this.getDirectories = ts.sys.getDirectories;
|
||||
this.fileExists = ts.sys.fileExists;
|
||||
this.readFile = ts.sys.readFile;
|
||||
this.readDirectory = ts.sys.readDirectory;
|
||||
this._snapshots = Object.create(null);
|
||||
this._filesInProject = new Set(_cmdLine.fileNames);
|
||||
this._filesAdded = new Set();
|
||||
this._dependencies = new utils.graph.Graph(s => s);
|
||||
this._dependenciesRecomputeList = [];
|
||||
this._fileNameToDeclaredModule = Object.create(null);
|
||||
this._projectVersion = 1;
|
||||
}
|
||||
log(_s) {
|
||||
// console.log(s);
|
||||
}
|
||||
trace(_s) {
|
||||
// console.log(s);
|
||||
}
|
||||
error(s) {
|
||||
console.error(s);
|
||||
}
|
||||
getCompilationSettings() {
|
||||
return this._cmdLine.options;
|
||||
}
|
||||
getProjectVersion() {
|
||||
return String(this._projectVersion);
|
||||
}
|
||||
getScriptFileNames() {
|
||||
const res = Object.keys(this._snapshots).filter(path => this._filesInProject.has(path) || this._filesAdded.has(path));
|
||||
return res;
|
||||
}
|
||||
getScriptVersion(filename) {
|
||||
filename = normalize(filename);
|
||||
const result = this._snapshots[filename];
|
||||
if (result) {
|
||||
return result.getVersion();
|
||||
}
|
||||
return 'UNKNWON_FILE_' + Math.random().toString(16).slice(2);
|
||||
}
|
||||
getScriptSnapshot(filename, resolve = true) {
|
||||
filename = normalize(filename);
|
||||
let result = this._snapshots[filename];
|
||||
if (!result && resolve) {
|
||||
try {
|
||||
result = new VinylScriptSnapshot(new Vinyl({
|
||||
path: filename,
|
||||
contents: (0, fs_1.readFileSync)(filename),
|
||||
base: this.getCompilationSettings().outDir,
|
||||
stat: (0, fs_1.statSync)(filename)
|
||||
}));
|
||||
this.addScriptSnapshot(filename, result);
|
||||
}
|
||||
catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
addScriptSnapshot(filename, snapshot) {
|
||||
this._projectVersion++;
|
||||
filename = normalize(filename);
|
||||
const old = this._snapshots[filename];
|
||||
if (!old && !this._filesInProject.has(filename) && !filename.endsWith('.d.ts')) {
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
// not very proper!
|
||||
this._filesAdded.add(filename);
|
||||
}
|
||||
if (!old || old.getVersion() !== snapshot.getVersion()) {
|
||||
this._dependenciesRecomputeList.push(filename);
|
||||
const node = this._dependencies.lookup(filename);
|
||||
if (node) {
|
||||
node.outgoing = Object.create(null);
|
||||
}
|
||||
// (cheap) check for declare module
|
||||
LanguageServiceHost._declareModule.lastIndex = 0;
|
||||
let match;
|
||||
while ((match = LanguageServiceHost._declareModule.exec(snapshot.getText(0, snapshot.getLength())))) {
|
||||
let declaredModules = this._fileNameToDeclaredModule[filename];
|
||||
if (!declaredModules) {
|
||||
this._fileNameToDeclaredModule[filename] = declaredModules = [];
|
||||
}
|
||||
declaredModules.push(match[2]);
|
||||
}
|
||||
}
|
||||
this._snapshots[filename] = snapshot;
|
||||
return old;
|
||||
}
|
||||
removeScriptSnapshot(filename) {
|
||||
this._filesInProject.delete(filename);
|
||||
this._filesAdded.delete(filename);
|
||||
this._projectVersion++;
|
||||
filename = normalize(filename);
|
||||
delete this._fileNameToDeclaredModule[filename];
|
||||
return delete this._snapshots[filename];
|
||||
}
|
||||
getCurrentDirectory() {
|
||||
return path.dirname(this._projectPath);
|
||||
}
|
||||
getDefaultLibFileName(options) {
|
||||
return ts.getDefaultLibFilePath(options);
|
||||
}
|
||||
// ---- dependency management
|
||||
collectDependents(filename, target) {
|
||||
while (this._dependenciesRecomputeList.length) {
|
||||
this._processFile(this._dependenciesRecomputeList.pop());
|
||||
}
|
||||
filename = normalize(filename);
|
||||
const node = this._dependencies.lookup(filename);
|
||||
if (node) {
|
||||
utils.collections.forEach(node.incoming, entry => target.push(entry.key));
|
||||
}
|
||||
}
|
||||
_processFile(filename) {
|
||||
if (filename.match(/.*\.d\.ts$/)) {
|
||||
return;
|
||||
}
|
||||
filename = normalize(filename);
|
||||
const snapshot = this.getScriptSnapshot(filename);
|
||||
if (!snapshot) {
|
||||
this._log('processFile', `Missing snapshot for: ${filename}`);
|
||||
return;
|
||||
}
|
||||
const info = ts.preProcessFile(snapshot.getText(0, snapshot.getLength()), true);
|
||||
// (1) ///-references
|
||||
info.referencedFiles.forEach(ref => {
|
||||
const resolvedPath = path.resolve(path.dirname(filename), ref.fileName);
|
||||
const normalizedPath = normalize(resolvedPath);
|
||||
this._dependencies.inertEdge(filename, normalizedPath);
|
||||
});
|
||||
// (2) import-require statements
|
||||
info.importedFiles.forEach(ref => {
|
||||
const stopDirname = normalize(this.getCurrentDirectory());
|
||||
let dirname = filename;
|
||||
let found = false;
|
||||
while (!found && dirname.indexOf(stopDirname) === 0) {
|
||||
dirname = path.dirname(dirname);
|
||||
const resolvedPath = path.resolve(dirname, ref.fileName);
|
||||
const normalizedPath = normalize(resolvedPath);
|
||||
if (this.getScriptSnapshot(normalizedPath + '.ts')) {
|
||||
this._dependencies.inertEdge(filename, normalizedPath + '.ts');
|
||||
found = true;
|
||||
}
|
||||
else if (this.getScriptSnapshot(normalizedPath + '.d.ts')) {
|
||||
this._dependencies.inertEdge(filename, normalizedPath + '.d.ts');
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
for (const key in this._fileNameToDeclaredModule) {
|
||||
if (this._fileNameToDeclaredModule[key] && ~this._fileNameToDeclaredModule[key].indexOf(ref.fileName)) {
|
||||
this._dependencies.inertEdge(filename, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
LanguageServiceHost._declareModule = /declare\s+module\s+('|")(.+)\1/g;
|
||||
608
build/lib/tsb/builder.ts
Normal file
608
build/lib/tsb/builder.ts
Normal file
@@ -0,0 +1,608 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { statSync, readFileSync } from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import * as utils from './utils';
|
||||
import * as colors from 'ansi-colors';
|
||||
import * as ts from 'typescript';
|
||||
import * as Vinyl from 'vinyl';
|
||||
|
||||
export interface IConfiguration {
|
||||
logFn: (topic: string, message: string) => void;
|
||||
_emitWithoutBasePath?: boolean;
|
||||
}
|
||||
|
||||
export interface CancellationToken {
|
||||
isCancellationRequested(): boolean;
|
||||
}
|
||||
|
||||
export namespace CancellationToken {
|
||||
export const None: CancellationToken = {
|
||||
isCancellationRequested() { return false; }
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITypeScriptBuilder {
|
||||
build(out: (file: Vinyl) => void, onError: (err: ts.Diagnostic) => void, token?: CancellationToken): Promise<any>;
|
||||
file(file: Vinyl): void;
|
||||
languageService: ts.LanguageService;
|
||||
}
|
||||
|
||||
function normalize(path: string): string {
|
||||
return path.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
export function createTypeScriptBuilder(config: IConfiguration, projectFile: string, cmd: ts.ParsedCommandLine): ITypeScriptBuilder {
|
||||
|
||||
const _log = config.logFn;
|
||||
|
||||
const host = new LanguageServiceHost(cmd, projectFile, _log);
|
||||
const service = ts.createLanguageService(host, ts.createDocumentRegistry());
|
||||
const lastBuildVersion: { [path: string]: string } = Object.create(null);
|
||||
const lastDtsHash: { [path: string]: string } = Object.create(null);
|
||||
const userWantsDeclarations = cmd.options.declaration;
|
||||
let oldErrors: { [path: string]: ts.Diagnostic[] } = Object.create(null);
|
||||
let headUsed = process.memoryUsage().heapUsed;
|
||||
let emitSourceMapsInStream = true;
|
||||
|
||||
// always emit declaraction files
|
||||
host.getCompilationSettings().declaration = true;
|
||||
|
||||
function file(file: Vinyl): void {
|
||||
// support gulp-sourcemaps
|
||||
if ((<any>file).sourceMap) {
|
||||
emitSourceMapsInStream = false;
|
||||
}
|
||||
|
||||
if (!file.contents) {
|
||||
host.removeScriptSnapshot(file.path);
|
||||
} else {
|
||||
host.addScriptSnapshot(file.path, new VinylScriptSnapshot(file));
|
||||
}
|
||||
}
|
||||
|
||||
function baseFor(snapshot: ScriptSnapshot): string {
|
||||
if (snapshot instanceof VinylScriptSnapshot) {
|
||||
return cmd.options.outDir || snapshot.getBase();
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function isExternalModule(sourceFile: ts.SourceFile): boolean {
|
||||
return (<any>sourceFile).externalModuleIndicator
|
||||
|| /declare\s+module\s+('|")(.+)\1/.test(sourceFile.getText());
|
||||
}
|
||||
|
||||
function build(out: (file: Vinyl) => void, onError: (err: any) => void, token = CancellationToken.None): Promise<any> {
|
||||
|
||||
function checkSyntaxSoon(fileName: string): Promise<ts.Diagnostic[]> {
|
||||
return new Promise<ts.Diagnostic[]>(resolve => {
|
||||
process.nextTick(function () {
|
||||
if (!host.getScriptSnapshot(fileName, false)) {
|
||||
resolve([]); // no script, no problems
|
||||
} else {
|
||||
resolve(service.getSyntacticDiagnostics(fileName));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkSemanticsSoon(fileName: string): Promise<ts.Diagnostic[]> {
|
||||
return new Promise<ts.Diagnostic[]>(resolve => {
|
||||
process.nextTick(function () {
|
||||
if (!host.getScriptSnapshot(fileName, false)) {
|
||||
resolve([]); // no script, no problems
|
||||
} else {
|
||||
resolve(service.getSemanticDiagnostics(fileName));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function emitSoon(fileName: string): Promise<{ fileName: string; signature?: string; files: Vinyl[] }> {
|
||||
|
||||
return new Promise(resolve => {
|
||||
process.nextTick(function () {
|
||||
|
||||
if (/\.d\.ts$/.test(fileName)) {
|
||||
// if it's already a d.ts file just emit it signature
|
||||
const snapshot = host.getScriptSnapshot(fileName);
|
||||
const signature = crypto.createHash('md5')
|
||||
.update(snapshot.getText(0, snapshot.getLength()))
|
||||
.digest('base64');
|
||||
|
||||
return resolve({
|
||||
fileName,
|
||||
signature,
|
||||
files: []
|
||||
});
|
||||
}
|
||||
|
||||
const output = service.getEmitOutput(fileName);
|
||||
const files: Vinyl[] = [];
|
||||
let signature: string | undefined;
|
||||
|
||||
for (const file of output.outputFiles) {
|
||||
if (!emitSourceMapsInStream && /\.js\.map$/.test(file.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/\.d\.ts$/.test(file.name)) {
|
||||
signature = crypto.createHash('md5')
|
||||
.update(file.text)
|
||||
.digest('base64');
|
||||
|
||||
if (!userWantsDeclarations) {
|
||||
// don't leak .d.ts files if users don't want them
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const vinyl = new Vinyl({
|
||||
path: file.name,
|
||||
contents: Buffer.from(file.text),
|
||||
base: !config._emitWithoutBasePath && baseFor(host.getScriptSnapshot(fileName)) || undefined
|
||||
});
|
||||
|
||||
if (!emitSourceMapsInStream && /\.js$/.test(file.name)) {
|
||||
const sourcemapFile = output.outputFiles.filter(f => /\.js\.map$/.test(f.name))[0];
|
||||
|
||||
if (sourcemapFile) {
|
||||
const extname = path.extname(vinyl.relative);
|
||||
const basename = path.basename(vinyl.relative, extname);
|
||||
const dirname = path.dirname(vinyl.relative);
|
||||
const tsname = (dirname === '.' ? '' : dirname + '/') + basename + '.ts';
|
||||
|
||||
const sourceMap = JSON.parse(sourcemapFile.text);
|
||||
sourceMap.sources[0] = tsname.replace(/\\/g, '/');
|
||||
(<any>vinyl).sourceMap = sourceMap;
|
||||
}
|
||||
}
|
||||
|
||||
files.push(vinyl);
|
||||
}
|
||||
|
||||
resolve({
|
||||
fileName,
|
||||
signature,
|
||||
files
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const newErrors: { [path: string]: ts.Diagnostic[] } = Object.create(null);
|
||||
const t1 = Date.now();
|
||||
|
||||
const toBeEmitted: string[] = [];
|
||||
const toBeCheckedSyntactically: string[] = [];
|
||||
const toBeCheckedSemantically: string[] = [];
|
||||
const filesWithChangedSignature: string[] = [];
|
||||
const dependentFiles: string[] = [];
|
||||
const newLastBuildVersion = new Map<string, string>();
|
||||
|
||||
for (const fileName of host.getScriptFileNames()) {
|
||||
if (lastBuildVersion[fileName] !== host.getScriptVersion(fileName)) {
|
||||
|
||||
toBeEmitted.push(fileName);
|
||||
toBeCheckedSyntactically.push(fileName);
|
||||
toBeCheckedSemantically.push(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise<void>(resolve => {
|
||||
|
||||
const semanticCheckInfo = new Map<string, number>();
|
||||
const seenAsDependentFile = new Set<string>();
|
||||
|
||||
function workOnNext() {
|
||||
|
||||
let promise: Promise<any> | undefined;
|
||||
// let fileName: string;
|
||||
|
||||
// someone told us to stop this
|
||||
if (token.isCancellationRequested()) {
|
||||
_log('[CANCEL]', '>>This compile run was cancelled<<');
|
||||
newLastBuildVersion.clear();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// (1st) emit code
|
||||
else if (toBeEmitted.length) {
|
||||
const fileName = toBeEmitted.pop()!;
|
||||
promise = emitSoon(fileName).then(value => {
|
||||
|
||||
for (const file of value.files) {
|
||||
_log('[emit code]', file.path);
|
||||
out(file);
|
||||
}
|
||||
|
||||
// remember when this was build
|
||||
newLastBuildVersion.set(fileName, host.getScriptVersion(fileName));
|
||||
|
||||
// remeber the signature
|
||||
if (value.signature && lastDtsHash[fileName] !== value.signature) {
|
||||
lastDtsHash[fileName] = value.signature;
|
||||
filesWithChangedSignature.push(fileName);
|
||||
}
|
||||
}).catch(e => {
|
||||
// can't just skip this or make a result up..
|
||||
host.error(`ERROR emitting ${fileName}`);
|
||||
host.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
// (2nd) check syntax
|
||||
else if (toBeCheckedSyntactically.length) {
|
||||
const fileName = toBeCheckedSyntactically.pop()!;
|
||||
_log('[check syntax]', fileName);
|
||||
promise = checkSyntaxSoon(fileName).then(diagnostics => {
|
||||
delete oldErrors[fileName];
|
||||
if (diagnostics.length > 0) {
|
||||
diagnostics.forEach(d => onError(d));
|
||||
newErrors[fileName] = diagnostics;
|
||||
|
||||
// stop the world when there are syntax errors
|
||||
toBeCheckedSyntactically.length = 0;
|
||||
toBeCheckedSemantically.length = 0;
|
||||
filesWithChangedSignature.length = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// (3rd) check semantics
|
||||
else if (toBeCheckedSemantically.length) {
|
||||
|
||||
let fileName = toBeCheckedSemantically.pop();
|
||||
while (fileName && semanticCheckInfo.has(fileName)) {
|
||||
fileName = toBeCheckedSemantically.pop()!;
|
||||
}
|
||||
|
||||
if (fileName) {
|
||||
_log('[check semantics]', fileName);
|
||||
promise = checkSemanticsSoon(fileName).then(diagnostics => {
|
||||
delete oldErrors[fileName!];
|
||||
semanticCheckInfo.set(fileName!, diagnostics.length);
|
||||
if (diagnostics.length > 0) {
|
||||
diagnostics.forEach(d => onError(d));
|
||||
newErrors[fileName!] = diagnostics;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// (4th) check dependents
|
||||
else if (filesWithChangedSignature.length) {
|
||||
while (filesWithChangedSignature.length) {
|
||||
const fileName = filesWithChangedSignature.pop()!;
|
||||
|
||||
if (!isExternalModule(service.getProgram()!.getSourceFile(fileName)!)) {
|
||||
_log('[check semantics*]', fileName + ' is an internal module and it has changed shape -> check whatever hasn\'t been checked yet');
|
||||
toBeCheckedSemantically.push(...host.getScriptFileNames());
|
||||
filesWithChangedSignature.length = 0;
|
||||
dependentFiles.length = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
host.collectDependents(fileName, dependentFiles);
|
||||
}
|
||||
}
|
||||
|
||||
// (5th) dependents contd
|
||||
else if (dependentFiles.length) {
|
||||
let fileName = dependentFiles.pop();
|
||||
while (fileName && seenAsDependentFile.has(fileName)) {
|
||||
fileName = dependentFiles.pop();
|
||||
}
|
||||
if (fileName) {
|
||||
seenAsDependentFile.add(fileName);
|
||||
const value = semanticCheckInfo.get(fileName);
|
||||
if (value === 0) {
|
||||
// already validated successfully -> look at dependents next
|
||||
host.collectDependents(fileName, dependentFiles);
|
||||
|
||||
} else if (typeof value === 'undefined') {
|
||||
// first validate -> look at dependents next
|
||||
dependentFiles.push(fileName);
|
||||
toBeCheckedSemantically.push(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (last) done
|
||||
else {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!promise) {
|
||||
promise = Promise.resolve();
|
||||
}
|
||||
|
||||
promise.then(function () {
|
||||
// change to change
|
||||
process.nextTick(workOnNext);
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
workOnNext();
|
||||
|
||||
}).then(() => {
|
||||
// store the build versions to not rebuilt the next time
|
||||
newLastBuildVersion.forEach((value, key) => {
|
||||
lastBuildVersion[key] = value;
|
||||
});
|
||||
|
||||
// print old errors and keep them
|
||||
utils.collections.forEach(oldErrors, entry => {
|
||||
entry.value.forEach(diag => onError(diag));
|
||||
newErrors[entry.key] = entry.value;
|
||||
});
|
||||
oldErrors = newErrors;
|
||||
|
||||
// print stats
|
||||
const headNow = process.memoryUsage().heapUsed;
|
||||
const MB = 1024 * 1024;
|
||||
_log(
|
||||
'[tsb]',
|
||||
`time: ${colors.yellow((Date.now() - t1) + 'ms')} + \nmem: ${colors.cyan(Math.ceil(headNow / MB) + 'MB')} ${colors.bgCyan('delta: ' + Math.ceil((headNow - headUsed) / MB))}`
|
||||
);
|
||||
headUsed = headNow;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
file,
|
||||
build,
|
||||
languageService: service
|
||||
};
|
||||
}
|
||||
|
||||
class ScriptSnapshot implements ts.IScriptSnapshot {
|
||||
|
||||
private readonly _text: string;
|
||||
private readonly _mtime: Date;
|
||||
|
||||
constructor(text: string, mtime: Date) {
|
||||
this._text = text;
|
||||
this._mtime = mtime;
|
||||
}
|
||||
|
||||
getVersion(): string {
|
||||
return this._mtime.toUTCString();
|
||||
}
|
||||
|
||||
getText(start: number, end: number): string {
|
||||
return this._text.substring(start, end);
|
||||
}
|
||||
|
||||
getLength(): number {
|
||||
return this._text.length;
|
||||
}
|
||||
|
||||
getChangeRange(_oldSnapshot: ts.IScriptSnapshot): ts.TextChangeRange | undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class VinylScriptSnapshot extends ScriptSnapshot {
|
||||
|
||||
private readonly _base: string;
|
||||
|
||||
constructor(file: Vinyl) {
|
||||
super(file.contents!.toString(), file.stat!.mtime);
|
||||
this._base = file.base;
|
||||
}
|
||||
|
||||
getBase(): string {
|
||||
return this._base;
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageServiceHost implements ts.LanguageServiceHost {
|
||||
|
||||
private readonly _snapshots: { [path: string]: ScriptSnapshot };
|
||||
private readonly _filesInProject: Set<string>;
|
||||
private readonly _filesAdded: Set<string>;
|
||||
private readonly _dependencies: utils.graph.Graph<string>;
|
||||
private readonly _dependenciesRecomputeList: string[];
|
||||
private readonly _fileNameToDeclaredModule: { [path: string]: string[] };
|
||||
|
||||
private _projectVersion: number;
|
||||
|
||||
constructor(
|
||||
private readonly _cmdLine: ts.ParsedCommandLine,
|
||||
private readonly _projectPath: string,
|
||||
private readonly _log: (topic: string, message: string) => void
|
||||
) {
|
||||
this._snapshots = Object.create(null);
|
||||
this._filesInProject = new Set(_cmdLine.fileNames);
|
||||
this._filesAdded = new Set();
|
||||
this._dependencies = new utils.graph.Graph<string>(s => s);
|
||||
this._dependenciesRecomputeList = [];
|
||||
this._fileNameToDeclaredModule = Object.create(null);
|
||||
|
||||
this._projectVersion = 1;
|
||||
}
|
||||
|
||||
log(_s: string): void {
|
||||
// console.log(s);
|
||||
}
|
||||
|
||||
trace(_s: string): void {
|
||||
// console.log(s);
|
||||
}
|
||||
|
||||
error(s: string): void {
|
||||
console.error(s);
|
||||
}
|
||||
|
||||
getCompilationSettings(): ts.CompilerOptions {
|
||||
return this._cmdLine.options;
|
||||
}
|
||||
|
||||
getProjectVersion(): string {
|
||||
return String(this._projectVersion);
|
||||
}
|
||||
|
||||
getScriptFileNames(): string[] {
|
||||
const res = Object.keys(this._snapshots).filter(path => this._filesInProject.has(path) || this._filesAdded.has(path));
|
||||
return res;
|
||||
}
|
||||
|
||||
getScriptVersion(filename: string): string {
|
||||
filename = normalize(filename);
|
||||
const result = this._snapshots[filename];
|
||||
if (result) {
|
||||
return result.getVersion();
|
||||
}
|
||||
return 'UNKNWON_FILE_' + Math.random().toString(16).slice(2);
|
||||
}
|
||||
|
||||
getScriptSnapshot(filename: string, resolve: boolean = true): ScriptSnapshot {
|
||||
filename = normalize(filename);
|
||||
let result = this._snapshots[filename];
|
||||
if (!result && resolve) {
|
||||
try {
|
||||
result = new VinylScriptSnapshot(new Vinyl(<any>{
|
||||
path: filename,
|
||||
contents: readFileSync(filename),
|
||||
base: this.getCompilationSettings().outDir,
|
||||
stat: statSync(filename)
|
||||
}));
|
||||
this.addScriptSnapshot(filename, result);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _declareModule = /declare\s+module\s+('|")(.+)\1/g;
|
||||
|
||||
addScriptSnapshot(filename: string, snapshot: ScriptSnapshot): ScriptSnapshot {
|
||||
this._projectVersion++;
|
||||
filename = normalize(filename);
|
||||
const old = this._snapshots[filename];
|
||||
if (!old && !this._filesInProject.has(filename) && !filename.endsWith('.d.ts')) {
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
// not very proper!
|
||||
this._filesAdded.add(filename);
|
||||
}
|
||||
if (!old || old.getVersion() !== snapshot.getVersion()) {
|
||||
this._dependenciesRecomputeList.push(filename);
|
||||
const node = this._dependencies.lookup(filename);
|
||||
if (node) {
|
||||
node.outgoing = Object.create(null);
|
||||
}
|
||||
|
||||
// (cheap) check for declare module
|
||||
LanguageServiceHost._declareModule.lastIndex = 0;
|
||||
let match: RegExpExecArray | null | undefined;
|
||||
while ((match = LanguageServiceHost._declareModule.exec(snapshot.getText(0, snapshot.getLength())))) {
|
||||
let declaredModules = this._fileNameToDeclaredModule[filename];
|
||||
if (!declaredModules) {
|
||||
this._fileNameToDeclaredModule[filename] = declaredModules = [];
|
||||
}
|
||||
declaredModules.push(match[2]);
|
||||
}
|
||||
}
|
||||
this._snapshots[filename] = snapshot;
|
||||
return old;
|
||||
}
|
||||
|
||||
removeScriptSnapshot(filename: string): boolean {
|
||||
this._filesInProject.delete(filename);
|
||||
this._filesAdded.delete(filename);
|
||||
this._projectVersion++;
|
||||
filename = normalize(filename);
|
||||
delete this._fileNameToDeclaredModule[filename];
|
||||
return delete this._snapshots[filename];
|
||||
}
|
||||
|
||||
getCurrentDirectory(): string {
|
||||
return path.dirname(this._projectPath);
|
||||
}
|
||||
|
||||
getDefaultLibFileName(options: ts.CompilerOptions): string {
|
||||
return ts.getDefaultLibFilePath(options);
|
||||
}
|
||||
|
||||
readonly directoryExists = ts.sys.directoryExists;
|
||||
readonly getDirectories = ts.sys.getDirectories;
|
||||
readonly fileExists = ts.sys.fileExists;
|
||||
readonly readFile = ts.sys.readFile;
|
||||
readonly readDirectory = ts.sys.readDirectory;
|
||||
|
||||
// ---- dependency management
|
||||
|
||||
collectDependents(filename: string, target: string[]): void {
|
||||
while (this._dependenciesRecomputeList.length) {
|
||||
this._processFile(this._dependenciesRecomputeList.pop()!);
|
||||
}
|
||||
filename = normalize(filename);
|
||||
const node = this._dependencies.lookup(filename);
|
||||
if (node) {
|
||||
utils.collections.forEach(node.incoming, entry => target.push(entry.key));
|
||||
}
|
||||
}
|
||||
|
||||
_processFile(filename: string): void {
|
||||
if (filename.match(/.*\.d\.ts$/)) {
|
||||
return;
|
||||
}
|
||||
filename = normalize(filename);
|
||||
const snapshot = this.getScriptSnapshot(filename);
|
||||
if (!snapshot) {
|
||||
this._log('processFile', `Missing snapshot for: ${filename}`);
|
||||
return;
|
||||
}
|
||||
const info = ts.preProcessFile(snapshot.getText(0, snapshot.getLength()), true);
|
||||
|
||||
// (1) ///-references
|
||||
info.referencedFiles.forEach(ref => {
|
||||
const resolvedPath = path.resolve(path.dirname(filename), ref.fileName);
|
||||
const normalizedPath = normalize(resolvedPath);
|
||||
|
||||
this._dependencies.inertEdge(filename, normalizedPath);
|
||||
});
|
||||
|
||||
// (2) import-require statements
|
||||
info.importedFiles.forEach(ref => {
|
||||
const stopDirname = normalize(this.getCurrentDirectory());
|
||||
let dirname = filename;
|
||||
let found = false;
|
||||
|
||||
while (!found && dirname.indexOf(stopDirname) === 0) {
|
||||
dirname = path.dirname(dirname);
|
||||
const resolvedPath = path.resolve(dirname, ref.fileName);
|
||||
const normalizedPath = normalize(resolvedPath);
|
||||
|
||||
if (this.getScriptSnapshot(normalizedPath + '.ts')) {
|
||||
this._dependencies.inertEdge(filename, normalizedPath + '.ts');
|
||||
found = true;
|
||||
|
||||
} else if (this.getScriptSnapshot(normalizedPath + '.d.ts')) {
|
||||
this._dependencies.inertEdge(filename, normalizedPath + '.d.ts');
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
for (const key in this._fileNameToDeclaredModule) {
|
||||
if (this._fileNameToDeclaredModule[key] && ~this._fileNameToDeclaredModule[key].indexOf(ref.fileName)) {
|
||||
this._dependencies.inertEdge(filename, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
130
build/lib/tsb/index.js
Normal file
130
build/lib/tsb/index.js
Normal file
@@ -0,0 +1,130 @@
|
||||
"use strict";
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.create = void 0;
|
||||
const Vinyl = require("vinyl");
|
||||
const through = require("through");
|
||||
const builder = require("./builder");
|
||||
const ts = require("typescript");
|
||||
const stream_1 = require("stream");
|
||||
const path_1 = require("path");
|
||||
const utils_1 = require("./utils");
|
||||
const fs_1 = require("fs");
|
||||
const log = require("fancy-log");
|
||||
const colors = require("ansi-colors");
|
||||
const transpiler_1 = require("./transpiler");
|
||||
class EmptyDuplex extends stream_1.Duplex {
|
||||
_write(_chunk, _encoding, callback) { callback(); }
|
||||
_read() { this.push(null); }
|
||||
}
|
||||
function createNullCompiler() {
|
||||
const result = function () { return new EmptyDuplex(); };
|
||||
result.src = () => new EmptyDuplex();
|
||||
return result;
|
||||
}
|
||||
const _defaultOnError = (err) => console.log(JSON.stringify(err, null, 4));
|
||||
function create(projectPath, existingOptions, config, onError = _defaultOnError) {
|
||||
function printDiagnostic(diag) {
|
||||
if (!diag.file || !diag.start) {
|
||||
onError(ts.flattenDiagnosticMessageText(diag.messageText, '\n'));
|
||||
}
|
||||
else {
|
||||
const lineAndCh = diag.file.getLineAndCharacterOfPosition(diag.start);
|
||||
onError(utils_1.strings.format('{0}({1},{2}): {3}', diag.file.fileName, lineAndCh.line + 1, lineAndCh.character + 1, ts.flattenDiagnosticMessageText(diag.messageText, '\n')));
|
||||
}
|
||||
}
|
||||
const parsed = ts.readConfigFile(projectPath, ts.sys.readFile);
|
||||
if (parsed.error) {
|
||||
printDiagnostic(parsed.error);
|
||||
return createNullCompiler();
|
||||
}
|
||||
const cmdLine = ts.parseJsonConfigFileContent(parsed.config, ts.sys, (0, path_1.dirname)(projectPath), existingOptions);
|
||||
if (cmdLine.errors.length > 0) {
|
||||
cmdLine.errors.forEach(printDiagnostic);
|
||||
return createNullCompiler();
|
||||
}
|
||||
function logFn(topic, message) {
|
||||
if (config.verbose) {
|
||||
log(colors.cyan(topic), message);
|
||||
}
|
||||
}
|
||||
// FULL COMPILE stream doing transpile, syntax and semantic diagnostics
|
||||
function createCompileStream(builder, token) {
|
||||
return through(function (file) {
|
||||
// give the file to the compiler
|
||||
if (file.isStream()) {
|
||||
this.emit('error', 'no support for streams');
|
||||
return;
|
||||
}
|
||||
builder.file(file);
|
||||
}, function () {
|
||||
// start the compilation process
|
||||
builder.build(file => this.queue(file), printDiagnostic, token).catch(e => console.error(e)).then(() => this.queue(null));
|
||||
});
|
||||
}
|
||||
// TRANSPILE ONLY stream doing just TS to JS conversion
|
||||
function createTranspileStream(transpiler) {
|
||||
return through(function (file) {
|
||||
// give the file to the compiler
|
||||
if (file.isStream()) {
|
||||
this.emit('error', 'no support for streams');
|
||||
return;
|
||||
}
|
||||
if (!file.contents) {
|
||||
return;
|
||||
}
|
||||
if (!config.transpileOnlyIncludesDts && file.path.endsWith('.d.ts')) {
|
||||
return;
|
||||
}
|
||||
if (!transpiler.onOutfile) {
|
||||
transpiler.onOutfile = file => this.queue(file);
|
||||
}
|
||||
transpiler.transpile(file);
|
||||
}, function () {
|
||||
transpiler.join().then(() => {
|
||||
this.queue(null);
|
||||
transpiler.onOutfile = undefined;
|
||||
});
|
||||
});
|
||||
}
|
||||
let result;
|
||||
if (config.transpileOnly) {
|
||||
const transpiler = new transpiler_1.Transpiler(logFn, printDiagnostic, projectPath, cmdLine);
|
||||
result = (() => createTranspileStream(transpiler));
|
||||
}
|
||||
else {
|
||||
const _builder = builder.createTypeScriptBuilder({ logFn }, projectPath, cmdLine);
|
||||
result = ((token) => createCompileStream(_builder, token));
|
||||
}
|
||||
result.src = (opts) => {
|
||||
let _pos = 0;
|
||||
const _fileNames = cmdLine.fileNames.slice(0);
|
||||
return new class extends stream_1.Readable {
|
||||
constructor() {
|
||||
super({ objectMode: true });
|
||||
}
|
||||
_read() {
|
||||
let more = true;
|
||||
let path;
|
||||
for (; more && _pos < _fileNames.length; _pos++) {
|
||||
path = _fileNames[_pos];
|
||||
more = this.push(new Vinyl({
|
||||
path,
|
||||
contents: (0, fs_1.readFileSync)(path),
|
||||
stat: (0, fs_1.statSync)(path),
|
||||
cwd: opts && opts.cwd,
|
||||
base: opts && opts.base || (0, path_1.dirname)(projectPath)
|
||||
}));
|
||||
}
|
||||
if (_pos >= _fileNames.length) {
|
||||
this.push(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
return result;
|
||||
}
|
||||
exports.create = create;
|
||||
164
build/lib/tsb/index.ts
Normal file
164
build/lib/tsb/index.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as Vinyl from 'vinyl';
|
||||
import * as through from 'through';
|
||||
import * as builder from './builder';
|
||||
import * as ts from 'typescript';
|
||||
import { Readable, Writable, Duplex } from 'stream';
|
||||
import { dirname } from 'path';
|
||||
import { strings } from './utils';
|
||||
import { readFileSync, statSync } from 'fs';
|
||||
import * as log from 'fancy-log';
|
||||
import colors = require('ansi-colors');
|
||||
import { Transpiler } from './transpiler';
|
||||
|
||||
export interface IncrementalCompiler {
|
||||
(token?: any): Readable & Writable;
|
||||
src(opts?: { cwd?: string; base?: string }): Readable;
|
||||
}
|
||||
|
||||
class EmptyDuplex extends Duplex {
|
||||
_write(_chunk: any, _encoding: string, callback: (err?: Error) => void): void { callback(); }
|
||||
_read() { this.push(null); }
|
||||
}
|
||||
|
||||
function createNullCompiler(): IncrementalCompiler {
|
||||
const result: IncrementalCompiler = function () { return new EmptyDuplex(); };
|
||||
result.src = () => new EmptyDuplex();
|
||||
return result;
|
||||
}
|
||||
|
||||
const _defaultOnError = (err: string) => console.log(JSON.stringify(err, null, 4));
|
||||
|
||||
export function create(
|
||||
projectPath: string,
|
||||
existingOptions: Partial<ts.CompilerOptions>,
|
||||
config: { verbose?: boolean; transpileOnly?: boolean; transpileOnlyIncludesDts?: boolean },
|
||||
onError: (message: string) => void = _defaultOnError
|
||||
): IncrementalCompiler {
|
||||
|
||||
function printDiagnostic(diag: ts.Diagnostic): void {
|
||||
|
||||
if (!diag.file || !diag.start) {
|
||||
onError(ts.flattenDiagnosticMessageText(diag.messageText, '\n'));
|
||||
} else {
|
||||
const lineAndCh = diag.file.getLineAndCharacterOfPosition(diag.start);
|
||||
onError(strings.format('{0}({1},{2}): {3}',
|
||||
diag.file.fileName,
|
||||
lineAndCh.line + 1,
|
||||
lineAndCh.character + 1,
|
||||
ts.flattenDiagnosticMessageText(diag.messageText, '\n'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = ts.readConfigFile(projectPath, ts.sys.readFile);
|
||||
if (parsed.error) {
|
||||
printDiagnostic(parsed.error);
|
||||
return createNullCompiler();
|
||||
}
|
||||
|
||||
const cmdLine = ts.parseJsonConfigFileContent(parsed.config, ts.sys, dirname(projectPath), existingOptions);
|
||||
if (cmdLine.errors.length > 0) {
|
||||
cmdLine.errors.forEach(printDiagnostic);
|
||||
return createNullCompiler();
|
||||
}
|
||||
|
||||
function logFn(topic: string, message: string): void {
|
||||
if (config.verbose) {
|
||||
log(colors.cyan(topic), message);
|
||||
}
|
||||
}
|
||||
|
||||
// FULL COMPILE stream doing transpile, syntax and semantic diagnostics
|
||||
function createCompileStream(builder: builder.ITypeScriptBuilder, token?: builder.CancellationToken): Readable & Writable {
|
||||
|
||||
return through(function (this: through.ThroughStream, file: Vinyl) {
|
||||
// give the file to the compiler
|
||||
if (file.isStream()) {
|
||||
this.emit('error', 'no support for streams');
|
||||
return;
|
||||
}
|
||||
builder.file(file);
|
||||
|
||||
}, function (this: { queue(a: any): void }) {
|
||||
// start the compilation process
|
||||
builder.build(
|
||||
file => this.queue(file),
|
||||
printDiagnostic,
|
||||
token
|
||||
).catch(e => console.error(e)).then(() => this.queue(null));
|
||||
});
|
||||
}
|
||||
|
||||
// TRANSPILE ONLY stream doing just TS to JS conversion
|
||||
function createTranspileStream(transpiler: Transpiler): Readable & Writable {
|
||||
return through(function (this: through.ThroughStream & { queue(a: any): void }, file: Vinyl) {
|
||||
// give the file to the compiler
|
||||
if (file.isStream()) {
|
||||
this.emit('error', 'no support for streams');
|
||||
return;
|
||||
}
|
||||
if (!file.contents) {
|
||||
return;
|
||||
}
|
||||
if (!config.transpileOnlyIncludesDts && file.path.endsWith('.d.ts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!transpiler.onOutfile) {
|
||||
transpiler.onOutfile = file => this.queue(file);
|
||||
}
|
||||
|
||||
transpiler.transpile(file);
|
||||
|
||||
}, function (this: { queue(a: any): void }) {
|
||||
transpiler.join().then(() => {
|
||||
this.queue(null);
|
||||
transpiler.onOutfile = undefined;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let result: IncrementalCompiler;
|
||||
if (config.transpileOnly) {
|
||||
const transpiler = new Transpiler(logFn, printDiagnostic, projectPath, cmdLine);
|
||||
result = <any>(() => createTranspileStream(transpiler));
|
||||
} else {
|
||||
const _builder = builder.createTypeScriptBuilder({ logFn }, projectPath, cmdLine);
|
||||
result = <any>((token: builder.CancellationToken) => createCompileStream(_builder, token));
|
||||
}
|
||||
|
||||
result.src = (opts?: { cwd?: string; base?: string }) => {
|
||||
let _pos = 0;
|
||||
const _fileNames = cmdLine.fileNames.slice(0);
|
||||
return new class extends Readable {
|
||||
constructor() {
|
||||
super({ objectMode: true });
|
||||
}
|
||||
_read() {
|
||||
let more: boolean = true;
|
||||
let path: string;
|
||||
for (; more && _pos < _fileNames.length; _pos++) {
|
||||
path = _fileNames[_pos];
|
||||
more = this.push(new Vinyl({
|
||||
path,
|
||||
contents: readFileSync(path),
|
||||
stat: statSync(path),
|
||||
cwd: opts && opts.cwd,
|
||||
base: opts && opts.base || dirname(projectPath)
|
||||
}));
|
||||
}
|
||||
if (_pos >= _fileNames.length) {
|
||||
this.push(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return <IncrementalCompiler>result;
|
||||
}
|
||||
220
build/lib/tsb/transpiler.js
Normal file
220
build/lib/tsb/transpiler.js
Normal file
@@ -0,0 +1,220 @@
|
||||
"use strict";
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Transpiler = void 0;
|
||||
const ts = require("typescript");
|
||||
const threads = require("node:worker_threads");
|
||||
const Vinyl = require("vinyl");
|
||||
const node_os_1 = require("node:os");
|
||||
function transpile(tsSrc, options) {
|
||||
const isAmd = /\n(import|export)/m.test(tsSrc);
|
||||
if (!isAmd && options.compilerOptions?.module === ts.ModuleKind.AMD) {
|
||||
// enforce NONE module-system for not-amd cases
|
||||
options = { ...options, ...{ compilerOptions: { ...options.compilerOptions, module: ts.ModuleKind.None } } };
|
||||
}
|
||||
const out = ts.transpileModule(tsSrc, options);
|
||||
return {
|
||||
jsSrc: out.outputText,
|
||||
diag: out.diagnostics ?? []
|
||||
};
|
||||
}
|
||||
if (!threads.isMainThread) {
|
||||
// WORKER
|
||||
threads.parentPort?.addListener('message', (req) => {
|
||||
const res = {
|
||||
jsSrcs: [],
|
||||
diagnostics: []
|
||||
};
|
||||
for (const tsSrc of req.tsSrcs) {
|
||||
const out = transpile(tsSrc, req.options);
|
||||
res.jsSrcs.push(out.jsSrc);
|
||||
res.diagnostics.push(out.diag);
|
||||
}
|
||||
threads.parentPort.postMessage(res);
|
||||
});
|
||||
}
|
||||
class TranspileWorker {
|
||||
constructor(outFileFn) {
|
||||
this.id = TranspileWorker.pool++;
|
||||
this._worker = new threads.Worker(__filename);
|
||||
this._durations = [];
|
||||
this._worker.addListener('message', (res) => {
|
||||
if (!this._pending) {
|
||||
console.error('RECEIVING data WITHOUT request');
|
||||
return;
|
||||
}
|
||||
const [resolve, reject, files, options, t1] = this._pending;
|
||||
const outFiles = [];
|
||||
const diag = [];
|
||||
for (let i = 0; i < res.jsSrcs.length; i++) {
|
||||
// inputs and outputs are aligned across the arrays
|
||||
const file = files[i];
|
||||
const jsSrc = res.jsSrcs[i];
|
||||
const diag = res.diagnostics[i];
|
||||
if (diag.length > 0) {
|
||||
diag.push(...diag);
|
||||
continue;
|
||||
}
|
||||
let SuffixTypes;
|
||||
(function (SuffixTypes) {
|
||||
SuffixTypes[SuffixTypes["Dts"] = 5] = "Dts";
|
||||
SuffixTypes[SuffixTypes["Ts"] = 3] = "Ts";
|
||||
SuffixTypes[SuffixTypes["Unknown"] = 0] = "Unknown";
|
||||
})(SuffixTypes || (SuffixTypes = {}));
|
||||
const suffixLen = file.path.endsWith('.d.ts') ? 5 /* SuffixTypes.Dts */
|
||||
: file.path.endsWith('.ts') ? 3 /* SuffixTypes.Ts */
|
||||
: 0 /* SuffixTypes.Unknown */;
|
||||
// check if output of a DTS-files isn't just "empty" and iff so
|
||||
// skip this file
|
||||
if (suffixLen === 5 /* SuffixTypes.Dts */ && _isDefaultEmpty(jsSrc)) {
|
||||
continue;
|
||||
}
|
||||
const outBase = options.compilerOptions?.outDir ?? file.base;
|
||||
const outPath = outFileFn(file.path);
|
||||
outFiles.push(new Vinyl({
|
||||
path: outPath,
|
||||
base: outBase,
|
||||
contents: Buffer.from(jsSrc),
|
||||
}));
|
||||
}
|
||||
this._pending = undefined;
|
||||
this._durations.push(Date.now() - t1);
|
||||
if (diag.length > 0) {
|
||||
reject(diag);
|
||||
}
|
||||
else {
|
||||
resolve(outFiles);
|
||||
}
|
||||
});
|
||||
}
|
||||
terminate() {
|
||||
// console.log(`Worker#${this.id} ENDS after ${this._durations.length} jobs (total: ${this._durations.reduce((p, c) => p + c, 0)}, avg: ${this._durations.reduce((p, c) => p + c, 0) / this._durations.length})`);
|
||||
this._worker.terminate();
|
||||
}
|
||||
get isBusy() {
|
||||
return this._pending !== undefined;
|
||||
}
|
||||
next(files, options) {
|
||||
if (this._pending !== undefined) {
|
||||
throw new Error('BUSY');
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this._pending = [resolve, reject, files, options, Date.now()];
|
||||
const req = {
|
||||
options,
|
||||
tsSrcs: files.map(file => String(file.contents))
|
||||
};
|
||||
this._worker.postMessage(req);
|
||||
});
|
||||
}
|
||||
}
|
||||
TranspileWorker.pool = 1;
|
||||
class Transpiler {
|
||||
constructor(logFn, _onError, configFilePath, _cmdLine) {
|
||||
this._onError = _onError;
|
||||
this._cmdLine = _cmdLine;
|
||||
this._workerPool = [];
|
||||
this._queue = [];
|
||||
this._allJobs = [];
|
||||
logFn('Transpile', `will use ${Transpiler.P} transpile worker`);
|
||||
this._getOutputFileName = (file) => {
|
||||
try {
|
||||
// windows: path-sep normalizing
|
||||
file = ts.normalizePath(file);
|
||||
if (!_cmdLine.options.configFilePath) {
|
||||
// this is needed for the INTERNAL getOutputFileNames-call below...
|
||||
_cmdLine.options.configFilePath = configFilePath;
|
||||
}
|
||||
const isDts = file.endsWith('.d.ts');
|
||||
if (isDts) {
|
||||
file = file.slice(0, -5) + '.ts';
|
||||
_cmdLine.fileNames.push(file);
|
||||
}
|
||||
const outfile = ts.getOutputFileNames(_cmdLine, file, true)[0];
|
||||
if (isDts) {
|
||||
_cmdLine.fileNames.pop();
|
||||
}
|
||||
return outfile;
|
||||
}
|
||||
catch (err) {
|
||||
console.error(file, _cmdLine.fileNames);
|
||||
console.error(err);
|
||||
throw new err;
|
||||
}
|
||||
};
|
||||
}
|
||||
async join() {
|
||||
// wait for all penindg jobs
|
||||
this._consumeQueue();
|
||||
await Promise.allSettled(this._allJobs);
|
||||
this._allJobs.length = 0;
|
||||
// terminate all worker
|
||||
this._workerPool.forEach(w => w.terminate());
|
||||
this._workerPool.length = 0;
|
||||
}
|
||||
transpile(file) {
|
||||
if (this._cmdLine.options.noEmit) {
|
||||
// not doing ANYTHING here
|
||||
return;
|
||||
}
|
||||
const newLen = this._queue.push(file);
|
||||
if (newLen > Transpiler.P ** 2) {
|
||||
this._consumeQueue();
|
||||
}
|
||||
}
|
||||
_consumeQueue() {
|
||||
if (this._queue.length === 0) {
|
||||
// no work...
|
||||
return;
|
||||
}
|
||||
// kinda LAZYily create workers
|
||||
if (this._workerPool.length === 0) {
|
||||
for (let i = 0; i < Transpiler.P; i++) {
|
||||
this._workerPool.push(new TranspileWorker(file => this._getOutputFileName(file)));
|
||||
}
|
||||
}
|
||||
const freeWorker = this._workerPool.filter(w => !w.isBusy);
|
||||
if (freeWorker.length === 0) {
|
||||
// OK, they will pick up work themselves
|
||||
return;
|
||||
}
|
||||
for (const worker of freeWorker) {
|
||||
if (this._queue.length === 0) {
|
||||
break;
|
||||
}
|
||||
const job = new Promise(resolve => {
|
||||
const consume = () => {
|
||||
const files = this._queue.splice(0, Transpiler.P);
|
||||
if (files.length === 0) {
|
||||
// DONE
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
// work on the NEXT file
|
||||
// const [inFile, outFn] = req;
|
||||
worker.next(files, { compilerOptions: this._cmdLine.options }).then(outFiles => {
|
||||
if (this.onOutfile) {
|
||||
outFiles.map(this.onOutfile, this);
|
||||
}
|
||||
consume();
|
||||
}).catch(err => {
|
||||
this._onError(err);
|
||||
});
|
||||
};
|
||||
consume();
|
||||
});
|
||||
this._allJobs.push(job);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.Transpiler = Transpiler;
|
||||
Transpiler.P = Math.floor((0, node_os_1.cpus)().length * .5);
|
||||
function _isDefaultEmpty(src) {
|
||||
return src
|
||||
.replace('"use strict";', '')
|
||||
.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1')
|
||||
.trim().length === 0;
|
||||
}
|
||||
285
build/lib/tsb/transpiler.ts
Normal file
285
build/lib/tsb/transpiler.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import * as threads from 'node:worker_threads';
|
||||
import * as Vinyl from 'vinyl';
|
||||
import { cpus } from 'node:os';
|
||||
|
||||
interface TranspileReq {
|
||||
readonly tsSrcs: string[];
|
||||
readonly options: ts.TranspileOptions;
|
||||
}
|
||||
|
||||
interface TranspileRes {
|
||||
readonly jsSrcs: string[];
|
||||
readonly diagnostics: ts.Diagnostic[][];
|
||||
}
|
||||
|
||||
function transpile(tsSrc: string, options: ts.TranspileOptions): { jsSrc: string; diag: ts.Diagnostic[] } {
|
||||
|
||||
const isAmd = /\n(import|export)/m.test(tsSrc);
|
||||
if (!isAmd && options.compilerOptions?.module === ts.ModuleKind.AMD) {
|
||||
// enforce NONE module-system for not-amd cases
|
||||
options = { ...options, ...{ compilerOptions: { ...options.compilerOptions, module: ts.ModuleKind.None } } };
|
||||
}
|
||||
const out = ts.transpileModule(tsSrc, options);
|
||||
return {
|
||||
jsSrc: out.outputText,
|
||||
diag: out.diagnostics ?? []
|
||||
};
|
||||
}
|
||||
|
||||
if (!threads.isMainThread) {
|
||||
// WORKER
|
||||
threads.parentPort?.addListener('message', (req: TranspileReq) => {
|
||||
const res: TranspileRes = {
|
||||
jsSrcs: [],
|
||||
diagnostics: []
|
||||
};
|
||||
for (const tsSrc of req.tsSrcs) {
|
||||
const out = transpile(tsSrc, req.options);
|
||||
res.jsSrcs.push(out.jsSrc);
|
||||
res.diagnostics.push(out.diag);
|
||||
}
|
||||
threads.parentPort!.postMessage(res);
|
||||
});
|
||||
}
|
||||
|
||||
class TranspileWorker {
|
||||
|
||||
private static pool = 1;
|
||||
|
||||
readonly id = TranspileWorker.pool++;
|
||||
|
||||
private _worker = new threads.Worker(__filename);
|
||||
private _pending?: [resolve: Function, reject: Function, file: Vinyl[], options: ts.TranspileOptions, t1: number];
|
||||
private _durations: number[] = [];
|
||||
|
||||
constructor(outFileFn: (fileName: string) => string) {
|
||||
|
||||
this._worker.addListener('message', (res: TranspileRes) => {
|
||||
if (!this._pending) {
|
||||
console.error('RECEIVING data WITHOUT request');
|
||||
return;
|
||||
}
|
||||
|
||||
const [resolve, reject, files, options, t1] = this._pending;
|
||||
|
||||
const outFiles: Vinyl[] = [];
|
||||
const diag: ts.Diagnostic[] = [];
|
||||
|
||||
for (let i = 0; i < res.jsSrcs.length; i++) {
|
||||
// inputs and outputs are aligned across the arrays
|
||||
const file = files[i];
|
||||
const jsSrc = res.jsSrcs[i];
|
||||
const diag = res.diagnostics[i];
|
||||
|
||||
if (diag.length > 0) {
|
||||
diag.push(...diag);
|
||||
continue;
|
||||
}
|
||||
const enum SuffixTypes {
|
||||
Dts = 5,
|
||||
Ts = 3,
|
||||
Unknown = 0
|
||||
}
|
||||
const suffixLen = file.path.endsWith('.d.ts') ? SuffixTypes.Dts
|
||||
: file.path.endsWith('.ts') ? SuffixTypes.Ts
|
||||
: SuffixTypes.Unknown;
|
||||
|
||||
// check if output of a DTS-files isn't just "empty" and iff so
|
||||
// skip this file
|
||||
if (suffixLen === SuffixTypes.Dts && _isDefaultEmpty(jsSrc)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const outBase = options.compilerOptions?.outDir ?? file.base;
|
||||
const outPath = outFileFn(file.path);
|
||||
|
||||
outFiles.push(new Vinyl({
|
||||
path: outPath,
|
||||
base: outBase,
|
||||
contents: Buffer.from(jsSrc),
|
||||
}));
|
||||
}
|
||||
|
||||
this._pending = undefined;
|
||||
this._durations.push(Date.now() - t1);
|
||||
|
||||
if (diag.length > 0) {
|
||||
reject(diag);
|
||||
} else {
|
||||
resolve(outFiles);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
terminate() {
|
||||
// console.log(`Worker#${this.id} ENDS after ${this._durations.length} jobs (total: ${this._durations.reduce((p, c) => p + c, 0)}, avg: ${this._durations.reduce((p, c) => p + c, 0) / this._durations.length})`);
|
||||
this._worker.terminate();
|
||||
}
|
||||
|
||||
get isBusy() {
|
||||
return this._pending !== undefined;
|
||||
}
|
||||
|
||||
next(files: Vinyl[], options: ts.TranspileOptions) {
|
||||
if (this._pending !== undefined) {
|
||||
throw new Error('BUSY');
|
||||
}
|
||||
return new Promise<Vinyl[]>((resolve, reject) => {
|
||||
this._pending = [resolve, reject, files, options, Date.now()];
|
||||
const req: TranspileReq = {
|
||||
options,
|
||||
tsSrcs: files.map(file => String(file.contents))
|
||||
};
|
||||
this._worker.postMessage(req);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class Transpiler {
|
||||
|
||||
static P = Math.floor(cpus().length * .5);
|
||||
|
||||
private readonly _getOutputFileName: (name: string) => string;
|
||||
|
||||
public onOutfile?: (file: Vinyl) => void;
|
||||
|
||||
private _workerPool: TranspileWorker[] = [];
|
||||
private _queue: Vinyl[] = [];
|
||||
private _allJobs: Promise<any>[] = [];
|
||||
|
||||
constructor(
|
||||
logFn: (topic: string, message: string) => void,
|
||||
private readonly _onError: (err: any) => void,
|
||||
configFilePath: string,
|
||||
private readonly _cmdLine: ts.ParsedCommandLine
|
||||
) {
|
||||
logFn('Transpile', `will use ${Transpiler.P} transpile worker`);
|
||||
|
||||
|
||||
// very complicated logic to re-use TS internal functions to know the output path
|
||||
// given a TS input path and its config
|
||||
type InternalTsApi = typeof ts & {
|
||||
normalizePath(path: string): string;
|
||||
getOutputFileNames(commandLine: ts.ParsedCommandLine, inputFileName: string, ignoreCase: boolean): readonly string[];
|
||||
};
|
||||
this._getOutputFileName = (file) => {
|
||||
try {
|
||||
|
||||
// windows: path-sep normalizing
|
||||
file = (<InternalTsApi>ts).normalizePath(file);
|
||||
|
||||
if (!_cmdLine.options.configFilePath) {
|
||||
// this is needed for the INTERNAL getOutputFileNames-call below...
|
||||
_cmdLine.options.configFilePath = configFilePath;
|
||||
}
|
||||
const isDts = file.endsWith('.d.ts');
|
||||
if (isDts) {
|
||||
file = file.slice(0, -5) + '.ts';
|
||||
_cmdLine.fileNames.push(file);
|
||||
}
|
||||
const outfile = (<InternalTsApi>ts).getOutputFileNames(_cmdLine, file, true)[0];
|
||||
if (isDts) {
|
||||
_cmdLine.fileNames.pop();
|
||||
}
|
||||
return outfile;
|
||||
|
||||
} catch (err) {
|
||||
console.error(file, _cmdLine.fileNames);
|
||||
console.error(err);
|
||||
throw new err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async join() {
|
||||
// wait for all penindg jobs
|
||||
this._consumeQueue();
|
||||
await Promise.allSettled(this._allJobs);
|
||||
this._allJobs.length = 0;
|
||||
|
||||
// terminate all worker
|
||||
this._workerPool.forEach(w => w.terminate());
|
||||
this._workerPool.length = 0;
|
||||
}
|
||||
|
||||
|
||||
transpile(file: Vinyl) {
|
||||
|
||||
if (this._cmdLine.options.noEmit) {
|
||||
// not doing ANYTHING here
|
||||
return;
|
||||
}
|
||||
|
||||
const newLen = this._queue.push(file);
|
||||
if (newLen > Transpiler.P ** 2) {
|
||||
this._consumeQueue();
|
||||
}
|
||||
}
|
||||
|
||||
private _consumeQueue(): void {
|
||||
|
||||
if (this._queue.length === 0) {
|
||||
// no work...
|
||||
return;
|
||||
}
|
||||
|
||||
// kinda LAZYily create workers
|
||||
if (this._workerPool.length === 0) {
|
||||
for (let i = 0; i < Transpiler.P; i++) {
|
||||
this._workerPool.push(new TranspileWorker(file => this._getOutputFileName(file)));
|
||||
}
|
||||
}
|
||||
|
||||
const freeWorker = this._workerPool.filter(w => !w.isBusy);
|
||||
if (freeWorker.length === 0) {
|
||||
// OK, they will pick up work themselves
|
||||
return;
|
||||
}
|
||||
|
||||
for (const worker of freeWorker) {
|
||||
if (this._queue.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const job = new Promise(resolve => {
|
||||
|
||||
const consume = () => {
|
||||
const files = this._queue.splice(0, Transpiler.P);
|
||||
if (files.length === 0) {
|
||||
// DONE
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
// work on the NEXT file
|
||||
// const [inFile, outFn] = req;
|
||||
worker.next(files, { compilerOptions: this._cmdLine.options }).then(outFiles => {
|
||||
if (this.onOutfile) {
|
||||
outFiles.map(this.onOutfile, this);
|
||||
}
|
||||
consume();
|
||||
}).catch(err => {
|
||||
this._onError(err);
|
||||
});
|
||||
};
|
||||
|
||||
consume();
|
||||
});
|
||||
|
||||
this._allJobs.push(job);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _isDefaultEmpty(src: string): boolean {
|
||||
return src
|
||||
.replace('"use strict";', '')
|
||||
.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1')
|
||||
.trim().length === 0;
|
||||
}
|
||||
124
build/lib/tsb/utils.js
Normal file
124
build/lib/tsb/utils.js
Normal file
@@ -0,0 +1,124 @@
|
||||
"use strict";
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.graph = exports.strings = exports.collections = void 0;
|
||||
var collections;
|
||||
(function (collections) {
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
function lookup(collection, key) {
|
||||
if (hasOwnProperty.call(collection, key)) {
|
||||
return collection[key];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
collections.lookup = lookup;
|
||||
function insert(collection, key, value) {
|
||||
collection[key] = value;
|
||||
}
|
||||
collections.insert = insert;
|
||||
function lookupOrInsert(collection, key, value) {
|
||||
if (hasOwnProperty.call(collection, key)) {
|
||||
return collection[key];
|
||||
}
|
||||
else {
|
||||
collection[key] = value;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
collections.lookupOrInsert = lookupOrInsert;
|
||||
function forEach(collection, callback) {
|
||||
for (const key in collection) {
|
||||
if (hasOwnProperty.call(collection, key)) {
|
||||
callback({
|
||||
key: key,
|
||||
value: collection[key]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
collections.forEach = forEach;
|
||||
function contains(collection, key) {
|
||||
return hasOwnProperty.call(collection, key);
|
||||
}
|
||||
collections.contains = contains;
|
||||
})(collections = exports.collections || (exports.collections = {}));
|
||||
var strings;
|
||||
(function (strings) {
|
||||
/**
|
||||
* The empty string. The one and only.
|
||||
*/
|
||||
strings.empty = '';
|
||||
strings.eolUnix = '\r\n';
|
||||
function format(value, ...rest) {
|
||||
return value.replace(/({\d+})/g, function (match) {
|
||||
const index = Number(match.substring(1, match.length - 1));
|
||||
return String(rest[index]) || match;
|
||||
});
|
||||
}
|
||||
strings.format = format;
|
||||
})(strings = exports.strings || (exports.strings = {}));
|
||||
var graph;
|
||||
(function (graph) {
|
||||
function newNode(data) {
|
||||
return {
|
||||
data: data,
|
||||
incoming: {},
|
||||
outgoing: {}
|
||||
};
|
||||
}
|
||||
graph.newNode = newNode;
|
||||
class Graph {
|
||||
constructor(_hashFn) {
|
||||
this._hashFn = _hashFn;
|
||||
this._nodes = {};
|
||||
// empty
|
||||
}
|
||||
traverse(start, inwards, callback) {
|
||||
const startNode = this.lookup(start);
|
||||
if (!startNode) {
|
||||
return;
|
||||
}
|
||||
this._traverse(startNode, inwards, {}, callback);
|
||||
}
|
||||
_traverse(node, inwards, seen, callback) {
|
||||
const key = this._hashFn(node.data);
|
||||
if (collections.contains(seen, key)) {
|
||||
return;
|
||||
}
|
||||
seen[key] = true;
|
||||
callback(node.data);
|
||||
const nodes = inwards ? node.outgoing : node.incoming;
|
||||
collections.forEach(nodes, (entry) => this._traverse(entry.value, inwards, seen, callback));
|
||||
}
|
||||
inertEdge(from, to) {
|
||||
const fromNode = this.lookupOrInsertNode(from);
|
||||
const toNode = this.lookupOrInsertNode(to);
|
||||
fromNode.outgoing[this._hashFn(to)] = toNode;
|
||||
toNode.incoming[this._hashFn(from)] = fromNode;
|
||||
}
|
||||
removeNode(data) {
|
||||
const key = this._hashFn(data);
|
||||
delete this._nodes[key];
|
||||
collections.forEach(this._nodes, (entry) => {
|
||||
delete entry.value.outgoing[key];
|
||||
delete entry.value.incoming[key];
|
||||
});
|
||||
}
|
||||
lookupOrInsertNode(data) {
|
||||
const key = this._hashFn(data);
|
||||
let node = collections.lookup(this._nodes, key);
|
||||
if (!node) {
|
||||
node = newNode(data);
|
||||
this._nodes[key] = node;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
lookup(data) {
|
||||
return collections.lookup(this._nodes, this._hashFn(data));
|
||||
}
|
||||
}
|
||||
graph.Graph = Graph;
|
||||
})(graph = exports.graph || (exports.graph = {}));
|
||||
140
build/lib/tsb/utils.ts
Normal file
140
build/lib/tsb/utils.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export module collections {
|
||||
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
|
||||
export function lookup<T>(collection: { [keys: string]: T }, key: string): T | null {
|
||||
if (hasOwnProperty.call(collection, key)) {
|
||||
return collection[key];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function insert<T>(collection: { [keys: string]: T }, key: string, value: T): void {
|
||||
collection[key] = value;
|
||||
}
|
||||
|
||||
export function lookupOrInsert<T>(collection: { [keys: string]: T }, key: string, value: T): T {
|
||||
if (hasOwnProperty.call(collection, key)) {
|
||||
return collection[key];
|
||||
} else {
|
||||
collection[key] = value;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export function forEach<T>(collection: { [keys: string]: T }, callback: (entry: { key: string; value: T }) => void): void {
|
||||
for (const key in collection) {
|
||||
if (hasOwnProperty.call(collection, key)) {
|
||||
callback({
|
||||
key: key,
|
||||
value: collection[key]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function contains(collection: { [keys: string]: any }, key: string): boolean {
|
||||
return hasOwnProperty.call(collection, key);
|
||||
}
|
||||
}
|
||||
|
||||
export module strings {
|
||||
|
||||
/**
|
||||
* The empty string. The one and only.
|
||||
*/
|
||||
export const empty = '';
|
||||
|
||||
export const eolUnix = '\r\n';
|
||||
|
||||
export function format(value: string, ...rest: any[]): string {
|
||||
return value.replace(/({\d+})/g, function (match) {
|
||||
const index = Number(match.substring(1, match.length - 1));
|
||||
return String(rest[index]) || match;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export module graph {
|
||||
|
||||
export interface Node<T> {
|
||||
data: T;
|
||||
incoming: { [key: string]: Node<T> };
|
||||
outgoing: { [key: string]: Node<T> };
|
||||
}
|
||||
|
||||
export function newNode<T>(data: T): Node<T> {
|
||||
return {
|
||||
data: data,
|
||||
incoming: {},
|
||||
outgoing: {}
|
||||
};
|
||||
}
|
||||
|
||||
export class Graph<T> {
|
||||
|
||||
private _nodes: { [key: string]: Node<T> } = {};
|
||||
|
||||
constructor(private _hashFn: (element: T) => string) {
|
||||
// empty
|
||||
}
|
||||
|
||||
traverse(start: T, inwards: boolean, callback: (data: T) => void): void {
|
||||
const startNode = this.lookup(start);
|
||||
if (!startNode) {
|
||||
return;
|
||||
}
|
||||
this._traverse(startNode, inwards, {}, callback);
|
||||
}
|
||||
|
||||
private _traverse(node: Node<T>, inwards: boolean, seen: { [key: string]: boolean }, callback: (data: T) => void): void {
|
||||
const key = this._hashFn(node.data);
|
||||
if (collections.contains(seen, key)) {
|
||||
return;
|
||||
}
|
||||
seen[key] = true;
|
||||
callback(node.data);
|
||||
const nodes = inwards ? node.outgoing : node.incoming;
|
||||
collections.forEach(nodes, (entry) => this._traverse(entry.value, inwards, seen, callback));
|
||||
}
|
||||
|
||||
inertEdge(from: T, to: T): void {
|
||||
const fromNode = this.lookupOrInsertNode(from);
|
||||
const toNode = this.lookupOrInsertNode(to);
|
||||
|
||||
fromNode.outgoing[this._hashFn(to)] = toNode;
|
||||
toNode.incoming[this._hashFn(from)] = fromNode;
|
||||
}
|
||||
|
||||
removeNode(data: T): void {
|
||||
const key = this._hashFn(data);
|
||||
delete this._nodes[key];
|
||||
collections.forEach(this._nodes, (entry) => {
|
||||
delete entry.value.outgoing[key];
|
||||
delete entry.value.incoming[key];
|
||||
});
|
||||
}
|
||||
|
||||
lookupOrInsertNode(data: T): Node<T> {
|
||||
const key = this._hashFn(data);
|
||||
let node = collections.lookup(this._nodes, key);
|
||||
|
||||
if (!node) {
|
||||
node = newNode(data);
|
||||
this._nodes[key] = node;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
lookup(data: T): Node<T> | null {
|
||||
return collections.lookup(this._nodes, this._hashFn(data));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user