diff --git a/src/transformer.ts b/src/transformer.ts index 68c156ae..c4074d9d 100755 --- a/src/transformer.ts +++ b/src/transformer.ts @@ -2,11 +2,12 @@ import {} from "ts-expose-internals"; import path from "path"; import ts from "typescript"; -import { cast, getImplicitExtensions } from "./utils"; +import { cast } from "./utils"; import { TsTransformPathsConfig, TsTransformPathsContext, TypeScriptThree, VisitorContext } from "./types"; import { nodeVisitor } from "./visitor"; import { createHarmonyFactory } from "./utils/harmony-factory"; import { Minimatch } from "minimatch"; +import { createParsedCommandLineForProgram } from "./utils/ts-helpers"; /* ****************************************************************************************************************** * * Transformer @@ -20,27 +21,31 @@ export default function transformer( if (!tsInstance) tsInstance = ts; const compilerOptions = program.getCompilerOptions(); - const implicitExtensions = getImplicitExtensions(compilerOptions); const rootDirs = compilerOptions.rootDirs?.filter(path.isAbsolute); return (transformationContext: ts.TransformationContext) => { + const pathsBasePath = compilerOptions.pathsBasePath ?? compilerOptions.baseUrl; + + if (!pathsBasePath || !compilerOptions.paths) return (sourceFile: ts.SourceFile) => sourceFile; + const tsTransformPathsContext: TsTransformPathsContext = { compilerOptions, config, elisionMap: new Map(), tsFactory: transformationContext.factory, - implicitExtensions, program, rootDirs, transformationContext, tsInstance, + pathsBasePath, + getCanonicalFileName: tsInstance.createGetCanonicalFileName(tsInstance.sys.useCaseSensitiveFileNames), tsThreeInstance: cast(tsInstance), excludeMatchers: config.exclude?.map((globPattern) => new Minimatch(globPattern, { matchBase: true })), + parsedCommandLine: createParsedCommandLineForProgram(tsInstance, program), + outputFileNamesCache: new Map(), }; return (sourceFile: ts.SourceFile) => { - if (!compilerOptions.baseUrl || !compilerOptions.paths) return sourceFile; - const visitorContext: VisitorContext = { ...tsTransformPathsContext, sourceFile, diff --git a/src/types.ts b/src/types.ts index 35d2397a..9dcd3009 100755 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ import tsThree from "./declarations/typescript3"; -import ts, { CompilerOptions } from "typescript"; +import ts, { CompilerOptions, GetCanonicalFileName, ParsedCommandLine } from 'typescript'; import { PluginConfig } from "ts-patch"; import { HarmonyFactory } from "./utils/harmony-factory"; import { IMinimatch } from "minimatch"; @@ -42,12 +42,15 @@ export interface TsTransformPathsContext { readonly tsFactory?: ts.NodeFactory; readonly program: ts.Program | tsThree.Program; readonly config: TsTransformPathsConfig; - readonly implicitExtensions: readonly string[]; readonly compilerOptions: CompilerOptions; readonly elisionMap: Map>; readonly transformationContext: ts.TransformationContext; readonly rootDirs?: string[]; readonly excludeMatchers: IMinimatch[] | undefined; + readonly parsedCommandLine: ParsedCommandLine; + readonly outputFileNamesCache: Map; + readonly pathsBasePath: string; + readonly getCanonicalFileName: GetCanonicalFileName; } export interface VisitorContext extends TsTransformPathsContext { diff --git a/src/utils/general-utils.ts b/src/utils/general-utils.ts index bcda80ba..5f4729aa 100755 --- a/src/utils/general-utils.ts +++ b/src/utils/general-utils.ts @@ -1,4 +1,3 @@ -import ts from "typescript"; import url from "url"; import path from "path"; @@ -7,21 +6,8 @@ import path from "path"; * ****************************************************************************************************************** */ export const isURL = (s: string): boolean => !!s && (!!url.parse(s).host || !!url.parse(s).hostname); -export const isBaseDir = (base: string, dir: string) => path.relative(base, dir)?.[0] !== "."; export const cast = (v: any): T => v; - -/** - * @returns Array of implicit extensions, given CompilerOptions - */ -export function getImplicitExtensions(options: ts.CompilerOptions) { - let res: string[] = [".ts", ".d.ts"]; - - let { allowJs, jsx } = options; - const allowJsx = !!jsx && (jsx !== ts.JsxEmit.None); - - allowJs && res.push(".js", ".cjs", ".mjs"); - allowJsx && res.push(".tsx"); - allowJs && allowJsx && res.push(".jsx"); - - return res; -} +export const isBaseDir = (baseDir: string, testDir: string): boolean => { + const relative = path.relative(baseDir, testDir); + return relative ? !relative.startsWith("..") && !path.isAbsolute(relative) : true; +}; diff --git a/src/utils/resolve-module-name.ts b/src/utils/resolve-module-name.ts new file mode 100755 index 00000000..efc0d9b6 --- /dev/null +++ b/src/utils/resolve-module-name.ts @@ -0,0 +1,151 @@ +import { VisitorContext } from "../types"; +import { isBaseDir, isURL } from "./general-utils"; +import * as path from "path"; +import { removeFileExtension, removeSuffix, ResolvedModuleFull } from "typescript"; +import { getOutputFile } from "./ts-helpers"; + +/* ****************************************************************************************************************** */ +// region: Types +/* ****************************************************************************************************************** */ + +export interface ResolvedModule { + /** + * Absolute path to resolved module + */ + resolvedPath: string | undefined; + /** + * Output path + */ + outputPath: string; + /** + * Resolved to URL + */ + isURL: boolean; +} + +enum IndexType { + NonIndex, + Explicit, + Implicit, + ImplicitPackage, +} + +// endregion + +/* ****************************************************************************************************************** */ +// region: Helpers +/* ****************************************************************************************************************** */ + +function getPathDetail(moduleName: string, resolvedModule: ResolvedModuleFull) { + let resolvedFileName = resolvedModule.originalPath ?? resolvedModule.resolvedFileName; + const implicitPackageIndex = resolvedModule.packageId?.subModuleName; + + const resolvedDir = implicitPackageIndex + ? removeSuffix(resolvedFileName, `/${implicitPackageIndex}`) + : path.dirname(resolvedFileName); + const resolvedBaseName = implicitPackageIndex ? void 0 : path.basename(resolvedFileName); + const resolvedBaseNameNoExtension = resolvedBaseName && removeFileExtension(resolvedBaseName); + const resolvedExtName = resolvedBaseName && path.extname(resolvedFileName); + + const baseName = !implicitPackageIndex ? path.basename(moduleName) : void 0; + const baseNameNoExtension = baseName && removeFileExtension(baseName); + const extName = baseName && path.extname(moduleName); + + // prettier-ignore + const indexType = + implicitPackageIndex ? IndexType.ImplicitPackage : + baseNameNoExtension === 'index' && resolvedBaseNameNoExtension === 'index' ? IndexType.Explicit : + baseNameNoExtension !== 'index' && resolvedBaseNameNoExtension === 'index' ? IndexType.Implicit : + IndexType.NonIndex; + + return { + baseName, + baseNameNoExtension, + extName, + resolvedBaseName, + resolvedBaseNameNoExtension, + resolvedExtName, + resolvedDir, + indexType, + implicitPackageIndex, + resolvedFileName, + }; +} + +// endregion + +/* ****************************************************************************************************************** */ +// region: Utils +/* ****************************************************************************************************************** */ + +/** + * Resolve a module name + */ +export function resolveModuleName(context: VisitorContext, moduleName: string): ResolvedModule | undefined { + const { tsInstance, compilerOptions, sourceFile, config, rootDirs } = context; + const { removeSuffix } = tsInstance; + + // Attempt to resolve with TS Compiler API + const { resolvedModule, failedLookupLocations } = tsInstance.resolveModuleName( + moduleName, + sourceFile.fileName, + compilerOptions, + tsInstance.sys + ); + + // Handle non-resolvable module + if (!resolvedModule) { + const maybeURL = failedLookupLocations[0]; + if (!isURL(maybeURL)) return void 0; + return { + isURL: true, + resolvedPath: void 0, + outputPath: maybeURL, + }; + } + + const { + indexType, + resolvedBaseNameNoExtension, + resolvedFileName, + implicitPackageIndex, + extName, + resolvedDir, + } = getPathDetail(moduleName, resolvedModule); + + /* Determine output filename */ + let outputBaseName = resolvedBaseNameNoExtension ?? ""; + + if (indexType === IndexType.Implicit) outputBaseName = removeSuffix(outputBaseName, "/index"); + if (outputBaseName && extName) outputBaseName = `${outputBaseName}${extName}`; + + /* Determine output dir */ + let srcFileOutputDir = path.dirname(getOutputFile(context, sourceFile.fileName)); + let moduleFileOutputDir = implicitPackageIndex ? resolvedDir : path.dirname(getOutputFile(context, resolvedFileName)); + + // Handle rootDirs remapping + if (config.useRootDirs && rootDirs) { + let fileRootDir = ""; + let moduleRootDir = ""; + for (const rootDir of rootDirs) { + if (isBaseDir(rootDir, moduleFileOutputDir) && rootDir.length > moduleRootDir.length) moduleRootDir = rootDir; + if (isBaseDir(rootDir, srcFileOutputDir) && rootDir.length > fileRootDir.length) fileRootDir = rootDir; + } + + /* Remove base dirs to make relative to root */ + if (fileRootDir && moduleRootDir) { + srcFileOutputDir = path.relative(fileRootDir, srcFileOutputDir); + moduleFileOutputDir = path.relative(moduleRootDir, moduleFileOutputDir); + } + } + + const outputDir = path.relative(srcFileOutputDir, moduleFileOutputDir); + + /* Compose final output path */ + let outputPath = tsInstance.normalizePath(path.join(outputDir, outputBaseName)); + outputPath = outputPath[0] === "." ? outputPath : `./${outputPath}`; + + return { isURL: false, outputPath, resolvedPath: resolvedFileName }; +} + +// endregion diff --git a/src/utils/resolve-path-update-node.ts b/src/utils/resolve-path-update-node.ts index c5bda7e1..ecba900b 100755 --- a/src/utils/resolve-path-update-node.ts +++ b/src/utils/resolve-path-update-node.ts @@ -1,16 +1,8 @@ import ts from "typescript"; -import tsThree from "../declarations/typescript3"; -import path from "path"; import { VisitorContext } from "../types"; -import { isBaseDir, isURL } from "./general-utils"; - -/* ****************************************************************************************************************** */ -// region: Config -/* ****************************************************************************************************************** */ - -const explicitExtensions = [".js", ".jsx", ".cjs", ".mjs"]; - -// endregion +import { isURL } from "./general-utils"; +import { isModulePathsMatch } from "./ts-helpers"; +import { resolveModuleName } from "./resolve-module-name"; /* ****************************************************************************************************************** */ // region: Node Updater Utility @@ -23,116 +15,44 @@ export function resolvePathAndUpdateNode( context: VisitorContext, node: ts.Node, moduleName: string, - updaterFn: (newPath: ts.StringLiteral) => ts.Node | tsThree.Node | undefined + updaterFn: (newPath: ts.StringLiteral) => ts.Node | undefined ): ts.Node | undefined { - const { sourceFile, compilerOptions, tsInstance, config, implicitExtensions, factory } = context; + const { sourceFile, tsInstance, factory } = context; + const { normalizePath } = tsInstance; + + /* Handle JSDoc statement tags */ const tags = getStatementTags(); // Skip if @no-transform-path specified - if (tags?.shouldSkip) return node; + if (tags.shouldSkip) return node; + + // Accommodate direct override via @transform-path tag + if (tags.overridePath) { + let transformedPath = !isURL(tags.overridePath) ? normalizePath(tags.overridePath) : tags.overridePath; + transformedPath = transformedPath[0] === "." || isURL(transformedPath) ? transformedPath : `./${transformedPath}`; + return updaterFn(factory.createStringLiteral(transformedPath)); + } - const resolutionResult = resolvePath(tags?.overridePath); + /* Resolve Module */ + // Skip if no paths match found + if (!isModulePathsMatch(context, moduleName)) return node; - // Skip if can't be resolved - if (!resolutionResult || !resolutionResult.outputPath) return node; + const res = resolveModuleName(context, moduleName); + if (!res) return void 0; - const { outputPath, filePath } = resolutionResult; + const { outputPath, resolvedPath } = res; - // Check if matches exclusion - if (filePath && context.excludeMatchers) - for (const matcher of context.excludeMatchers) if (matcher.match(filePath)) return node; + /* Skip if matches exclusion */ + if (context.excludeMatchers) + for (const matcher of context.excludeMatchers) + if (matcher.match(outputPath) || (resolvedPath && matcher.match(resolvedPath))) return node; - return updaterFn(factory.createStringLiteral(outputPath)) as ts.Node | undefined; + return updaterFn(factory.createStringLiteral(outputPath)); /* ********************************************************* * * Helpers * ********************************************************* */ - function resolvePath(overridePath: string | undefined): { outputPath: string; filePath?: string } | undefined { - /* Handle overridden path -- ie. @transform-path ../my/path) */ - if (overridePath) { - return { - outputPath: filePathToOutputPath(overridePath, path.extname(overridePath)), - filePath: overridePath, - }; - } - - /* Have Compiler API attempt to resolve */ - const { resolvedModule, failedLookupLocations } = tsInstance.resolveModuleName( - moduleName, - sourceFile.fileName, - compilerOptions, - tsInstance.sys - ); - - // No transform for node-modules - if (resolvedModule?.isExternalLibraryImport) return void 0; - - /* Handle non-resolvable module */ - if (!resolvedModule) { - const maybeURL = failedLookupLocations[0]; - if (!isURL(maybeURL)) return void 0; - return { outputPath: maybeURL }; - } - - /* Handle resolved module */ - const { extension, resolvedFileName } = resolvedModule; - return { - outputPath: filePathToOutputPath(resolvedFileName, extension), - filePath: resolvedFileName, - }; - } - - function filePathToOutputPath(filePath: string, extension: string | undefined) { - if (path.isAbsolute(filePath)) { - let sourceFileDir = tsInstance.normalizePath(path.dirname(sourceFile.fileName)); - let moduleDir = path.dirname(filePath); - - /* Handle rootDirs mapping */ - if (config.useRootDirs && context.rootDirs) { - let fileRootDir = ""; - let moduleRootDir = ""; - for (const rootDir of context.rootDirs) { - if (isBaseDir(rootDir, filePath) && rootDir.length > moduleRootDir.length) moduleRootDir = rootDir; - if (isBaseDir(rootDir, sourceFile.fileName) && rootDir.length > fileRootDir.length) fileRootDir = rootDir; - } - - /* Remove base dirs to make relative to root */ - if (fileRootDir && moduleRootDir) { - sourceFileDir = path.relative(fileRootDir, sourceFileDir); - moduleDir = path.relative(moduleRootDir, moduleDir); - } - } - - /* Make path relative */ - filePath = tsInstance.normalizePath(path.join(path.relative(sourceFileDir, moduleDir), path.basename(filePath))); - } - - /* Fixup filename */ - if (extension) { - const isImplicitIndex = - path.basename(filePath, extension) === 'index' && - path.basename(moduleName, path.extname(moduleName)) !== 'index'; - - // Remove implicit index - if (isImplicitIndex) filePath = path.dirname(filePath); - // Remove implicit extension - else if (implicitExtensions.includes(extension)) - filePath = filePath.slice(0, -extension.length) + maybeGetExplicitExtension(filePath, extension); - } - - return filePath[0] === "." || isURL(filePath) ? filePath : `./${filePath}`; - } - - function maybeGetExplicitExtension(filePath: string, resolvedExtension: string): string { - const moduleExtension = path.extname(moduleName); - if (moduleExtension && !explicitExtensions.includes(moduleExtension)) return ""; - - return path.basename(moduleName, moduleExtension) === path.basename(filePath, resolvedExtension) - ? moduleExtension - : ""; - } - function getStatementTags() { let targetNode = tsInstance.isStatement(node) ? node diff --git a/src/utils/ts-helpers.ts b/src/utils/ts-helpers.ts new file mode 100755 index 00000000..4accc83f --- /dev/null +++ b/src/utils/ts-helpers.ts @@ -0,0 +1,80 @@ +import ts, { ParsedCommandLine, Program } from 'typescript'; +import path from 'path'; +import { VisitorContext } from '../types'; + +/* ****************************************************************************************************************** */ +// region: TS Helpers +/* ****************************************************************************************************************** */ + +/** + * Generates a ParsedCommandLine for Program + */ +export function createParsedCommandLineForProgram(tsInstance: typeof ts, program: Program): ParsedCommandLine { + const compilerOptions = program.getCompilerOptions(); + const maybePcl: ParsedCommandLine | undefined = compilerOptions.configFilePath + ? tsInstance.getParsedCommandLineOfConfigFile(compilerOptions.configFilePath, {}, tsInstance.sys as any) + : void 0; + + return ( + maybePcl ?? + tsInstance.parseJsonConfigFileContent( + { files: program.getRootFileNames(), compilerOptions }, + tsInstance.sys as any, + program.getCurrentDirectory() + ) + ); +} + +/** + * Determine output file path for source file + */ +export function getOutputFile( + context: VisitorContext, + fileName: string +): string { + const { tsInstance, parsedCommandLine, outputFileNamesCache, program, compilerOptions } = context; + if (outputFileNamesCache.has(fileName)) return outputFileNamesCache.get(fileName)!; + + let res: string | undefined = void 0; + const [tsMajor, tsMinor] = tsInstance.versionMajorMinor.split("."); + + // TS 3.7+ supports getOutputFileNames + if (isTsProjectSourceFile(context, fileName) && (+tsMajor >= 4 || +tsMinor >= 7)) { + try { + res = tsInstance.getOutputFileNames(parsedCommandLine, fileName, tsInstance.sys?.useCaseSensitiveFileNames)[0]; + } catch (e) { + console.warn( + `Failed to resolve output name for ${fileName}. Please report a GH issue at: ` + + `https://github.com/LeDDGroup/typescript-transform-paths/issues` + ); + debugger; + } + } + + if (!res) res = manualResolve(); + + outputFileNamesCache.set(fileName, res); + + return tsInstance.normalizePath(res); + + function manualResolve(): string { + const srcDir = program.getCommonSourceDirectory(); + const destDir = compilerOptions.outDir ?? srcDir; + return path.resolve(destDir, path.relative(srcDir, fileName)); + } +} + +/** + * Determine if moduleName matches config in paths + */ +export function isModulePathsMatch(context: VisitorContext, moduleName: string): boolean { + const { matchPatternOrExact, getOwnKeys } = context.tsInstance; + return !!matchPatternOrExact(getOwnKeys(context.compilerOptions.paths!), moduleName); +} + +export function isTsProjectSourceFile(context: VisitorContext, filePath: string): boolean { + const { tsInstance, program } = context; + return !!program.getRootFileNames().find((f) => tsInstance.normalizePath(filePath) === tsInstance.normalizePath(f)); +} + +// endregion diff --git a/test/projects/specific/src/index.ts b/test/projects/specific/src/index.ts index dc8ca148..1dca9f24 100755 --- a/test/projects/specific/src/index.ts +++ b/test/projects/specific/src/index.ts @@ -24,4 +24,4 @@ export { GeneralConstB } from "#root/general.js"; export const b = 3; -export { ConstB } from '#elision' +export { ConstB } from "#elision"; diff --git a/test/projects/specific/src/packages/pkg-a/index.d.ts b/test/projects/specific/src/packages/pkg-a/index.d.ts new file mode 100755 index 00000000..cb6ca4d7 --- /dev/null +++ b/test/projects/specific/src/packages/pkg-a/index.d.ts @@ -0,0 +1,2 @@ +export type PackageAType = null +export declare const packageAConst: PackageAType diff --git a/test/projects/specific/src/packages/pkg-a/index.js b/test/projects/specific/src/packages/pkg-a/index.js new file mode 100755 index 00000000..08f0c5a5 --- /dev/null +++ b/test/projects/specific/src/packages/pkg-a/index.js @@ -0,0 +1,3 @@ +module.exports = { + packageAConst: null, +}; diff --git a/test/projects/specific/src/packages/pkg-a/package.json b/test/projects/specific/src/packages/pkg-a/package.json new file mode 100755 index 00000000..b1207ef2 --- /dev/null +++ b/test/projects/specific/src/packages/pkg-a/package.json @@ -0,0 +1,6 @@ +{ + "name": "pkg-a", + "version": "0.0.0-alpha", + "private": "true", + "main": "./index.js" +} diff --git a/test/projects/specific/src/packages/pkg-a/sub-pkg/main.d.ts b/test/projects/specific/src/packages/pkg-a/sub-pkg/main.d.ts new file mode 100755 index 00000000..8a398064 --- /dev/null +++ b/test/projects/specific/src/packages/pkg-a/sub-pkg/main.d.ts @@ -0,0 +1,2 @@ +export type SubPackageType = null +export declare const subPackageConst: SubPackageType diff --git a/test/projects/specific/src/packages/pkg-a/sub-pkg/main.js b/test/projects/specific/src/packages/pkg-a/sub-pkg/main.js new file mode 100755 index 00000000..18eb020b --- /dev/null +++ b/test/projects/specific/src/packages/pkg-a/sub-pkg/main.js @@ -0,0 +1,3 @@ +module.exports = { + subPackageConst: null, +}; diff --git a/test/projects/specific/src/packages/pkg-a/sub-pkg/package.json b/test/projects/specific/src/packages/pkg-a/sub-pkg/package.json new file mode 100755 index 00000000..b53f2aed --- /dev/null +++ b/test/projects/specific/src/packages/pkg-a/sub-pkg/package.json @@ -0,0 +1,6 @@ +{ + "name": "sub-pkg", + "version": "0.0.0-alpha", + "private": "true", + "main": "main.js" +} diff --git a/test/projects/specific/src/packages/pkg-b/package.json b/test/projects/specific/src/packages/pkg-b/package.json new file mode 100755 index 00000000..65a449b5 --- /dev/null +++ b/test/projects/specific/src/packages/pkg-b/package.json @@ -0,0 +1,6 @@ +{ + "name": "pkg-b", + "version": "0.0.0-alpha", + "private": "true", + "main": "./subdir/main.js" +} diff --git a/test/projects/specific/src/packages/pkg-b/subdir/main.d.ts b/test/projects/specific/src/packages/pkg-b/subdir/main.d.ts new file mode 100755 index 00000000..929df8c8 --- /dev/null +++ b/test/projects/specific/src/packages/pkg-b/subdir/main.d.ts @@ -0,0 +1,2 @@ +export type PackageBType = null +export declare const packageBConst: PackageBType diff --git a/test/projects/specific/src/packages/pkg-b/subdir/main.js b/test/projects/specific/src/packages/pkg-b/subdir/main.js new file mode 100755 index 00000000..3b89af4b --- /dev/null +++ b/test/projects/specific/src/packages/pkg-b/subdir/main.js @@ -0,0 +1,3 @@ +module.exports = { + packageBConst: null, +}; diff --git a/test/projects/specific/src/packages/pkg-c/main.d.ts b/test/projects/specific/src/packages/pkg-c/main.d.ts new file mode 100755 index 00000000..20249846 --- /dev/null +++ b/test/projects/specific/src/packages/pkg-c/main.d.ts @@ -0,0 +1,2 @@ +export type PackageCType = null +export declare const packageCConst: PackageCType diff --git a/test/projects/specific/src/packages/pkg-c/main.js b/test/projects/specific/src/packages/pkg-c/main.js new file mode 100755 index 00000000..363f8780 --- /dev/null +++ b/test/projects/specific/src/packages/pkg-c/main.js @@ -0,0 +1,3 @@ +module.exports = { + packageCConst: null, +}; diff --git a/test/projects/specific/src/packages/pkg-c/package.json b/test/projects/specific/src/packages/pkg-c/package.json new file mode 100755 index 00000000..18b3a7df --- /dev/null +++ b/test/projects/specific/src/packages/pkg-c/package.json @@ -0,0 +1,6 @@ +{ + "name": "pkg-c", + "version": "0.0.0-alpha", + "private": "true", + "main": "./main.js" +} diff --git a/test/projects/specific/src/sub-packages.ts b/test/projects/specific/src/sub-packages.ts new file mode 100755 index 00000000..36f16d60 --- /dev/null +++ b/test/projects/specific/src/sub-packages.ts @@ -0,0 +1,13 @@ +export { packageAConst, PackageAType } from "#packages/pkg-a"; +export { packageBConst, PackageBType } from "#packages/pkg-b"; +export { packageCConst, PackageCType } from "#packages/pkg-c"; +export { SubPackageType, subPackageConst } from "#packages/pkg-a/sub-pkg"; + +// This path should resolve to './packages/pkg-c/main' +export { packageCConst as C2 } from "#packages/pkg-c/main"; +// This path should resolve to './packages/pkg-c/main.js', due to explicit extension +export { packageCConst as C3 } from "#packages/pkg-c/main.js"; +// This path should resolve to './packages/pkg-a/sub-pkg/main' +export { subPackageConst as C4 } from "#packages/pkg-a/sub-pkg/main"; +// This path should resolve to './packages/pkg-a/sub-pkg/main.js', due to explicit extension +export { subPackageConst as C5 } from "#packages/pkg-a/sub-pkg/main.js"; diff --git a/test/projects/specific/tsconfig.json b/test/projects/specific/tsconfig.json index 03638111..4ea47cbb 100755 --- a/test/projects/specific/tsconfig.json +++ b/test/projects/specific/tsconfig.json @@ -17,7 +17,8 @@ "#root/*": [ "./src/*", "./generated/*" ], "#exclusion/*": [ "./src/excluded/*" ], "#elision": [ "./src/type-elision" ], - "#elision/*": [ "./src/type-elision/*" ] + "#elision/*": [ "./src/type-elision/*" ], + "#packages/*": [ "./src/packages/*" ] }, "resolveJsonModule": true } diff --git a/test/tests/transformer/specific.test.ts b/test/tests/transformer/specific.test.ts index c81f6f5a..1d90274f 100755 --- a/test/tests/transformer/specific.test.ts +++ b/test/tests/transformer/specific.test.ts @@ -25,6 +25,7 @@ describe(`Transformer -> Specific Cases`, () => { const indexFile = ts.normalizePath(path.join(projectRoot, "src/index.ts")); const tagFile = ts.normalizePath(path.join(projectRoot, "src/tags.ts")); const typeElisionIndex = ts.normalizePath(path.join(projectRoot, "src/type-elision/index.ts")); + const subPackagesFile = ts.normalizePath(path.join(projectRoot, "src/sub-packages.ts")); const baseConfig: TsTransformPathsConfig = { exclude: ["**/excluded/**", "excluded-file.*"] }; describe.each(testTsModules)(`TypeScript %s`, (s, tsInstance) => { @@ -135,7 +136,7 @@ describe(`Transformer -> Specific Cases`, () => { ); }); - test(`Preserves explicit js/jsx extensions`, () => { + test(`Preserves explicit extensions`, () => { expect(normalEmit[indexFile].js).toMatch(`export { JsonValue } from "./data.json";`); expect(normalEmit[indexFile].js).toMatch(`export { GeneralConstA } from "./general";`); expect(normalEmit[indexFile].js).toMatch(`export { GeneralConstB } from "./general.js";`); @@ -148,5 +149,26 @@ describe(`Transformer -> Specific Cases`, () => { expect(normalEmit[indexFile].js).toMatch(`export { ConstB } from "./type-elision"`); expect(normalEmit[indexFile].dts).toMatch(`export { ConstB } from "./type-elision"`); }); + + test(`Resolves sub-modules properly`, () => { + const { js, dts } = normalEmit[subPackagesFile]; + expect(js).toMatch(`export { packageBConst } from "./packages/pkg-b";`); + expect(js).toMatch(`export { packageAConst } from "./packages/pkg-a";`); + expect(js).toMatch(`export { packageCConst } from "./packages/pkg-c";`); + expect(js).toMatch(`export { subPackageConst } from "./packages/pkg-a/sub-pkg";`); + expect(js).toMatch(`export { packageCConst as C2 } from "./packages/pkg-c/main";`); + expect(js).toMatch(`export { packageCConst as C3 } from "./packages/pkg-c/main.js";`); + expect(js).toMatch(`export { subPackageConst as C4 } from "./packages/pkg-a/sub-pkg/main";`); + expect(js).toMatch(`export { subPackageConst as C5 } from "./packages/pkg-a/sub-pkg/main.js;"`); + + expect(dts).toMatch(`export { packageAConst, PackageAType } from "./packages/pkg-a";`); + expect(dts).toMatch(`export { packageBConst, PackageBType } from "./packages/pkg-b";`); + expect(dts).toMatch(`export { packageCConst, PackageCType } from "./packages/pkg-c";`); + expect(dts).toMatch(`export { SubPackageType, subPackageConst } from "./packages/pkg-a/sub-pkg";`); + expect(dts).toMatch(`export { packageCConst as C2 } from "./packages/pkg-c/main";`); + expect(dts).toMatch(`export { packageCConst as C3 } from "./packages/pkg-c/main.js";`); + expect(dts).toMatch(`export { subPackageConst as C4 } from "./packages/pkg-a/sub-pkg/main";`); + expect(dts).toMatch(`export { subPackageConst as C5 } from "./packages/pkg-a/sub-pkg/main.js";`); + }); }); });