Skip to content

Different behavior for type reference directive in nodenext based on whether types are in @types or not聽#47444

Open

Description

Bug Report

馃攷 Search Terms

type reference directive node12 nodenext

馃捇 Fourslash server test case

I used fourslash server tests to verify this because it treats files in node_modules as a real tsc run would, whereas the compiler test suite treats them as if they鈥檙e root files for compilation, which messes up what I鈥檓 trying to demonstrate.

I have two dependencies in node_modules here that are structurally identical, but one is inside @types and the other is not. The one in @types is resolved by the primaryLookup of resolveTypeReferenceDirective in moduleNameResolver.ts, which

  • only looks in typeRoots (including @types)
  • only accepts .ts and .d.ts files (not the cts/mts variants)
  • does not look at exports

The one outside of @types, by merit of not being in typeRoots, uses the secondaryLookup function, which uses a very different algorithm that brings in some Node12/NodeNext resolution features. It

  • looks in node_modules/* first before trying node_modules/@types/*
  • accepts cts/mts file extensions
  • looks at exports with conditions node, require, types

This test case shows how this discrepancy can make a difference in resolution based only on whether the package is inside typeRoots or not.

I don鈥檛 know what the expected behavior is here, but it feels like it should probably be the same whether the package is in @types or regular node_modules.

// @Filename: /tsconfig.json
//// {
////     "compilerOptions": {
////         "module": "nodenext",
////         "types": ["inside-at-types", "outside-at-types"]
////     }
//// }

// @Filename: /node_modules/@types/inside-at-types/package.json
//// {
////   "name": "@types/inside-at-types",
////   "version": "1.0.0",
////   "types": "./index.d.ts",
////   "exports": {
////     ".": {
////       "default": "./main.mjs"
////     }
////   }
//// }

// @Filename: /node_modules/@types/inside-at-types/index.d.ts
//// export {};
//// declare global {
////   var typesFieldInsideAtTypes: any;
//// }

// @Filename: /node_modules/@types/inside-at-types/main.d.mts
//// export {};
//// declare global {
////   var exportsInsideAtTypes: any;
//// }

// @Filename: /node_modules/outside-at-types/package.json
//// {
////   "name": "outside-at-types",
////   "version": "1.0.0",
////   "types": "./index.d.ts",
////   "exports": {
////     ".": {
////       "default": "./main.mjs"
////     }
////   }
//// }

// @Filename: /node_modules/outside-at-types/index.d.ts
//// export {};
//// declare global {
////   var typesFieldOutsideAtTypes: any;
//// }

// @Filename: /node_modules/outside-at-types/main.d.mts
//// export {};
//// declare global {
////   var exportsOutsideAtTypes: any;
//// }

// @Filename: /index.ts
//// // One pair of these should be resolved;
//// // the other unresolved. Currently, they're mixed.
////
//// typesFieldInsideAtTypes;
//// typesFieldOutsideAtTypes;

//// exportsInsideAtTypes;
//// exportsOutsideAtTypes;

goTo.file("/index.ts");
verify.baselineSyntacticAndSemanticDiagnostics();

馃檨 Actual behavior

exportsInsideAtTypes and typesFieldOutsideAtTypes are unresolved (meaning inside @types resolves to the types field while outside @types resolves to the exports field)

馃檪 Expected behavior

Either both typesFieldInsideAtTypes and typesFieldOutsideAtTypes or both exportsInsideAtTypes and exportsOutsideAtTypes should be unresolved, while the other pair is resolved.

(Daniel Rosenwasser (@DanielRosenwasser) Wesley Wigham (@weswigham))

Activity

  1. andrewbranch commented on Jan 14, 2022

    @andrewbranch
    MemberAuthor

    Note: there are quite possibly differences between primaryLookup and secondaryLookup observable without node12/nodenext, but exports makes it really easy to find a repro.

  2. weswigham commented on Jan 14, 2022

    @weswigham
    Member

    I agree that they should definitely behave the same - I image we probably need to pass forward the settings used for primaryLookup to secondaryLookup. (Which means we shouldn't respect exports for triple slash references). Once that's consistent, we can discuss weather triple slash references should use non-old-cjs resolution settings under some circumstances.

  3. andrewbranch commented on Aug 18, 2022

    @andrewbranch
    MemberAuthor

    Wesley Wigham (@weswigham) I鈥檓 coming back around to this, but I feel like your assessment above may have changed in light of #47732. I feel like it would make sense to base the resolution mode of automatic type reference directives on the package.json type. That would make automatic type references behave the same as manual type references inside a .ts root file. Thoughts?

  4. andrewbranch commented on Aug 18, 2022

    @andrewbranch
    MemberAuthor

    Further, it seems odd to me that triple-slash references, regardless of requesting resolution mode, wouldn鈥檛 use exports when both CJS- and ESM-mode imports do.

    You might say that a triple-slash reference isn鈥檛 an import, so it doesn鈥檛 seem obvious that it should care about exports. But triple-slash references have always pulled from the types field which is analogous to the main field, which is the place where a require of the package name resolves to. So extending that analogy, it seems odd not to look for exports["."] in a mode where a require/import would look there. Especially considering the possible existence of ESM-only dependencies in an ESM-only project, where the dependency might not feel the need to specify a main or types at all.

  5. weswigham commented on Aug 18, 2022

    @weswigham
    Member

    Yeah, the important note was consistency in resolution between the two scopes - it's certainly more likely useful to always follow exports/other node style resolution rules for triple slash refs. (Though it'd probably annoy some people who've repurposed them over at deno :) ) It should definitely remain independent of the project's moduleResolution, I think.

  6. andrewbranch commented on Aug 18, 2022

    @andrewbranch
    MemberAuthor

    Wait, why should it be independent of moduleResolution? It鈥檚 not independent of moduleResolution today because the secondary lookup does use the moduleResolutionState.features based on module resolution.

  7. weswigham commented on Aug 18, 2022

    @weswigham
    Member

    I'm thinking we should probably have triple-slash refs always use, eg, nodenext rules, this way refs work across DT regardless of the consuming package's tsconfig settings/if they are inside a typeRoot.

    Also, the secondary lookup does use the features passed in - but type reference resolution already always passes in a fixed feature set, regardless of compiler options.

  8. andrewbranch commented on Aug 18, 2022

    @andrewbranch
    MemberAuthor

    I don鈥檛 think so

    let features = getDefaultNodeResolutionFeatures(options);

  9. weswigham commented on Aug 18, 2022

    @weswigham
    Member

    Riiiight, I changed it from always-cjs to option-dependent when I added the mode flags... It should probably be changed to always-latest-node (which I mentioned as an option in that code comment near where you linked). The further proliferation of resolution settings makes me seriously want to avoid having a lot of variants of how TS resolves triple-slash refs just because we're forced to have a lot of variants for imports. Since only we read them, we can choose just one standard resolution scheme - it doesn't need to be configurable.

  10. andrewbranch commented on Aug 18, 2022

    @andrewbranch
    MemberAuthor

    I think that seems reasonable, but do you still want to allow CJS/ESM mode overrides? If you want to do nodenext-ish resolution for type reference directives no matter the moduleResolution setting, we will need to pick a set of conditions and whether NodeResolutionFeatures.EsmMode is set in modes where those are otherwise meaningless.

  11. weswigham commented on Aug 18, 2022

    @weswigham
    Member

    Yeah, I think the mode override is meaningful for pulling in augmenting modules only accessible under import/require conditions, and the conditions should probably be the same node, types, import || require list. The janky thing, I guess, is the the containing file's mode determines the default mode of the reference, as it does now and with imports. Even if the moduleResolution doesn't imply the containing file should have a mode. We'd have to change file mode calculation to be unconditional (even if we don't use it during emit or normal import module resolution), rather than only doing it when moduleResolution is a nodeish one, to get that to work.

  12. andrewbranch commented on Aug 18, 2022

    @andrewbranch
    MemberAuthor

    For the proposed minimal and hybrid, I鈥檓 fairly comfortable with saying it鈥檚 always import. For node, 馃し?

  13. weswigham commented on Aug 18, 2022

    @weswigham
    Member

    I don't think the condition TS uses should depend on the resolution mode of the project - it'd make using the same declaration file in a minimal, node, and node16 project pretty hard. Given that, always doing the nodenext mode calculation seems like the way to go, insofar as it probably minimizes the need for mode assertions.

  14. andrewbranch commented on Aug 18, 2022

    @andrewbranch
    MemberAuthor

    But in those other modes, package.json type explicitly doesn鈥檛 matter, nor do .cts and .mts extensions. I think I don鈥檛 even want to set impliedNodeFormat in them. We could set both import and require 馃槄. I would suggest neither, but I think that would break things.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Needs InvestigationThis issue needs a team member to investigate its status.

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions