-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Proposal: compute module format based on package.json visible to declarationDir
/outDir
#54546
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
Changes from all commits
fb29417
0dd327f
da63af1
6fadcda
8be83db
8a7c262
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -134,6 +134,9 @@ import { | |
getNormalizedAbsolutePathWithoutRoot, | ||
getNormalizedPathComponents, | ||
getOutputDeclarationFileName, | ||
getOutputDeclarationFileNameWithoutConfigFile, | ||
getOutputJSFileName, | ||
getOutputJSFileNameWithoutConfigFile, | ||
getOutputPathsForBundle, | ||
getPackageScopeForPath, | ||
getPathFromPathComponents, | ||
|
@@ -1336,6 +1339,16 @@ export function getImpliedNodeFormatForFileWorker( | |
} | ||
} | ||
|
||
function moduleFormatNeedsPackageJsonLookup(fileName: string, options: CompilerOptions) { | ||
switch (getEmitModuleResolutionKind(options)) { | ||
case ModuleResolutionKind.Node16: | ||
case ModuleResolutionKind.NodeNext: | ||
return fileExtensionIsOneOf(fileName, [Extension.Dts, Extension.Ts, Extension.Tsx, Extension.Js, Extension.Jsx]); | ||
default: | ||
return false; | ||
} | ||
} | ||
|
||
/** @internal */ | ||
export const plainJSErrors: Set<number> = new Set([ | ||
// binder errors | ||
|
@@ -1491,6 +1504,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg | |
let files: SourceFile[]; | ||
let symlinks: SymlinkCache | undefined; | ||
let commonSourceDirectory: string; | ||
let assumedCommonSourceDirectory: string | undefined; | ||
let typeChecker: TypeChecker; | ||
let classifiableNames: Set<__String>; | ||
const ambientModuleNameToUnmodifiedFileName = new Map<string, string>(); | ||
|
@@ -2053,6 +2067,10 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg | |
return commonSourceDirectory; | ||
} | ||
|
||
function getAssumedCommonSourceDirectory() { | ||
return commonSourceDirectory ?? (assumedCommonSourceDirectory ??= ts_getCommonSourceDirectory(options, () => rootNames, host.getCurrentDirectory(), getCanonicalFileName)); | ||
} | ||
|
||
function getClassifiableNames() { | ||
if (!classifiableNames) { | ||
// Initialize a checker so that all our files are bound. | ||
|
@@ -2367,7 +2385,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg | |
const seenPackageNames = new Map<string, SeenPackageName>(); | ||
|
||
for (const oldSourceFile of oldSourceFiles) { | ||
const sourceFileOptions = getCreateSourceFileOptions(oldSourceFile.fileName, moduleResolutionCache, host, options); | ||
const sourceFileOptions = getCreateSourceFileOptions(oldSourceFile.fileName); | ||
let newSourceFile = host.getSourceFileByPath | ||
? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.resolvedPath, sourceFileOptions, /*onError*/ undefined, shouldCreateNewSourceFile || sourceFileOptions.impliedNodeFormat !== oldSourceFile.impliedNodeFormat) | ||
: host.getSourceFile(oldSourceFile.fileName, sourceFileOptions, /*onError*/ undefined, shouldCreateNewSourceFile || sourceFileOptions.impliedNodeFormat !== oldSourceFile.impliedNodeFormat); // TODO: GH#18217 | ||
|
@@ -3508,11 +3526,58 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg | |
return result; | ||
} | ||
|
||
function getCreateSourceFileOptions(fileName: string, moduleResolutionCache: ModuleResolutionCache | undefined, host: CompilerHost, options: CompilerOptions): CreateSourceFileOptions { | ||
function getImpliedNodeFormatForFile(fileName: string) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’m open to suggestions for a different name to differentiate from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think functionality needs to be moved under getImpliedNodeFormatForFileWorker with optional new methods that are required since we expose that method publicly and we want it to be same as what program does to figure out file's implied format. |
||
// Map to an output file before looking up a package.json that might | ||
// contain `"type": "module"`. This allows the same input `.ts` file to be | ||
// compiled as both CommonJS and ESM depending on the tsconfig's `outDir`. | ||
let fileNameForModuleFormatDetection = getNormalizedAbsolutePath(fileName, currentDirectory); | ||
if (moduleFormatNeedsPackageJsonLookup(fileName, options)) { | ||
const projectReference = getResolvedProjectReferenceToRedirect(fileName); | ||
if (projectReference) { | ||
// We have a source file that is an input to a referenced project. | ||
// This should only happen with `useSourceOfProjectReferenceRedirect` | ||
// enabled (i.e. TS Server scenarios). | ||
Debug.assert(useSourceOfProjectReferenceRedirect); | ||
fileNameForModuleFormatDetection = | ||
getOutputDeclarationFileName( | ||
fileNameForModuleFormatDetection, | ||
projectReference.commandLine, | ||
!host.useCaseSensitiveFileNames()) || | ||
getOutputJSFileName( | ||
fileNameForModuleFormatDetection, | ||
projectReference.commandLine, | ||
!host.useCaseSensitiveFileNames()) || | ||
fileNameForModuleFormatDetection; | ||
} | ||
else { | ||
fileNameForModuleFormatDetection = | ||
getOutputDeclarationFileNameWithoutConfigFile( | ||
fileNameForModuleFormatDetection, | ||
options, | ||
!host.useCaseSensitiveFileNames(), | ||
currentDirectory, | ||
getAssumedCommonSourceDirectory) || | ||
getOutputJSFileNameWithoutConfigFile( | ||
fileNameForModuleFormatDetection, | ||
options, | ||
!host.useCaseSensitiveFileNames(), | ||
currentDirectory, | ||
getAssumedCommonSourceDirectory) || | ||
fileNameForModuleFormatDetection; | ||
Comment on lines
+3554
to
+3566
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I considered checking to see if a result computed from |
||
} | ||
} | ||
return getImpliedNodeFormatForFileWorker( | ||
fileNameForModuleFormatDetection, | ||
moduleResolutionCache?.getPackageJsonInfoCache(), | ||
host, | ||
options); | ||
} | ||
|
||
function getCreateSourceFileOptions(fileName: string): CreateSourceFileOptions { | ||
// It's a _little odd_ that we can't set `impliedNodeFormat` until the program step - but it's the first and only time we have a resolution cache | ||
// and a freshly made source file node on hand at the same time, and we need both to set the field. Persisting the resolution cache all the way | ||
// to the check and emit steps would be bad - so we much prefer detecting and storing the format information on the source file node upfront. | ||
const result = getImpliedNodeFormatForFileWorker(getNormalizedAbsolutePath(fileName, currentDirectory), moduleResolutionCache?.getPackageJsonInfoCache(), host, options); | ||
const result = getImpliedNodeFormatForFile(fileName); | ||
const languageVersion = getEmitScriptTarget(options); | ||
const setExternalModuleIndicator = getSetExternalModuleIndicator(options); | ||
return typeof result === "object" ? | ||
|
@@ -3610,7 +3675,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg | |
} | ||
|
||
// We haven't looked for this file, do so now and cache result | ||
const sourceFileOptions = getCreateSourceFileOptions(fileName, moduleResolutionCache, host, options); | ||
const sourceFileOptions = getCreateSourceFileOptions(fileName); | ||
const file = host.getSourceFile( | ||
fileName, | ||
sourceFileOptions, | ||
|
@@ -4304,6 +4369,10 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg | |
} | ||
} | ||
|
||
if (assumedCommonSourceDirectory && assumedCommonSourceDirectory !== getCommonSourceDirectory()) { | ||
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.The_project_root_is_ambiguous_but_is_required_to_determine_the_module_format_of_output_js_files_Supply_the_rootDir_compiler_option_to_disambiguate)); | ||
} | ||
|
||
if (options.useDefineForClassFields && languageVersion === ScriptTarget.ES3) { | ||
createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_when_option_target_is_ES3, "useDefineForClassFields"); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { | ||
verifyTsc, | ||
} from "../helpers/tsc"; | ||
import { loadProjectFromFiles } from "../helpers/vfs"; | ||
|
||
describe("unittests:: tsbuild:: nodeModulesOutDirPackageJson", () => { | ||
verifyTsc({ | ||
scenario: "nodeModulesOutDirPackageJson", | ||
subScenario: "recognizes package.json in outDir of referenced project", | ||
commandLineArgs: ["-b", "/src/tsconfig.json", "-v"], | ||
fs: () => loadProjectFromFiles({ | ||
"/shared/tsconfig.json": JSON.stringify({ | ||
compilerOptions: { | ||
target: "es5", | ||
composite: true, | ||
outDir: "dist" | ||
} | ||
}), | ||
"/shared/red.ts": "export const red = 'fish';", | ||
"/shared/dist/package.json": JSON.stringify({ type: "module" }), | ||
|
||
"/src/tsconfig.json": JSON.stringify({ | ||
compilerOptions: { | ||
target: "es5", | ||
module: "nodenext", | ||
noEmit: true, | ||
}, | ||
references: [{ path: "../shared" }] | ||
}), | ||
"/src/index.ts": "import { red } from '../shared/red';", | ||
}), | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
/src/a.ts(1,19): error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './b.js'? | ||
|
||
|
||
==== /tsconfig.json (0 errors) ==== | ||
{ | ||
"compilerOptions": { | ||
"module": "nodenext", | ||
"declaration": true, | ||
"declarationDir": "out", | ||
"emitDeclarationOnly": true, | ||
"rootDir": "." | ||
}, | ||
"include": ["src"] | ||
} | ||
|
||
==== /src/a.ts (1 errors) ==== | ||
import { b } from "./b"; | ||
~~~~~ | ||
!!! error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './b.js'? | ||
|
||
==== /src/b.ts (0 errors) ==== | ||
export const b = 0; | ||
|
||
==== /out/package.json (0 errors) ==== | ||
{ "type": "module" } | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
//// [tests/cases/conformance/node/nodeModulesOutDirPackageJson05_declarationDir.ts] //// | ||
|
||
//// [package.json] | ||
{ "type": "module" } | ||
|
||
//// [a.ts] | ||
import { b } from "./b"; | ||
|
||
//// [b.ts] | ||
export const b = 0; | ||
|
||
|
||
|
||
|
||
//// [a.d.ts] | ||
export {}; | ||
//// [b.d.ts] | ||
export declare const b = 0; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
=== /src/a.ts === | ||
import { b } from "./b"; | ||
>b : Symbol(b, Decl(a.ts, 0, 8)) | ||
|
||
=== /src/b.ts === | ||
export const b = 0; | ||
>b : Symbol(b, Decl(b.ts, 0, 12)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
=== /src/a.ts === | ||
import { b } from "./b"; | ||
>b : any | ||
|
||
=== /src/b.ts === | ||
export const b = 0; | ||
>b : 0 | ||
>0 : 0 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
/src/a.ts(1,19): error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './b.js'? | ||
|
||
|
||
==== /out/package.json (0 errors) ==== | ||
{ "type": "module" } | ||
|
||
==== /src/a.ts (1 errors) ==== | ||
import { b } from "./b"; | ||
~~~~~ | ||
!!! error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './b.js'? | ||
|
||
==== /src/b.ts (0 errors) ==== | ||
export const b = 0; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
//// [tests/cases/conformance/node/nodeModulesOutDirPackageJson01.ts] //// | ||
|
||
//// [package.json] | ||
{ "type": "module" } | ||
|
||
//// [a.ts] | ||
import { b } from "./b"; | ||
|
||
//// [b.ts] | ||
export const b = 0; | ||
|
||
|
||
//// [/out/a.js] | ||
export {}; | ||
//// [/out/b.js] | ||
export var b = 0; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
=== /src/a.ts === | ||
import { b } from "./b"; | ||
>b : Symbol(b, Decl(a.ts, 0, 8)) | ||
|
||
=== /src/b.ts === | ||
export const b = 0; | ||
>b : Symbol(b, Decl(b.ts, 0, 12)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
=== /src/a.ts === | ||
import { b } from "./b"; | ||
>b : any | ||
|
||
=== /src/b.ts === | ||
export const b = 0; | ||
>b : 0 | ||
>0 : 0 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/src/a.ts(1,19): error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './b.js'? | ||
|
||
|
||
==== /tsconfig.json (0 errors) ==== | ||
{ | ||
"compilerOptions": { | ||
"module": "nodenext", | ||
"outDir": "out" | ||
} | ||
} | ||
|
||
==== /src/a.ts (1 errors) ==== | ||
import { b } from "./b"; | ||
~~~~~ | ||
!!! error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './b.js'? | ||
|
||
==== /src/b.ts (0 errors) ==== | ||
export const b = 0; | ||
|
||
==== /out/package.json (0 errors) ==== | ||
{ "type": "module" } | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
//// [tests/cases/conformance/node/nodeModulesOutDirPackageJson02_tsconfig.ts] //// | ||
|
||
//// [package.json] | ||
{ "type": "module" } | ||
|
||
//// [a.ts] | ||
import { b } from "./b"; | ||
|
||
//// [b.ts] | ||
export const b = 0; | ||
|
||
|
||
//// [/out/a.js] | ||
export {}; | ||
//// [/out/b.js] | ||
export const b = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need separate functions for these. whats different. i couldnt make out from the function,
configFile used in the helper method only needs options and filenames (root file names) so we have that in program so we could change to
Pick<ParsedCommandLine, "options" | "fileNames">
and that should work right. or am i missing something.