Initial support for minimal compiler plugin API

This commit is contained in:
Ron Buckton
2019-08-14 13:07:44 -07:00
parent 11c28d6dd0
commit 3f4c71adfd
24 changed files with 929 additions and 41 deletions
+59 -3
View File
@@ -94,18 +94,74 @@ const localize = async () => {
const lkgPreBuild = parallel(generateLibs, series(buildScripts, generateDiagnostics));
const buildTsc = () => buildProject("src/tsc");
task("tsc", series(lkgPreBuild, buildTsc));
task("tsc").description = "Builds the command-line compiler";
const cleanTsc = () => cleanProject("src/tsc");
cleanTasks.push(cleanTsc);
task("clean-tsc", cleanTsc);
task("clean-tsc", parallel(cleanTsc));
task("clean-tsc").description = "Cleans outputs for the command-line compiler";
const watchTsc = () => watchProject("src/tsc");
task("watch-tsc", series(lkgPreBuild, parallel(watchLib, watchDiagnostics, watchTsc)));
task("watch-tsc").description = "Watch for changes and rebuild the command-line compiler only.";
const buildApi = (() => {
const flattenCompiler = async () => flatten("src/compiler/tsconfig.json", "built/local/api.tsconfig.json", {
compilerOptions: {
removeComments: false,
stripInternal: true,
emitDeclarationOnly: true,
declaration: true,
declarationMap: false,
outFile: "api.out.js"
}
});
const buildApiOut = () => buildProject("built/local/api.tsconfig.json");
const generateApiDts = () => src("built/local/api.out.d.ts", { base: "built/local" })
.pipe(newer("built/local/api.d.ts"))
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(transform(content => content.replace(/^(\s*)(export )?const enum (\S+) {(\s*)$/gm, "$1$2enum $3 {$4")))
.pipe(transform(content => `${content}\nexport = typeof ts;\n`))
.pipe(prependFile(copyright))
.pipe(rename("api.d.ts"))
.pipe(dest("built/local"));
const generateApiJs = () => src(copyright)
.pipe(newer("built/local/api.js"))
.pipe(transform(content => `${content}// NOTE: 'api' only exports types used by plugins. You can access the actual api from the context\n// provided to a plugin hook.\nmodule.exports = {};`))
.pipe(rename("api.js"))
.pipe(dest("built/local"));
return series(flattenCompiler, buildApiOut, generateApiDts, generateApiJs);
})();
task("api", series(lkgPreBuild, buildApi));
task("api").description = "Build the compiler plugin API";
const cleanApi = async () => {
if (fs.existsSync("built/local/api.tsconfig.json")) {
await cleanProject("built/local/api.tsconfig.json");
}
await del([
"built/local/api.tsconfig.json",
"built/local/api.out.js",
"built/local/api.out.d.ts",
"built/local/tsc.d.ts"
]);
}
cleanTasks.push(cleanApi);
task("clean-api", cleanApi);
task("clean-api").description = "Clean the outputs of the compiler plugin API.";
const watchApi = () => watch([
"src/compiler/tsconfig.json",
"src/compiler/**/*.ts",
], series(lkgPreBuild, buildApi));
task("watch-api", series(lkgPreBuild, parallel(watchDiagnostics, watchApi)));
// Pre-build steps when targeting the built/local compiler.
const localPreBuild = parallel(generateLibs, series(buildScripts, generateDiagnostics, buildTsc));
@@ -377,13 +433,13 @@ task("other-outputs").description = "Builds miscelaneous scripts and documents d
const buildFoldStart = async () => { if (fold.isTravis()) console.log(fold.start("build")); };
const buildFoldEnd = async () => { if (fold.isTravis()) console.log(fold.end("build")); };
task("local", series(buildFoldStart, preBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs), buildFoldEnd));
task("local", series(buildFoldStart, preBuild, parallel(localize, buildTsc, buildServer, buildServices, buildLssl, buildOtherOutputs, buildApi), buildFoldEnd));
task("local").description = "Builds the full compiler and services";
task("local").flags = {
" --built": "Compile using the built version of the compiler."
};
task("watch-local", series(preBuild, parallel(watchLib, watchDiagnostics, watchTsc, watchServices, watchServer, watchLssl)));
task("watch-local", series(preBuild, parallel(watchLib, watchDiagnostics, watchTsc, watchServices, watchServer, watchLssl, watchApi)));
task("watch-local").description = "Watches for changes to projects in src/ (but does not execute tests).";
task("watch-local").flags = {
" --built": "Compile using the built version of the compiler."
+35 -2
View File
@@ -2022,7 +2022,8 @@ namespace ts {
}
}
function isNullOrUndefined(x: any): x is null | undefined {
/*@internal*/
export function isNullOrUndefined(x: any): x is null | undefined {
// tslint:disable-next-line:no-null-keyword
return x === undefined || x === null;
}
@@ -2063,6 +2064,7 @@ namespace ts {
setConfigFileInOptions(options, sourceFile);
let projectReferences: ProjectReference[] | undefined;
const { fileNames, wildcardDirectories, spec } = getFileNames();
const plugins = getPlugins();
return {
options,
fileNames,
@@ -2072,7 +2074,8 @@ namespace ts {
errors,
wildcardDirectories,
compileOnSave: !!raw.compileOnSave,
configFileSpecs: spec
configFileSpecs: spec,
plugins
};
function getFileNames(): ExpandResult {
@@ -2164,6 +2167,36 @@ namespace ts {
return result;
}
function getPlugins() {
let plugins: (string | [string, any?])[] | undefined;
if (hasProperty(raw, "plugins") && !isNullOrUndefined(raw.plugins)) {
if (isArray(raw.plugins)) {
for (const plugin of raw.plugins) {
if (isPlugin(plugin)) {
if (!plugins) {
plugins = [plugin];
}
else {
plugins.push(plugin);
}
}
else {
// TODO(rbuckton): report diagnostic
}
}
}
else {
// TODO(rbuckton): report diagnostic
}
}
return plugins;
}
function isPlugin(plugin: any): plugin is string | [string, any] {
return typeof plugin === "string" ||
isArray(plugin) && plugin.length >= 1 && plugin.length <= 2 && typeof plugin[0] === "string";
}
function createCompilerDiagnosticOnlyIfJson(message: DiagnosticMessage, arg0?: string, arg1?: string) {
if (!sourceFile) {
errors.push(createCompilerDiagnostic(message, arg0, arg1));
+16
View File
@@ -3180,6 +3180,22 @@
"category": "Error",
"code": 5075
},
"'package.json' field '{0}' requires a value of type {1}.": {
"category": "Error",
"code": 5076
},
"Plugin '{0}' could not be found: {1}.": {
"category": "Error",
"code": 5077
},
"Plugin dependencies for '{0}' could not resolved.": {
"category": "Error",
"code": 5078
},
"Plugin '{0}' failed while executing the '{1}' hook: {2}": {
"category": "Error",
"code": 5079
},
"Generates a sourcemap for each corresponding '.d.ts' file.": {
"category": "Message",
+289
View File
@@ -0,0 +1,289 @@
namespace ts {
type Hook = "activate" | "deactivate" | "preEmit";
type HookFunction<K extends Hook> = NonNullable<CompilerPluginModule[K]>;
type ExecuteUserCodeResult<T> =
| { error: Diagnostic, result: undefined }
| { error: undefined, result: T | undefined };
interface PluginEntry {
active: boolean; // Tracks whether the plugin has been activated.
compilerPlugin: CompilerPlugin;
}
/*@internal*/
export interface GetPluginsResult {
plugins: CompilerPlugin[];
diagnostics?: Diagnostic[];
}
interface PackageJson {
typescriptPlugin?: PluginPackageSettings;
}
interface PluginPackageSettings {
activationEvents?: string[];
pluginDependencies?: string[];
}
/**
* Resolves the supplied plugins (and their dependencies) relative to an initial directory.
*/
/*@internal*/
export function getPlugins(host: ModuleLoaderHost, initialDir: string, plugins: ReadonlyArray<string | [string, any?]>) {
debugger;
interface ResolvedModule {
module: {};
moduleName: string;
modulePath: string | undefined;
packageJsonPath?: string;
}
const compilerPlugins: CompilerPlugin[] = [];
const compilerPluginMap = createMap<CompilerPlugin>();
const diagnostics: Diagnostic[] = [];
for (const plugin of plugins) {
processPlugin(initialDir, plugin);
}
return { plugins: compilerPlugins, diagnostics };
function processPlugin(initialDir: string, plugin: string | [string, any?]) {
const originalName = isArray(plugin) ? plugin[0] : <string>plugin;
const options = isArray(plugin) && plugin.length > 0 ? plugin[1] : undefined;
let candidates: string[];
if (isExternalModuleNameRelative(originalName) || isTypeScriptPlugin(originalName)) {
candidates = [originalName];
}
else {
candidates = [addTypeScriptPluginPrefix(originalName), originalName];
}
let compilerPlugin: CompilerPlugin | undefined;
for (const candidate of candidates) {
compilerPlugin = compilerPluginMap.get(candidate);
if (compilerPlugin) {
if (!isNullOrUndefined(options) && compilerPlugin.options === undefined) {
compilerPlugin.options = options;
}
return;
}
}
let resolvedModule: ResolvedModule | undefined;
let firstError: { stack?: string, message?: string } | undefined;
for (const candidate of candidates) {
const result = host.require(initialDir, candidate);
if (result.module) {
resolvedModule = {
module: result.module,
moduleName: candidate,
modulePath: result.modulePath,
packageJsonPath: !isExternalModuleNameRelative(candidate) && result.modulePath
? findPackageJsonPath(host, result.modulePath)
: undefined
};
break;
}
if (!firstError) firstError = result.error;
}
if (!resolvedModule) {
const error = Debug.assertDefined(firstError);
reportError(Diagnostics.Plugin_0_could_not_be_found_Colon_1, originalName, error.message);
return;
}
const packageJson = resolvedModule.packageJsonPath
? readJson(resolvedModule.packageJsonPath, host) as PackageJson
: undefined;
let activationEvents: string[] | undefined;
let pluginDependencies: string[] | undefined;
if (!isNullOrUndefined(packageJson) && typeof packageJson === "object") {
if (hasProperty(packageJson, "typescriptPlugin") && !isNullOrUndefined(packageJson.typescriptPlugin)) {
const typescriptPlugin = packageJson.typescriptPlugin;
if (typeof typescriptPlugin === "object") {
if (hasProperty(typescriptPlugin, "activationEvents") && !isNullOrUndefined(typescriptPlugin.activationEvents)) {
if (isArray(typescriptPlugin.activationEvents)) {
for (const event of typescriptPlugin.activationEvents) {
if (typeof event !== "string") {
reportError(Diagnostics.package_json_field_0_requires_a_value_of_type_1, "typescriptPlugin.activationEvents[]", "string");
}
else {
activationEvents = append(activationEvents, event);
}
}
}
else {
reportError(Diagnostics.package_json_field_0_requires_a_value_of_type_1, "typescriptPlugin.activationEvents", "Array");
}
}
if (hasProperty(typescriptPlugin, "pluginDependencies") && !isNullOrUndefined(typescriptPlugin.pluginDependencies)) {
if (isArray(typescriptPlugin.pluginDependencies)) {
for (const dependency of typescriptPlugin.pluginDependencies) {
if (typeof dependency !== "string") {
reportError(Diagnostics.package_json_field_0_requires_a_value_of_type_1, "typescriptPlugin.pluginDependencies[]", "string");
}
else {
pluginDependencies = append(pluginDependencies, dependency);
}
}
}
else {
reportError(Diagnostics.package_json_field_0_requires_a_value_of_type_1, "typescriptPlugin.pluginDependencies", "Array");
}
}
}
else {
reportError(Diagnostics.package_json_field_0_requires_a_value_of_type_1, "typescriptPlugin", "object");
}
}
}
compilerPlugin = {
originalName,
name: resolvedModule.moduleName,
path: resolvedModule.modulePath,
plugin: resolvedModule.module,
pluginDependencies,
activationEvents,
options
};
compilerPluginMap.set(compilerPlugin.name, compilerPlugin);
if (compilerPlugin.pluginDependencies) {
if (compilerPlugin.path) {
const initialDir = getDirectoryPath(compilerPlugin.path);
for (const plugin of compilerPlugin.pluginDependencies) {
processPlugin(initialDir, plugin);
}
}
else {
reportError(Diagnostics.Plugin_dependencies_for_0_could_not_resolved, compilerPlugin.name);
}
}
compilerPlugins.push(compilerPlugin);
}
function reportError(message: DiagnosticMessage, arg0?: string, arg1?: string) {
diagnostics.push(createCompilerDiagnostic(message, arg0, arg1));
}
}
function findPackageJsonPath(host: ModuleResolutionHost, modulePath: string) {
while (true) {
const candidate = combinePaths(modulePath, "package.json");
if (host.fileExists(candidate)) {
return candidate;
}
const parentPath = getDirectoryPath(modulePath);
if (modulePath === parentPath || !parentPath) {
return undefined;
}
modulePath = parentPath;
}
}
function isTypeScriptPlugin(moduleName: string) {
return /^(?:@[^\\/]+[\\/])?typescript-plugin-\w/.test(moduleName);
}
function addTypeScriptPluginPrefix(moduleName: string) {
// a module name like 'foo', 'foo/bar', or '@foo/bar`
return moduleName.replace(/^(@[^\\/]+[\\/])?/, "$1typescript-plugin-");
}
/**
* Creates a `CompilerPluginHost` used to manage plugin lifetime.
*/
/*@internal*/
export function createPluginHost(plugins: ReadonlyArray<CompilerPlugin>): CompilerPluginHost {
const entries: PluginEntry[] = plugins.map(compilerPlugin => ({ compilerPlugin, active: false }));
return {
preEmit,
deactivate
};
function executeUserCode<K extends Hook>(compilerPlugin: CompilerPlugin, hook: K, ...args: Parameters<HookFunction<K>>): ExecuteUserCodeResult<ReturnType<HookFunction<K>>> {
const hookFunction: HookFunction<K> | undefined = compilerPlugin.plugin[hook]!;
if (typeof hookFunction === "function") {
try {
const result = hookFunction.apply(compilerPlugin.plugin, args);
return { result, error: undefined };
}
catch (error) {
if (error instanceof OperationCanceledException) {
throw error;
}
return { result: undefined, error: createUserCodeDiagnostic(compilerPlugin, hook, error) };
}
}
return { result: undefined, error: undefined };
}
function createContext(compilerHost: CompilerHost, { options }: CompilerPlugin): CompilerPluginContext {
return { ts, compilerHost, options };
}
function createUserCodeDiagnostic(compilerPlugin: CompilerPlugin, hook: Hook, error: { message?: string, stack?: string }) {
return createCompilerDiagnostic(Diagnostics.Plugin_0_failed_while_executing_the_1_hook_Colon_2, compilerPlugin.name, hook, error.stack || error.message || error.toString());
}
function activate(host: CompilerHost, activationEventName: string) {
const activeCompilerPlugins: CompilerPlugin[] = [];
let diagnostics: ReadonlyArray<Diagnostic> | undefined;
for (const entry of entries) {
if (!entry.active && contains(entry.compilerPlugin.activationEvents, activationEventName)) {
const { error, result: activationResult } = executeUserCode(entry.compilerPlugin, "activate", createContext(host, entry.compilerPlugin));
if (error) {
diagnostics = concatenate(diagnostics, [error]);
}
else {
if (activationResult) diagnostics = concatenate(diagnostics, activationResult.diagnostics);
entry.active = true;
}
}
if (entry.active) {
activeCompilerPlugins.push(entry.compilerPlugin);
}
}
return { activeCompilerPlugins, diagnostics };
}
function preEmit(host: CompilerHost, program: Program, targetSourceFile?: SourceFile, cancellationToken?: CancellationToken): CompilerPluginPreEmitResult {
debugger;
const activationResult = activate(host, /*activationEventName*/ "preEmit");
const activeCompilerPlugins = activationResult.activeCompilerPlugins;
let diagnostics = activationResult.diagnostics;
let customTransformers: CustomTransformers | undefined;
for (const plugin of activeCompilerPlugins) {
const { error, result: preEmitResult } = executeUserCode(plugin, "preEmit", createContext(host, plugin), program, targetSourceFile, cancellationToken);
if (error) {
diagnostics = concatenate(diagnostics, [error]);
}
else if (preEmitResult) {
diagnostics = concatenate(diagnostics, preEmitResult.diagnostics);
customTransformers = combineCustomTransformers(customTransformers, preEmitResult.customTransformers);
}
}
return { diagnostics, customTransformers };
}
function deactivate(host: CompilerHost): CompilerPluginDeactivationResult {
debugger;
let diagnostics: Diagnostic[] | undefined;
for (const entry of entries) {
if (entry.active) {
entry.active = false;
const { error } = executeUserCode(entry.compilerPlugin, "deactivate", createContext(host, entry.compilerPlugin));
if (error) {
diagnostics = append(diagnostics, error);
}
}
}
return { diagnostics };
}
}
}
+24 -6
View File
@@ -725,6 +725,7 @@ namespace ts {
const ambientModuleNameToUnmodifiedFileName = createMap<string>();
// Todo:: Use this to report why file was included in --extendedDiagnostics
let refFileMap: MultiMap<ts.RefFile> | undefined;
const pluginHost = createProgramOptions.plugins && createPluginHost(createProgramOptions.plugins);
const cachedSemanticDiagnosticsForFile: DiagnosticCache<Diagnostic> = {};
const cachedDeclarationDiagnosticsForFile: DiagnosticCache<DiagnosticWithLocation> = {};
@@ -910,6 +911,8 @@ namespace ts {
host.onReleaseOldSourceFile!(resolvedProjectReference.sourceFile, oldProgram!.getCompilerOptions(), /*hasSourceFileByPath*/ false);
}
});
oldProgram.dispose();
}
// unconditionally set oldProgram to undefined to prevent it from being captured in closure
@@ -958,7 +961,8 @@ namespace ts {
getResolvedProjectReferenceToRedirect,
getResolvedProjectReferenceByPath,
forEachResolvedProjectReference,
emitBuildInfo
emitBuildInfo,
dispose
};
verifyCompilerOptions();
@@ -967,6 +971,12 @@ namespace ts {
return program;
function dispose() {
if (pluginHost) {
pluginHost.deactivate(host);
}
}
function compareDefaultLibFiles(a: SourceFile, b: SourceFile) {
return compareValues(getDefaultLibFilePriority(a), getDefaultLibFilePriority(b));
}
@@ -1544,11 +1554,18 @@ namespace ts {
function emitWorker(program: Program, sourceFile: SourceFile | undefined, writeFileCallback: WriteFileCallback | undefined, cancellationToken: CancellationToken | undefined, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult {
let declarationDiagnostics: ReadonlyArray<Diagnostic> = [];
if (!emitOnlyDtsFiles) {
if (options.noEmit) {
return { diagnostics: declarationDiagnostics, sourceMaps: undefined, emittedFiles: undefined, emitSkipped: true };
}
if (!emitOnlyDtsFiles && options.noEmit) {
return { diagnostics: declarationDiagnostics, sourceMaps: undefined, emittedFiles: undefined, emitSkipped: true };
}
let pluginDiagnostics: ReadonlyArray<Diagnostic> | undefined;
if (pluginHost) {
const result = pluginHost.preEmit(host, program, sourceFile, cancellationToken);
pluginDiagnostics = result.diagnostics;
customTransformers = combineCustomTransformers(customTransformers, result.customTransformers);
}
if (!emitOnlyDtsFiles) {
// If the noEmitOnError flag is set, then check if we have any errors so far. If so,
// immediately bail out. Note that we pass 'undefined' for 'sourceFile' so that we
// get any preEmit diagnostics, not just the ones
@@ -1557,7 +1574,8 @@ namespace ts {
...program.getOptionsDiagnostics(cancellationToken),
...program.getSyntacticDiagnostics(sourceFile, cancellationToken),
...program.getGlobalDiagnostics(cancellationToken),
...program.getSemanticDiagnostics(sourceFile, cancellationToken)
...program.getSemanticDiagnostics(sourceFile, cancellationToken),
...(pluginDiagnostics || [])
];
if (diagnostics.length === 0 && getEmitDeclarations(program.getCompilerOptions())) {
+10
View File
@@ -651,6 +651,7 @@ namespace ts {
base64decode?(input: string): string;
base64encode?(input: string): string;
/*@internal*/ bufferFrom?(input: string, encoding?: string): Buffer;
/*@internal*/ require?(baseDir: string, moduleName: string): RequireResult;
}
export interface FileWatcher {
@@ -838,6 +839,15 @@ namespace ts {
bufferFrom,
base64decode: input => bufferFrom(input, "base64").toString("utf8"),
base64encode: input => bufferFrom(input).toString("base64"),
require: (baseDir, moduleName) => {
try {
const modulePath = resolveJSModule(moduleName, baseDir, nodeSystem);
return { module: require(modulePath), modulePath, error: undefined };
}
catch (error) {
return { module: undefined, modulePath: undefined, error };
}
}
};
return nodeSystem;
+1
View File
@@ -49,6 +49,7 @@
"transformer.ts",
"emitter.ts",
"watchUtilities.ts",
"plugin.ts",
"program.ts",
"builderState.ts",
"builder.ts",
+113
View File
@@ -2947,6 +2947,27 @@ namespace ts {
file: Path;
}
/* @internal */
export interface CompilerPluginDeactivationResult {
diagnostics?: ReadonlyArray<Diagnostic>;
}
/**
* The CompilerPluginHost provides an interface for interacting with the compiler plugin model.
*/
/*@internal*/
export interface CompilerPluginHost {
/**
* Trigger the `preEmit` hooks for plugins.
*/
preEmit(host: CompilerHost, program: Program, targetSourceFile?: SourceFile, cancellationToken?: CancellationToken): CompilerPluginPreEmitResult;
/**
* Deactivate any active plugins.
*/
deactivate(host: CompilerHost): CompilerPluginDeactivationResult;
}
// TODO: This should implement TypeCheckerHost but that's an internal type.
export interface Program extends ScriptReferenceHost {
@@ -3038,6 +3059,10 @@ namespace ts {
/*@internal*/ getResolvedProjectReferenceByPath(projectReferencePath: Path): ResolvedProjectReference | undefined;
/*@internal*/ getProgramBuildInfo?(): ProgramBuildInfo | undefined;
/*@internal*/ emitBuildInfo(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult;
/**
* Dispose of any resources held by the program and deactivate any active plugins.
*/
/*@internal*/ dispose(): void;
}
/* @internal */
@@ -4871,6 +4896,7 @@ namespace ts {
errors: Diagnostic[];
wildcardDirectories?: MapLike<WatchDirectoryFlags>;
compileOnSave?: boolean;
plugins?: (string | [string, any?])[];
/* @internal */ configFileSpecs?: ConfigFileSpecs;
}
@@ -4901,11 +4927,98 @@ namespace ts {
/* @internal */ spec: ConfigFileSpecs;
}
/**
* A context object passed to a plugin during activation.
*/
export interface CompilerPluginContext {
/**
* The running instance of the TypeScript compiler.
*/
readonly ts: typeof ts;
/**
* The current CompilerHost.
*/
readonly compilerHost: CompilerHost;
/**
* Configuration options for the plugin.
*/
readonly options: MapLike<any>;
}
/**
* An optional result that can be returned from the `CompilerPluginModule.activate` hook.
*/
export interface CompilerPluginActivationResult {
diagnostics?: ReadonlyArray<Diagnostic>;
}
/**
* An optional result that can be returned from the `CompilerPluginModule.preEmit` hook.
*/
export interface CompilerPluginPreEmitResult {
diagnostics?: ReadonlyArray<Diagnostic>;
customTransformers?: CustomTransformers;
}
/**
* Describes the supported shape of the main module for a compiler plugin.
*/
export interface CompilerPluginModule {
/**
* The `activate` hook is invoked when a plugin is activated for the first time within a `Program`.
* @param context The current plugin context.
*/
activate?(context: CompilerPluginContext): CompilerPluginActivationResult | void;
/**
* The `preEmit` hook is invoked after type check has completed and immediately before emit.
* @param context The current plugin context.
* @param program The current `Program`.
* @param targetSourceFile The `SourceFile` that is about to be emitted, or `undefined` when emitting all outputs.
* @param cancellationToken A `CancellationToken` that can be used to abort an operation when running in the language service.
*/
preEmit?(context: CompilerPluginContext, program: Program, targetSourceFile?: SourceFile, cancellationToken?: CancellationToken): CompilerPluginPreEmitResult | void;
/**
* The `deactivate` hook is invoked when a plugin should be deactivated so that it can free up any shared resources.
* @param context The current plugin context.
*/
deactivate?(context: CompilerPluginContext): void;
}
export interface CompilerPlugin {
/** The rsolved package name for the plugin. */
name: string;
/** The path to the plugin. */
path: string | undefined;
/** The originally-specified package name for the plugin */
originalName?: string;
/** Any configuration options provided to the plugin. */
options?: any;
/** The hooks supported by the plugin. */
activationEvents?: string[];
/** Plugin dependencies that should be loaded before this plugin. */
pluginDependencies?: string[];
/** The resolved module object for the plugin. */
plugin: CompilerPluginModule;
}
/* @internal */
export interface ModuleLoaderHost extends ModuleResolutionHost {
require(initialDir: string, moduleName: string): RequireResult;
}
/* @internal */
export type RequireResult =
| { module: {}, modulePath?: string, error: undefined }
| { module: undefined, modulePath?: undefined, error: { stack?: string, message?: string } };
export interface CreateProgramOptions {
rootNames: ReadonlyArray<string>;
options: CompilerOptions;
projectReferences?: ReadonlyArray<ProjectReference>;
host?: CompilerHost;
plugins?: ReadonlyArray<CompilerPlugin>;
oldProgram?: Program;
configFileParsingDiagnostics?: ReadonlyArray<Diagnostic>;
}
+10
View File
@@ -4712,6 +4712,16 @@ namespace ts {
return false;
}
}
export function combineCustomTransformers(left: CustomTransformers | undefined, right: CustomTransformers | undefined): CustomTransformers | undefined {
if (!left) return right;
if (!right) return left;
return {
before: concatenate(left.before, right.before),
after: concatenate(left.after, right.after),
afterDeclarations: concatenate(left.afterDeclarations, right.afterDeclarations)
};
}
}
namespace ts {
+26 -3
View File
@@ -238,20 +238,43 @@ namespace compiler {
return new CompilationResult(host, compilerOptions, /*program*/ undefined, /*result*/ undefined, project.errors);
}
if (project.config) {
rootFiles = project.config.fileNames;
compilerOptions = project.config.options;
return compileProject(host, project.config, project.file);
}
}
delete compilerOptions.project;
}
return compileProject(host, { options: compilerOptions, fileNames: rootFiles || [], errors: [], });
}
export function compileProject(host: fakes.CompilerHost, project: ts.ParsedCommandLine, configFileName?: string) {
const compilerOptions = { ...project.options };
let plugins: ts.CreateProgramOptions["plugins"];
if (project.plugins) {
if (!configFileName) throw new Error("Cannot load plugins without a config file.");
if (!host.sys.require) throw new Error("Plugins cannot be loaded if provided System does not support `require()`.");
const result = ts.getPlugins(host.sys as ts.ModuleLoaderHost, ts.getDirectoryPath(configFileName), project.plugins);
if (ts.some(result.diagnostics)) {
return new CompilationResult(host, compilerOptions, /*program*/ undefined, /*result*/ undefined, result.diagnostics);
}
plugins = result.plugins;
}
// establish defaults (aligns with old harness)
if (compilerOptions.target === undefined) compilerOptions.target = ts.ScriptTarget.ES3;
if (compilerOptions.newLine === undefined) compilerOptions.newLine = ts.NewLineKind.CarriageReturnLineFeed;
if (compilerOptions.skipDefaultLibCheck === undefined) compilerOptions.skipDefaultLibCheck = true;
if (compilerOptions.noErrorTruncation === undefined) compilerOptions.noErrorTruncation = true;
const program = ts.createProgram(rootFiles || [], compilerOptions, host);
const program = ts.createProgram({
rootNames: project.fileNames || [],
options: compilerOptions,
host,
plugins
});
const emitResult = program.emit();
const errors = ts.getPreEmitDiagnostics(program);
return new CompilationResult(host, compilerOptions, program, emitResult, errors);
+64 -1
View File
@@ -8,6 +8,7 @@ namespace fakes {
executingFilePath?: string;
newLine?: "\r\n" | "\n";
env?: Record<string, string>;
useFakeLoader?: boolean;
}
/**
@@ -20,16 +21,29 @@ namespace fakes {
public readonly newLine: string;
public readonly useCaseSensitiveFileNames: boolean;
public exitCode: number | undefined;
public require?: ts.System["require"];
private readonly _executingFilePath: string | undefined;
private readonly _env: Record<string, string> | undefined;
constructor(vfs: vfs.FileSystem, { executingFilePath, newLine = "\r\n", env }: SystemOptions = {}) {
constructor(vfs: vfs.FileSystem, { executingFilePath, newLine = "\r\n", env, useFakeLoader }: SystemOptions = {}) {
this.vfs = vfs.isReadonly ? vfs.shadow() : vfs;
this.useCaseSensitiveFileNames = !this.vfs.ignoreCase;
this.newLine = newLine;
this._executingFilePath = executingFilePath;
this._env = env;
if (useFakeLoader) {
const loader = new ModuleLoader(this.vfs);
this.require = (baseDir: string, moduleName: string): ts.RequireResult => {
try {
const modulePath = ts.resolveJSModule(moduleName, baseDir, this);
return { module: loader.require(modulePath), modulePath, error: undefined };
}
catch (error) {
return { module: undefined, modulePath: undefined, error };
}
};
}
}
public write(message: string) {
@@ -375,6 +389,55 @@ namespace fakes {
}
}
type ModuleWrapper = (require: Function, module: any, exports: any, dirname: string, filename: string) => void;
interface ModuleObject {
exports: any;
}
class ModuleLoader {
readonly vfs: vfs.FileSystem;
private _modules = ts.createMap<ModuleObject>();
constructor(vfs: vfs.FileSystem) {
this.vfs = vfs;
}
require(modulePath: string) {
// this is a very basic loader that is only intended to load a single file for now.
let module = this._modules.get(modulePath);
if (!module) {
module = { exports: {} };
this._modules.set(modulePath, module);
const moduleContent = this.vfs.readFileSync(modulePath, "utf8");
const linkedModule = this.link(modulePath, moduleContent);
linkedModule(ts.notImplemented, module, module.exports, ts.getDirectoryPath(modulePath), modulePath);
}
return module.exports;
}
private link(modulePath: string, moduleContent: string) {
let vm: typeof import("vm") | undefined;
try {
vm = require("vm");
}
catch {
// ignore
}
return vm ? this.linkUsingNodeVM(vm, modulePath, moduleContent) :
this.linkUsingFunction(modulePath, moduleContent);
}
private linkUsingNodeVM(vm: typeof import("vm"), modulePath: string, moduleContent: string): ModuleWrapper {
return vm.runInThisContext(`(function (require, module, exports, __dirname, __filename) {${moduleContent}\n})`, { filename: modulePath });
}
private linkUsingFunction(modulePath: string, moduleContent: string): ModuleWrapper {
const indirectEval = eval;
return indirectEval(`(function (require, module, exports, __dirname, __filename) {${moduleContent}\n//# sourceURL=${modulePath}\n})`);
}
}
export type ExpectedDiagnosticMessage = [ts.DiagnosticMessage, ...(string | number)[]];
export interface ExpectedDiagnosticMessageChain {
message: ExpectedDiagnosticMessage;
+9 -3
View File
@@ -816,7 +816,9 @@ namespace Harness {
compilerOptions: ts.CompilerOptions | undefined,
// Current directory is needed for rwcRunner to be able to use currentDirectory defined in json file
currentDirectory: string | undefined,
symlinks?: vfs.FileSet
symlinks?: vfs.FileSet,
plugins?: ts.ParsedCommandLine["plugins"],
configFileName?: string
): compiler.CompilationResult {
const options: ts.CompilerOptions & HarnessOptions = compilerOptions ? ts.cloneCompilerOptions(compilerOptions) : { noResolve: false };
options.target = options.target || ts.ScriptTarget.ES3;
@@ -857,8 +859,12 @@ namespace Harness {
if (symlinks) {
fs.apply(symlinks);
}
const host = new fakes.CompilerHost(fs, options);
const result = compiler.compileFiles(host, programFileNames, options);
const sys = new fakes.System(fs, { useFakeLoader: !!(plugins && configFileName) });
const host = new fakes.CompilerHost(sys, options);
const result = plugins && configFileName
? compiler.compileProject(host, { options, fileNames: programFileNames, errors: [], plugins }, configFileName)
: compiler.compileFiles(host, programFileNames, options);
result.symlinks = symlinks;
return result;
}
+1 -1
View File
@@ -789,7 +789,7 @@ namespace Harness.LanguageService {
return mockHash(s);
}
require(_initialDir: string, _moduleName: string): ts.server.RequireResult {
require(_initialDir: string, _moduleName: string): ts.RequireResult {
switch (_moduleName) {
// Adds to the Quick Info a fixed string and a string from the config file
// and replaces the first display part
+1 -1
View File
@@ -346,7 +346,7 @@ interface Array<T> { length: number; [n: number]: T; }`
private readonly currentDirectory: string;
private readonly customWatchFile: HostWatchFile | undefined;
private readonly customRecursiveWatchDirectory: HostWatchDirectory | undefined;
public require: ((initialPath: string, moduleName: string) => server.RequireResult) | undefined;
public require: ((initialPath: string, moduleName: string) => RequireResult) | undefined;
constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderorSymLinkList: ReadonlyArray<FileOrFolderOrSymLink>, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean, private readonly environmentVariables?: Map<string>) {
this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
-2
View File
@@ -5,7 +5,6 @@ declare namespace ts.server {
data: any;
}
type RequireResult = { module: {}, error: undefined } | { module: undefined, error: { stack?: string, message?: string } };
export interface ServerHost extends System {
watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher;
watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher;
@@ -15,6 +14,5 @@ declare namespace ts.server {
clearImmediate(timeoutId: any): void;
gc?(): void;
trace?(s: string): void;
require?(initialPath: string, moduleName: string): RequireResult;
}
}
+8 -2
View File
@@ -162,13 +162,17 @@ class CompilerTest {
const units = testCaseContent.testUnitData;
this.harnessSettings = testCaseContent.settings;
let tsConfigOptions: ts.CompilerOptions | undefined;
let plugins: ts.ParsedCommandLine["plugins"];
let tsConfigFileName: string | undefined;
this.tsConfigFiles = [];
if (testCaseContent.tsConfig) {
assert.equal(testCaseContent.tsConfig.fileNames.length, 0, `list of files in tsconfig is not currently supported`);
assert.equal(testCaseContent.tsConfig.raw.exclude, undefined, `exclude in tsconfig is not currently supported`);
tsConfigOptions = ts.cloneCompilerOptions(testCaseContent.tsConfig.options);
this.tsConfigFiles.push(this.createHarnessTestFile(testCaseContent.tsConfigFileUnitData!, rootDir, ts.combinePaths(rootDir, tsConfigOptions.configFilePath!)));
plugins = testCaseContent.tsConfig.plugins;
tsConfigFileName = ts.combinePaths(rootDir, tsConfigOptions.configFilePath!);
this.tsConfigFiles.push(this.createHarnessTestFile(testCaseContent.tsConfigFileUnitData!, rootDir, tsConfigFileName));
}
else {
const baseUrl = this.harnessSettings.baseUrl;
@@ -210,7 +214,9 @@ class CompilerTest {
this.harnessSettings,
/*options*/ tsConfigOptions,
/*currentDirectory*/ this.harnessSettings.currentDirectory,
testCaseContent.symlinks
testCaseContent.symlinks,
plugins,
tsConfigFileName
);
this.options = this.result.options;
+20 -4
View File
@@ -142,16 +142,30 @@ namespace ts {
sys.write(JSON.stringify(convertToTSConfig(configParseResult, configFileName, sys), null, 4) + sys.newLine);
return sys.exit(ExitStatus.Success);
}
let plugins: ReadonlyArray<CompilerPlugin> | undefined;
if (configParseResult.plugins) {
if (sys.require) {
const result = getPlugins(sys as ModuleLoaderHost, getDirectoryPath(configFileName), configParseResult.plugins);
plugins = result.plugins;
if (some(result.diagnostics)) {
// TODO(rbuckton): report diagnostics
}
}
else {
// TODO(rbuckton): report diagnostic
}
}
updateReportDiagnostic(configParseResult.options);
if (isWatchSet(configParseResult.options)) {
reportWatchModeWithoutSysSupport();
// TODO(rbuckton): watch mode support
createWatchOfConfigFile(configParseResult, commandLineOptions);
}
else if (isIncrementalCompilation(configParseResult.options)) {
performIncrementalCompilation(configParseResult);
}
else {
performCompilation(configParseResult.fileNames, configParseResult.projectReferences, configParseResult.options, getConfigFileParsingDiagnostics(configParseResult));
performCompilation(configParseResult.fileNames, configParseResult.projectReferences, configParseResult.options, plugins, getConfigFileParsingDiagnostics(configParseResult));
}
}
else {
@@ -169,7 +183,7 @@ namespace ts {
performIncrementalCompilation(commandLine);
}
else {
performCompilation(commandLine.fileNames, /*references*/ undefined, commandLineOptions);
performCompilation(commandLine.fileNames, /*references*/ undefined, commandLineOptions, /*plugins*/ undefined);
}
}
}
@@ -235,7 +249,7 @@ namespace ts {
undefined;
}
function performCompilation(rootNames: string[], projectReferences: ReadonlyArray<ProjectReference> | undefined, options: CompilerOptions, configFileParsingDiagnostics?: ReadonlyArray<Diagnostic>) {
function performCompilation(rootNames: string[], projectReferences: ReadonlyArray<ProjectReference> | undefined, options: CompilerOptions, plugins: ReadonlyArray<CompilerPlugin> | undefined, configFileParsingDiagnostics?: ReadonlyArray<Diagnostic>) {
const host = createCompilerHost(options);
const currentDirectory = host.getCurrentDirectory();
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames());
@@ -247,7 +261,8 @@ namespace ts {
options,
projectReferences,
host,
configFileParsingDiagnostics
configFileParsingDiagnostics,
plugins
};
const program = createProgram(programOptions);
const exitStatus = emitFilesAndReportErrorsAndGetExitStatus(
@@ -257,6 +272,7 @@ namespace ts {
createReportErrorSummary(options)
);
reportStatistics(program);
program.dispose();
return sys.exit(exitStatus);
}
+3 -2
View File
@@ -903,10 +903,11 @@ namespace ts.server {
sys.require = (initialDir: string, moduleName: string): RequireResult => {
try {
return { module: require(resolveJSModule(moduleName, initialDir, sys)), error: undefined };
const modulePath = resolveJSModule(moduleName, initialDir, sys);
return { module: require(modulePath), modulePath, error: undefined };
}
catch (error) {
return { module: undefined, error };
return { module: undefined, modulePath: undefined, error };
}
};
+73 -11
View File
@@ -2668,6 +2668,7 @@ declare namespace ts {
errors: Diagnostic[];
wildcardDirectories?: MapLike<WatchDirectoryFlags>;
compileOnSave?: boolean;
plugins?: (string | [string, any?])[];
}
export enum WatchDirectoryFlags {
None = 0,
@@ -2677,11 +2678,81 @@ declare namespace ts {
fileNames: string[];
wildcardDirectories: MapLike<WatchDirectoryFlags>;
}
/**
* A context object passed to a plugin during activation.
*/
interface CompilerPluginContext {
/**
* The running instance of the TypeScript compiler.
*/
readonly ts: typeof ts;
/**
* The current CompilerHost.
*/
readonly compilerHost: CompilerHost;
/**
* Configuration options for the plugin.
*/
readonly options: MapLike<any>;
}
/**
* An optional result that can be returned from the `CompilerPluginModule.activate` hook.
*/
interface CompilerPluginActivationResult {
diagnostics?: ReadonlyArray<Diagnostic>;
}
/**
* An optional result that can be returned from the `CompilerPluginModule.preEmit` hook.
*/
interface CompilerPluginPreEmitResult {
diagnostics?: ReadonlyArray<Diagnostic>;
customTransformers?: CustomTransformers;
}
/**
* Describes the supported shape of the main module for a compiler plugin.
*/
interface CompilerPluginModule {
/**
* The `activate` hook is invoked when a plugin is activated for the first time within a `Program`.
* @param context The current plugin context.
*/
activate?(context: CompilerPluginContext): CompilerPluginActivationResult | void;
/**
* The `preEmit` hook is invoked after type check has completed and immediately before emit.
* @param context The current plugin context.
* @param program The current `Program`.
* @param targetSourceFile The `SourceFile` that is about to be emitted, or `undefined` when emitting all outputs.
* @param cancellationToken A `CancellationToken` that can be used to abort an operation when running in the language service.
*/
preEmit?(context: CompilerPluginContext, program: Program, targetSourceFile?: SourceFile, cancellationToken?: CancellationToken): CompilerPluginPreEmitResult | void;
/**
* The `deactivate` hook is invoked when a plugin should be deactivated so that it can free up any shared resources.
* @param context The current plugin context.
*/
deactivate?(context: CompilerPluginContext): void;
}
interface CompilerPlugin {
/** The rsolved package name for the plugin. */
name: string;
/** The path to the plugin. */
path: string | undefined;
/** The originally-specified package name for the plugin */
originalName?: string;
/** Any configuration options provided to the plugin. */
options?: any;
/** The hooks supported by the plugin. */
activationEvents?: string[];
/** Plugin dependencies that should be loaded before this plugin. */
pluginDependencies?: string[];
/** The resolved module object for the plugin. */
plugin: CompilerPluginModule;
}
export interface CreateProgramOptions {
rootNames: ReadonlyArray<string>;
options: CompilerOptions;
projectReferences?: ReadonlyArray<ProjectReference>;
host?: CompilerHost;
plugins?: ReadonlyArray<CompilerPlugin>;
oldProgram?: Program;
configFileParsingDiagnostics?: ReadonlyArray<Diagnostic>;
}
@@ -4280,6 +4351,8 @@ declare namespace ts {
declare namespace ts {
function createPrinter(printerOptions?: PrinterOptions, handlers?: PrintHandlers): Printer;
}
declare namespace ts {
}
declare namespace ts {
export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName?: string): string | undefined;
export function resolveTripleslashReference(moduleName: string, containingFile: string): string;
@@ -5803,16 +5876,6 @@ declare namespace ts.server {
compressionKind: string;
data: any;
}
type RequireResult = {
module: {};
error: undefined;
} | {
module: undefined;
error: {
stack?: string;
message?: string;
};
};
interface ServerHost extends System {
watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher;
watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher;
@@ -5822,7 +5885,6 @@ declare namespace ts.server {
clearImmediate(timeoutId: any): void;
gc?(): void;
trace?(s: string): void;
require?(initialPath: string, moduleName: string): RequireResult;
}
}
declare namespace ts.server {
+73
View File
@@ -2668,6 +2668,7 @@ declare namespace ts {
errors: Diagnostic[];
wildcardDirectories?: MapLike<WatchDirectoryFlags>;
compileOnSave?: boolean;
plugins?: (string | [string, any?])[];
}
export enum WatchDirectoryFlags {
None = 0,
@@ -2677,11 +2678,81 @@ declare namespace ts {
fileNames: string[];
wildcardDirectories: MapLike<WatchDirectoryFlags>;
}
/**
* A context object passed to a plugin during activation.
*/
interface CompilerPluginContext {
/**
* The running instance of the TypeScript compiler.
*/
readonly ts: typeof ts;
/**
* The current CompilerHost.
*/
readonly compilerHost: CompilerHost;
/**
* Configuration options for the plugin.
*/
readonly options: MapLike<any>;
}
/**
* An optional result that can be returned from the `CompilerPluginModule.activate` hook.
*/
interface CompilerPluginActivationResult {
diagnostics?: ReadonlyArray<Diagnostic>;
}
/**
* An optional result that can be returned from the `CompilerPluginModule.preEmit` hook.
*/
interface CompilerPluginPreEmitResult {
diagnostics?: ReadonlyArray<Diagnostic>;
customTransformers?: CustomTransformers;
}
/**
* Describes the supported shape of the main module for a compiler plugin.
*/
interface CompilerPluginModule {
/**
* The `activate` hook is invoked when a plugin is activated for the first time within a `Program`.
* @param context The current plugin context.
*/
activate?(context: CompilerPluginContext): CompilerPluginActivationResult | void;
/**
* The `preEmit` hook is invoked after type check has completed and immediately before emit.
* @param context The current plugin context.
* @param program The current `Program`.
* @param targetSourceFile The `SourceFile` that is about to be emitted, or `undefined` when emitting all outputs.
* @param cancellationToken A `CancellationToken` that can be used to abort an operation when running in the language service.
*/
preEmit?(context: CompilerPluginContext, program: Program, targetSourceFile?: SourceFile, cancellationToken?: CancellationToken): CompilerPluginPreEmitResult | void;
/**
* The `deactivate` hook is invoked when a plugin should be deactivated so that it can free up any shared resources.
* @param context The current plugin context.
*/
deactivate?(context: CompilerPluginContext): void;
}
interface CompilerPlugin {
/** The rsolved package name for the plugin. */
name: string;
/** The path to the plugin. */
path: string | undefined;
/** The originally-specified package name for the plugin */
originalName?: string;
/** Any configuration options provided to the plugin. */
options?: any;
/** The hooks supported by the plugin. */
activationEvents?: string[];
/** Plugin dependencies that should be loaded before this plugin. */
pluginDependencies?: string[];
/** The resolved module object for the plugin. */
plugin: CompilerPluginModule;
}
export interface CreateProgramOptions {
rootNames: ReadonlyArray<string>;
options: CompilerOptions;
projectReferences?: ReadonlyArray<ProjectReference>;
host?: CompilerHost;
plugins?: ReadonlyArray<CompilerPlugin>;
oldProgram?: Program;
configFileParsingDiagnostics?: ReadonlyArray<Diagnostic>;
}
@@ -4280,6 +4351,8 @@ declare namespace ts {
declare namespace ts {
function createPrinter(printerOptions?: PrinterOptions, handlers?: PrintHandlers): Printer;
}
declare namespace ts {
}
declare namespace ts {
export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName?: string): string | undefined;
export function resolveTripleslashReference(moduleName: string, containingFile: string): string;
@@ -0,0 +1,40 @@
//// [tests/cases/conformance/plugins/transformerPlugin.ts] ////
//// [package.json]
{
"name": "typescript-plugin-transform",
"version": "1.0.0",
"main": "index.js",
"typescriptPlugin": {
"activationEvents": ["preEmit"]
}
}
//// [index.js]
exports.preEmit = function (context, program, targetSourceFile) {
const ts = context.ts;
return { customTransformers: { after: [replaceUndefinedWithVoid0] } };
function replaceUndefinedWithVoid0(context) {
const previousOnSubstituteNode = context.onSubstituteNode;
context.enableSubstitution(ts.SyntaxKind.Identifier);
context.onSubstituteNode = (hint, node) => {
node = previousOnSubstituteNode(hint, node);
if (hint === ts.EmitHint.Expression && ts.isIdentifier(node) && node.escapedText === "undefined") {
node = ts.createPartiallyEmittedExpression(
ts.addSyntheticTrailingComment(
ts.setTextRange(
ts.createVoidZero(),
node),
ts.SyntaxKind.MultiLineCommentTrivia, "undefined"));
}
return node;
};
return (file) => file;
}
};
//// [main.ts]
const a = undefined;
//// [main.js]
var a = void 0 /*undefined*/;
@@ -0,0 +1,5 @@
=== tests/cases/conformance/plugins/main.ts ===
const a = undefined;
>a : Symbol(a, Decl(main.ts, 0, 5))
>undefined : Symbol(undefined)
@@ -0,0 +1,5 @@
=== tests/cases/conformance/plugins/main.ts ===
const a = undefined;
>a : any
>undefined : undefined
@@ -0,0 +1,44 @@
// @noImplicitReferences: true
// @filename: node_modules/typescript-plugin-transform/package.json
{
"name": "typescript-plugin-transform",
"version": "1.0.0",
"main": "index.js",
"typescriptPlugin": {
"activationEvents": ["preEmit"]
}
}
// @filename: node_modules/typescript-plugin-transform/index.js
exports.preEmit = function (context, program, targetSourceFile) {
const ts = context.ts;
return { customTransformers: { after: [replaceUndefinedWithVoid0] } };
function replaceUndefinedWithVoid0(context) {
const previousOnSubstituteNode = context.onSubstituteNode;
context.enableSubstitution(ts.SyntaxKind.Identifier);
context.onSubstituteNode = (hint, node) => {
node = previousOnSubstituteNode(hint, node);
if (hint === ts.EmitHint.Expression && ts.isIdentifier(node) && node.escapedText === "undefined") {
node = ts.createPartiallyEmittedExpression(
ts.addSyntheticTrailingComment(
ts.setTextRange(
ts.createVoidZero(),
node),
ts.SyntaxKind.MultiLineCommentTrivia, "undefined"));
}
return node;
};
return (file) => file;
}
};
// @filename: tsconfig.json
{
"compilerOptions": {},
"plugins": [
"transform"
]
}
// @filename: main.ts
const a = undefined;