Skip to content

[WIP] Destructuring re-exports using type from symlinked node-modules results in relative paths used in import() type #1348

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
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
93 changes: 88 additions & 5 deletions internal/modulespecifiers/specifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,87 @@ func GetModuleSpecifiers(
return result
}

func tryGetModuleNameFromSymlinkedPackage(
pathObj ModulePath,
info Info,
importingSourceFile SourceFileForSpecifierGeneration,
host ModuleSpecifierGenerationHost,
options *core.CompilerOptions,
userPreferences UserPreferences,
packageNameOnly bool,
) string {
// Check if this file is part of a package by looking for package.json
realPath := pathObj.FileName
currentDir := tspath.GetDirectoryPath(realPath)

// Walk up to find package.json
for len(currentDir) > 0 {
packageJsonPath := tspath.CombinePaths(currentDir, "package.json")
if host.FileExists(packageJsonPath) {
packageInfo := host.GetPackageJsonInfo(packageJsonPath)
if packageInfo != nil && packageInfo.GetContents() != nil {
packageNameVal, hasName := packageInfo.GetContents().Name.GetValue()
if hasName && len(packageNameVal) > 0 {
// Found a package - now check if it's accessible from a node_modules directory
// by looking for a symlink in node_modules that points to this package
symlinkPath := findSymlinkToPackage(packageNameVal, currentDir, info.SourceDirectory, host)
if len(symlinkPath) > 0 {
// Found a symlink - construct the module path through node_modules
relativePathInPackage := tspath.GetRelativePathFromDirectory(currentDir, realPath, tspath.ComparePathsOptions{UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames()})
nodeModulesPath := tspath.CombinePaths(symlinkPath, relativePathInPackage)

// Create a new ModulePath with the node_modules path and try again
nodeModulesModulePath := ModulePath{
FileName: nodeModulesPath,
IsInNodeModules: true,
IsRedirect: pathObj.IsRedirect,
}

// Recursively call tryGetModuleNameAsNodeModule with the node_modules path
return tryGetModuleNameAsNodeModule(nodeModulesModulePath, info, importingSourceFile, host, options, userPreferences, packageNameOnly, core.ResolutionModeNone)
}
}
}
break
}

parentDir := tspath.GetDirectoryPath(currentDir)
if parentDir == currentDir {
break
}
currentDir = parentDir
}

return ""
}

func findSymlinkToPackage(packageName string, packageRealDir string, importingDir string, host ModuleSpecifierGenerationHost) string {
// Walk up from importing directory looking for node_modules
currentDir := importingDir
for len(currentDir) > 0 {
nodeModulesPath := tspath.CombinePaths(currentDir, "node_modules", packageName)
packageJsonPath := tspath.CombinePaths(nodeModulesPath, "package.json")
if host.FileExists(packageJsonPath) {
// Found the package in node_modules - check if it's the same package
packageInfo := host.GetPackageJsonInfo(packageJsonPath)
if packageInfo != nil && packageInfo.GetContents() != nil {
foundPackageName, hasName := packageInfo.GetContents().Name.GetValue()
if hasName && foundPackageName == packageName {
return nodeModulesPath
}
}
}

parentDir := tspath.GetDirectoryPath(currentDir)
if parentDir == currentDir {
break
}
currentDir = parentDir
}

return ""
}

func tryGetModuleNameFromAmbientModule(moduleSymbol *ast.Symbol, checker CheckerShape) string {
for _, decl := range moduleSymbol.Declarations {
if isNonGlobalAmbientModule(decl) && (!ast.IsModuleAugmentationExternal(decl) || !tspath.IsExternalModuleNameRelative(decl.Name().AsStringLiteral().Text)) {
Expand Down Expand Up @@ -337,10 +418,10 @@ func computeModuleSpecifiers(
var relativeSpecifiers []string

for _, modulePath := range modulePaths {
var specifier string
if modulePath.IsInNodeModules {
specifier = tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences /*packageNameOnly*/, false, options.OverrideImportMode)
}
// Always try to get a node module specifier first, regardless of IsInNodeModules flag
// This handles symlinked packages where the real path doesn't contain node_modules
// but the file can still be addressed as a package
specifier := tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences /*packageNameOnly*/, false, options.OverrideImportMode)
if len(specifier) > 0 && !(forAutoImport && isExcludedByRegex(specifier, preferences.excludeRegexes)) {
nodeModulesSpecifiers = append(nodeModulesSpecifiers, specifier)
if modulePath.IsRedirect {
Expand Down Expand Up @@ -645,7 +726,9 @@ func tryGetModuleNameAsNodeModule(
) string {
parts := getNodeModulePathParts(pathObj.FileName)
if parts == nil {
return ""
// For symlinked packages, the real path may not contain node_modules
// Try to check if this file is part of a package that can be accessed via node_modules
return tryGetModuleNameFromSymlinkedPackage(pathObj, info, importingSourceFile, host, options, userPreferences, packageNameOnly)
}

// Simplify the full file path to something that can be resolved by Node.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//// [tests/cases/compiler/destructuringReexportSymlinkImportType.ts] ////

//// [package.json]
{
"name": "package-b",
"main": "./index.js",
"types": "./index.d.ts"
}

//// [index.d.ts]
export interface B {
value: string;
}

//// [types.ts]
import type { B } from "package-b";
export type { B };

//// [main.ts]
import type { B } from "./types";

export function useB(param: B): B {
return param;
}


//// [types.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//// [main.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.useB = useB;
function useB(param) {
return param;
}


//// [types.d.ts]
import type { B } from "package-b";
export type { B };
//// [main.d.ts]
import type { B } from "./types";
export declare function useB(param: B): B;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//// [tests/cases/compiler/destructuringReexportSymlinkImportType.ts] ////

=== /real-packages/package-b/index.d.ts ===
export interface B {
>B : Symbol(B, Decl(index.d.ts, 0, 0))

value: string;
>value : Symbol(value, Decl(index.d.ts, 0, 20))
}

=== /project/src/types.ts ===
import type { B } from "package-b";
>B : Symbol(B, Decl(types.ts, 0, 13))

export type { B };
>B : Symbol(B, Decl(types.ts, 1, 13))

=== /project/src/main.ts ===
import type { B } from "./types";
>B : Symbol(B, Decl(main.ts, 0, 13))

export function useB(param: B): B {
>useB : Symbol(useB, Decl(main.ts, 0, 33))
>param : Symbol(param, Decl(main.ts, 2, 21))
>B : Symbol(B, Decl(main.ts, 0, 13))
>B : Symbol(B, Decl(main.ts, 0, 13))

return param;
>param : Symbol(param, Decl(main.ts, 2, 21))
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//// [tests/cases/compiler/destructuringReexportSymlinkImportType.ts] ////

=== /real-packages/package-b/index.d.ts ===
export interface B {
value: string;
>value : string
}

=== /project/src/types.ts ===
import type { B } from "package-b";
>B : B

export type { B };
>B : B

=== /project/src/main.ts ===
import type { B } from "./types";
>B : B

export function useB(param: B): B {
>useB : (param: B) => B
>param : B

return param;
>param : B
}

Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,75 @@ __exportStar(require("./keys"), exports);

//// [keys.d.ts]
import { MetadataAccessor } from "@raymondfeng/pkg2";
export declare const ADMIN: MetadataAccessor<boolean, import("../../pkg1/dist").IdType>;
export declare const ADMIN: MetadataAccessor<boolean, import("@raymondfeng/pkg2").IdType>;
//// [index.d.ts]
export * from './keys';


//// [DtsFileErrors]


monorepo/pkg3/dist/keys.d.ts(2,83): error TS2694: Namespace '"monorepo/pkg2/dist/index"' has no exported member 'IdType'.


==== monorepo/pkg3/tsconfig.json (0 errors) ====
{
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"declaration": true
}
}

==== monorepo/pkg3/dist/index.d.ts (0 errors) ====
export * from './keys';

==== monorepo/pkg3/dist/keys.d.ts (1 errors) ====
import { MetadataAccessor } from "@raymondfeng/pkg2";
export declare const ADMIN: MetadataAccessor<boolean, import("@raymondfeng/pkg2").IdType>;
~~~~~~
!!! error TS2694: Namespace '"monorepo/pkg2/dist/index"' has no exported member 'IdType'.

==== monorepo/pkg1/dist/index.d.ts (0 errors) ====
export * from './types';
==== monorepo/pkg1/dist/types.d.ts (0 errors) ====
export declare type A = {
id: string;
};
export declare type B = {
id: number;
};
export declare type IdType = A | B;
export declare class MetadataAccessor<T, D extends IdType = IdType> {
readonly key: string;
private constructor();
toString(): string;
static create<T, D extends IdType = IdType>(key: string): MetadataAccessor<T, D>;
}
==== monorepo/pkg1/package.json (0 errors) ====
{
"name": "@raymondfeng/pkg1",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"typings": "dist/index.d.ts"
}
==== monorepo/pkg2/dist/index.d.ts (0 errors) ====
import "./secondary";
export * from './types';
==== monorepo/pkg2/dist/types.d.ts (0 errors) ====
export {MetadataAccessor} from '@raymondfeng/pkg1';
==== monorepo/pkg2/dist/secondary.d.ts (0 errors) ====
export {IdType} from '@raymondfeng/pkg1';
==== monorepo/pkg2/package.json (0 errors) ====
{
"name": "@raymondfeng/pkg2",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"typings": "dist/index.d.ts"
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,75 @@
//// [keys.d.ts]
import { MetadataAccessor } from "@raymondfeng/pkg2";
-export declare const ADMIN: MetadataAccessor<boolean, import("@raymondfeng/pkg2/dist/secondary").IdType>;
+export declare const ADMIN: MetadataAccessor<boolean, import("../../pkg1/dist").IdType>;
+export declare const ADMIN: MetadataAccessor<boolean, import("@raymondfeng/pkg2").IdType>;
//// [index.d.ts]
export * from './keys';
export * from './keys';
+
+
+//// [DtsFileErrors]
+
+
+monorepo/pkg3/dist/keys.d.ts(2,83): error TS2694: Namespace '"monorepo/pkg2/dist/index"' has no exported member 'IdType'.
+
+
+==== monorepo/pkg3/tsconfig.json (0 errors) ====
+ {
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src",
+ "target": "es5",
+ "module": "commonjs",
+ "strict": true,
+ "esModuleInterop": true,
+ "declaration": true
+ }
+ }
+
+==== monorepo/pkg3/dist/index.d.ts (0 errors) ====
+ export * from './keys';
+
+==== monorepo/pkg3/dist/keys.d.ts (1 errors) ====
+ import { MetadataAccessor } from "@raymondfeng/pkg2";
+ export declare const ADMIN: MetadataAccessor<boolean, import("@raymondfeng/pkg2").IdType>;
+ ~~~~~~
+!!! error TS2694: Namespace '"monorepo/pkg2/dist/index"' has no exported member 'IdType'.
+
+==== monorepo/pkg1/dist/index.d.ts (0 errors) ====
+ export * from './types';
+==== monorepo/pkg1/dist/types.d.ts (0 errors) ====
+ export declare type A = {
+ id: string;
+ };
+ export declare type B = {
+ id: number;
+ };
+ export declare type IdType = A | B;
+ export declare class MetadataAccessor<T, D extends IdType = IdType> {
+ readonly key: string;
+ private constructor();
+ toString(): string;
+ static create<T, D extends IdType = IdType>(key: string): MetadataAccessor<T, D>;
+ }
+==== monorepo/pkg1/package.json (0 errors) ====
+ {
+ "name": "@raymondfeng/pkg1",
+ "version": "1.0.0",
+ "description": "",
+ "main": "dist/index.js",
+ "typings": "dist/index.d.ts"
+ }
+==== monorepo/pkg2/dist/index.d.ts (0 errors) ====
+ import "./secondary";
+ export * from './types';
+==== monorepo/pkg2/dist/types.d.ts (0 errors) ====
+ export {MetadataAccessor} from '@raymondfeng/pkg1';
+==== monorepo/pkg2/dist/secondary.d.ts (0 errors) ====
+ export {IdType} from '@raymondfeng/pkg1';
+==== monorepo/pkg2/package.json (0 errors) ====
+ {
+ "name": "@raymondfeng/pkg2",
+ "version": "1.0.0",
+ "description": "",
+ "main": "dist/index.js",
+ "typings": "dist/index.d.ts"
+ }
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import {MetadataAccessor} from "@raymondfeng/pkg2";
>MetadataAccessor : typeof MetadataAccessor

export const ADMIN = MetadataAccessor.create<boolean>('1');
>ADMIN : MetadataAccessor<boolean, import("../../pkg1/dist").IdType>
>MetadataAccessor.create<boolean>('1') : MetadataAccessor<boolean, import("../../pkg1/dist").IdType>
>MetadataAccessor.create : <T, D extends import("../../pkg1/dist").IdType = import("../../pkg1/dist").IdType>(key: string) => MetadataAccessor<T, D>
>ADMIN : MetadataAccessor<boolean, import("@raymondfeng/pkg2").IdType>
>MetadataAccessor.create<boolean>('1') : MetadataAccessor<boolean, import("@raymondfeng/pkg2").IdType>
>MetadataAccessor.create : <T, D extends import("@raymondfeng/pkg2").IdType = import("@raymondfeng/pkg2").IdType>(key: string) => MetadataAccessor<T, D>
>MetadataAccessor : typeof MetadataAccessor
>create : <T, D extends import("../../pkg1/dist").IdType = import("../../pkg1/dist").IdType>(key: string) => MetadataAccessor<T, D>
>create : <T, D extends import("@raymondfeng/pkg2").IdType = import("@raymondfeng/pkg2").IdType>(key: string) => MetadataAccessor<T, D>
>'1' : "1"

=== monorepo/pkg1/dist/index.d.ts ===
Expand Down
Loading