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

cts, mts, cjs, mjs, etc extension support #67

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/compiler/builderState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ namespace ts {
);
const firstDts = firstOrUndefined(emitOutput.outputFiles);
if (firstDts) {
Debug.assert(fileExtensionIs(firstDts.name, Extension.Dts), "File extension for signature expected to be dts", () => `Found: ${getAnyExtensionFromPath(firstDts.name)} for ${firstDts.name}:: All output files: ${JSON.stringify(emitOutput.outputFiles.map(f => f.name))}`);
Debug.assert(fileExtensionIsOneOf(firstDts.name, [Extension.Dts, Extension.Dmts, Extension.Dcts]), "File extension for signature expected to be dts", () => `Found: ${getAnyExtensionFromPath(firstDts.name)} for ${firstDts.name}:: All output files: ${JSON.stringify(emitOutput.outputFiles.map(f => f.name))}`);
latestSignature = (computeHash || generateDjb2Hash)(firstDts.text);
if (exportedModulesMapCache && latestSignature !== prevSignature) {
updateExportedModules(sourceFile, emitOutput.exportedModulesFromDeclarationEmit, exportedModulesMapCache);
Expand Down
12 changes: 12 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30386,6 +30386,12 @@ namespace ts {
}

function checkAssertion(node: AssertionExpression) {
if (node.kind === SyntaxKind.TypeAssertionExpression) {
const file = getSourceFileOfNode(node);
if (file && fileExtensionIsOneOf(file.fileName, [Extension.Cts, Extension.Mts])) {
grammarErrorOnNode(node, Diagnostics.This_syntax_is_reserved_in_files_with_this_extension_Use_an_as_expression_instead);
}
}
return checkAssertionWorker(node, node.type, node.expression);
}

Expand Down Expand Up @@ -41549,6 +41555,12 @@ namespace ts {
return false;
}

if (node.typeParameters && !(length(node.typeParameters) > 1 || node.typeParameters.hasTrailingComma || node.typeParameters[0].constraint)) {
if (file && fileExtensionIsOneOf(file.fileName, [Extension.Mts, Extension.Cts])) {
grammarErrorOnNode(node.typeParameters[0], Diagnostics.This_syntax_is_reserved_in_files_with_this_extension_Add_a_trailing_comma_or_explicit_constraint);
}
}

const { equalsGreaterThanToken } = node;
const startLine = getLineAndCharacterOfPosition(file, equalsGreaterThanToken.pos).line;
const endLine = getLineAndCharacterOfPosition(file, equalsGreaterThanToken.end).line;
Expand Down
45 changes: 28 additions & 17 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3214,7 +3214,7 @@ namespace ts {

let jsonOnlyIncludeRegexes: readonly RegExp[] | undefined;
if (validatedIncludeSpecs && validatedIncludeSpecs.length > 0) {
for (const file of host.readDirectory(basePath, supportedExtensionsWithJsonIfResolveJsonModule, validatedExcludeSpecs, validatedIncludeSpecs, /*depth*/ undefined)) {
for (const file of host.readDirectory(basePath, flatten(supportedExtensionsWithJsonIfResolveJsonModule), validatedExcludeSpecs, validatedIncludeSpecs, /*depth*/ undefined)) {
if (fileExtensionIs(file, Extension.Json)) {
// Valid only if *.json specified
if (!jsonOnlyIncludeRegexes) {
Expand Down Expand Up @@ -3439,16 +3439,24 @@ namespace ts {
* extension priority.
*
* @param file The path to the file.
* @param extensionPriority The priority of the extension.
* @param context The expansion context.
*/
function hasFileWithHigherPriorityExtension(file: string, literalFiles: ESMap<string, string>, wildcardFiles: ESMap<string, string>, extensions: readonly string[], keyMapper: (value: string) => string) {
const extensionPriority = getExtensionPriority(file, extensions);
const adjustedExtensionPriority = adjustExtensionPriority(extensionPriority, extensions);
for (let i = ExtensionPriority.Highest; i < adjustedExtensionPriority; i++) {
const higherPriorityExtension = extensions[i];
const higherPriorityPath = keyMapper(changeExtension(file, higherPriorityExtension));
function hasFileWithHigherPriorityExtension(file: string, literalFiles: ESMap<string, string>, wildcardFiles: ESMap<string, string>, extensions: readonly string[][], keyMapper: (value: string) => string) {
weswigham marked this conversation as resolved.
Show resolved Hide resolved
const extensionGroup = forEach(extensions, group => fileExtensionIsOneOf(file, group) ? group : undefined);
if (!extensionGroup) {
return false;
}
for (const ext of extensionGroup) {
if (fileExtensionIs(file, ext)) {
break;
weswigham marked this conversation as resolved.
Show resolved Hide resolved
}
const higherPriorityPath = keyMapper(changeExtension(file, ext));
weswigham marked this conversation as resolved.
Show resolved Hide resolved
if (literalFiles.has(higherPriorityPath) || wildcardFiles.has(higherPriorityPath)) {
if (ext === Extension.Dts && (fileExtensionIs(file, Extension.Js) || fileExtensionIs(file, Extension.Jsx))) {
// LEGACY BEHAVIOR: An off-by-one bug somewhere in the extension priority system for wildcard module loading allowed declaration
// files to be loaded alongside their js(x) counterparts. We regard this as generally undesirable, but retain the behavior to
// prevent breakage.
continue;
}
return true;
}
}
Expand All @@ -3461,15 +3469,18 @@ namespace ts {
* already been included.
*
* @param file The path to the file.
* @param extensionPriority The priority of the extension.
* @param context The expansion context.
*/
function removeWildcardFilesWithLowerPriorityExtension(file: string, wildcardFiles: ESMap<string, string>, extensions: readonly string[], keyMapper: (value: string) => string) {
const extensionPriority = getExtensionPriority(file, extensions);
const nextExtensionPriority = getNextLowestExtensionPriority(extensionPriority, extensions);
for (let i = nextExtensionPriority; i < extensions.length; i++) {
const lowerPriorityExtension = extensions[i];
const lowerPriorityPath = keyMapper(changeExtension(file, lowerPriorityExtension));
function removeWildcardFilesWithLowerPriorityExtension(file: string, wildcardFiles: ESMap<string, string>, extensions: readonly string[][], keyMapper: (value: string) => string) {
const extensionGroup = forEach(extensions, group => fileExtensionIsOneOf(file, group) ? group : undefined);
if (!extensionGroup) {
return;
}
for (let i = extensionGroup.length - 1; i >= 0; i--) {
const ext = extensionGroup[i];
if (fileExtensionIs(file, ext)) {
break;
weswigham marked this conversation as resolved.
Show resolved Hide resolved
}
const lowerPriorityPath = keyMapper(changeExtension(file, ext));
wildcardFiles.delete(lowerPriorityPath);
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5975,6 +5975,14 @@
"category": "Error",
"code": 7057
},
"This syntax is reserved in files with this extension. Use an `as` expression instead.": {
weswigham marked this conversation as resolved.
Show resolved Hide resolved
"category": "Error",
"code": 7058
},
"This syntax is reserved in files with this extension. Add a trailing comma or explicit constraint.": {
"category": "Error",
"code": 7059
},

"You cannot rename this element.": {
"category": "Error",
Expand Down
34 changes: 13 additions & 21 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ namespace ts {
return getOutputPathsForBundle(options, forceDtsPaths);
}
else {
const ownOutputFilePath = getOwnEmitOutputFilePath(sourceFile.fileName, host, getOutputExtension(sourceFile, options));
const ownOutputFilePath = getOwnEmitOutputFilePath(sourceFile.fileName, host, getOutputExtension(sourceFile.fileName, options));
const isJsonFile = isJsonSourceFile(sourceFile);
// If json file emits to the same location skip writing it, if emitDeclarationOnly skip writing it
const isJsonEmittedToSameLocation = isJsonFile &&
Expand All @@ -106,26 +106,23 @@ namespace ts {
return (options.sourceMap && !options.inlineSourceMap) ? jsFilePath + ".map" : undefined;
}

// JavaScript files are always LanguageVariant.JSX, as JSX syntax is allowed in .js files also.
// So for JavaScript files, '.jsx' is only emitted if the input was '.jsx', and JsxEmit.Preserve.
// For TypeScript, the only time to emit with a '.jsx' extension, is on JSX input, and JsxEmit.Preserve
/* @internal */
export function getOutputExtension(sourceFile: SourceFile, options: CompilerOptions): Extension {
if (isJsonSourceFile(sourceFile)) {
export function getOutputExtension(fileName: string, options: CompilerOptions): Extension {
weswigham marked this conversation as resolved.
Show resolved Hide resolved
if (fileExtensionIs(fileName, Extension.Json)) {
return Extension.Json;
}

if (options.jsx === JsxEmit.Preserve) {
if (isSourceFileJS(sourceFile)) {
if (fileExtensionIs(sourceFile.fileName, Extension.Jsx)) {
return Extension.Jsx;
}
}
else if (sourceFile.languageVariant === LanguageVariant.JSX) {
// TypeScript source file preserving JSX syntax
if (fileExtensionIsOneOf(fileName, [Extension.Jsx, Extension.Tsx])) {
return Extension.Jsx;
}
}
if (fileExtensionIsOneOf(fileName, [Extension.Mts, Extension.Mjs])) {
return Extension.Mjs;
}
if (fileExtensionIsOneOf(fileName, [Extension.Cts, Extension.Cjs])) {
return Extension.Cjs;
}
return Extension.Js;
weswigham marked this conversation as resolved.
Show resolved Hide resolved
}

Expand All @@ -140,10 +137,9 @@ namespace ts {

/* @internal */
export function getOutputDeclarationFileName(inputFileName: string, configFile: ParsedCommandLine, ignoreCase: boolean, getCommonSourceDirectory?: () => string) {
Debug.assert(!fileExtensionIs(inputFileName, Extension.Dts) && !fileExtensionIs(inputFileName, Extension.Json));
return changeExtension(
getOutputPathWithoutChangingExt(inputFileName, configFile, ignoreCase, configFile.options.declarationDir || configFile.options.outDir, getCommonSourceDirectory),
Extension.Dts
getDeclarationEmitExtensionForPath(inputFileName)
);
}

Expand All @@ -152,11 +148,7 @@ namespace ts {
const isJsonFile = fileExtensionIs(inputFileName, Extension.Json);
const outputFileName = changeExtension(
getOutputPathWithoutChangingExt(inputFileName, configFile, ignoreCase, configFile.options.outDir, getCommonSourceDirectory),
isJsonFile ?
Extension.Json :
configFile.options.jsx === JsxEmit.Preserve && (fileExtensionIs(inputFileName, Extension.Tsx) || fileExtensionIs(inputFileName, Extension.Jsx)) ?
Extension.Jsx :
Extension.Js
getOutputExtension(inputFileName, configFile.options)
);
return !isJsonFile || comparePaths(inputFileName, outputFileName, Debug.checkDefined(configFile.options.configFilePath), ignoreCase) !== Comparison.EqualTo ?
outputFileName :
Expand Down Expand Up @@ -238,7 +230,7 @@ namespace ts {
export function getCommonSourceDirectoryOfConfig({ options, fileNames }: ParsedCommandLine, ignoreCase: boolean): string {
return getCommonSourceDirectory(
options,
() => filter(fileNames, file => !(options.noEmitForJsFiles && fileExtensionIsOneOf(file, supportedJSExtensions)) && !fileExtensionIs(file, Extension.Dts)),
() => filter(fileNames, file => !(options.noEmitForJsFiles && fileExtensionIsOneOf(file, supportedJSExtensionsFlat)) && !fileExtensionIs(file, Extension.Dts)),
getDirectoryPath(normalizeSlashes(Debug.checkDefined(options.configFilePath))),
createGetCanonicalFileName(!ignoreCase)
);
Expand Down
57 changes: 49 additions & 8 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1244,11 +1244,12 @@ namespace ts {
function loadModuleFromFile(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
if (extensions === Extensions.Json || extensions === Extensions.TSConfig) {
const extensionLess = tryRemoveExtension(candidate, Extension.Json);
return (extensionLess === undefined && extensions === Extensions.Json) ? undefined : tryAddingExtensions(extensionLess || candidate, extensions, onlyRecordFailures, state);
const extension = extensionLess ? candidate.substring(extensionLess.length) : "";
return (extensionLess === undefined && extensions === Extensions.Json) ? undefined : tryAddingExtensions(extensionLess || candidate, extensions, extension, onlyRecordFailures, state);
}

// First, try adding an extension. An import of "foo" could be matched by a file "foo.ts", or "foo.js" by "foo.js.ts"
const resolvedByAddingExtension = tryAddingExtensions(candidate, extensions, onlyRecordFailures, state);
const resolvedByAddingExtension = tryAddingExtensions(candidate, extensions, "", onlyRecordFailures, state);
if (resolvedByAddingExtension) {
return resolvedByAddingExtension;
}
Expand All @@ -1257,16 +1258,16 @@ namespace ts {
// e.g. "./foo.js" can be matched by "./foo.ts" or "./foo.d.ts"
if (hasJSFileExtension(candidate)) {
const extensionless = removeFileExtension(candidate);
const extension = candidate.substring(extensionless.length);
if (state.traceEnabled) {
const extension = candidate.substring(extensionless.length);
trace(state.host, Diagnostics.File_name_0_has_a_1_extension_stripping_it, candidate, extension);
}
return tryAddingExtensions(extensionless, extensions, onlyRecordFailures, state);
return tryAddingExtensions(extensionless, extensions, extension, onlyRecordFailures, state);
}
}

/** Try to return an existing file that adds one of the `extensions` to `candidate`. */
function tryAddingExtensions(candidate: string, extensions: Extensions, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
function tryAddingExtensions(candidate: string, extensions: Extensions, originalExtension: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
if (!onlyRecordFailures) {
// check if containing folder exists - if it doesn't then just record failures for all supported extensions without disk probing
const directory = getDirectoryPath(candidate);
Expand All @@ -1277,11 +1278,51 @@ namespace ts {

switch (extensions) {
case Extensions.DtsOnly:
return tryExtension(Extension.Dts);
switch (originalExtension) {
case Extension.Mjs:
case Extension.Mts:
case Extension.Dmts:
return tryExtension(Extension.Dmts);
case Extension.Cjs:
case Extension.Cts:
case Extension.Dcts:
return tryExtension(Extension.Dcts);
case Extension.Json:
candidate += Extension.Json;
return tryExtension(Extension.Dts);
default: return tryExtension(Extension.Dts);
}
case Extensions.TypeScript:
return tryExtension(Extension.Ts) || tryExtension(Extension.Tsx) || tryExtension(Extension.Dts);
switch (originalExtension) {
case Extension.Mjs:
case Extension.Mts:
case Extension.Dmts:
return tryExtension(Extension.Mts) || tryExtension(Extension.Dmts);
case Extension.Cjs:
case Extension.Cts:
case Extension.Dcts:
return tryExtension(Extension.Cts) || tryExtension(Extension.Dcts);
case Extension.Json:
candidate += Extension.Json;
return tryExtension(Extension.Dts);
default:
return tryExtension(Extension.Ts) || tryExtension(Extension.Tsx) || tryExtension(Extension.Dts);
}
case Extensions.JavaScript:
return tryExtension(Extension.Js) || tryExtension(Extension.Jsx);
switch (originalExtension) {
case Extension.Mjs:
case Extension.Mts:
case Extension.Dmts:
return tryExtension(Extension.Mjs);
case Extension.Cjs:
case Extension.Cts:
case Extension.Dcts:
return tryExtension(Extension.Cjs);
case Extension.Json:
return tryExtension(Extension.Json);
default:
return tryExtension(Extension.Js) || tryExtension(Extension.Jsx);
}
case Extensions.TSConfig:
case Extensions.Json:
return tryExtension(Extension.Json);
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/moduleSpecifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ namespace ts.moduleSpecifiers {
function tryGetAnyFileFromPath(host: ModuleSpecifierResolutionHost, path: string) {
if (!host.fileExists) return;
// We check all js, `node` and `json` extensions in addition to TS, since node module resolution would also choose those over the directory
const extensions = getSupportedExtensions({ allowJs: true }, [{ extension: "node", isMixedContent: false }, { extension: "json", isMixedContent: false, scriptKind: ScriptKind.JSON }]);
const extensions = flatten(getSupportedExtensions({ allowJs: true }, [{ extension: "node", isMixedContent: false }, { extension: "json", isMixedContent: false, scriptKind: ScriptKind.JSON }]));
for (const e of extensions) {
const fullPath = path + e;
if (host.fileExists(fullPath)) {
Expand Down
Loading