Skip to content

Commit 92c08fd

Browse files
committed
Add support for .cjs and .mjs input files
1 parent 32934a9 commit 92c08fd

34 files changed

+594
-51
lines changed

src/compiler/binder.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,16 @@ namespace ts {
248248

249249
if (!file.locals) {
250250
bind(file);
251+
// `bindSourceFileAsExternalModule` is called in `setCommonJsModuleIndicator`,
252+
// but if `setCommonJsModuleIndicator` was not called, as it won't be for `.cjs`
253+
// files with no imports or exports, then `bindSourceFileAsExternalModule`
254+
// will not have been called, which would cause the checker to crash.
255+
256+
// This also can't be moved to `bindSourceFileIfExternalModule`,
257+
// as that gets called after CommonJS exports are bound.
258+
if (isCommonJsModule(file) && !file.commonJsModuleIndicator) {
259+
bindSourceFileAsExternalModule();
260+
}
251261
file.symbolCount = symbolCount;
252262
file.classifiableNames = classifiableNames;
253263
delayedBindJSDocTypedefTag();
@@ -283,7 +293,7 @@ namespace ts {
283293
return true;
284294
}
285295
else {
286-
return !!file.externalModuleIndicator;
296+
return isExternalModule(file);
287297
}
288298
}
289299

@@ -2154,7 +2164,7 @@ namespace ts {
21542164
return Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode_Class_definitions_are_automatically_in_strict_mode;
21552165
}
21562166

2157-
if (file.externalModuleIndicator) {
2167+
if (isExternalModule(file)) {
21582168
return Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode_Modules_are_automatically_in_strict_mode;
21592169
}
21602170

@@ -2223,7 +2233,7 @@ namespace ts {
22232233
return Diagnostics.Invalid_use_of_0_Class_definitions_are_automatically_in_strict_mode;
22242234
}
22252235

2226-
if (file.externalModuleIndicator) {
2236+
if (isExternalModule(file)) {
22272237
return Diagnostics.Invalid_use_of_0_Modules_are_automatically_in_strict_mode;
22282238
}
22292239

@@ -2244,7 +2254,7 @@ namespace ts {
22442254
return Diagnostics.Function_declarations_are_not_allowed_inside_blocks_in_strict_mode_when_targeting_ES3_or_ES5_Class_definitions_are_automatically_in_strict_mode;
22452255
}
22462256

2247-
if (file.externalModuleIndicator) {
2257+
if (isExternalModule(file)) {
22482258
return Diagnostics.Function_declarations_are_not_allowed_inside_blocks_in_strict_mode_when_targeting_ES3_or_ES5_Modules_are_automatically_in_strict_mode;
22492259
}
22502260

@@ -2460,7 +2470,7 @@ namespace ts {
24602470
bindSpecialPropertyDeclaration(expr);
24612471
}
24622472
if (isInJSFile(expr) &&
2463-
file.commonJsModuleIndicator &&
2473+
isCommonJsModule(file) &&
24642474
isModuleExportsAccessExpression(expr) &&
24652475
!lookupSymbolForNameWorker(blockScopeContainer, "module" as __String)) {
24662476
declareSymbol(file.locals!, /*parent*/ undefined, expr.expression,
@@ -2743,7 +2753,7 @@ namespace ts {
27432753
}
27442754

27452755
function setCommonJsModuleIndicator(node: Node) {
2746-
if (file.externalModuleIndicator) {
2756+
if (isExternalModule(file)) {
27472757
return false;
27482758
}
27492759
if (!file.commonJsModuleIndicator) {
@@ -2866,7 +2876,7 @@ namespace ts {
28662876
if (hasDynamicName(node)) {
28672877
break;
28682878
}
2869-
else if ((thisContainer as SourceFile).commonJsModuleIndicator) {
2879+
else if (isCommonJsModule(thisContainer as SourceFile)) {
28702880
declareSymbol(thisContainer.symbol.exports!, thisContainer.symbol, node, SymbolFlags.Property | SymbolFlags.ExportValue, SymbolFlags.None);
28712881
}
28722882
else {
@@ -3133,7 +3143,7 @@ namespace ts {
31333143
function bindCallExpression(node: CallExpression) {
31343144
// We're only inspecting call expressions to detect CommonJS modules, so we can skip
31353145
// this check if we've already seen the module indicator
3136-
if (!file.commonJsModuleIndicator && isRequireCall(node, /*checkArgumentIsStringLiteralLike*/ false)) {
3146+
if (!file.commonJsModuleIndicator && !fileExtensionIs(file.fileName || "", Extension.Mjs) && isRequireCall(node, /*checkArgumentIsStringLiteralLike*/ false)) {
31373147
setCommonJsModuleIndicator(node);
31383148
}
31393149
}

src/compiler/checker.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,7 +1382,7 @@ namespace ts {
13821382
const useFile = getSourceFileOfNode(usage);
13831383
const declContainer = getEnclosingBlockScopeContainer(declaration);
13841384
if (declarationFile !== useFile) {
1385-
if ((moduleKind && (declarationFile.externalModuleIndicator || useFile.externalModuleIndicator)) ||
1385+
if ((moduleKind && (isExternalModule(declarationFile) || isExternalModule(useFile))) ||
13861386
(!outFile(compilerOptions)) ||
13871387
isInTypeQuery(usage) ||
13881388
declaration.flags & NodeFlags.Ambient) {
@@ -1717,7 +1717,7 @@ namespace ts {
17171717
isInExternalModule = true;
17181718
// falls through
17191719
case SyntaxKind.ModuleDeclaration:
1720-
const moduleExports = getSymbolOfNode(location as SourceFile | ModuleDeclaration).exports || emptySymbols;
1720+
const moduleExports = getSymbolOfNode(location as SourceFile | ModuleDeclaration)?.exports || emptySymbols;
17211721
if (location.kind === SyntaxKind.SourceFile || (isModuleDeclaration(location) && location.flags & NodeFlags.Ambient && !isGlobalScopeAugmentation(location))) {
17221722

17231723
// It's an external module. First see if the module has an export default and if the local
@@ -1751,7 +1751,7 @@ namespace ts {
17511751

17521752
// ES6 exports are also visible locally (except for 'default'), but commonjs exports are not (except typedefs)
17531753
if (name !== InternalSymbolName.Default && (result = lookup(moduleExports, name, meaning & SymbolFlags.ModuleMember))) {
1754-
if (isSourceFile(location) && location.commonJsModuleIndicator && !result.declarations.some(isJSDocTypeAlias)) {
1754+
if (isSourceFile(location) && isCommonJsModule(location) && !result.declarations.some(isJSDocTypeAlias)) {
17551755
result = undefined;
17561756
}
17571757
else {
@@ -1945,7 +1945,7 @@ namespace ts {
19451945
if (!result) {
19461946
if (lastLocation) {
19471947
Debug.assert(lastLocation.kind === SyntaxKind.SourceFile);
1948-
if ((lastLocation as SourceFile).commonJsModuleIndicator && name === "exports" && meaning & lastLocation.symbol.flags) {
1948+
if (isCommonJsModule(lastLocation as SourceFile) && name === "exports" && meaning & lastLocation.symbol.flags) {
19491949
return lastLocation.symbol;
19501950
}
19511951
}
@@ -2467,7 +2467,7 @@ namespace ts {
24672467
return hasExportAssignmentSymbol(moduleSymbol);
24682468
}
24692469
// JS files have a synthetic default if they do not contain ES2015+ module syntax (export = is not valid in js) _and_ do not have an __esModule marker
2470-
return !file.externalModuleIndicator && !resolveExportByName(moduleSymbol, escapeLeadingUnderscores("__esModule"), /*sourceNode*/ undefined, dontResolveAlias);
2470+
return !isExternalModule(file) && !resolveExportByName(moduleSymbol, escapeLeadingUnderscores("__esModule"), /*sourceNode*/ undefined, dontResolveAlias);
24712471
}
24722472

24732473
function getTargetOfImportClause(node: ImportClause, dontResolveAlias: boolean): Symbol | undefined {
@@ -8568,7 +8568,7 @@ namespace ts {
85688568
declaration.parent.kind === SyntaxKind.BinaryExpression)) {
85698569
return getWidenedTypeForAssignmentDeclaration(symbol);
85708570
}
8571-
else if (symbol.flags & SymbolFlags.ValueModule && declaration && isSourceFile(declaration) && declaration.commonJsModuleIndicator) {
8571+
else if (symbol.flags & SymbolFlags.ValueModule && declaration && isSourceFile(declaration) && isCommonJsModule(declaration)) {
85728572
const resolvedModule = resolveExternalModuleSymbol(symbol);
85738573
if (resolvedModule !== symbol) {
85748574
if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) {
@@ -22523,11 +22523,11 @@ namespace ts {
2252322523

2252422524
if (isSourceFile(container)) {
2252522525
// look up in the source file's locals or exports
22526-
if (container.commonJsModuleIndicator) {
22526+
if (isCommonJsModule(container)) {
2252722527
const fileSymbol = getSymbolOfNode(container);
2252822528
return fileSymbol && getTypeOfSymbol(fileSymbol);
2252922529
}
22530-
else if (container.externalModuleIndicator) {
22530+
else if (isExternalModule(container)) {
2253122531
// TODO: Maybe issue a better error than 'object is possibly undefined'
2253222532
return undefinedType;
2253322533
}
@@ -22893,7 +22893,7 @@ namespace ts {
2289322893
// Don't contextually type `this` as `exports` in `exports.Point = function(x, y) { this.x = x; this.y = y; }`
2289422894
if (inJs && isIdentifier(expression)) {
2289522895
const sourceFile = getSourceFileOfNode(parent);
22896-
if (sourceFile.commonJsModuleIndicator && getResolvedSymbol(expression) === sourceFile.symbol) {
22896+
if (isCommonJsModule(sourceFile) && getResolvedSymbol(expression) === sourceFile.symbol) {
2289722897
return undefined;
2289822898
}
2289922899
}
@@ -27788,12 +27788,17 @@ namespace ts {
2778827788
}
2778927789

2779027790
function checkImportMetaProperty(node: MetaProperty) {
27791-
if (moduleKind !== ModuleKind.ESNext && moduleKind !== ModuleKind.System) {
27792-
error(node, Diagnostics.The_import_meta_meta_property_is_only_allowed_when_the_module_option_is_esnext_or_system);
27791+
const file = getSourceFileOfNode(node);
27792+
if (fileExtensionIs(file.fileName, Extension.Cjs)) {
27793+
error(node, Diagnostics.The_import_meta_meta_property_is_not_allowed_in_CommonJS_module_files);
27794+
Debug.assert(file.externalModuleIndicator === undefined, "Containing `.cjs` file should not be a module.");
27795+
} else {
27796+
if (moduleKind !== ModuleKind.ESNext && moduleKind !== ModuleKind.System) {
27797+
error(node, Diagnostics.The_import_meta_meta_property_is_only_allowed_when_the_module_option_is_esnext_or_system);
27798+
}
27799+
Debug.assert(!!file.externalModuleIndicator, "Containing file should be a module.");
2779327800
}
27794-
const file = getSourceFileOfNode(node);
2779527801
Debug.assert(!!(file.flags & NodeFlags.PossiblyContainsImportMeta), "Containing file is missing import meta node flag.");
27796-
Debug.assert(!!file.externalModuleIndicator, "Containing file should be a module.");
2779727802
return node.name.escapedText === "meta" ? getGlobalImportMetaType() : errorType;
2779827803
}
2779927804

@@ -35466,7 +35471,8 @@ namespace ts {
3546635471
}
3546735472

3546835473
function checkGrammarModuleElementContext(node: Statement, errorMessage: DiagnosticMessage): boolean {
35469-
const isInAppropriateContext = node.parent.kind === SyntaxKind.SourceFile || node.parent.kind === SyntaxKind.ModuleBlock || node.parent.kind === SyntaxKind.ModuleDeclaration;
35474+
const isInAppropriateContext = (node.parent.kind === SyntaxKind.SourceFile && !fileExtensionIs((<SourceFile>node.parent).fileName, Extension.Cjs))
35475+
|| node.parent.kind === SyntaxKind.ModuleBlock || node.parent.kind === SyntaxKind.ModuleDeclaration;
3547035476
if (!isInAppropriateContext) {
3547135477
grammarErrorOnFirstToken(node, errorMessage);
3547235478
}

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,6 +1180,10 @@
11801180
"category": "Error",
11811181
"code": 1388
11821182
},
1183+
"The 'import.meta' meta-property is not allowed in CommonJS module files.": {
1184+
"category": "Error",
1185+
"code": 1389
1186+
},
11831187

11841188
"The types of '{0}' are incompatible between these types.": {
11851189
"category": "Error",

src/compiler/emitter.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,11 @@ namespace ts {
116116
return Extension.Json;
117117
}
118118

119+
const { fileName } = sourceFile;
120+
119121
if (options.jsx === JsxEmit.Preserve) {
120122
if (isSourceFileJS(sourceFile)) {
121-
if (fileExtensionIs(sourceFile.fileName, Extension.Jsx)) {
123+
if (fileExtensionIs(fileName, Extension.Jsx)) {
122124
return Extension.Jsx;
123125
}
124126
}
@@ -127,6 +129,16 @@ namespace ts {
127129
return Extension.Jsx;
128130
}
129131
}
132+
133+
// Preserve the output extension for `.cjs` and `.mjs` files
134+
if (fileExtensionIs(fileName, Extension.Cjs)) {
135+
return Extension.Cjs;
136+
}
137+
138+
if (fileExtensionIs(fileName, Extension.Mjs)) {
139+
return Extension.Mjs;
140+
}
141+
130142
return Extension.Js;
131143
}
132144

src/compiler/moduleNameResolver.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ namespace ts {
6565
*/
6666
enum Extensions {
6767
TypeScript, /** '.ts', '.tsx', or '.d.ts' */
68-
JavaScript, /** '.js' or '.jsx' */
68+
JavaScript, /** '.js', '.cjs', '.mjs' or '.jsx' */
6969
Json, /** '.json' */
7070
TSConfig, /** '.json' with `tsconfig` used instead of `index` */
7171
DtsOnly /** Only '.d.ts' */
@@ -1096,6 +1096,7 @@ namespace ts {
10961096
case Extensions.TypeScript:
10971097
return tryExtension(Extension.Ts) || tryExtension(Extension.Tsx) || tryExtension(Extension.Dts);
10981098
case Extensions.JavaScript:
1099+
// `.cjs` and `.mjs` file extensions must always have the file extension
10991100
return tryExtension(Extension.Js) || tryExtension(Extension.Jsx);
11001101
case Extensions.TSConfig:
11011102
case Extensions.Json:
@@ -1235,7 +1236,7 @@ namespace ts {
12351236
function extensionIsOk(extensions: Extensions, extension: Extension): boolean {
12361237
switch (extensions) {
12371238
case Extensions.JavaScript:
1238-
return extension === Extension.Js || extension === Extension.Jsx;
1239+
return extension === Extension.Js || extension === Extension.Cjs || extension === Extension.Mjs || extension === Extension.Jsx;
12391240
case Extensions.TSConfig:
12401241
case Extensions.Json:
12411242
return extension === Extension.Json;

src/compiler/moduleSpecifiers.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ namespace ts.moduleSpecifiers {
1717
ending: getEnding(),
1818
};
1919
function getEnding(): Ending {
20+
// `.mjs` files must always use file extensions in import specifiers
21+
if (fileExtensionIs(importingSourceFile.fileName || "", ".mjs")) {
22+
return Ending.JsExtension;
23+
}
2024
switch (importModuleSpecifierEnding) {
2125
case "minimal": return Ending.Minimal;
2226
case "index": return Ending.Index;
@@ -309,6 +313,8 @@ namespace ts.moduleSpecifiers {
309313
const relativePath = normalizedSourcePath !== undefined ? ensurePathIsNonModuleName(getRelativePathFromDirectory(normalizedSourcePath, normalizedTargetPath, getCanonicalFileName)) : normalizedTargetPath;
310314
return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs
311315
? removeExtensionAndIndexPostFix(relativePath, ending, compilerOptions)
316+
// `.cjs` and `.mjs` files must always be imported with the file extension:
317+
: fileExtensionIsOneOf(relativePath, [Extension.Cjs, Extension.Mjs]) ? relativePath
312318
: removeFileExtension(relativePath);
313319
}
314320

@@ -490,7 +496,8 @@ namespace ts.moduleSpecifiers {
490496
}
491497

492498
function removeExtensionAndIndexPostFix(fileName: string, ending: Ending, options: CompilerOptions): string {
493-
if (fileExtensionIs(fileName, Extension.Json)) return fileName;
499+
// `.cjs` and `.mjs` files must always be imported with the file extension, as Node doesn't resolve them by default:
500+
if (fileExtensionIsOneOf(fileName, [Extension.Cjs, Extension.Mjs, Extension.Json])) return fileName;
494501
const noExtension = removeFileExtension(fileName);
495502
switch (ending) {
496503
case Ending.Minimal:
@@ -513,6 +520,8 @@ namespace ts.moduleSpecifiers {
513520
case Extension.Tsx:
514521
return options.jsx === JsxEmit.Preserve ? Extension.Jsx : Extension.Js;
515522
case Extension.Js:
523+
case Extension.Cjs:
524+
case Extension.Mjs:
516525
case Extension.Jsx:
517526
case Extension.Json:
518527
return ext;

src/compiler/parser.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,11 @@ namespace ts {
636636

637637
// See also `isExternalOrCommonJsModule` in utilities.ts
638638
export function isExternalModule(file: SourceFile): boolean {
639-
return file.externalModuleIndicator !== undefined;
639+
return file.externalModuleIndicator !== undefined || fileExtensionIs(file.fileName || "", Extension.Mjs);
640+
}
641+
642+
export function isCommonJsModule(file: SourceFile): boolean {
643+
return file.commonJsModuleIndicator !== undefined || fileExtensionIs(file.fileName || "", Extension.Cjs);
640644
}
641645

642646
// Produces a new SourceFile for the 'newText' provided. The 'textChangeRange' parameter
@@ -1157,6 +1161,8 @@ namespace ts {
11571161
// this is quite rare comparing to other nodes and createNode should be as fast as possible
11581162
let sourceFile = factory.createSourceFile(statements, endOfFileToken, flags);
11591163
setTextRangePosWidth(sourceFile, 0, sourceText.length);
1164+
1165+
sourceFile.fileName = fileName;
11601166
setExternalModuleIndicator(sourceFile);
11611167

11621168
// If we parsed this as an external module, it may contain top-level await
@@ -1168,7 +1174,6 @@ namespace ts {
11681174
sourceFile.bindDiagnostics = [];
11691175
sourceFile.bindSuggestionDiagnostics = undefined;
11701176
sourceFile.languageVersion = languageVersion;
1171-
sourceFile.fileName = fileName;
11721177
sourceFile.languageVariant = getLanguageVariant(scriptKind);
11731178
sourceFile.isDeclarationFile = isDeclarationFile;
11741179
sourceFile.scriptKind = scriptKind;
@@ -7047,6 +7052,9 @@ namespace ts {
70477052
}
70487053

70497054
function setExternalModuleIndicator(sourceFile: SourceFile) {
7055+
// `.cjs` files are never ECMAScript modules
7056+
if (fileExtensionIs(sourceFile.fileName, Extension.Cjs)) return;
7057+
70507058
// Try to use the first top-level import/export when available, then
70517059
// fall back to looking for an 'import.meta' somewhere in the tree if necessary.
70527060
sourceFile.externalModuleIndicator =

0 commit comments

Comments
 (0)