@tscc/tscc
Version:
A typescript transpiler and bundler that wires up tsickle and closure compiler seamlessly
284 lines (283 loc) • 14.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TsError = void 0;
const tscc_spec_1 = require("@tscc/tscc-spec");
const ts = require("typescript");
const path = require("path");
class TsError extends Error {
constructor(diagnostics) {
super(ts.formatDiagnostics(diagnostics, ts.createCompilerHost({})));
this.diagnostics = diagnostics;
}
}
exports.TsError = TsError;
class TsccSpecWithTS extends tscc_spec_1.TsccSpec {
constructor(tsccSpec, basePath, parsedConfig, projectRoot) {
super(tsccSpec, basePath);
this.parsedConfig = parsedConfig;
this.projectRoot = projectRoot;
this.tsCompilerHost = ts.createCompilerHost(this.parsedConfig.options);
this.validateSpecWithTS();
}
static loadTsConfigFromArgs(tsArgs, specRoot, onWarning) {
const { options, fileNames, errors } = ts.parseCommandLine(tsArgs);
if (errors.length) {
throw new TsError(errors);
}
if (fileNames.length) {
onWarning(`Files provided via TS args are ignored.`);
}
// If "--project" argument is supplied - load tsconfig from there, merge things with this.
// Otherwise, we lookup from tsccSpecPath - this is what is different to "tsc" (which looks up
// the current working directory).
// I think this is a more reasonable behavior, since many users will just put spec.json and
// tsconfig.json at the same directory, they will otherwise have to provide the same information
// twice, once for tscc and once for tsc.
const configFileName = TsccSpecWithTS.findConfigFileAndThrow(options.project, specRoot);
return TsccSpecWithTS.loadTsConfigFromResolvedPath(configFileName, options);
}
// compilerOptions is a JSON object in the form of tsconfig.json's compilerOption value.
// Its value will override compiler options.
static loadTsConfigFromPath(tsConfigPath, specRoot, compilerOptions) {
const configFileName = TsccSpecWithTS.findConfigFileAndThrow(tsConfigPath, specRoot);
let options = {}, errors;
if (compilerOptions) {
({ options, errors } = ts.convertCompilerOptionsFromJson(compilerOptions, path.dirname(configFileName)));
if (errors.length) {
throw new TsError(errors);
}
}
return TsccSpecWithTS.loadTsConfigFromResolvedPath(configFileName, options);
}
// At least one among searchPath and defaultLocation must be non-null.
static findConfigFileAndThrow(searchPath, defaultLocation) {
const configFileName = TsccSpecWithTS.resolveSpecFile(searchPath, 'tsconfig.json', defaultLocation);
if (configFileName === undefined) {
throw new tscc_spec_1.TsccSpecError(`Cannot find tsconfig at ${TsccSpecWithTS.toDisplayedPath(searchPath !== null && searchPath !== void 0 ? searchPath : defaultLocation)}.`);
}
return configFileName;
}
static loadTsConfigFromResolvedPath(configFileName, options) {
const compilerHost = Object.create(ts.sys);
compilerHost.onUnRecoverableConfigFileDiagnostic = (diagnostic) => { throw new TsError([diagnostic]); };
const parsedConfig = ts.getParsedCommandLineOfConfigFile(configFileName, options, compilerHost);
if (parsedConfig.errors.length) {
throw new TsError(parsedConfig.errors);
}
const projectRoot = path.dirname(configFileName);
return { projectRoot, parsedConfig };
}
static loadSpecWithTS(tsccSpecJSONOrItsPath, tsConfigPathOrTsArgs, compilerOptionsOverride, onTsccWarning = noop) {
// When TS project root is not provided, it will be assumed to be the location of tscc spec file.
let { tsccSpecJSON, tsccSpecJSONPath } = TsccSpecWithTS.loadSpecRaw(tsccSpecJSONOrItsPath);
let specRoot = path.dirname(tsccSpecJSONPath);
let { projectRoot, parsedConfig } = Array.isArray(tsConfigPathOrTsArgs) ?
TsccSpecWithTS.loadTsConfigFromArgs(tsConfigPathOrTsArgs, specRoot, onTsccWarning) :
TsccSpecWithTS.loadTsConfigFromPath(tsConfigPathOrTsArgs, specRoot, compilerOptionsOverride);
TsccSpecWithTS.pruneCompilerOptions(parsedConfig.options, onTsccWarning);
return new TsccSpecWithTS(tsccSpecJSON, tsccSpecJSONPath, parsedConfig, projectRoot);
}
/**
* Prune compiler options
* - "module" to "commonjs"
* - Warn when rootDir or outDir is used - these options are about `tsc` output directory structure,
* which is of no use with tscc.
* - Warn when target language is ES3 – Tsickle does not assume that the output can be lower than ES5,
*/
static pruneCompilerOptions(options, onWarning) {
if (options.module !== ts.ModuleKind.CommonJS) {
if (typeof options.module !== 'undefined') {
onWarning(`Module option is set. tsickle converts TypeScript modules to Closure modules`
+ `via CommonJS internally, so it will be overridden to "commonjs".`);
}
options.module = ts.ModuleKind.CommonJS;
}
if (options.outDir) {
onWarning(`--outDir option is set, but it is no-op for tscc.` +
`Use prefix option in spec file to control output directory.`);
options.outDir = undefined;
}
/**
* {@link https://github.com/angular/tsickle/commit/2050e902ea0fa59aa36f414cab192155167a9b06}
* tsickle throws if `rootDir` is not provided, for presumably "internal" reasons. In tscc,
* it normalizes paths to absolute paths, so the presence of `rootDir` does not have any
* visible effect. If it is not supplied, we provide a default value of the config file's
* containing root directory. Note that ts.CompilerOptions.configFilePath is an internal
* property.
*/
const { configFilePath } = options;
const rootDir = configFilePath ? path.parse(configFilePath).root : '/';
if (options.rootDir) {
onWarning(`--rootDir is set, but it is no-op for tscc. It will internally set to ${rootDir}.`);
}
options.rootDir = rootDir;
// See https://github.com/angular/tsickle/commit/c0123da31e2924ad45c3f0a02d536e750028de7b,
// where a check to emit `const` declaration is done by target === ScriptTarget.ES5.
if (options.target === ts.ScriptTarget.ES3) {
onWarning(`tsickle does not support targetting ES3, and it will be overridden to ES5`
+ `Consider setting compilationFlags.language_out to ECMASCRIPT3`);
options.target = ts.ScriptTarget.ES5;
}
else if (typeof options.target === 'undefined') {
// Prevent TS from treating unspecified target as a default value ES3.
options.target = ts.ScriptTarget.ES5;
}
if (!options.importHelpers) {
onWarning(`tsickle uses a custom tslib optimized for closure compiler. importHelpers flag is set.`);
options.importHelpers = true;
}
if (options.removeComments) {
onWarning(`Closure compiler relies on type annotations, removeComments flag is unset.`);
options.removeComments = false;
}
if (options.inlineSourceMap) {
onWarning(`Inlining sourcemap is not supported. inlineSourceMap flag is unset.`);
options.inlineSourceMap = false;
// inlineSource option depends on sourceMap or inlineSourceMap being enabled
// so enabling sourceMap in order not to break tsc.
options.sourceMap = true;
}
if (options.incremental) {
// Incremental compilation produces an additional .tsbuildinfo file. This triggers
// unrecognized file extension error, so we disable it.
// Currently I'm not sure that among typescript and closure compiler, which impacts the
// compilation time more. If it is closure compiler, there would not be much sense to
// support incremental compilation, for closure compiler does not support it. Otherwise,
// I may try to attempt implementing it. To do so, we have to write intermediate output
// like what we do with --debug.persistArtifacts.
onWarning(`Incremental compilation is not supported. incremental flag is unset.`);
options.incremental = false;
}
// Silently unset flags related to declaration
options.declaration && (options.declaration = false);
options.declarationMap && (options.declarationMap = false);
}
validateSpecWithTS() {
// Checks that each of entry files is provided in tsConfig.
const fileNames = this.getAbsoluteFileNamesSet();
const modules = this.getOrderedModuleSpecs();
for (let module of modules) {
if (!fileNames.has(module.entry)) {
throw new tscc_spec_1.TsccSpecError(`An entry file ${module.entry} is not provided ` +
`in a typescript project ${this.projectRoot}.`);
}
}
}
getTSRoot() {
return this.projectRoot;
}
getCompilerOptions() {
return this.parsedConfig.options;
}
getCompilerHost() {
return this.tsCompilerHost;
}
getOutputFileNames() {
return this.getOrderedModuleSpecs()
.map(moduleSpec => {
const { moduleName } = moduleSpec;
return this.absolute(this.getOutputPrefix('cc')) + moduleName + '.js';
});
}
getDefaultFlags() {
let { target, sourceMap, inlineSources } = this.parsedConfig.options;
const defaultFlags = {};
defaultFlags["language_in"] = TsccSpecWithTS.tsTargetToCcTarget[target];
// Closure compiler's default behavior is to output ECMASCRIPT_NEXT so that transpilation is
// applied only when users explicitly request it. See:
// https://github.com/google/closure-compiler/wiki/Releases#november-7-2021-v20211107
// In version <=0.7.5, we had this set to ECMASCRIPT5.
defaultFlags["language_out"] = "ECMASCRIPT_NEXT";
defaultFlags["compilation_level"] = "ADVANCED";
if (this.getOrderedModuleSpecs().length > 1) {
// Multi-chunk build uses --chunk and --chunk_output_path_prefix.
// This path will appear in a sourcemap that closure compiler generates - need to use
// relative path in order not to leak global directory structure.
defaultFlags["chunk_output_path_prefix"] = this.relativeFromCwd(this.getOutputPrefix('cc'));
}
else {
// Single-chunk build uses --js_output_file.
defaultFlags["js_output_file"] =
this.relativeFromCwd(this.getOutputPrefix('cc')) +
this.getOrderedModuleSpecs()[0].moduleName + '.js';
}
defaultFlags["chunk_output_type"] =
this.tsccSpec.chunkFormat && TsccSpecWithTS.chunkFormatToCcType[this.tsccSpec.chunkFormat] ||
"GLOBAL_NAMESPACE";
defaultFlags["generate_exports"] = true;
defaultFlags["export_local_property_definitions"] = true;
if (sourceMap) {
defaultFlags["create_source_map"] = "%outname%.map";
defaultFlags["apply_input_source_maps"] = true;
}
if (inlineSources) {
defaultFlags["source_map_include_content"] = true;
}
return defaultFlags;
}
getBaseCompilerFlags() {
const baseFlags = this.tsccSpec.compilerFlags || {};
const defaultFlags = this.getDefaultFlags();
const flagsMap = Object.assign(defaultFlags, baseFlags);
const outFlags = [];
const pushFlag = (key, value) => {
if (typeof value === 'boolean') {
if (value === true)
outFlags.push('--' + key);
}
else {
outFlags.push('--' + key, String(value));
}
};
for (let [key, value] of Object.entries(flagsMap)) {
if (Array.isArray(value)) {
for (let val of value) {
pushFlag(key, val);
}
}
else {
pushFlag(key, value);
}
}
return outFlags;
}
getAbsoluteFileNamesSet() {
return new Set(this.parsedConfig.fileNames
.map(fileName => path.resolve(this.projectRoot, fileName)));
}
resolveExternalModuleTypeReference(moduleName) {
var _a;
const resolved = ts.resolveTypeReferenceDirective(moduleName,
// Following convention of Typescript source code
path.join(this.projectRoot, '__inferred type names__.ts'), this.getCompilerOptions(), this.getCompilerHost());
if (resolved && resolved.resolvedTypeReferenceDirective &&
resolved.resolvedTypeReferenceDirective.isExternalLibraryImport) {
return (_a = resolved.resolvedTypeReferenceDirective.resolvedFileName) !== null && _a !== void 0 ? _a : null;
}
return null;
}
getProjectHash() {
return require('crypto').createHash('sha256')
.update(this.basePath + JSON.stringify(this.tsccSpec) +
this.projectRoot + JSON.stringify(this.parsedConfig.options))
.digest('hex');
}
}
exports.default = TsccSpecWithTS;
TsccSpecWithTS.tsTargetToCcTarget = {
[ts.ScriptTarget.ES5]: "ECMASCRIPT5_STRICT",
[ts.ScriptTarget.ES2015]: "ECMASCRIPT_2015",
[ts.ScriptTarget.ES2016]: "ECMASCRIPT_2016",
[ts.ScriptTarget.ES2017]: "ECMASCRIPT_2017",
[ts.ScriptTarget.ES2018]: "ECMASCRIPT_2018",
[ts.ScriptTarget.ES2019]: "ECMASCRIPT_2019",
[ts.ScriptTarget.ES2020]: "ECMASCRIPT_2020",
[ts.ScriptTarget.ES2021]: "ECMASCRIPT_2021",
[ts.ScriptTarget.ES2022]: "ECMASCRIPT_NEXT",
[ts.ScriptTarget.ESNext]: "ECMASCRIPT_NEXT"
};
TsccSpecWithTS.chunkFormatToCcType = {
['global']: "GLOBAL_NAMESPACE",
['module']: "ES_MODULES"
};
function noop() { }