Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ export const tsTsxJsJsxRegex = /\.([cm]?[tj]s|[tj]sx)$/i;
export const jsJsx = /\.([cm]?js|jsx)$/i;
export const jsJsxMap = /\.([cm]?js|jsx)\.map$/i;
export const jsonRegex = /\.json$/i;

/** Stands in for `containingFile` in a custom `resolveModuleName` call. */
export const unknownContainingFileName = '__ts-loader-unknown-file__.ts';
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,9 +374,9 @@ const validLoaderOptions: ValidLoaderOptions[] = [
'reportFiles',
'allowTsInNodeModules',
'projectReferences',
// Accepted for backwards compatibility but inert: the TypeScript 7 API has
// no custom resolver hook. See test/execution-tests/3.0.1_resolveModuleName.
'resolveModuleName',
// Accepted for backwards compatibility but inert: the TypeScript 7 API has
// no type-reference-directive resolver hook.
'resolveTypeReferenceDirective',
'useCaseSensitiveFileNames',
];
Expand Down
111 changes: 104 additions & 7 deletions src/typeScriptApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
CreateSnapshotParams,
Diagnostic,
EmitOutput,
ModuleResolver,
ParsedCommandLine,
Program,
Snapshot,
Expand All @@ -27,6 +28,7 @@ import {
} from './diagnostics';
import type {
LoaderOptions,
ParentModuleResolver,
PendingDeclarationFile,
SyntheticProgram,
TypeScriptInstance,
Expand Down Expand Up @@ -61,6 +63,7 @@ export function createTypeScriptInstance(
projectFileNamesCache: new Map(),
changedFilesThisBuild: new Set(),
watcherChangedFiles: undefined,
customResolveModuleName: loaderOptions.resolveModuleName,
};
}

Expand Down Expand Up @@ -752,7 +755,11 @@ function prepareSnapshotForFile(
const primaryProjectPath = typeScriptInstance.configFilePath;
const primaryProject = snapshot.getConfiguredProject(primaryProjectPath);

if (!forceSyntheticRoot && primaryProject?.program.getSourceFile(fileName)) {
if (
!forceSyntheticRoot &&
!typeScriptInstance.customResolveModuleName &&
primaryProject?.program.getSourceFile(fileName)
) {
return { snapshot };
}

Expand Down Expand Up @@ -811,7 +818,7 @@ function ensureOrphanProgram(

orphanRootFileNames.set(canonicalFileName, fileName);
const program = orphanProgramParams(typeScriptInstance, primaryConfig);
const configuration = JSON.stringify(program);
const configuration = serializeOrphanProgramParams(program);
if (orphanProgram) {
orphanProgram.configuration = configuration;
return {
Expand Down Expand Up @@ -848,7 +855,7 @@ function reconfigureOrphanProgramIfStale(
orphanProgram: SyntheticProgram,
): { snapshot: Snapshot; syntheticProgramId: SyntheticProjectId } {
const program = orphanProgramParams(typeScriptInstance, primaryConfig);
const configuration = JSON.stringify(program);
const configuration = serializeOrphanProgramParams(program);
if (orphanProgram.configuration === configuration) {
return {
snapshot: typeScriptInstance.snapshot!,
Expand Down Expand Up @@ -879,10 +886,97 @@ function orphanProgramParams(
]),
].sort(),
compilerOptions: primaryConfig.options,
options: { projectReferences: primaryConfig.projectReferences },
options: {
projectReferences: primaryConfig.projectReferences,
moduleResolver: getCustomModuleResolver(
typeScriptInstance,
primaryConfig.options,
),
},
};
}

/** `ModuleResolver` holds the API client, so it's serialized by id. */
function serializeOrphanProgramParams(
program: ReturnType<typeof orphanProgramParams>,
): string {
return JSON.stringify({
...program,
options: {
...program.options,
moduleResolver: program.options.moduleResolver?.id,
},
});
}

/**
* Adapts the `resolveModuleName` loader option (classic ts-loader's
* signature) to the API's resolver callback. Only synthetic programs accept
* a resolver, so with this option set every file compiles in the orphan
* program (see prepareSnapshotForFile). Recreated when compiler options
* change, since the default resolution behind `parentResolver` depends on them.
*/
function getCustomModuleResolver(
typeScriptInstance: TypeScriptInstance,
compilerOptions: CompilerOptions,
): ModuleResolver | undefined {
const customResolveModuleName = typeScriptInstance.customResolveModuleName;
if (!customResolveModuleName) {
return undefined;
}
const configuration = JSON.stringify(compilerOptions);
if (
typeScriptInstance.customModuleResolver?.configuration === configuration
) {
return typeScriptInstance.customModuleResolver.resolver;
}

// A resolver replaced here may still back the current snapshot's program,
// so it isn't disposed; api.close() releases it.
const { api } = typeScriptInstance;
const defaultResolver = api.createModuleResolver(compilerOptions);
const resolver = api.createModuleResolver(compilerOptions, {
resolveModuleName(
moduleName,
containingDirectory,
resolutionMode,
options,
) {
const parentResolver: ParentModuleResolver = (
parentModuleName,
containingFile,
) =>
defaultResolver.resolveModuleName(
parentModuleName,
path.dirname(containingFile),
resolutionMode,
{ snapshot: options.snapshot },
);
// The API only says which directory the import is in, not which file.
const containingFile = path.join(
containingDirectory,
constants.unknownContainingFileName,
);
const resolvedModule = customResolveModuleName(
moduleName,
containingFile,
compilerOptions,
undefined,
parentResolver,
)?.resolvedModule;
return resolvedModule
? {
resolvedFileName: resolvedModule.resolvedFileName,
originalPath: resolvedModule.originalPath,
packageId: resolvedModule.packageId,
}
: undefined;
},
});
typeScriptInstance.customModuleResolver = { resolver, configuration };
return resolver;
}

/**
* The name used for API calls. The API rejects a file with an unrecognized
* extension (e.g. a Vue SFC's `<script>` block) since it can't infer a
Expand Down Expand Up @@ -1368,9 +1462,12 @@ export function recheckAllTransitiveDependants(
);

for (const [comparableFileName, fileName] of filesToCheck) {
const program = primaryFileNames.has(comparableFileName)
? primaryProgram
: graphProgram;
// With a custom resolver, only the orphan program resolves imports correctly.
const program =
primaryFileNames.has(comparableFileName) &&
!typeScriptInstance.customResolveModuleName
? primaryProgram
: graphProgram;
const diagnostics = dedupeDiagnostics([
...program.getSyntacticDiagnostics(fileName),
...program.getSemanticDiagnostics(fileName),
Expand Down
27 changes: 26 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { Colors } from './colors';
import type {
API,
CompilerOptions,
Diagnostic,
ModuleResolver,
ParsedCommandLine,
ResolveModuleNameResult,
Snapshot,
SyntheticProjectId,
} from 'typescript/sync';
Expand Down Expand Up @@ -56,11 +59,30 @@ export interface LoaderOptions {
string | ((program: unknown, getProgram: () => unknown) => unknown);
allowTsInNodeModules: boolean;
projectReferences?: boolean;
resolveModuleName?: (...args: unknown[]) => unknown;
resolveModuleName?: CustomResolveModuleName;
resolveTypeReferenceDirective?: (...args: unknown[]) => unknown;
useCaseSensitiveFileNames?: boolean;
}

export interface ParentModuleResolver {
(moduleName: string, containingFile: string): ResolveModuleNameResult;
}

/**
* Classic ts-loader's signature. `containingFile` is a placeholder in the
* right directory (the API doesn't say which file is importing), and there's
* no `compilerHost`.
*/
interface CustomResolveModuleName {
(
moduleName: string,
containingFile: string,
compilerOptions: CompilerOptions,
compilerHost: undefined,
parentResolver: ParentModuleResolver,
): ResolveModuleNameResult | undefined;
}

export interface LoaderOptionsCache {
[name: string]: WeakMap<LoaderOptions, LoaderOptions>;
}
Expand Down Expand Up @@ -138,6 +160,9 @@ export interface TypeScriptInstance {
* unbundled project file is rechecked then.
*/
watcherChangedFiles: Set<string> | undefined;
customResolveModuleName?: CustomResolveModuleName;
/** See getCustomModuleResolver. */
customModuleResolver?: { resolver: ModuleResolver; configuration: string };
}

interface PendingTypeScriptDiagnostics {
Expand Down
11 changes: 11 additions & 0 deletions test/comparison-tests/resolveModuleName/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { HelloWorld } from 'foo';
import { makeHello } from 'baz';
import { greeting } from './greeting';

export const def: HelloWorld = makeHello(1, 2);

// Deliberate type error: only reported this way if 'baz' and 'foo' resolved
// through the custom resolver - otherwise it's a TS2307 "Cannot find module".
export const wrong: string = def.hello;

console.log(greeting, def, wrong);
3 changes: 3 additions & 0 deletions test/comparison-tests/resolveModuleName/baz-pkg/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { HelloWorld } from 'foo';

export declare function makeHello(hello: number, world: number): HelloWorld;
3 changes: 3 additions & 0 deletions test/comparison-tests/resolveModuleName/baz-pkg/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
exports.makeHello = function (hello, world) {
return { hello: hello, world: world };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({

/***/ "./app.ts":
/*!****************!*\
!*** ./app.ts ***!
\****************/
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {

"use strict";
eval("{\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nexports.wrong = exports.def = void 0;\nconst baz_1 = __webpack_require__(/*! baz */ \"./baz-pkg/index.js\");\nconst greeting_1 = __webpack_require__(/*! ./greeting */ \"./greeting.ts\");\nexports.def = (0, baz_1.makeHello)(1, 2);\n// Deliberate type error: only reported this way if 'baz' and 'foo' resolved\n// through the custom resolver - otherwise it's a TS2307 \"Cannot find module\".\nexports.wrong = exports.def.hello;\nconsole.log(greeting_1.greeting, exports.def, exports.wrong);\n\n\n//# sourceURL=webpack:///./app.ts?\n}");

/***/ }),

/***/ "./baz-pkg/index.js":
/*!**************************!*\
!*** ./baz-pkg/index.js ***!
\**************************/
/***/ ((__unused_webpack_module, exports) => {

eval("{exports.makeHello = function (hello, world) {\n return { hello: hello, world: world };\n};\n\n\n//# sourceURL=webpack:///./baz-pkg/index.js?\n}");

/***/ }),

/***/ "./greeting.ts":
/*!*********************!*\
!*** ./greeting.ts ***!
\*********************/
/***/ ((__unused_webpack_module, exports) => {

"use strict";
eval("{\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nexports.greeting = void 0;\nexports.greeting = 'hello';\n\n\n//# sourceURL=webpack:///./greeting.ts?\n}");

/***/ })

/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module is referenced by other modules so it can't be inlined
/******/ var __webpack_exports__ = __webpack_require__("./app.ts");
/******/
/******/ })()
;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
asset bundle.js 3.34 KiB [emitted] (name: main)
./app.ts 482 bytes [built] [code generated]
./baz-pkg/index.js 90 bytes [built] [code generated]
./greeting.ts 132 bytes [built] [code generated]
webpack compiled successfully
Loading
Loading