ts-patch
Version:
Patch typescript to support custom transformers in tsconfig.json
610 lines (609 loc) • 29.4 kB
JavaScript
var tsp = (function () {
"use strict";
var tsp;
(function (tsp) {
const os = require("os");
const path = require("path");
const fs = require("fs");
tsp.diagnosticMap = new WeakMap();
tsp.supportedExtensions = [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"];
tsp.tsExtensions = [".ts", ".mts", ".cts"];
function diagnosticExtrasFactory(program) {
const diagnostics = tsp.diagnosticMap.get(program) || tsp.diagnosticMap.set(program, []).get(program);
const addDiagnostic = (diag) => diagnostics.push(diag);
const removeDiagnostic = (index) => { diagnostics.splice(index, 1); };
return { addDiagnostic, removeDiagnostic, diagnostics };
}
tsp.diagnosticExtrasFactory = diagnosticExtrasFactory;
function getTmpDir(subPath) {
const tmpDir = path.resolve(os.tmpdir(), "tsp", subPath);
if (!fs.existsSync(tmpDir))
fs.mkdirSync(tmpDir, { recursive: true });
return tmpDir;
}
tsp.getTmpDir = getTmpDir;
function getTsInstance() {
return (typeof ts !== "undefined" ? ts : module.exports);
}
tsp.getTsInstance = getTsInstance;
class TsPatchError extends Error {
constructor(message, diagnostic) {
super(message);
this.diagnostic = diagnostic;
}
}
tsp.TsPatchError = TsPatchError;
})(tsp || (tsp = {}));
var tsp;
(function (tsp) {
const Module = require("module");
const path = require("path");
const fs = require("fs");
const crypto = require("crypto");
function getEsmLibrary() {
try {
return require("esm");
}
catch (e) {
if (e.code === "MODULE_NOT_FOUND")
throw new tsp.TsPatchError(`Plugin is an ESM module. To enable experimental ESM support, ` +
`install the 'esm' package as a (dev)-dependency or global.`);
else
throw e;
}
}
function registerEsmIntercept(registerConfig) {
const originalRequire = Module.prototype.require;
const builtFiles = new Map();
const getHash = () => {
let hash;
do {
hash = crypto.randomBytes(16).toString("hex");
} while (builtFiles.has(hash));
return hash;
};
const cleanup = () => {
for (const { 1: filePath } of builtFiles) {
delete require.cache[filePath];
try {
fs.rmSync(filePath, { force: true, maxRetries: 3 });
}
catch (e) {
if (process.env.NODE_ENV !== "production")
console.warn(`[ts-patch] Warning: Failed to delete temporary esm cache file: ${filePath}.`);
}
}
builtFiles.clear();
Module.prototype.require = originalRequire;
};
try {
Module.prototype.require = wrappedRequire;
}
catch (e) {
cleanup();
}
function wrappedRequire(request) {
try {
return originalRequire.apply(this, arguments);
}
catch (e) {
if (e.code === "ERR_REQUIRE_ESM") {
const resolvedPath = Module._resolveFilename(request, this, false);
const resolvedPathExt = path.extname(resolvedPath);
if (Module._cache[resolvedPath])
return Module._cache[resolvedPath].exports;
let targetFilePath;
if (tsp.tsExtensions.includes(resolvedPathExt)) {
if (!builtFiles.has(resolvedPath)) {
const tsCode = fs.readFileSync(resolvedPath, "utf8");
const newPath = resolvedPath.replace(/\.ts$/, ".mts");
const jsCode = registerConfig.tsNodeInstance.compile(tsCode, newPath);
const outputFileName = getHash() + ".mjs";
const outputFilePath = path.join(tsp.getTmpDir("esm"), outputFileName);
fs.writeFileSync(outputFilePath, jsCode, "utf8");
builtFiles.set(resolvedPath, outputFilePath);
targetFilePath = outputFilePath;
}
else {
targetFilePath = builtFiles.get(resolvedPath);
}
}
else {
targetFilePath = resolvedPath;
}
const newModule = new Module(request, this);
newModule.filename = resolvedPath;
newModule.paths = Module._nodeModulePaths(resolvedPath);
Module._cache[resolvedPath] = newModule;
const res = getEsmLibrary()(newModule)(targetFilePath);
newModule.filename = resolvedPath;
return res;
}
throw e;
}
}
return cleanup;
}
tsp.registerEsmIntercept = registerEsmIntercept;
})(tsp || (tsp = {}));
var tsp;
(function (tsp) {
const crypto = require("crypto");
function createTransformersFromPattern(opt) {
const { factory, config, program, ls, registerConfig } = opt;
const { transform, after, afterDeclarations, name, type, transformProgram, ...cleanConfig } = config;
if (!transform)
throw new tsp.TsPatchError("Not a valid config entry: \"transform\" key not found");
const transformerKind = after ? "after" :
afterDeclarations ? "afterDeclarations" :
"before";
let pluginFactoryResult;
switch (config.type) {
case "ls":
if (!ls)
throw new tsp.TsPatchError(`Plugin ${transform} needs a LanguageService`);
pluginFactoryResult = factory(ls, cleanConfig);
break;
case "config":
pluginFactoryResult = factory(cleanConfig);
break;
case "compilerOptions":
pluginFactoryResult = factory(program.getCompilerOptions(), cleanConfig);
break;
case "checker":
pluginFactoryResult = factory(program.getTypeChecker(), cleanConfig);
break;
case undefined:
case "program":
const { addDiagnostic, removeDiagnostic, diagnostics } = tsp.diagnosticExtrasFactory(program);
pluginFactoryResult = factory(program, cleanConfig, {
ts: tsp.getTsInstance(),
addDiagnostic,
removeDiagnostic,
diagnostics,
library: tsp.currentLibrary
});
break;
case "raw":
pluginFactoryResult = (ctx) => factory(ctx, program, cleanConfig);
break;
default:
throw new tsp.TsPatchError(`Invalid plugin type found in tsconfig.json: '${config.type}'`);
}
let transformerFactories;
switch (typeof pluginFactoryResult) {
case "function":
transformerFactories = [pluginFactoryResult];
break;
case "object":
const factoryOrFactories = pluginFactoryResult[transformerKind];
if (typeof factoryOrFactories === "function") {
transformerFactories = [pluginFactoryResult[transformerKind]];
break;
}
else if (Array.isArray(factoryOrFactories)) {
transformerFactories = [...factoryOrFactories];
break;
}
default:
throw new tsp.TsPatchError(`Invalid plugin result: expected a function or an object with a '${transformerKind}' property`);
}
const wrappedFactories = [];
for (const transformerFactory of transformerFactories) {
if (!transformerFactory || typeof transformerFactory !== "function")
throw new tsp.TsPatchError(`Invalid plugin entry point! Expected a transformer factory function or an object with a '${transformerKind}' property`);
const wrapper = wrapTransformerFactory(transformerFactory, registerConfig, true);
wrappedFactories.push(wrapper);
}
const res = {
[transformerKind]: wrappedFactories
};
return res;
}
function wrapTransformerFactory(transformerFn, requireConfig, wrapInnerFunction) {
const wrapper = function tspWrappedFactory(...args) {
let res;
try {
tsp.registerPlugin(requireConfig);
if (!wrapInnerFunction) {
res = transformerFn(...args);
}
else {
const resFn = transformerFn(...args);
if (typeof resFn !== "function")
throw new tsp.TsPatchError("Invalid plugin: expected a function");
res = wrapTransformerFactory(resFn, requireConfig, false);
}
}
finally {
tsp.unregisterPlugin();
}
return res;
};
return wrapper;
}
class PluginCreator {
constructor(configs, options) {
this.plugins = [];
this.configs = configs;
this.options = options;
const { resolveBaseDir } = options;
this.plugins = configs
.filter(config => config.transform !== undefined)
.map(config => new tsp.TspPlugin(config, { resolveBaseDir }));
this.needsTscJsDocParsing = this.plugins.some(plugin => plugin.packageConfig?.tscOptions?.parseAllJsDoc === true);
}
mergeTransformers(into, source) {
const slice = (input) => (Array.isArray(input) ? input.slice() : [input]);
if (source.before)
into.before.push(...slice(source.before));
if (source.after)
into.after.push(...slice(source.after));
if (source.afterDeclarations)
into.afterDeclarations.push(...slice(source.afterDeclarations));
return this;
}
createSourceTransformers(params, customTransformers) {
const transformers = { before: [], after: [], afterDeclarations: [] };
const [ls, program] = ("ls" in params) ? [params.ls, params.ls.getProgram()] : [void 0, params.program];
for (const plugin of this.plugins) {
if (plugin.kind !== "SourceTransformer")
continue;
const { config } = plugin;
const createFactoryResult = plugin.createFactory();
if (!createFactoryResult)
continue;
const { factory, registerConfig } = createFactoryResult;
this.mergeTransformers(transformers, createTransformersFromPattern({
factory: factory,
registerConfig,
config,
program,
ls
}));
}
if (customTransformers)
this.mergeTransformers(transformers, customTransformers);
return transformers;
}
createProgramTransformers() {
const res = new Map();
for (const plugin of this.plugins) {
if (plugin.kind !== "ProgramTransformer")
continue;
const { config } = plugin;
const createFactoryResult = plugin.createFactory();
if (createFactoryResult === undefined)
continue;
const { registerConfig, factory: unwrappedFactory } = createFactoryResult;
const factory = wrapTransformerFactory(unwrappedFactory, registerConfig, false);
const transformerKey = crypto
.createHash("md5")
.update(JSON.stringify({ factory, config }))
.digest("hex");
res.set(transformerKey, [factory, config]);
}
return res;
}
}
tsp.PluginCreator = PluginCreator;
})(tsp || (tsp = {}));
var tsp;
(function (tsp) {
const path = require("path");
const fs = require("fs");
const requireStack = [];
function getPackagePath(entryFilePath) {
let currentDir = path.dirname(entryFilePath);
const seenPaths = new Set();
while (currentDir !== path.parse(currentDir).root) {
if (seenPaths.has(currentDir))
return undefined;
seenPaths.add(currentDir);
const potentialPkgPath = path.join(currentDir, "package.json");
if (fs.existsSync(potentialPkgPath))
return potentialPkgPath;
currentDir = path.resolve(currentDir, "..");
}
return undefined;
}
class TspPlugin {
constructor(config, createOptions) {
this.config = { ...config };
this.validateConfig();
this._createOptions = createOptions;
this.importKey = config.import || "default";
this.kind = config.transformProgram === true ? "ProgramTransformer" : "SourceTransformer";
const { resolveBaseDir } = createOptions;
const configTransformValue = config.transform;
this.tsConfigPath = config.tsConfig && path.resolve(resolveBaseDir, config.tsConfig);
const entryFilePath = require.resolve(configTransformValue, { paths: [resolveBaseDir] });
this.entryFilePath = entryFilePath;
let pluginPackageConfig;
const modulePackagePath = getPackagePath(entryFilePath);
if (modulePackagePath) {
const modulePkgJsonContent = fs.readFileSync(modulePackagePath, "utf8");
const modulePkgJson = JSON.parse(modulePkgJsonContent);
pluginPackageConfig = modulePkgJson.tsp;
if (pluginPackageConfig === null || typeof pluginPackageConfig !== "object")
pluginPackageConfig = undefined;
}
this.packageConfig = pluginPackageConfig;
}
validateConfig() {
const { config } = this;
const configTransformValue = config.transform;
if (!configTransformValue)
throw new tsp.TsPatchError(`Invalid plugin config: missing "transform" value`);
if (config.resolvePathAliases && !config.tsConfig) {
console.warn(`[ts-patch] Warning: resolvePathAliases needs a tsConfig value pointing to a tsconfig.json for transformer" ${configTransformValue}.`);
}
}
createFactory() {
const { entryFilePath, config, tsConfigPath, importKey } = this;
const configTransformValue = config.transform;
if (requireStack.includes(entryFilePath))
return;
requireStack.push(entryFilePath);
let isEsm = config.isEsm;
if (isEsm == null) {
const impliedModuleFormat = tsp.tsShim.getImpliedNodeFormatForFile(entryFilePath, undefined, tsp.tsShim.sys, { moduleResolution: tsp.tsShim.ModuleResolutionKind.Node16 });
isEsm = impliedModuleFormat === tsp.tsShim.ModuleKind.ESNext;
}
const isTs = configTransformValue.match(/\.[mc]?ts$/) != null;
const registerConfig = {
isTs,
isEsm,
tsConfig: tsConfigPath,
pluginConfig: config
};
tsp.registerPlugin(registerConfig);
try {
const commonjsModule = loadEntryFile();
const factoryModule = (typeof commonjsModule === "function") ? { default: commonjsModule } : commonjsModule;
const factory = factoryModule[importKey];
if (!factory)
throw new tsp.TsPatchError(`tsconfig.json > plugins: "${configTransformValue}" does not have an export "${importKey}": ` +
require("util").inspect(factoryModule));
if (typeof factory !== "function") {
throw new tsp.TsPatchError(`tsconfig.json > plugins: "${configTransformValue}" export "${importKey}" is not a plugin: ` +
require("util").inspect(factory));
}
return {
factory,
registerConfig: registerConfig
};
}
finally {
requireStack.pop();
tsp.unregisterPlugin();
}
function loadEntryFile() {
let res;
try {
res = require(entryFilePath);
}
catch (e) {
if (e.code === "ERR_REQUIRE_ESM") {
if (!registerConfig.isEsm) {
tsp.unregisterPlugin();
registerConfig.isEsm = true;
tsp.registerPlugin(registerConfig);
return loadEntryFile();
}
else {
throw new tsp.TsPatchError(`Cannot load ESM transformer "${configTransformValue}" from "${entryFilePath}". Please file a bug report`);
}
}
else
throw e;
}
return res;
}
}
}
tsp.TspPlugin = TspPlugin;
})(tsp || (tsp = {}));
var tsp;
(function (tsp) {
const path = require("path");
let configStack = [];
function getTsNode() {
try {
return require("ts-node");
}
catch (e) {
if (e.code === "MODULE_NOT_FOUND")
throw new tsp.TsPatchError(`Cannot use a typescript-based transformer without ts-node installed. ` +
`Add ts-node as a (dev)-dependency or install globally.`);
else
throw e;
}
}
function getTsConfigPaths() {
try {
return require("tsconfig-paths");
}
catch (e) {
if (e.code === "MODULE_NOT_FOUND")
throw new tsp.TsPatchError(`resolvePathAliases requires the library: tsconfig-paths. ` +
`Add tsconfig-paths as a (dev)-dependency or install globally.`);
else
throw e;
}
}
function getCompilerOptions(tsConfig) {
const configFile = tsp.tsShim.readConfigFile(tsConfig, tsp.tsShim.sys.readFile);
const parsedConfig = configFile && tsp.tsShim.parseJsonConfigFileContent(configFile.config, tsp.tsShim.sys, path.dirname(tsConfig));
return parsedConfig.options;
}
function unregisterPlugin() {
const activeRegisterConfig = configStack.pop();
if (activeRegisterConfig.tsConfigPathsCleanup) {
activeRegisterConfig.tsConfigPathsCleanup();
delete activeRegisterConfig.tsConfigPathsCleanup;
}
if (activeRegisterConfig.tsNodeInstance) {
activeRegisterConfig.tsNodeInstance.enabled(false);
}
if (activeRegisterConfig.esmInterceptCleanup) {
activeRegisterConfig.esmInterceptCleanup();
delete activeRegisterConfig.esmInterceptCleanup;
}
}
tsp.unregisterPlugin = unregisterPlugin;
function registerPlugin(registerConfig) {
if (!registerConfig)
throw new tsp.TsPatchError("requireConfig is required");
configStack.push(registerConfig);
const { isTs, isEsm, tsConfig, pluginConfig } = registerConfig;
if (isEsm) {
registerConfig.esmInterceptCleanup = tsp.registerEsmIntercept(registerConfig);
}
if (isTs) {
const tsNode = getTsNode();
let tsNodeInstance;
if (registerConfig.tsNodeInstance) {
tsNodeInstance = registerConfig.tsNodeInstance;
tsNode.register(tsNodeInstance);
}
else {
tsNodeInstance = tsNode.register({
transpileOnly: true,
...(tsConfig ? { project: tsConfig } : { skipProject: true }),
compilerOptions: {
target: isEsm ? "ESNext" : "ES2018",
jsx: "react",
esModuleInterop: true,
module: isEsm ? "ESNext" : "commonjs",
}
});
}
tsNodeInstance.enabled(true);
registerConfig.tsNodeInstance = tsNodeInstance;
}
if (tsConfig && pluginConfig.resolvePathAliases) {
registerConfig.compilerOptions ?? (registerConfig.compilerOptions = getCompilerOptions(tsConfig));
const { paths, baseUrl } = registerConfig.compilerOptions;
if (paths && baseUrl) {
registerConfig.tsConfigPathsCleanup = getTsConfigPaths().register({ baseUrl, paths });
}
}
}
tsp.registerPlugin = registerPlugin;
})(tsp || (tsp = {}));
var tsp;
(function (tsp) {
const activeProgramTransformers = new Set();
const { dirname } = require("path");
function getProjectDir(compilerOptions) {
return compilerOptions.configFilePath && dirname(compilerOptions.configFilePath);
}
function getProjectConfig(compilerOptions, rootFileNames) {
let configFilePath = compilerOptions.configFilePath;
let projectDir = getProjectDir(compilerOptions);
if (configFilePath === undefined) {
const baseDir = (rootFileNames.length > 0) ? dirname(rootFileNames[0]) : projectDir ?? process.cwd();
configFilePath = tsp.tsShim.findConfigFile(baseDir, tsp.tsShim.sys.fileExists);
if (configFilePath) {
const config = readConfig(configFilePath);
compilerOptions = { ...config.options, ...compilerOptions };
projectDir = getProjectDir(compilerOptions);
}
}
return ({ projectDir, compilerOptions });
}
function readConfig(configFileNamePath) {
const projectDir = dirname(configFileNamePath);
const result = tsp.tsShim.readConfigFile(configFileNamePath, tsp.tsShim.sys.readFile);
if (result.error)
throw new tsp.TsPatchError("Error in tsconfig.json: " + result.error.messageText);
return tsp.tsShim.parseJsonConfigFileContent(result.config, tsp.tsShim.sys, projectDir, undefined, configFileNamePath);
}
function preparePluginsFromCompilerOptions(plugins) {
if (!plugins)
return [];
if ((plugins.length === 1) && plugins[0].customTransformers) {
const { before = [], after = [] } = plugins[0].customTransformers;
return [
...before.map((item) => ({ transform: item })),
...after.map((item) => ({ transform: item, after: true })),
];
}
return plugins;
}
function createProgram(rootNamesOrOptions, options, host, oldProgram, configFileParsingDiagnostics) {
let rootNames;
const createOpts = !Array.isArray(rootNamesOrOptions) ? rootNamesOrOptions : void 0;
if (createOpts) {
rootNames = createOpts.rootNames;
options = createOpts.options;
host = createOpts.host;
oldProgram = createOpts.oldProgram;
configFileParsingDiagnostics = createOpts.configFileParsingDiagnostics;
}
else {
options = options;
rootNames = rootNamesOrOptions;
}
const projectConfig = getProjectConfig(options, rootNames);
if (["tsc", "tsserver", "tsserverlibrary"].includes(tsp.currentLibrary)) {
options = projectConfig.compilerOptions;
if (createOpts)
createOpts.options = options;
}
const plugins = preparePluginsFromCompilerOptions(options.plugins);
const pluginCreator = new tsp.PluginCreator(plugins, { resolveBaseDir: projectConfig.projectDir ?? process.cwd() });
if (tsp.currentLibrary === "tsc" && tsp.tsShim.JSDocParsingMode && pluginCreator.needsTscJsDocParsing) {
host.jsDocParsingMode = tsp.tsShim.JSDocParsingMode.ParseAll;
}
let program = createOpts ?
tsp.tsShim.originalCreateProgram(createOpts) :
tsp.tsShim.originalCreateProgram(rootNames, options, host, oldProgram, configFileParsingDiagnostics);
const programTransformers = pluginCreator.createProgramTransformers();
for (const [transformerKey, [programTransformer, config]] of programTransformers) {
if (activeProgramTransformers.has(transformerKey))
continue;
activeProgramTransformers.add(transformerKey);
const newProgram = programTransformer(program, host, config, { ts: tsp.getTsInstance() });
if (typeof newProgram?.["emit"] === "function")
program = newProgram;
activeProgramTransformers.delete(transformerKey);
}
if (!program.originalEmit) {
program.originalEmit = program.emit;
program.emit = newEmit;
}
function newEmit(targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers, ...additionalArgs) {
const transformers = pluginCreator.createSourceTransformers({ program }, customTransformers);
const result = program.originalEmit(targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, transformers, ...additionalArgs);
for (const diagnostic of tsp.diagnosticMap.get(program) || [])
if (!result.diagnostics.includes(diagnostic))
result.diagnostics.push(diagnostic);
return result;
}
return program;
}
tsp.createProgram = createProgram;
})(tsp || (tsp = {}));
var tsp;
(function (tsp) {
tsp.tsShim = new Proxy({}, {
get(_, key) {
const target = tsp.getTsInstance();
if (target) {
return target[key];
}
else {
try {
return eval(key);
}
catch (e) {
throw new tsp.TsPatchError(`Failed to find "${key}" in TypeScript shim`, e);
}
}
},
});
})(tsp || (tsp = {}));
return tsp;
})();