"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.CancellationToken = void 0; exports.createTypeScriptBuilder = createTypeScriptBuilder; const fs = 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"); const source_map_1 = require("source-map"); var CancellationToken; (function (CancellationToken) { CancellationToken.None = { isCancellationRequested() { return false; } }; })(CancellationToken || (exports.CancellationToken = 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('sha256') .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('sha256') .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'; let sourceMap = JSON.parse(sourcemapFile.text); sourceMap.sources[0] = tsname.replace(/\\/g, '/'); // check for an "input source" map and combine them // in step 1 we extract all line edit from the input source map, and // in step 2 we apply the line edits to the typescript source map const snapshot = host.getScriptSnapshot(fileName); if (snapshot instanceof VinylScriptSnapshot && snapshot.sourceMap) { const inputSMC = new source_map_1.SourceMapConsumer(snapshot.sourceMap); const tsSMC = new source_map_1.SourceMapConsumer(sourceMap); let didChange = false; const smg = new source_map_1.SourceMapGenerator({ file: sourceMap.file, sourceRoot: sourceMap.sourceRoot }); // step 1 const lineEdits = new Map(); inputSMC.eachMapping(m => { if (m.originalLine === m.generatedLine) { // same line mapping let array = lineEdits.get(m.originalLine); if (!array) { array = []; lineEdits.set(m.originalLine, array); } array.push([m.originalColumn, m.generatedColumn]); } else { // NOT SUPPORTED } }); // step 2 tsSMC.eachMapping(m => { didChange = true; const edits = lineEdits.get(m.originalLine); let originalColumnDelta = 0; if (edits) { for (const [from, to] of edits) { if (to >= m.originalColumn) { break; } originalColumnDelta = from - to; } } smg.addMapping({ source: m.source, name: m.name, generated: { line: m.generatedLine, column: m.generatedColumn }, original: { line: m.originalLine, column: m.originalColumn + originalColumnDelta } }); }); if (didChange) { [tsSMC, inputSMC].forEach((consumer) => { consumer.sources.forEach((sourceFile) => { smg._sources.add(sourceFile); const sourceContent = consumer.sourceContentFor(sourceFile); if (sourceContent !== null) { smg.setSourceContent(sourceFile, sourceContent); } }); }); sourceMap = JSON.parse(smg.toString()); // const filename = '/Users/jrieken/Code/vscode/src2/' + vinyl.relative + '.map'; // fs.promises.mkdir(path.dirname(filename), { recursive: true }).then(async () => { // await fs.promises.writeFile(filename, smg.toString()); // await fs.promises.writeFile('/Users/jrieken/Code/vscode/src2/' + vinyl.relative, vinyl.contents); // }); } } 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 }; } class ScriptSnapshot { _text; _mtime; 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 { _base; sourceMap; constructor(file) { super(file.contents.toString(), file.stat.mtime); this._base = file.base; this.sourceMap = file.sourceMap; } getBase() { return this._base; } } class LanguageServiceHost { _cmdLine; _projectPath; _log; _snapshots; _filesInProject; _filesAdded; _dependencies; _dependenciesRecomputeList; _fileNameToDeclaredModule; _projectVersion; constructor(_cmdLine, _projectPath, _log) { this._cmdLine = _cmdLine; this._projectPath = _projectPath; this._log = _log; 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: fs.readFileSync(filename), base: this.getCompilationSettings().outDir, stat: fs.statSync(filename) })); this.addScriptSnapshot(filename, result); } catch (e) { // ignore } } return result; } static _declareModule = /declare\s+module\s+('|")(.+)\1/g; 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); } directoryExists = ts.sys.directoryExists; getDirectories = ts.sys.getDirectories; fileExists = ts.sys.fileExists; readFile = ts.sys.readFile; readDirectory = ts.sys.readDirectory; // ---- 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); let resolvedPath = path.resolve(dirname, ref.fileName); if (resolvedPath.endsWith('.js')) { resolvedPath = resolvedPath.slice(0, -3); } 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); } } } }); } } //# sourceMappingURL=builder.js.map