diff --git a/Gulpfile.js b/Gulpfile.js index be2c4f06a38..df9b13e971b 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -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." diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 9ba03842cec..e80f1c320a0 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -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)); diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 3c282ca092f..b9254a2bdff 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -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", diff --git a/src/compiler/plugin.ts b/src/compiler/plugin.ts new file mode 100644 index 00000000000..78469239aa8 --- /dev/null +++ b/src/compiler/plugin.ts @@ -0,0 +1,289 @@ +namespace ts { + type Hook = "activate" | "deactivate" | "preEmit"; + type HookFunction = NonNullable; + + type ExecuteUserCodeResult = + | { 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) { + debugger; + interface ResolvedModule { + module: {}; + moduleName: string; + modulePath: string | undefined; + packageJsonPath?: string; + } + + const compilerPlugins: CompilerPlugin[] = []; + const compilerPluginMap = createMap(); + 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] : 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): CompilerPluginHost { + const entries: PluginEntry[] = plugins.map(compilerPlugin => ({ compilerPlugin, active: false })); + return { + preEmit, + deactivate + }; + + function executeUserCode(compilerPlugin: CompilerPlugin, hook: K, ...args: Parameters>): ExecuteUserCodeResult>> { + const hookFunction: HookFunction | 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 | 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 }; + } + } +} \ No newline at end of file diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 1c7cb0ca2c4..3ffb94fce91 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -725,6 +725,7 @@ namespace ts { const ambientModuleNameToUnmodifiedFileName = createMap(); // Todo:: Use this to report why file was included in --extendedDiagnostics let refFileMap: MultiMap | undefined; + const pluginHost = createProgramOptions.plugins && createPluginHost(createProgramOptions.plugins); const cachedSemanticDiagnosticsForFile: DiagnosticCache = {}; const cachedDeclarationDiagnosticsForFile: DiagnosticCache = {}; @@ -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 = []; - 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 | 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())) { diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index b49446dd33e..16e2604abeb 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -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; diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index c90c6f35b34..68e557a746e 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -49,6 +49,7 @@ "transformer.ts", "emitter.ts", "watchUtilities.ts", + "plugin.ts", "program.ts", "builderState.ts", "builder.ts", diff --git a/src/compiler/types.ts b/src/compiler/types.ts index ec9ed32b822..3561861c7f3 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2947,6 +2947,27 @@ namespace ts { file: Path; } + /* @internal */ + export interface CompilerPluginDeactivationResult { + diagnostics?: ReadonlyArray; + } + + /** + * 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; 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; + } + + /** + * An optional result that can be returned from the `CompilerPluginModule.activate` hook. + */ + export interface CompilerPluginActivationResult { + diagnostics?: ReadonlyArray; + } + + /** + * An optional result that can be returned from the `CompilerPluginModule.preEmit` hook. + */ + export interface CompilerPluginPreEmitResult { + diagnostics?: ReadonlyArray; + 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; options: CompilerOptions; projectReferences?: ReadonlyArray; host?: CompilerHost; + plugins?: ReadonlyArray; oldProgram?: Program; configFileParsingDiagnostics?: ReadonlyArray; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index b3a23f031d4..51260a69123 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -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 { diff --git a/src/harness/compiler.ts b/src/harness/compiler.ts index 70bda90ef32..e0b8051bd48 100644 --- a/src/harness/compiler.ts +++ b/src/harness/compiler.ts @@ -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); diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index 522cac95f30..374b64d77ca 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -8,6 +8,7 @@ namespace fakes { executingFilePath?: string; newLine?: "\r\n" | "\n"; env?: Record; + 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 | 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(); + + 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; diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 03d03ba010f..d578f6f81d0 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -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; } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 03ccbb5d6de..46abd3af329 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -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 diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index ffb1e19f653..22a0fb5d474 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -346,7 +346,7 @@ interface Array { 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, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean, private readonly environmentVariables?: Map) { this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); diff --git a/src/server/types.ts b/src/server/types.ts index ce68a33d3b7..2618ad57f19 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -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; } } diff --git a/src/testRunner/compilerRunner.ts b/src/testRunner/compilerRunner.ts index c007d5409ea..73d9e002989 100644 --- a/src/testRunner/compilerRunner.ts +++ b/src/testRunner/compilerRunner.ts @@ -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; diff --git a/src/tsc/tsc.ts b/src/tsc/tsc.ts index d27c1aace4e..c402b22b37a 100644 --- a/src/tsc/tsc.ts +++ b/src/tsc/tsc.ts @@ -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 | 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 | undefined, options: CompilerOptions, configFileParsingDiagnostics?: ReadonlyArray) { + function performCompilation(rootNames: string[], projectReferences: ReadonlyArray | undefined, options: CompilerOptions, plugins: ReadonlyArray | undefined, configFileParsingDiagnostics?: ReadonlyArray) { 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); } diff --git a/src/tsserver/server.ts b/src/tsserver/server.ts index b9a5792d838..66b189daf4c 100644 --- a/src/tsserver/server.ts +++ b/src/tsserver/server.ts @@ -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 }; } }; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 3c660bbc6db..196912acde4 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2668,6 +2668,7 @@ declare namespace ts { errors: Diagnostic[]; wildcardDirectories?: MapLike; compileOnSave?: boolean; + plugins?: (string | [string, any?])[]; } export enum WatchDirectoryFlags { None = 0, @@ -2677,11 +2678,81 @@ declare namespace ts { fileNames: string[]; wildcardDirectories: MapLike; } + /** + * 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; + } + /** + * An optional result that can be returned from the `CompilerPluginModule.activate` hook. + */ + interface CompilerPluginActivationResult { + diagnostics?: ReadonlyArray; + } + /** + * An optional result that can be returned from the `CompilerPluginModule.preEmit` hook. + */ + interface CompilerPluginPreEmitResult { + diagnostics?: ReadonlyArray; + 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; options: CompilerOptions; projectReferences?: ReadonlyArray; host?: CompilerHost; + plugins?: ReadonlyArray; oldProgram?: Program; configFileParsingDiagnostics?: ReadonlyArray; } @@ -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 { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 515cbd77d05..a4bc9bd1942 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2668,6 +2668,7 @@ declare namespace ts { errors: Diagnostic[]; wildcardDirectories?: MapLike; compileOnSave?: boolean; + plugins?: (string | [string, any?])[]; } export enum WatchDirectoryFlags { None = 0, @@ -2677,11 +2678,81 @@ declare namespace ts { fileNames: string[]; wildcardDirectories: MapLike; } + /** + * 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; + } + /** + * An optional result that can be returned from the `CompilerPluginModule.activate` hook. + */ + interface CompilerPluginActivationResult { + diagnostics?: ReadonlyArray; + } + /** + * An optional result that can be returned from the `CompilerPluginModule.preEmit` hook. + */ + interface CompilerPluginPreEmitResult { + diagnostics?: ReadonlyArray; + 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; options: CompilerOptions; projectReferences?: ReadonlyArray; host?: CompilerHost; + plugins?: ReadonlyArray; oldProgram?: Program; configFileParsingDiagnostics?: ReadonlyArray; } @@ -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; diff --git a/tests/baselines/reference/transformerPlugin.js b/tests/baselines/reference/transformerPlugin.js new file mode 100644 index 00000000000..a9be8a8ab77 --- /dev/null +++ b/tests/baselines/reference/transformerPlugin.js @@ -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*/; diff --git a/tests/baselines/reference/transformerPlugin.symbols b/tests/baselines/reference/transformerPlugin.symbols new file mode 100644 index 00000000000..17adf63ede4 --- /dev/null +++ b/tests/baselines/reference/transformerPlugin.symbols @@ -0,0 +1,5 @@ +=== tests/cases/conformance/plugins/main.ts === +const a = undefined; +>a : Symbol(a, Decl(main.ts, 0, 5)) +>undefined : Symbol(undefined) + diff --git a/tests/baselines/reference/transformerPlugin.types b/tests/baselines/reference/transformerPlugin.types new file mode 100644 index 00000000000..e90d5444657 --- /dev/null +++ b/tests/baselines/reference/transformerPlugin.types @@ -0,0 +1,5 @@ +=== tests/cases/conformance/plugins/main.ts === +const a = undefined; +>a : any +>undefined : undefined + diff --git a/tests/cases/conformance/plugins/transformerPlugin.ts b/tests/cases/conformance/plugins/transformerPlugin.ts new file mode 100644 index 00000000000..8676f7b32d1 --- /dev/null +++ b/tests/cases/conformance/plugins/transformerPlugin.ts @@ -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; \ No newline at end of file