Skip to content
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

Initial support for module: node12 #44501

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2374,7 +2374,7 @@ namespace ts {

function checkStrictModeLabeledStatement(node: LabeledStatement) {
// Grammar checking for labeledStatement
if (inStrictMode && options.target! >= ScriptTarget.ES2015) {
if (inStrictMode && getEmitScriptTarget(options) >= ScriptTarget.ES2015) {
if (isDeclarationStatement(node.statement) || isVariableStatement(node.statement)) {
errorOnFirstToken(node.label, Diagnostics.A_label_is_not_allowed_here);
}
Expand Down
115 changes: 75 additions & 40 deletions src/compiler/checker.ts

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,9 @@ namespace ts {
es6: ModuleKind.ES2015,
es2015: ModuleKind.ES2015,
es2020: ModuleKind.ES2020,
esnext: ModuleKind.ESNext
esnext: ModuleKind.ESNext,
node12: ModuleKind.Node12,
nodenext: ModuleKind.NodeNext,
})),
affectsModuleResolution: true,
affectsEmit: true,
Expand Down
16 changes: 12 additions & 4 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -920,7 +920,7 @@
"category": "Error",
"code": 1322
},
"Dynamic imports are only supported when the '--module' flag is set to 'es2020', 'esnext', 'commonjs', 'amd', 'system', or 'umd'.": {
"Dynamic imports are only supported when the '--module' flag is set to 'es2020', 'esnext', 'commonjs', 'amd', 'system', 'umd', 'node12', or 'nodenext'.": {
"category": "Error",
"code": 1323
},
Expand Down Expand Up @@ -992,7 +992,7 @@
"category": "Error",
"code": 1342
},
"The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'esnext', or 'system'.": {
"The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'esnext', 'system', 'node12', or 'nodenext'.": {
"category": "Error",
"code": 1343
},
Expand Down Expand Up @@ -1116,7 +1116,7 @@
"category": "Message",
"code": 1377
},
"Top-level 'await' expressions are only allowed when the 'module' option is set to 'esnext' or 'system', and the 'target' option is set to 'es2017' or higher.": {
"Top-level 'await' expressions are only allowed when the 'module' option is set to 'esnext', 'system', or 'nodenext', and the 'target' option is set to 'es2017' or higher.": {
"category": "Error",
"code": 1378
},
Expand Down Expand Up @@ -1324,7 +1324,7 @@
"category": "Error",
"code": 1431
},
"Top-level 'for await' loops are only allowed when the 'module' option is set to 'esnext' or 'system', and the 'target' option is set to 'es2017' or higher.": {
"Top-level 'for await' loops are only allowed when the 'module' option is set to 'esnext', 'system', or 'nodenext', and the 'target' option is set to 'es2017' or higher.": {
"category": "Error",
"code": 1432
},
Expand Down Expand Up @@ -1372,6 +1372,14 @@
"category": "Error",
"code": 1443
},
"The 'import.meta' meta-property is not allowed in files which will build into CommonJS output.": {
"category": "Error",
"code": 1444
},
"Module '{0}' cannot be imported using this construct. The specifier only resolves to an es module, which cannot be imported synchronously. Use dynamic import instead.": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
"Module '{0}' cannot be imported using this construct. The specifier only resolves to an es module, which cannot be imported synchronously. Use dynamic import instead.": {
"Module '{0}' cannot be imported using this construct. The specifier only resolves to an ES module, which cannot be imported synchronously. Use a dynamic import instead.": {

"category": "Error",
"code": 1445
},

"The types of '{0}' are incompatible between these types.": {
"category": "Error",
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/factory/emitHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ namespace ts {
// ES2018 Helpers

function createAssignHelper(attributesSegments: Expression[]) {
if (context.getCompilerOptions().target! >= ScriptTarget.ES2015) {
if (getEmitScriptTarget(context.getCompilerOptions()) >= ScriptTarget.ES2015) {
return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("Object"), "assign"),
/*typeArguments*/ undefined,
attributesSegments);
Expand Down
1 change: 1 addition & 0 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5127,6 +5127,7 @@ namespace ts {
node.transformFlags =
propagateChildrenFlags(node.statements) |
propagateChildFlags(node.endOfFileToken);
node.impliedNodeFormat = source.impliedNodeFormat;
return node;
}

Expand Down
6 changes: 3 additions & 3 deletions src/compiler/factory/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ namespace ts {
if (compilerOptions.importHelpers && isEffectiveExternalModule(sourceFile, compilerOptions)) {
let namedBindings: NamedImportBindings | undefined;
const moduleKind = getEmitModuleKind(compilerOptions);
if (moduleKind >= ModuleKind.ES2015 && moduleKind <= ModuleKind.ESNext) {
if ((moduleKind >= ModuleKind.ES2015 && moduleKind <= ModuleKind.ESNext) || sourceFile.impliedNodeFormat === ModuleKind.ESNext) {
// use named imports
const helpers = getEmitHelpers(sourceFile);
if (helpers) {
Expand Down Expand Up @@ -538,9 +538,9 @@ namespace ts {
}

const moduleKind = getEmitModuleKind(compilerOptions);
let create = (hasExportStarsToExportValues || (compilerOptions.esModuleInterop && hasImportStarOrImportDefault))
let create = (hasExportStarsToExportValues || (getESModuleInterop(compilerOptions) && hasImportStarOrImportDefault))
&& moduleKind !== ModuleKind.System
&& moduleKind < ModuleKind.ES2015;
&& (moduleKind < ModuleKind.ES2015 || node.impliedNodeFormat === ModuleKind.CommonJS);
if (!create) {
const helpers = getEmitHelpers(node);
if (helpers) {
Expand Down
21 changes: 19 additions & 2 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ namespace ts {
typesVersions?: MapLike<MapLike<string[]>>;
main?: string;
tsconfig?: string;
type?: string;
}

interface PackageJson extends PackageJsonPathFields {
Expand Down Expand Up @@ -826,7 +827,20 @@ namespace ts {
else {
let moduleResolution = compilerOptions.moduleResolution;
if (moduleResolution === undefined) {
moduleResolution = getEmitModuleKind(compilerOptions) === ModuleKind.CommonJS ? ModuleResolutionKind.NodeJs : ModuleResolutionKind.Classic;
switch (getEmitModuleKind(compilerOptions)) {
case ModuleKind.CommonJS:
moduleResolution = ModuleResolutionKind.NodeJs;
break;
case ModuleKind.Node12:
moduleResolution = ModuleResolutionKind.Node12;
break;
case ModuleKind.NodeNext:
moduleResolution = ModuleResolutionKind.NodeNext;
break;
default:
moduleResolution = ModuleResolutionKind.Classic;
break;
}
if (traceEnabled) {
trace(host, Diagnostics.Module_resolution_kind_is_not_specified_using_0, ModuleResolutionKind[moduleResolution]);
}
Expand All @@ -839,6 +853,8 @@ namespace ts {

perfLogger.logStartResolveModule(moduleName /* , containingFile, ModuleResolutionKind[moduleResolution]*/);
switch (moduleResolution) {
case ModuleResolutionKind.Node12:
case ModuleResolutionKind.NodeNext: // TODO: Implement node12/nodenext resolution rules
case ModuleResolutionKind.NodeJs:
result = nodeModuleNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference);
break;
Expand Down Expand Up @@ -1319,7 +1335,8 @@ namespace ts {
versionPaths: VersionPaths | undefined;
}

function getPackageJsonInfo(packageDirectory: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PackageJsonInfo | undefined {
/*@internal*/
export function getPackageJsonInfo(packageDirectory: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PackageJsonInfo | undefined {
const { host, traceEnabled } = state;
const packageJsonPath = combinePaths(packageDirectory, "package.json");
if (onlyRecordFailures) {
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/moduleSpecifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,14 @@ namespace ts.moduleSpecifiers {
case Extension.Jsx:
case Extension.Json:
return ext;
case Extension.Dmts:
case Extension.Mts:
case Extension.Mjs:
return Extension.Mjs;
case Extension.Dcts:
case Extension.Cts:
case Extension.Cjs:
return Extension.Cjs;
default:
return undefined;
}
Expand Down
63 changes: 58 additions & 5 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,55 @@ namespace ts {
configFileParseResult.errors;
}

/**
* A function for determining if a given file is esm or cjs format, assuming modern node module resolution rules, as configured by the
* `options` parameter.
*
* @param fileName The normalized absolute path to check the format of (it need not exist on disk)
* @param [packageJsonInfoCache] A cache for package file lookups - it's best to have a cache when this function is called often
* @param host The ModuleResolutionHost which can perform the filesystem lookups for package json data
* @param options The compiler options to perform the analysis under - relevant options are `moduleResolution` and `traceResolution`
* @returns `undefined` if the path has no relevant implied format, `ModuleKind.ESNext` for esm format, and `ModuleKind.CommonJS` for cjs format
*/
export function getImpliedNodeFormatForFile(fileName: Path, packageJsonInfoCache: PackageJsonInfoCache | undefined, host: ModuleResolutionHost, options: CompilerOptions): ModuleKind.ESNext | ModuleKind.CommonJS | undefined {
switch (getEmitModuleResolutionKind(options)) {
case ModuleResolutionKind.Node12:
case ModuleResolutionKind.NodeNext:
return fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Mjs]) ? ModuleKind.ESNext :
fileExtensionIsOneOf(fileName, [Extension.Dcts, Extension.Cts, Extension.Cjs]) ? ModuleKind.CommonJS :
fileExtensionIsOneOf(fileName, [Extension.Dts, Extension.Ts, Extension.Tsx, Extension.Js, Extension.Jsx]) ? lookupFromPackageJson() :
undefined; // other extensions, like `json` or `tsbuildinfo`, are set as `undefined` here but they should never be fed through the transformer pipeline
default:
return undefined;
}
function lookupFromPackageJson(): ModuleKind.ESNext | ModuleKind.CommonJS {
const state: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: if getPackageJsonInfo is going to be exported, it might be nice if its parameter types (ModuleResolutionState) were too. Alternatively, Parameters<typeof getPackageJsonInfo>[2]?

host: ModuleResolutionHost;
compilerOptions: CompilerOptions;
traceEnabled: boolean;
failedLookupLocations: Push<string>;
resultFromCache?: ResolvedModuleWithFailedLookupLocations;
packageJsonInfoCache: PackageJsonInfoCache | undefined;
} = {
host,
compilerOptions: options,
traceEnabled: isTraceEnabled(options, host),
failedLookupLocations: [],
packageJsonInfoCache
};
const parts = getPathComponents(fileName);
parts.pop();
while (parts.length > 0) {
const pkg = getPackageJsonInfo(getPathFromPathComponents(parts), /*onlyRecordFailures*/ false, state);
if (pkg) {
return pkg.packageJsonContent?.type === "module" ? ModuleKind.ESNext : ModuleKind.CommonJS;
}
parts.pop();
}
Comment on lines +777 to +785
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to use forEachAncestorDirectory?

return ModuleKind.CommonJS;
}
}

/**
* Determine if source file needs to be re-created even if its text hasn't changed
*/
Expand Down Expand Up @@ -1455,8 +1504,8 @@ namespace ts {

for (const oldSourceFile of oldSourceFiles) {
let newSourceFile = host.getSourceFileByPath
? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.resolvedPath, options.target!, /*onError*/ undefined, shouldCreateNewSourceFile)
: host.getSourceFile(oldSourceFile.fileName, options.target!, /*onError*/ undefined, shouldCreateNewSourceFile); // TODO: GH#18217
? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.resolvedPath, getEmitScriptTarget(options), /*onError*/ undefined, shouldCreateNewSourceFile)
: host.getSourceFile(oldSourceFile.fileName, getEmitScriptTarget(options), /*onError*/ undefined, shouldCreateNewSourceFile); // TODO: GH#18217

if (!newSourceFile) {
return StructureIsReused.Not;
Expand Down Expand Up @@ -2616,7 +2665,7 @@ namespace ts {
// We haven't looked for this file, do so now and cache result
const file = host.getSourceFile(
fileName,
options.target!,
getEmitScriptTarget(options),
hostErrorMessage => addFilePreprocessingFileExplainingDiagnostic(/*file*/ undefined, reason, Diagnostics.Cannot_read_file_0_Colon_1, [fileName, hostErrorMessage]),
shouldCreateNewSourceFile
);
Expand Down Expand Up @@ -2649,6 +2698,10 @@ namespace ts {
file.path = path;
file.resolvedPath = toPath(fileName);
file.originalFileName = originalFileName;
// 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.
file.impliedNodeFormat = getImpliedNodeFormatForFile(file.resolvedPath, moduleResolutionCache?.getPackageJsonInfoCache(), host, options);
addFileIncludeReason(file, reason);

if (host.useCaseSensitiveFileNames()) {
Expand Down Expand Up @@ -3181,7 +3234,7 @@ namespace ts {
createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "noImplicitUseStrict", "alwaysStrict");
}

const languageVersion = options.target || ScriptTarget.ES3;
const languageVersion = getEmitScriptTarget(options);

const firstNonAmbientExternalModuleSourceFile = find(files, f => isExternalModule(f) && !f.isDeclarationFile);
if (options.isolatedModules) {
Expand Down Expand Up @@ -3466,7 +3519,7 @@ namespace ts {
message = Diagnostics.File_is_library_specified_here;
break;
}
const target = forEachEntry(targetOptionDeclaration.type, (value, key) => value === options.target ? key : undefined);
const target = forEachEntry(targetOptionDeclaration.type, (value, key) => value === getEmitScriptTarget(options) ? key : undefined);
configFileNode = target ? getOptionsSyntaxByValue("target", target) : undefined;
message = Diagnostics.File_is_default_library_for_target_specified_here;
break;
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ namespace ts {
return transformECMAScriptModule;
case ModuleKind.System:
return transformSystemModule;
case ModuleKind.Node12:
case ModuleKind.NodeNext:
return transformNodeModule;
default:
return transformModule;
}
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/transformers/classFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ namespace ts {
function transformSourceFile(node: SourceFile) {
const options = context.getCompilerOptions();
if (node.isDeclarationFile
|| useDefineForClassFields && options.target === ScriptTarget.ESNext) {
|| useDefineForClassFields && getEmitScriptTarget(options) === ScriptTarget.ESNext) {
return node;
}
const visited = visitEachChild(node, visitor, context);
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/transformers/jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ namespace ts {
// When there are no attributes, React wants "null"
}
else {
const target = compilerOptions.target;
const target = getEmitScriptTarget(compilerOptions);
if (target && target >= ScriptTarget.ES2018) {
objectProperties = factory.createObjectLiteralExpression(
flatten<SpreadAssignment | PropertyAssignment>(
Expand Down
Loading