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 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 6 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
55 changes: 51 additions & 4 deletions internal/modulespecifiers/specifiers.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package modulespecifiers

import (
"maps"

Check failure on line 4 in internal/modulespecifiers/specifiers.go

View workflow job for this annotation

GitHub Actions / copilot

"fmt" imported and not used
"slices"
"strings"

Expand Down Expand Up @@ -338,9 +338,10 @@

for _, modulePath := range modulePaths {
var specifier string
if modulePath.IsInNodeModules {
specifier = tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, userPreferences /*packageNameOnly*/, false, options.OverrideImportMode)
}
// Try to generate a node module specifier for all paths, not just those marked IsInNodeModules
// This handles symlinked packages where the real path doesn't contain node_modules
// but a package name can still be determined
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,6 +646,50 @@
return processEnding(shortest, allowedEndings, compilerOptions, host)
}

func tryGetPackageNameForSymlinkedFile(fileName string, info Info, host ModuleSpecifierGenerationHost) string {
// Look for package.json in the file's directory or parent directories
currentDir := tspath.GetDirectoryPath(fileName)
for len(currentDir) > 0 {
packageJsonPath := tspath.CombinePaths(currentDir, "package.json")
if host.FileExists(packageJsonPath) {
packageInfo := host.GetPackageJsonInfo(packageJsonPath)
if packageInfo != nil && packageInfo.GetContents() != nil {
packageName, ok := packageInfo.GetContents().Name.GetValue()
if ok && len(packageName) > 0 {

Check failure on line 658 in internal/modulespecifiers/specifiers.go

View workflow job for this annotation

GitHub Actions / copilot

undefined: fmt
// Check if this package can be resolved from the importing source directory
// by looking for a symlink in node_modules
if canResolveAsPackage(packageName, info.SourceDirectory, host) {

Check failure on line 661 in internal/modulespecifiers/specifiers.go

View workflow job for this annotation

GitHub Actions / copilot

undefined: fmt
return packageName
}
}
}
}
parentDir := tspath.GetDirectoryPath(currentDir)
if parentDir == currentDir {
break
}
currentDir = parentDir
}
return ""
}

func canResolveAsPackage(packageName string, importingDir string, host ModuleSpecifierGenerationHost) bool {
// Walk up from importing directory looking for node_modules that contains this package
currentDir := importingDir
for len(currentDir) > 0 {
nodeModulesPath := tspath.CombinePaths(currentDir, "node_modules", packageName)
if host.FileExists(tspath.CombinePaths(nodeModulesPath, "package.json")) {
return true
}
parentDir := tspath.GetDirectoryPath(currentDir)
if parentDir == currentDir {
break
}
currentDir = parentDir
}
return false
}

func tryGetModuleNameAsNodeModule(
pathObj ModulePath,
info Info,
Expand All @@ -657,7 +702,9 @@
) string {
parts := getNodeModulePathParts(pathObj.FileName)
if parts == nil {
return ""
// For symlinked packages, the real path may not contain node_modules
// Try to infer package name by looking for package.json
return tryGetPackageNameForSymlinkedFile(pathObj.FileName, info, host)
}

// 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,61 @@
//// [tests/cases/compiler/destructuringReexportSymlinkImportType.ts] ////

//// [package.json]
{
"name": "package-b",
"type": "module",
"exports": {
".": "./index.js"
}
}

//// [index.js]
export {};

//// [index.d.ts]
export interface B {
b: "b";
}

//// [package.json]
{
"name": "package-a",
"type": "module",
"imports": {
"#re_export": "./src/re_export.ts"
},
"exports": {
".": "./dist/index.js"
}
}


//// [re_export.ts]
import type { B } from "package-b";
declare function foo(): Promise<B>
export const re = { foo };

//// [index.ts]
import { re } from "#re_export";
const { foo } = re;
export { foo };


//// [re_export.js]
export const re = { foo };
//// [index.js]
import { re } from "#re_export";
const { foo } = re;
export { foo };


//// [re_export.d.ts]
import type { B } from "package-b";
declare function foo(): Promise<B>;
export declare const re: {
foo: typeof foo;
};
export {};
//// [index.d.ts]
declare const foo: () => Promise<import("package-b").B>;
export { foo };
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//// [tests/cases/compiler/destructuringReexportSymlinkImportType.ts] ////

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

declare function foo(): Promise<B>
>foo : Symbol(foo, Decl(re_export.ts, 0, 35))
>Promise : Symbol(Promise, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.es2018.promise.d.ts, --, --))
>B : Symbol(B, Decl(re_export.ts, 0, 13))

export const re = { foo };
>re : Symbol(re, Decl(re_export.ts, 2, 12))
>foo : Symbol(foo, Decl(re_export.ts, 2, 19))

=== /packages/a/src/index.ts ===
import { re } from "#re_export";
>re : Symbol(re, Decl(index.ts, 0, 8))

const { foo } = re;
>foo : Symbol(foo, Decl(index.ts, 1, 7))
>re : Symbol(re, Decl(index.ts, 0, 8))

export { foo };
>foo : Symbol(foo, Decl(index.ts, 2, 8))

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

b: "b";
>b : Symbol(b, Decl(index.d.ts, 0, 20))
}

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

=== /packages/a/src/re_export.ts ===
import type { B } from "package-b";
>B : B

declare function foo(): Promise<B>
>foo : () => Promise<B>

export const re = { foo };
>re : { foo: () => Promise<B>; }
>{ foo } : { foo: () => Promise<B>; }
>foo : () => Promise<B>

=== /packages/a/src/index.ts ===
import { re } from "#re_export";
>re : { foo: () => Promise<import("package-b").B>; }

const { foo } = re;
>foo : () => Promise<import("package-b").B>
>re : { foo: () => Promise<import("package-b").B>; }

export { foo };
>foo : () => Promise<import("package-b").B>

=== /packages/b/index.d.ts ===
export interface B {
b: "b";
>b : "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
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
->ADMIN : MetadataAccessor<boolean, import("monorepo/pkg1/dist/types").IdType>
->MetadataAccessor.create<boolean>('1') : MetadataAccessor<boolean, import("monorepo/pkg1/dist/types").IdType>
->MetadataAccessor.create : <T, D extends import("monorepo/pkg1/dist/types").IdType = import("monorepo/pkg1/dist/types").IdType>(key: string) => MetadataAccessor<T, D>
+>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("monorepo/pkg1/dist/types").IdType = import("monorepo/pkg1/dist/types").IdType>(key: string) => MetadataAccessor<T, D>
+>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
Loading