/*@internal*/ namespace ts { type MatchingKeys = K extends (T[K] extends TMatch ? K : never) ? K : never; type TryExcludeVoid = [T] extends [void] ? void : Exclude; type Replace = T extends Match ? Replace : T; type Promised = T extends PromiseLike ? U : T; type Hook = MatchingKeys, (context: CompilerPluginContext, ...args: any[]) => PromiseLike | CompilerPluginResult | void>; type HookFunction = Required[K]; type HookParameters = HookFunction extends (context: CompilerPluginContext, ...args: infer P) => any ? Readonly

: never; type HookReturnTypes = { [K in Hook]: TryExcludeVoid>>> }; type HookReturnType = HookReturnTypes[K]; type HostHook = Exclude; type HostHookReturnType = Replace, void, CompilerPluginResult>; type HostHookAggregate = (state: HostHookReturnType, userCodeResult: HookReturnType, args: ArgumentsHolder) => HostHookReturnType; interface ArgumentsHolder { arguments: HookParameters; } type ExecuteUserCodeResult = | { error: Diagnostic, result: undefined } | { error: undefined, result: T | undefined }; 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. */ export function getPlugins(host: ModuleLoaderHost, initialDir: string, plugins: ReadonlyArray) { interface ResolvedModule { getModule(): RequireResult; 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) { try { const modulePath = resolveJSModule(candidate, initialDir, host); resolvedModule = { getModule: () => host.require(initialDir, modulePath), moduleName: candidate, modulePath, packageJsonPath: !isExternalModuleNameRelative(candidate) && modulePath ? findPackageJsonPath(host, modulePath) : undefined }; break; } catch (e) { if (!firstError) { firstError = e; } } } 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, loadPlugin: resolvedModule.getModule, 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)); } } export function registerPluginApiModules(host: ModuleLoaderHost) { host.registerModule(createPluginApiModuleFactory()); } function createPluginApiModuleFactory(): ModuleFactory { return { id: ["typescript"], load: () => ts }; } 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. */ export function createPluginHost(compilerPlugins: ReadonlyArray, compilerHost: CompilerHost, compilerOptions: CompilerOptions): CompilerPluginHost { interface Plugin { state: "unloaded" | "inactive" | "active" | "active-failed" | "failed"; // Tracks whether the plugin has been activated. compilerPlugin: CompilerPlugin; context: CompilerPluginContext; timer: performance.Timer; module?: CompilerPluginModule; } const plugins: Plugin[] = compilerPlugins.map(compilerPlugin => ({ compilerPlugin, state: "unloaded", context: createContext(compilerPlugin), timer: performance.createTimer(`userCode:${compilerPlugin.name}`) })); return { deactivate, preParse: (...args) => executeHostHook("preParse", args, (state, result, argsHolder) => { state.diagnostics = concatenate(state.diagnostics, result.diagnostics); state.rootNames = result.rootNames || state.rootNames; state.projectReferences = result.projectReferences || state.projectReferences; state.preprocessors = concatenate(state.preprocessors, result.preprocessors); const previousArgs = argsHolder.arguments[0]; if (state.projectReferences !== previousArgs.projectReferences || state.rootNames !== previousArgs.rootNames) { argsHolder.arguments = [{ projectReferences: state.projectReferences || previousArgs.projectReferences, rootNames: state.rootNames || previousArgs.rootNames }]; } return state; }, {}), preEmit: (...args) => executeHostHook("preEmit", args, (state, result) => { state.diagnostics = concatenate(state.diagnostics, result.diagnostics); state.customTransformers = combineCustomTransformers(state.customTransformers, result.customTransformers); return state; }, {}) }; /** * Gets the module object of a loaded compiler plugin. */ function getModule(plugin: Plugin) { Debug.assert(plugin.state !== "unloaded" && plugin.module !== undefined, "Cannot execute a plugin hook on an unloaded plugin."); return plugin.module!; } /** * Gets the plugin hook function for a plugin module. */ function getHook(pluginModule: CompilerPluginModule, hook: K): CompilerPluginModule[K] { const hookAction = pluginModule[hook]; return typeof hookAction === "function" ? hookAction : undefined; } /** * Selects plugins matching the provided activation event and plugin activation state. */ function selectPlugins({ eventName, state }: { eventName?: Hook, state?: Plugin["state"] }) { const result: Plugin[] = []; for (const entry of plugins) { if (state !== undefined && entry.state !== state) continue; if (eventName !== undefined && !contains(entry.compilerPlugin.activationEvents, eventName)) continue; result.push(entry); } return result; } /** * Loads any unloaded plugins for the provided activation event. */ function load(eventName: Hook): ReadonlyArray | undefined { let diagnostics: readonly Diagnostic[] | undefined; for (const plugin of selectPlugins({ eventName, state: "unloaded" })) { const result = plugin.compilerPlugin.loadPlugin(); if (result.error) { // The plugin failed to load. Mark the plugin as failed to prevent any attempt to load it again in this Program. plugin.state = "failed"; plugin.module = undefined; diagnostics = concatenate(diagnostics, [createLoadDiagnostic(plugin.compilerPlugin, result.error)]); } else { // The plugin was loaded and is currently inactive. plugin.state = "inactive"; plugin.module = result.module; } } return diagnostics; } /** * Executes a user-defined plugin hook. */ async function executeUserHook(plugin: Plugin, hook: K, ...args: HookParameters): Promise>> { Debug.assert(plugin.state !== "failed", "Cannot execute a plugin hook on a plugin that failed to load."); Debug.assert(plugin.state !== "active-failed" || hook === "deactivate", "Cannot execute a plugin hook on a failed plugin."); Debug.assert(plugin.state !== "inactive" || hook === "activate", "Cannot activate a plugin that is already active."); const pluginModule = getModule(plugin); try { const hookAction = getHook(pluginModule, hook); if (hookAction) { const result: HookReturnType | undefined = await hookAction.call(pluginModule, plugin.context, ...args); return { error: undefined, result }; } } catch (error) { if (error instanceof OperationCanceledException) { throw error; } if (hook === "activate" || hook === "deactivate") { // If we encounter an error while activating or deactivating a hook, set the plugin's state to // indicate an unconditional failure and release our reference to the module. plugin.state = "failed"; plugin.module = undefined; } else { // For any other lifecycle hook, indicate that the plugin has failed but is still active. // This will allow us to deactivate the plugin later. plugin.state = "active-failed"; } return { error: createUserCodeDiagnostic(plugin.compilerPlugin, hook, error), result: undefined }; } return { error: undefined, result: undefined }; } function createContext({ options }: CompilerPlugin): CompilerPluginContext { return Object.freeze({ ts, compilerHost, compilerOptions, options }); } /** * Activates inactive plugins registered for the requested activation event. */ async function activate(eventName: Hook): Promise | undefined> { let diagnostics = load(eventName); for (const plugin of selectPlugins({ eventName, state: "inactive" })) { const { error, result } = await executeUserHook(plugin, "activate", { }); if (error) { // The plugin failed to activate and its state is already set to `"failed"`. diagnostics = concatenate(diagnostics, [error]); } else { plugin.state = "active"; if (result) diagnostics = concatenate(diagnostics, result.diagnostics); } } return diagnostics; } /** * Deactivates active plugins. */ async function deactivate(): Promise { let diagnostics: ReadonlyArray | undefined; for (const state of ["active-failed", "active"] as const) { for (const plugin of selectPlugins({ state })) { const { error } = await executeUserHook(plugin, "deactivate"); if (error) { diagnostics = concatenate(diagnostics, [error]); } else { plugin.state = state === "active-failed" ? "failed" : "inactive"; } } } return { diagnostics }; } async function executeHostHook( hook: K, args: HookParameters, aggregate: HostHookAggregate, state: HostHookReturnType, ): Promise> { state.diagnostics = concatenate(state.diagnostics, await activate(hook)); const argsHolder: ArgumentsHolder = { arguments: args }; for (const plugin of selectPlugins({ eventName: hook, state: "active" })) { const userCodeResult = await executeUserHook(plugin, hook, ...argsHolder.arguments); if (userCodeResult.error) { state.diagnostics = concatenate(state.diagnostics, [userCodeResult.error]); } else if (userCodeResult.result) { state = aggregate(state, userCodeResult.result, argsHolder); } } return state; } } function createLoadDiagnostic(compilerPlugin: CompilerPlugin, error: { message?: string, stack?: string }) { debugger; return createCompilerDiagnostic(Diagnostics.Plugin_0_could_not_be_loaded, compilerPlugin.name, error.stack || error.message || error.toString()); } function createUserCodeDiagnostic(compilerPlugin: CompilerPlugin, hook: Hook, error: { message?: string, stack?: string }) { debugger; return createCompilerDiagnostic(Diagnostics.Plugin_0_failed_while_executing_the_1_hook_Colon_2, compilerPlugin.name, hook, error.stack || error.message || error.toString()); } }