Skip to content

export * from … sometimes behaves differently in tsgo than in tsc or tsgo LSP #1943

@chriskrycho

Description

@chriskrycho

Steps to reproduce

In a private monorepo setup, when invoking tsgo against a package that depends on another package in the repo, an import fails to resolve when it is looking up a type that is re-exported with the export * from syntax. It resolves correctly with tsc v5.9.3 and using the language server supplied with @typescript/native-preview (same correct behavior in both the official VS Code preview extension and Zed’s experimental extension wrapping the native LSP).

The original export is a simple function export:

export function stubDynamicConfig<T>(config: DynamicConfiguration<T>): {
  setValue: (value: T) => void;
} {
  // ...
}

The re-export is written like this, using an import map to map the location relative to this package:

export * from "#feature-flags/dynamic-configuration/dynamic-config-harness.testutil.ts";

The import is written like this, relying on an export map in the exporting package to resolve the location:

import { stubDynamicConfig } from "@vanta/feature-flags/dynamic-config/testing";

Both packages are using "type": "commonjs" implicitly by leaving type unset.

Project tsconfig.json
{
  "extends": "../../tsconfig.base.apps.json",
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "dist",
    "exactOptionalPropertyTypes": false
  },
  "references": [
    { "path": "../../packages/common" },
    { "path": "../../packages/codegen-common" },
    { "path": "../../packages/core-infra" },
    { "path": "../../packages/core-product" },
    { "path": "../../packages/event-schema" },
    { "path": "../../packages/event-streaming" },
    { "path": "../../packages/feature-flags" },
    { "path": "../../packages/god-objects" },
    { "path": "../../packages/server-common" },
    { "path": "../../packages/tag" },
    { "path": "../../packages/teams" },
    { "path": "../../packages/penetration-test" },
    { "path": "../../packages/uploaded-document" }
  ],
  "include": ["../../packages/ts-reset/**/*.d.ts", "src/**/*.ts"],
  "exclude": ["**/node_modules", "src/cli.ts"]
}
The referenced tsconfig.base.apps.json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "noEmit": true,
    "composite": false
  }
}
The referenced tsconfig.base.json
{
  "compilerOptions": {
    // Module resolution options
    "module": "node20",
    "resolveJsonModule": true,
    "esModuleInterop": true,
    // Typechecking options
    "exactOptionalPropertyTypes": true,
    "strict": true,
    "noImplicitReturns": true,
    "noImplicitOverride": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noFallthroughCasesInSwitch": true,
    "allowUnreachableCode": false,
    "useUnknownInCatchVariables": true,
    "target": "es2023",
    "lib": ["es2023", "ESNext.Collection", "ESNext.Iterator"],
    "types": ["node", "mocha"],
    "experimentalDecorators": true,
    // Performance-related options
    "skipLibCheck": true,
    "preserveConstEnums": true,
    "incremental": true,
    "composite": true,
    // Output options
    "pretty": true,
    "preserveWatchOutput": true,
    "noErrorTruncation": false
  }
}
Relevant bits of package.json for importing package
{
  "name": "@vanta/some-app",
  "main": "index.js",
  "exports": {},
  "imports": {
    "#some-app/*.json": {
      "node": "./dist/*.json",
      "default": "./src/*.json"
    },
    "#some-app/*.ts": {
      "types": "./src/*.ts",
      "node": "./dist/*.js",
      "default": "./src/*.ts"
    }
  },
  "dependencies": {
    "@vanta/feature-flags": "workspace:*"
  }
}
Relevant bits of package.json for exporting package
{
  "name": "@vanta/feature-flags",
  "exports": {
    "./dynamic-config": {
      "types": "./dist/dynamic-configuration/api.d.ts",
      "node": "./dist/dynamic-configuration/api.js",
      "default": "./src/dynamic-configuration/api.ts"
    },
    "./dynamic-config/eval": {
      "types": "./dist/dynamic-configuration/eval.d.ts",
      "node": "./dist/dynamic-configuration/eval.js",
      "default": "./src/dynamic-configuration/eval.ts"
    },
    "./dynamic-config/testing": {
      "types": "./dist/dynamic-configuration/testing.testutil.d.ts",
      "node": "./dist/dynamic-configuration/testing.testutil.js",
      "default": "./src/dynamic-configuration/testing.testutil.ts"
    }
  },
  "imports": {
    "#feature-flags/*.json": {
      "node": "./dist/*.json",
      "default": "./src/*.json"
    },
    "#feature-flags/*.ts": {
      "types": "./src/*.ts",
      "node": "./dist/*.js",
      "default": "./src/*.ts"
    }
  }
}

I am trying to make an actual reproduction in this repository and will comment if/when I am able to do so.

Behavior with typescript@5.8

The re-export works.

Behavior with tsgo

Module '"@vanta/feature-flags/dynamic-config/testing"' has no exported member 'stubDynamicConfig'.

Re-exporting a named item instead does resolve correctly, so I am using this as a workaround:

export { stubDynamicConfig } from "#feature-flags/dynamic-configuration/dynamic-config-harness.testutil.ts";

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions