diff --git a/src/index.ts b/src/index.ts index 45b95a5f1..a2070c664 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ import { WebpackLoaderCallback, WebpackLoaderContext, } from './interfaces'; +import { installTransformers } from './transformers'; import { appendSuffixesIfMatch, arrify, @@ -36,6 +37,7 @@ function loader(this: WebpackLoaderContext, contents: string) { this.cacheable && this.cacheable(); const callback = this.async() as WebpackLoaderCallback; const options = getLoaderOptions(this); + installTransformers(options); const instanceOrError = getTypeScriptInstance(options, this); if (instanceOrError.error !== undefined) { @@ -43,7 +45,7 @@ function loader(this: WebpackLoaderContext, contents: string) { return; } const instance = instanceOrError.instance!; - buildSolutionReferences(instance, this); + buildSolutionReferences(instance, this); // Warning: references are built before hooks are set in initializeInstance successLoader(this, contents, callback, instance); } @@ -59,14 +61,14 @@ function successLoader( const filePath = instance.loaderOptions.appendTsSuffixTo.length > 0 || - instance.loaderOptions.appendTsxSuffixTo.length > 0 + instance.loaderOptions.appendTsxSuffixTo.length > 0 ? appendSuffixesIfMatch( - { - '.ts': instance.loaderOptions.appendTsSuffixTo, - '.tsx': instance.loaderOptions.appendTsxSuffixTo, - }, - rawFilePath - ) + { + '.ts': instance.loaderOptions.appendTsSuffixTo, + '.tsx': instance.loaderOptions.appendTsxSuffixTo, + }, + rawFilePath + ) : rawFilePath; const fileVersion = updateFileInCache( @@ -107,10 +109,10 @@ function makeSourceMapAndFinish( ? ' The most common cause for this is having errors when building referenced projects.' : !instance.loaderOptions.allowTsInNodeModules && filePath.indexOf('node_modules') !== -1 - ? ' By default, ts-loader will not compile .ts files in node_modules.\n' + + ? ' By default, ts-loader will not compile .ts files in node_modules.\n' + 'You should not need to recompile .ts files there, but if you really want to, use the allowTsInNodeModules option.\n' + 'See: https://github.com/Microsoft/TypeScript/issues/12358' - : ''; + : ''; callback( new Error( @@ -272,7 +274,6 @@ function makeLoaderOptions(instanceName: string, loaderOptions: LoaderOptions) { compilerOptions: {}, appendTsSuffixTo: [], appendTsxSuffixTo: [], - transformers: {}, happyPackMode: false, colors: true, onlyCompileBundledFiles: false, @@ -426,7 +427,7 @@ function getEmit( // the real dependency that webpack should watch is the JS output file. addDependency( getInputFileNameFromOutput(instance, path.resolve(resolvedFileName)) || - originalFileName + originalFileName ); } } @@ -439,16 +440,16 @@ function getEmit( '@' + (isReferencedFile(instance, defFilePath) ? instance - .solutionBuilderHost!.getInputFileStamp(defFilePath) - .toString() + .solutionBuilderHost!.getInputFileStamp(defFilePath) + .toString() : ( - instance.files.get(instance.filePathKeyMapper(defFilePath)) || - instance.otherFiles.get( - instance.filePathKeyMapper(defFilePath) - ) || { - version: '?', - } - ).version) + instance.files.get(instance.filePathKeyMapper(defFilePath)) || + instance.otherFiles.get( + instance.filePathKeyMapper(defFilePath) + ) || { + version: '?', + } + ).version) ); return getOutputAndSourceMapFromOutputFiles(outputFiles); @@ -602,7 +603,6 @@ function getTranspilationEmit( diagnostics, } = instance.compiler.transpileModule(contents, { compilerOptions: { ...instance.compilerOptions, rootDir: undefined }, - transformers: instance.transformers, reportDiagnostics: true, fileName, }); @@ -658,5 +658,5 @@ export = loader; // eslint-disable-next-line @typescript-eslint/no-namespace namespace loader { // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface Options extends LoaderOptions {} + export interface Options extends LoaderOptions { } } diff --git a/src/instances.ts b/src/instances.ts index d554d195e..a2a118ca6 100644 --- a/src/instances.ts +++ b/src/instances.ts @@ -108,8 +108,8 @@ function createFilePathKeyMapper( const filePathKey = pathResolve(filePath); cachedPath = fileNameLowerCaseRegExp.test(filePathKey) ? (filePathKey.replace(fileNameLowerCaseRegExp, ch => - ch.toLowerCase() - ) as FilePathKey) + ch.toLowerCase() + ) as FilePathKey) : filePathKey; filePathMapperCache.set(filePath, cachedPath); } @@ -197,15 +197,15 @@ function successfulTypeScriptInstance( const appendTsTsxSuffixesIfRequired = loaderOptions.appendTsSuffixTo.length > 0 || - loaderOptions.appendTsxSuffixTo.length > 0 + loaderOptions.appendTsxSuffixTo.length > 0 ? (filePath: string) => - appendSuffixesIfMatch( - { - '.ts': loaderOptions.appendTsSuffixTo, - '.tsx': loaderOptions.appendTsxSuffixTo, - }, - filePath - ) + appendSuffixesIfMatch( + { + '.ts': loaderOptions.appendTsSuffixTo, + '.tsx': loaderOptions.appendTsxSuffixTo, + }, + filePath + ) : (filePath: string) => filePath; if (loaderOptions.transpileOnly) { @@ -222,7 +222,6 @@ function successfulTypeScriptInstance( version: 0, program: undefined, // temporary, to be set later dependencyGraph: new Map(), - transformers: {} as typescript.CustomTransformers, // this is only set temporarily, custom transformers are created further down colors, initialSetupPending: true, reportTranspileErrors: true, @@ -245,8 +244,8 @@ function successfulTypeScriptInstance( try { const filesToLoad = loaderOptions.onlyCompileBundledFiles ? configParseResult.fileNames.filter(fileName => - dtsDtsxOrDtsDtsxMapRegex.test(fileName) - ) + dtsDtsxOrDtsDtsxMapRegex.test(fileName) + ) : configParseResult.fileNames; filesToLoad.forEach(filePath => { normalizedFilePath = path.normalize(filePath); @@ -279,7 +278,6 @@ function successfulTypeScriptInstance( otherFiles, languageService: null, version: 0, - transformers: {} as typescript.CustomTransformers, // this is only set temporarily, custom transformers are created further down dependencyGraph: new Map(), colors, initialSetupPending: true, @@ -345,42 +343,16 @@ export function initializeInstance( instance.initialSetupPending = false; - // same strategy as https://github.com/s-panferov/awesome-typescript-loader/pull/531/files - let { getCustomTransformers: customerTransformers } = instance.loaderOptions; - let getCustomTransformers = Function.prototype; - - if (typeof customerTransformers === 'function') { - getCustomTransformers = customerTransformers; - } else if (typeof customerTransformers === 'string') { - try { - customerTransformers = require(customerTransformers); - } catch (err) { - throw new Error( - `Failed to load customTransformers from "${instance.loaderOptions.getCustomTransformers}": ${err.message}` - ); - } - - if (typeof customerTransformers !== 'function') { - throw new Error( - `Custom transformers in "${ - instance.loaderOptions.getCustomTransformers - }" should export a function, got ${typeof getCustomTransformers}` - ); - } - getCustomTransformers = customerTransformers; - } - if (instance.loaderOptions.transpileOnly) { - const program = (instance.program = + instance.program = instance.configParseResult.projectReferences !== undefined ? instance.compiler.createProgram({ - rootNames: instance.configParseResult.fileNames, - options: instance.configParseResult.options, - projectReferences: instance.configParseResult.projectReferences, - }) - : instance.compiler.createProgram([], instance.compilerOptions)); + rootNames: instance.configParseResult.fileNames, + options: instance.configParseResult.options, + projectReferences: instance.configParseResult.projectReferences, + }) + : instance.compiler.createProgram([], instance.compilerOptions); - instance.transformers = getCustomTransformers(program); // Setup watch run for solution building if (instance.solutionBuilderHost) { addAssetHooks(loader, instance); @@ -412,7 +384,6 @@ export function initializeInstance( instance.builderProgram = instance.watchOfFilesAndCompilerOptions.getProgram(); instance.program = instance.builderProgram.getProgram(); - instance.transformers = getCustomTransformers(instance.program); } else { instance.servicesHost = makeServicesHost( getScriptRegexp(instance), @@ -425,10 +396,6 @@ export function initializeInstance( instance.servicesHost, instance.compiler.createDocumentRegistry() ); - - instance.transformers = getCustomTransformers( - instance.languageService!.getProgram() - ); } addAssetHooks(loader, instance); @@ -504,6 +471,7 @@ export function buildSolutionReferences( instance.configParseResult.projectReferences!.map(ref => ref.path), { verbose: true } ); + solutionBuilder.build(); instance.solutionBuilderHost.ensureAllReferenceTimestamps(); instancesBySolutionBuilderConfigs.set( @@ -580,13 +548,13 @@ function getOutputPathWithoutChangingExt( ) { return outputDir ? (instance.compiler as any).resolvePath( - outputDir, - (instance.compiler as any).getRelativePathFromDirectory( - rootDirOfOptions(instance, configFile), - inputFileName, - ignoreCase - ) + outputDir, + (instance.compiler as any).getRelativePathFromDirectory( + rootDirOfOptions(instance, configFile), + inputFileName, + ignoreCase ) + ) : inputFileName; } @@ -612,8 +580,8 @@ function getOutputJSFileName( ? '.json' : fileExtensionIs(inputFileName, '.tsx') && configFile.options.jsx === instance.compiler.JsxEmit.Preserve - ? '.jsx' - : '.js' + ? '.jsx' + : '.js' ); return !isJsonFile || (instance.compiler as any).comparePaths( @@ -743,8 +711,7 @@ export function getEmitFromWatchHost(instance: TSInstance, filePath?: string) { const result = builderProgram.emitNextAffectedFile( writeFile, /*cancellationToken*/ undefined, - /*emitOnlyDtsFiles*/ false, - instance.transformers + /*emitOnlyDtsFiles*/ false ); if (!result) { break; @@ -792,8 +759,7 @@ export function getEmitOutput(instance: TSInstance, filePath: string) { sourceFile, writeFile, /*cancellationToken*/ undefined, - /*emitOnlyDtsFiles*/ false, - instance.transformers + /*emitOnlyDtsFiles*/ false ); return outputFiles; } else { diff --git a/src/interfaces.ts b/src/interfaces.ts index 41077a6b9..7c0cc124f 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -39,7 +39,7 @@ export interface CacheableHost extends HostMayBeCacheable { export interface ModuleResolutionHostMayBeCacheable extends typescript.ModuleResolutionHost, - HostMayBeCacheable { + HostMayBeCacheable { readFile(filePath: string, encoding?: string): string | undefined; trace: NonNullable; directoryExists: NonNullable< @@ -65,11 +65,11 @@ export interface ModuleResolutionHostMayBeCacheable export interface ServiceHostWhichMayBeCacheable extends typescript.LanguageServiceHost, - HostMayBeCacheable {} + HostMayBeCacheable { } export interface WatchHost extends typescript.WatchCompilerHostOfFilesAndCompilerOptions, - HostMayBeCacheable { + HostMayBeCacheable { invokeFileWatcher: WatchFactory['invokeFileWatcher']; updateRootFileNames(): void; outputFiles: Map; @@ -104,7 +104,7 @@ export type FilePathKey = string & { __filePathKeyBrand: any }; export interface SolutionBuilderWithWatchHost extends typescript.SolutionBuilderWithWatchHost, - WatchFactory { + WatchFactory { diagnostics: SolutionDiagnostics; writtenFiles: typescript.OutputFile[]; configFileInfo: Map; @@ -209,7 +209,6 @@ export interface TSInstance { version: number; dependencyGraph: DependencyGraph; filesWithErrors?: TSFiles; - transformers: typescript.CustomTransformers; colors: Chalk; otherFiles: TSFiles; @@ -282,10 +281,10 @@ export interface LoaderOptions { appendTsxSuffixTo: (RegExp | string)[]; happyPackMode: boolean; getCustomTransformers: - | string - | (( - program: typescript.Program - ) => typescript.CustomTransformers | undefined); + | string + | (( + program: typescript.Program + ) => typescript.CustomTransformers | undefined); experimentalWatchApi: boolean; allowTsInNodeModules: boolean; experimentalFileCaching: boolean; diff --git a/src/servicesHost.ts b/src/servicesHost.ts index 383d437a2..445e1db63 100644 --- a/src/servicesHost.ts +++ b/src/servicesHost.ts @@ -52,8 +52,8 @@ function makeResolversAndModuleResolutionHost( compilerOptions.newLine === constants.CarriageReturnLineFeedCode ? constants.CarriageReturnLineFeed : compilerOptions.newLine === constants.LineFeedCode - ? constants.LineFeed - : constants.EOL; + ? constants.LineFeed + : constants.EOL; // loader.context seems to work fine on Linux / Mac regardless causes problems for @types resolution on Windows for TypeScript < 2.3 const getCurrentDirectory = () => loader.context; @@ -229,9 +229,7 @@ export function makeServicesHost( // used for (/// ) see https://github.com/Realytics/fork-ts-checker-webpack-plugin/pull/250#issuecomment-485061329 resolveTypeReferenceDirectives, - resolveModuleNames, - - getCustomTransformers: () => instance.transformers, + resolveModuleNames }; return servicesHost; @@ -980,12 +978,12 @@ export function makeSolutionBuilderHost( configInfo.tsbuildInfoFile = instance.compiler .getTsBuildInfoEmitOutputFilePath ? instance.compiler.getTsBuildInfoEmitOutputFilePath( - configInfo.config.options - ) + configInfo.config.options + ) : // before api - (instance.compiler as any).getOutputPathForBuildInfo( - configInfo.config.options - ); + (instance.compiler as any).getOutputPathForBuildInfo( + configInfo.config.options + ); } function getOutputFileAndKeyFromReferencedProject( @@ -994,9 +992,9 @@ export function makeSolutionBuilderHost( const outputFile = ensureOutputFile(outputFileName); return outputFile !== undefined ? { - key: getOutputFileKeyFromReferencedProject(outputFileName)!, - outputFile, - } + key: getOutputFileKeyFromReferencedProject(outputFileName)!, + outputFile, + } : undefined; } @@ -1076,10 +1074,10 @@ export function makeSolutionBuilderHost( const text = compiler.sys.readFile(outputFileName); return text !== undefined ? { - name: outputFileName, - text, - writeByteOrderMark: false, - } + name: outputFileName, + text, + writeByteOrderMark: false, + } : undefined; } @@ -1125,8 +1123,8 @@ export function makeSolutionBuilderHost( existing == missingFileModifiedTime ? compiler.FileWatcherEventKind.Created : newTime === missingFileModifiedTime - ? compiler.FileWatcherEventKind.Deleted - : compiler.FileWatcherEventKind.Changed; + ? compiler.FileWatcherEventKind.Deleted + : compiler.FileWatcherEventKind.Changed; solutionBuilderHost.invokeFileWatcher(fileName, eventKind); } } @@ -1239,7 +1237,7 @@ function resolveModule( resolutionResult = { resolvedFileName, originalFileName }; } } - } catch (e) {} + } catch (e) { } const tsResolution = resolveModuleName( moduleName, @@ -1258,7 +1256,7 @@ function resolveModule( return resolutionResult! === undefined || resolutionResult.resolvedFileName === - tsResolutionResult.resolvedFileName || + tsResolutionResult.resolvedFileName || isJsImplementationOfTypings(resolutionResult!, tsResolutionResult) ? tsResolutionResult : resolutionResult!; diff --git a/src/transformers.ts b/src/transformers.ts new file mode 100644 index 000000000..66863a3fb --- /dev/null +++ b/src/transformers.ts @@ -0,0 +1,140 @@ +import * as ts from 'typescript'; +import { LoaderOptions } from './interfaces'; + +export function getCustomTransformers( + loaderOptions: LoaderOptions, + program: ts.Program +) { + // same strategy as https://github.com/s-panferov/awesome-typescript-loader/pull/531/files + let { getCustomTransformers: customerTransformers } = loaderOptions; + let getCustomTransformers = Function.prototype; + + if (typeof customerTransformers === 'function') { + getCustomTransformers = customerTransformers; + } else if (typeof customerTransformers === 'string') { + try { + customerTransformers = require(customerTransformers); + } catch (err) { + throw new Error( + `Failed to load customTransformers from "${loaderOptions.getCustomTransformers}": ${err.message}` + ); + } + + if (typeof customerTransformers !== 'function') { + throw new Error( + `Custom transformers in "${loaderOptions.getCustomTransformers + }" should export a function, got ${typeof getCustomTransformers}` + ); + } + getCustomTransformers = customerTransformers; + } + return getCustomTransformers(program); +} + +/** + * @returns a ts.CustomTransformers merged from tr1 and tr2 (does not check for duplicates) + */ +function mergeTransformers(tr1: ts.CustomTransformers | undefined, tr2: ts.CustomTransformers | undefined): ts.CustomTransformers | undefined { + if (tr1) { + const result: ts.CustomTransformers = { + after: [], + before: [], + afterDeclarations: [] + }; + //add tr1 values in result + if (tr1.after) { + result.after!.push(...tr1.after); + } + if (tr1.before) { + result.before!.push(...tr1.before); + } + if (tr1.afterDeclarations) { + result.afterDeclarations!.push(...tr1.afterDeclarations); + } + + //add tr2 values + if (tr2?.after) { + result.after!.push(...tr2.after); + } + if (tr2?.before) { + result.before!.push(...tr2.before); + } + if (tr2?.afterDeclarations) { + result.afterDeclarations!.push(...tr2.afterDeclarations); + } + + return result; + } else { + return tr2; + } +} + +/** + * Patches typescript program to pass custom transformers to the program.emit function. + * In 2021, this is the only way since it is not possible to pass transformers to SolutionBuilder + * SolutionBuilder is used when watch is enabled + */ +let patched = false; +export function installTransformers(loaderOptions: LoaderOptions) { + if (loaderOptions.getCustomTransformers === undefined) { + return; //no need to patch if there is no transformer + } + if (patched === true) { + return; //do not patch twice + } + patched = true; + const originalCreateProgram = ts.createProgram; + + //override createProgram in order to override emit function and use overrideTransformers + function createProgram( + rootNamesOrOptions: ReadonlyArray | ts.CreateProgramOptions, + options?: ts.CompilerOptions, + host?: ts.CompilerHost, + oldProgram?: ts.Program, + configFileParsingDiagnostics?: ReadonlyArray + ): ts.Program { + let program: ts.Program; + if (Array.isArray(rootNamesOrOptions)) { + program = originalCreateProgram( + rootNamesOrOptions as ReadonlyArray, + options!, + host, + oldProgram, + configFileParsingDiagnostics + ); + } else { + program = originalCreateProgram( + rootNamesOrOptions as ts.CreateProgramOptions + ); + } + + const transformers: ts.CustomTransformers | undefined = getCustomTransformers(loaderOptions, program); + + const originalEmit = program.emit; + + function newEmit( + targetSourceFile?: ts.SourceFile, + writeFile?: ts.WriteFileCallback, + cancellationToken?: ts.CancellationToken, + emitOnlyDtsFiles?: boolean, + customTransformers?: ts.CustomTransformers + ): ts.EmitResult { + /* Invoke TS emit */ + const result: ts.EmitResult = originalEmit( + targetSourceFile, + writeFile, + cancellationToken, + emitOnlyDtsFiles, + mergeTransformers(transformers, customTransformers) + ); + + return result; + } + + program.emit = newEmit; + + return program; + } + + Object.assign(ts, { createProgram }); +}