diff --git a/docs/packages/TypeScript_ESTree.mdx b/docs/packages/TypeScript_ESTree.mdx index d865ebb717dc..c00bd325b6ad 100644 --- a/docs/packages/TypeScript_ESTree.mdx +++ b/docs/packages/TypeScript_ESTree.mdx @@ -33,6 +33,12 @@ Parses the given string of code with the options provided and returns an ESTree- ```ts interface ParseOptions { + /** + * Specify the `sourceType`. + * For more details, see https://github.com/typescript-eslint/typescript-eslint/pull/9121 + */ + sourceType?: SourceType; + /** * Prevents the parser from throwing an error if it receives an invalid AST from TypeScript. * This case only usually occurs when attempting to lint invalid code. diff --git a/packages/typescript-estree/src/create-program/createSourceFile.ts b/packages/typescript-estree/src/create-program/createSourceFile.ts index bb5bc9d7b8fc..096264c5844f 100644 --- a/packages/typescript-estree/src/create-program/createSourceFile.ts +++ b/packages/typescript-estree/src/create-program/createSourceFile.ts @@ -23,6 +23,7 @@ function createSourceFile(parseSettings: ParseSettings): ts.SourceFile { { languageVersion: ts.ScriptTarget.Latest, jsDocParsingMode: parseSettings.jsDocParsingMode, + setExternalModuleIndicator: parseSettings.setExternalModuleIndicator, }, /* setParentNodes */ true, getScriptKind(parseSettings.filePath, parseSettings.jsx), diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index 2bae667104fc..35783e0acbe5 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -1,3 +1,5 @@ +import path from 'node:path'; + import debug from 'debug'; import * as ts from 'typescript'; @@ -46,6 +48,14 @@ export function createParseSettings( ? tsestreeOptions.tsconfigRootDir : process.cwd(); const passedLoggerFn = typeof tsestreeOptions.loggerFn === 'function'; + const filePath = ensureAbsolutePath( + typeof tsestreeOptions.filePath === 'string' && + tsestreeOptions.filePath !== '' + ? tsestreeOptions.filePath + : getFileName(tsestreeOptions.jsx), + tsconfigRootDir, + ); + const extension = path.extname(filePath).toLowerCase() as ts.Extension; const jsDocParsingMode = ((): ts.JSDocParsingMode => { switch (tsestreeOptions.jsDocParsingMode) { case 'all': @@ -81,13 +91,17 @@ export function createParseSettings( tsestreeOptions.extraFileExtensions.every(ext => typeof ext === 'string') ? tsestreeOptions.extraFileExtensions : [], - filePath: ensureAbsolutePath( - typeof tsestreeOptions.filePath === 'string' && - tsestreeOptions.filePath !== '' - ? tsestreeOptions.filePath - : getFileName(tsestreeOptions.jsx), - tsconfigRootDir, - ), + filePath, + setExternalModuleIndicator: + tsestreeOptions.sourceType === 'module' || + (tsestreeOptions.sourceType === undefined && + extension === ts.Extension.Mjs) || + (tsestreeOptions.sourceType === undefined && + extension === ts.Extension.Mts) + ? (file): void => { + file.externalModuleIndicator = true; + } + : undefined, jsDocParsingMode, jsx: tsestreeOptions.jsx === true, loc: tsestreeOptions.loc === true, diff --git a/packages/typescript-estree/src/parseSettings/index.ts b/packages/typescript-estree/src/parseSettings/index.ts index 41a21c044946..2e9b341d3dc0 100644 --- a/packages/typescript-estree/src/parseSettings/index.ts +++ b/packages/typescript-estree/src/parseSettings/index.ts @@ -72,6 +72,14 @@ export interface MutableParseSettings { */ filePath: string; + /** + * Sets the external module indicator on the source file. + * Used by Typescript to determine if a sourceFile is an external module. + * + * needed to always parsing `mjs`/`mts` files as ESM + */ + setExternalModuleIndicator?: (file: ts.SourceFile) => void; + /** * JSDoc parsing style to pass through to TypeScript */ diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 55f4d3b05ebd..44af134611c2 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -3,6 +3,7 @@ import type { DebugLevel, JSDocParsingMode, ProjectServiceOptions, + SourceType, } from '@typescript-eslint/types'; import type * as ts from 'typescript'; @@ -15,6 +16,12 @@ export type { ProjectServiceOptions } from '@typescript-eslint/types'; ////////////////////////////////////////////////////////// interface ParseOptions { + /** + * Specify the `sourceType`. + * For more details, see https://github.com/typescript-eslint/typescript-eslint/pull/9121 + */ + sourceType?: SourceType; + /** * Prevents the parser from throwing an error if it receives an invalid AST from TypeScript. * This case only usually occurs when attempting to lint invalid code. diff --git a/packages/typescript-estree/tests/lib/parse.test.ts b/packages/typescript-estree/tests/lib/parse.test.ts index dc750b9f9b63..fe9883ffe5a7 100644 --- a/packages/typescript-estree/tests/lib/parse.test.ts +++ b/packages/typescript-estree/tests/lib/parse.test.ts @@ -342,6 +342,134 @@ describe('parseAndGenerateServices', () => { }); }); + describe('ESM parsing', () => { + describe('TLA(Top Level Await)', () => { + const config: TSESTreeOptions = { + projectService: false, + comment: true, + tokens: true, + range: true, + loc: true, + }; + const code = 'await(1)'; + + const testParse = ({ + sourceType, + ext, + shouldAllowTLA = false, + }: { + sourceType?: 'module' | 'script'; + ext: '.js' | '.ts' | '.mjs' | '.mts'; + shouldAllowTLA?: boolean; + }): void => { + const ast = parser.parse(code, { + ...config, + sourceType, + filePath: `file${ext}`, + }); + const expressionType = ( + ast.body[0] as parser.TSESTree.ExpressionStatement + ).expression.type; + + it(`parse(): should ${ + shouldAllowTLA ? 'allow' : 'not allow' + } TLA for ${ext} file with sourceType = ${sourceType}`, () => { + expect(expressionType).toBe( + shouldAllowTLA + ? parser.AST_NODE_TYPES.AwaitExpression + : parser.AST_NODE_TYPES.CallExpression, + ); + }); + }; + const testParseAndGenerateServices = ({ + sourceType, + ext, + shouldAllowTLA = false, + }: { + sourceType?: 'module' | 'script'; + ext: '.js' | '.ts' | '.mjs' | '.mts'; + shouldAllowTLA?: boolean; + }): void => { + const result = parser.parseAndGenerateServices(code, { + ...config, + sourceType, + filePath: `file${ext}`, + }); + const expressionType = ( + result.ast.body[0] as parser.TSESTree.ExpressionStatement + ).expression.type; + + it(`parseAndGenerateServices(): should ${ + shouldAllowTLA ? 'allow' : 'not allow' + } TLA for ${ext} file with sourceType = ${sourceType}`, () => { + expect(expressionType).toBe( + shouldAllowTLA + ? parser.AST_NODE_TYPES.AwaitExpression + : parser.AST_NODE_TYPES.CallExpression, + ); + }); + }; + + testParse({ ext: '.js' }); + testParse({ ext: '.ts' }); + testParse({ ext: '.mjs', shouldAllowTLA: true }); + testParse({ ext: '.mts', shouldAllowTLA: true }); + + testParse({ sourceType: 'module', ext: '.js', shouldAllowTLA: true }); + testParse({ sourceType: 'module', ext: '.ts', shouldAllowTLA: true }); + testParse({ sourceType: 'module', ext: '.mjs', shouldAllowTLA: true }); + testParse({ sourceType: 'module', ext: '.mts', shouldAllowTLA: true }); + + testParse({ sourceType: 'script', ext: '.js' }); + testParse({ sourceType: 'script', ext: '.ts' }); + testParse({ sourceType: 'script', ext: '.mjs' }); + testParse({ sourceType: 'script', ext: '.mts' }); + + testParseAndGenerateServices({ ext: '.js' }); + testParseAndGenerateServices({ ext: '.ts' }); + testParseAndGenerateServices({ ext: '.mjs', shouldAllowTLA: true }); + testParseAndGenerateServices({ ext: '.mts', shouldAllowTLA: true }); + + testParseAndGenerateServices({ + sourceType: 'module', + ext: '.js', + shouldAllowTLA: true, + }); + testParseAndGenerateServices({ + sourceType: 'module', + ext: '.ts', + shouldAllowTLA: true, + }); + testParseAndGenerateServices({ + sourceType: 'module', + ext: '.mjs', + shouldAllowTLA: true, + }); + testParseAndGenerateServices({ + sourceType: 'module', + ext: '.mts', + shouldAllowTLA: true, + }); + + testParseAndGenerateServices({ + sourceType: 'script', + ext: '.js', + }); + testParseAndGenerateServices({ + sourceType: 'script', + ext: '.ts', + }); + testParseAndGenerateServices({ + sourceType: 'script', + ext: '.mjs', + }); + testParseAndGenerateServices({ + sourceType: 'script', + ext: '.mts', + }); + }); + }); + if (process.env.TYPESCRIPT_ESLINT_PROJECT_SERVICE !== 'true') { describe('invalid file error messages', () => { const PROJECT_DIR = resolve(FIXTURES_DIR, '../invalidFileErrors'); diff --git a/packages/typescript-estree/typings/typescript.d.ts b/packages/typescript-estree/typings/typescript.d.ts index 8d370d1ea696..039c9a062590 100644 --- a/packages/typescript-estree/typings/typescript.d.ts +++ b/packages/typescript-estree/typings/typescript.d.ts @@ -3,7 +3,7 @@ import 'typescript'; // these additions are marked as internal to typescript declare module 'typescript' { interface SourceFile { - externalModuleIndicator?: Node; + externalModuleIndicator?: Node | true; parseDiagnostics: DiagnosticWithLocation[]; }
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: