@tscc/tscc
Version:
A typescript transpiler and bundler that wires up tsickle and closure compiler seamlessly
367 lines (366 loc) • 18.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TsccError = exports.CcError = exports.TEMP_DIR = void 0;
const StreamArray = require("stream-json/streamers/StreamArray");
const tsickle = require("tsickle");
const ts = require("typescript");
const default_libs_1 = require("./default_libs");
const ClosureDependencyGraph_1 = require("./graph/ClosureDependencyGraph");
const TypescriptDependencyGraph_1 = require("./graph/TypescriptDependencyGraph");
const Logger_1 = require("./log/Logger");
const spinner = require("./log/spinner");
const facade_1 = require("./tsickle_patches/facade");
const array_utils_1 = require("./shared/array_utils");
const PartialMap_1 = require("./shared/PartialMap");
const vinyl_utils_1 = require("./shared/vinyl_utils");
const escape_goog_identifier_1 = require("./shared/escape_goog_identifier");
const spawn_compiler_1 = require("./spawn_compiler");
const TsccSpecWithTS_1 = require("./spec/TsccSpecWithTS");
const decorator_property_transformer_1 = require("./transformer/decorator_property_transformer");
const rest_property_transformer_1 = require("./transformer/rest_property_transformer");
const dts_requiretype_transformer_1 = require("./transformer/dts_requiretype_transformer");
const goog_namespace_transformer_1 = require("./transformer/goog_namespace_transformer");
const external_module_support_1 = require("./external_module_support");
const fs = require("fs");
const path = require("path");
const stream = require("stream");
const util_1 = require("util");
const fsExtra = require("fs-extra");
const vfs = require("vinyl-fs");
const upath = require("upath");
const chalk = require("chalk");
exports.TEMP_DIR = ".tscc_temp";
/** @internal */
async function tscc(tsccSpecJSONOrItsPath, tsConfigPathOrTsArgs, compilerOptionsOverride) {
var _a;
const tsccLogger = new Logger_1.default(chalk.green("TSCC: "), process.stderr);
const tsLogger = new Logger_1.default(chalk.blue("TS: "), process.stderr);
const tsccSpec = TsccSpecWithTS_1.default.loadSpecWithTS(tsccSpecJSONOrItsPath, tsConfigPathOrTsArgs, compilerOptionsOverride, (msg) => { tsccLogger.log(msg); });
const program = ts.createProgram([...tsccSpec.getAbsoluteFileNamesSet()], tsccSpec.getCompilerOptions(), tsccSpec.getCompilerHost());
const diagnostics = ts.getPreEmitDiagnostics(program);
if (diagnostics.length)
throw new TsccSpecWithTS_1.TsError(diagnostics);
const tsDepsGraph = new TypescriptDependencyGraph_1.default(program);
tsccSpec.getOrderedModuleSpecs().forEach(moduleSpec => tsDepsGraph.addRootFile(moduleSpec.entry));
(0, array_utils_1.union)(tsccSpec.getExternalModuleNames(), (_a = tsccSpec.getCompilerOptions().types) !== null && _a !== void 0 ? _a : [])
.map(tsccSpec.resolveExternalModuleTypeReference, tsccSpec)
.map(tsDepsGraph.addRootFile, tsDepsGraph);
// If user explicitly provided `types` compiler option, it is more likely that its type is actually
// used in user code.
const transformerHost = getTsickleHost(tsccSpec, tsDepsGraph, tsLogger);
/**
* Ideally, the dependency graph should be determined from ts sourceFiles, and the compiler
* process can be spawned asynchronously before calling tsickle.
* Then, we will be able to set `tsickleHost.shouldSkipTsickleProcessing` and the order of
* files that are transpiled by tsickle. This has an advantage in that we can stream JSONs
* in order that they came out from tsickle, cuz Closure compiler requires JSON files to be
* sorted exactly as how js files would be sorted.
*
* As I recall, it was unsafe to use ModuleManifest returned from tsickle, cuz it does
* not include forwardDeclares or something.
* For now, we are computing the graph from the tsickle output in order to reuse
* codes from closure-tools-helper.
*/
const closureDepsGraph = new ClosureDependencyGraph_1.default();
const tsickleOutput = new PartialMap_1.default();
const { writeFile, writeExterns, externPath } = getWriteFileImpl(tsccSpec, tsickleOutput, closureDepsGraph);
const stdInStream = new stream.Readable({ read: function () { } });
const pushImmediately = (arg) => setImmediate(pushToStream, stdInStream, arg);
// ----- start tsickle call -----
pushImmediately("[");
// Manually push tslib, goog(base.js), goog.reflect, which are required in compilation
const defaultLibsProvider = (0, default_libs_1.default)(tsccSpec.getTSRoot());
defaultLibsProvider.libs.forEach(({ path, id }) => {
// ..only when user-provided sources do not provide such modules
if (closureDepsGraph.hasModule(id))
return;
writeFile(path, fs.readFileSync(path, 'utf8'));
});
// Manually push gluing modules
(0, external_module_support_1.getGluingModules)(tsccSpec, transformerHost).forEach(({ path, content }) => {
writeFile(path, content);
});
// Manually push jsFiles, if there are any
const jsFiles = tsccSpec.getJsFiles();
if (jsFiles.length) {
jsFiles.forEach(path => {
writeFile(path, fs.readFileSync(path, 'utf8'));
});
}
let result;
try {
(0, facade_1.applyPatches)();
result = tsickle.emit(program, transformerHost, writeFile, undefined, undefined, false, {
afterTs: [
goog_namespace_transformer_1.googNamespaceTransformer,
(0, dts_requiretype_transformer_1.default)(tsccSpec, transformerHost),
(0, decorator_property_transformer_1.default)(transformerHost),
(0, rest_property_transformer_1.default)(transformerHost)
]
});
}
finally {
(0, facade_1.restorePatches)(); // Make sure that our patches are removed even if tsickle.emit throws.
}
// If tsickle errors, print diagnostics and exit.
if (result.diagnostics.length)
throw new TsccSpecWithTS_1.TsError(result.diagnostics);
const { src, flags } = closureDepsGraph.getSortedFilesAndFlags(tsccSpec.getOrderedModuleSpecs().map(entry => (Object.assign({ moduleId: transformerHost.pathToModuleName('', entry.entry) }, entry))));
pushTsickleOutputToStream(src, tsccSpec, tsickleOutput, stdInStream, tsccLogger);
// Write externs to a temp file.
// ..only after attaching tscc's generated externs
const externs = tsickle.getGeneratedExterns(result.externs, tsccSpec.getTSRoot()) +
(0, external_module_support_1.getExternsForExternalModules)(tsccSpec, transformerHost);
writeExterns(externs);
pushImmediately("]");
pushImmediately(null);
/// ----- end tsickle call -----
/**
* Spawn compiler process with module dependency information
*/
const ccLogger = new Logger_1.default(chalk.redBright("ClosureCompiler: "), process.stderr);
spinner.startTask("Closure Compiler");
const compilerProcess = (0, spawn_compiler_1.default)([
...tsccSpec.getBaseCompilerFlags(),
...flags,
'--json_streams', "BOTH",
'--externs', externPath,
...(0, array_utils_1.riffle)('--externs', defaultLibsProvider.externs)
], ccLogger, tsccSpec.debug().persistArtifacts);
const compilerProcessClose = new Promise((resolve, reject) => {
function onCompilerProcessClose(code) {
if (code === 0) {
spinner.succeed();
spinner.unstick();
tsccLogger.log(`Compilation success.`);
if (tsccSpec.debug().persistArtifacts) {
tsccLogger.log(tsccSpec.getOutputFileNames().join('\n'));
}
resolve();
}
else {
spinner.fail(`Closure compiler error`);
spinner.unstick();
reject(new CcError(`Closure compiler has exited with code ${code}`));
}
}
compilerProcess.on("close", onCompilerProcessClose);
});
stdInStream
.pipe(compilerProcess.stdin);
// Use gulp-style transform streams to post-process cc output - see shared/vinyl_utils.ts.
// TODO support returning gulp stream directly
const useSourceMap = tsccSpec.getCompilerOptions().sourceMap;
const writeCompilationOutput = (0, util_1.promisify)(stream.pipeline)(compilerProcess.stdout,
// jsonStreaming: true option makes the Parser of the stream-json package to fail gracefully
// when no data is streamed. Currently this is not included in @types/stream-json. TODO make a
// PR in Definitelytyped about this.
StreamArray.withParser({ jsonStreaming: true }), new vinyl_utils_1.ClosureJsonToVinyl(useSourceMap, tsccLogger), new vinyl_utils_1.RemoveTempGlobalAssignments(tsccLogger), vfs.dest('.', { sourcemaps: '.' }));
await Promise.all([compilerProcessClose, writeCompilationOutput]);
}
exports.default = tscc;
class CcError extends Error {
}
exports.CcError = CcError;
class TsccError extends Error {
}
exports.TsccError = TsccError;
class UnexpectedFileError extends TsccError {
}
/**
* Remove `//# sourceMappingURL=...` from source TS output which typescript generates when
* sourceMap is enabled. Closure Compiler does not recognize attached sourcemaps in Vinyl
* if this comment is present.
* TODO if closure is actually looking for sourcemaps within that url, check that if we can provide
* sourcemap in such a way that closure can find it, and remove this workaround.
*/
function removeSourceMappingUrl(tsOutput) {
return tsOutput.replace(reSourceMappingURL, '');
}
const reSourceMappingURL = /^\/\/[#@]\s*sourceMappingURL\s*=\s*.*?\s*$/mi;
function getWriteFileImpl(spec, tsickleVinylOutput, closureDepsGraph) {
const tempFileDir = path.join(process.cwd(), exports.TEMP_DIR, spec.getProjectHash());
fsExtra.mkdirpSync(tempFileDir);
// Closure compiler produces an error if output file's name is the same as one of
// input files, which are in this case .js files. However, if such a file is an intermediate file
// generated by TS, it is a legitimate usage. So we make file paths coming from TS virtual by
// appending '.tsickle' to it.
// See GH issue #82: When Windows-style path is used as a 'path' property of input, the Compiler
// does not recognize path separators and fails to resolve paths in sourcemaps. Hence we replace
// paths to unix-style paths just before we add it to input JSON object.
const toVirtualPath = (filePath) => {
if (tsOutputs.includes(filePath))
filePath += '.tsickle';
let relPath = path.relative(spec.getTSRoot(), filePath);
if (process.platform === 'win32') {
// Convert to unix-style path only on Windows; on Unix, Windows-style path separator
// is a valid directory/file name.
relPath = upath.normalize(relPath);
}
return relPath;
};
const tsOutputs = [...spec.getAbsoluteFileNamesSet()].map(fileName => {
let ext = path.extname(fileName);
return fileName.slice(0, -ext.length) + '.js';
});
const writeFile = (filePath, contents) => {
// Typescript calls writeFile with not normalized path. 'spec.getAbsoluteFileNamesSet' returns
// normalized paths. Fixes GH issue #81.
filePath = path.normalize(filePath);
// Typescript calls writeFileCallback with absolute path.
// On the contrary, "file" property of sourcemaps are relative path from ts project root.
// For consistency, we convert absolute paths here to path relative to ts project root.
if (spec.debug().persistArtifacts) {
// filePath may contain colons which are not allowed in the middle of a path
// such colons are a part of 'root', we are merely stripping it out.
let filePathMinusRoot = filePath.substring(path.parse(filePath).root.length);
fsExtra.outputFileSync(path.join(tempFileDir, filePathMinusRoot), contents);
}
switch (path.extname(filePath)) {
case '.js': {
if (spec.getCompilerOptions().sourceMap) {
contents = removeSourceMappingUrl(contents);
}
closureDepsGraph.addSourceByContent(filePath, contents);
tsickleVinylOutput.set(filePath, {
src: contents,
path: toVirtualPath(filePath)
});
return;
}
case '.map': {
let sourceFilePath = filePath.slice(0, -4);
tsickleVinylOutput.set(sourceFilePath, {
sourceMap: contents
});
return;
}
default:
throw new UnexpectedFileError(`Unrecognized file emitted from tsc: ${filePath}.`);
}
};
const writeExterns = (contents) => {
fs.writeFileSync(externPath, contents);
};
const externPath = path.join(tempFileDir, "externs_generated.js");
return { writeFile, writeExterns, externPath };
}
function pushToStream(stream, ...args) {
for (let arg of args)
stream.push(arg);
}
function pushTsickleOutputToStream(src, // file names, ordered to be pushed to compiler sequentially
tsccSpec, tsickleVinylOutput, stdInStream, logger) {
let isFirstFile = true;
const pushToStdInStream = (...args) => {
pushToStream(stdInStream, ...args);
};
const pushVinylToStdInStream = (json) => {
if (isFirstFile)
isFirstFile = false;
else
pushToStdInStream(",");
pushToStdInStream(JSON.stringify(json));
};
if (tsccSpec.debug().persistArtifacts) {
logger.log(`File orders:`);
src.forEach(sr => logger.log(sr));
}
setImmediate(() => {
src.forEach(name => {
let out = tsickleVinylOutput.get(name);
if (!out) {
logger.log(`File not emitted from tsickle: ${name}`);
}
else {
pushVinylToStdInStream(out);
}
});
});
}
function getTsickleHost(tsccSpec, tsDependencyGraph, logger) {
const options = tsccSpec.getCompilerOptions();
const compilerHost = tsccSpec.getCompilerHost();
// Non-absolute file names are resolved from the TS project root.
const fileNamesSet = tsccSpec.getAbsoluteFileNamesSet();
const externalModuleData = tsccSpec.getExternalModuleDataMap();
const ignoreWarningsPath = tsccSpec.debug().ignoreWarningsPath || ["/node_modules/"];
const transformerHost = {
// required since tsickle 0.41.0, currently only used in transpiling `goog.tsMigration*ExportsShim`.
rootDirsRelative(filename) {
return filename;
},
shouldSkipTsickleProcessing(fileName) {
// Non-absolute files are resolved relative to a typescript project root.
const absFileName = path.resolve(tsccSpec.getTSRoot(), fileName);
// This may include script(non-module) files that is specified in tsconfig. Such files
// are not discoverable by dependency checking.
if (fileNamesSet.has(absFileName))
return false;
// Previously, we've processed all files that are in the same node_modules directory of type
// declaration file for external modules. The current behavior with including transitive
// dependencies only will have the same effect on such files, because `ts.createProgram`
// anyway adds only such files to the program. So this update will in effect include strictly
// larger set of files.
return !tsDependencyGraph.hasFile(absFileName);
},
shouldIgnoreWarningsForPath(fileName) {
return true; // Just a stub, maybe add configuration later.
// controls whether a warning will cause compilation failure.
},
googmodule: true,
transformDecorators: true,
transformTypesToClosure: true,
// This controlls whether @suppress annotation will be added to fileoverview comments or
// not. https://github.com/angular/tsickle/commit/e83542d20cfabb17b2012013917d8c6df35fd227
// Prior to this commit, tsickle had added @suppress annotations unconditionally.
generateExtraSuppressions: true,
typeBlackListPaths: new Set(),
untyped: false,
logWarning(warning) {
if (warning.file) {
let { fileName } = warning.file;
for (let i = 0, l = ignoreWarningsPath.length; i < l; i++) {
if (fileName.indexOf(ignoreWarningsPath[i]) !== -1)
return;
}
}
logger.log(ts.formatDiagnostic(warning, compilerHost));
},
options,
/**
* The name suggests that it supports import from './dir' that resolves to './dir/index.ts'.
* In effect, enabling this make `pathToModuleName` to be fed with
*/
convertIndexImportShorthand: true,
moduleResolutionHost: compilerHost,
fileNameToModuleId: (fileName) => path.relative(process.cwd(), fileName),
/**
* Unlike the default function that tsickle uses, here we are actually resolving
* the imported name with typescript's API. This is safer for consumers may use
* custom path mapping using "baseUrl", "paths" , but at the cost of relinquishing
* deterministic output based on a single file.
*/
pathToModuleName: (context, fileName) => {
// 'tslib' is always considered as an external module.
if (fileName === 'tslib')
return 'tslib';
if (externalModuleData.has(fileName)) {
let data = externalModuleData.get(fileName);
// Module names specified as external are not resolved, which in effect cause
// googmodule transformer to emit module names verbatim in `goog.require()`.
if (!data.isFilePath)
return (0, escape_goog_identifier_1.escapeGoogAdmissibleName)(fileName);
}
// Resolve module via ts API
const resolved = ts.resolveModuleName(fileName, context, options, compilerHost);
if (resolved && resolved.resolvedModule) {
fileName = resolved.resolvedModule.resolvedFileName;
}
// resolve relative to the ts project root.
fileName = path.relative(tsccSpec.getTSRoot(), fileName);
return (0, escape_goog_identifier_1.escapeGoogAdmissibleName)(fileName);
}
};
return transformerHost;
}