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

Add moduleDetection compiler flag to allow for changing how modules are parsed #47495

Merged
merged 10 commits into from
Mar 11, 2022

Conversation

weswigham
Copy link
Member

@weswigham weswigham commented Jan 18, 2022

The default setting is 'auto', where JSX containing files under react-jsx and react-jsxdev are
always parsed as modules, and esm-format files under module: node12+ are always parsed as modules,
in addition to the 'legacy' detection mode's conditions for other files. (Declaration files are exempt from
these new conditions)

The 'legacy' mode preserves TS's behavior prior to the introduction of this flag - a file is
parsed as a module if it contains an import, export, or import.meta expression.

In addition, there is a 'force' mode that forces all non-declaration files to be parsed as modules.
(Declaration files are still only modules if they contain a top-level import or export.)

This technically breaks the parser API, but it's kinda-sorta backwards compatible so long
as you don't need the functionality associated with more recent compiler flags.

While "auto" is a mostly backwards compatible new default (technically it breaks react-jsx output, but it does so in a case where the output was nonfunctional and fixes it, so it's fine), most people probably just want to use "force" unless they intend to have script files in their compilation.

"moduleDetection": "auto" (the new default):
Fixes #46723
Fixes #40501
Fixes #47237

"moduleDetection": "force":
Fixes #18232
Fixes #14279

…re parsed

The default setting is 'auto', where JSX containing files under react-jsx and react-jsxdev are
always parsed as modules, and esm-format files under module: node12+ are always parsed as modules,
in addition to the 'legacy' detection mode's conditions for other files. (Declaration files are exempt from
these new conditions)

The 'legacy' mode preserves TS's behavior prior to the introduction of this flag - a file is
parsed as a module if it contains an import, export, or import.meta expression.

In addition, there is a 'force' mode that forces all non-declaration files to be parsed as modules.
(Declaration files are still only modules if they contain a top-level import or export.)

This technically breaks the parser API, but it's kinda-sorta backwards compatible so long
as you don't need the functionality associated with more recent compiler flags.
@typescript-bot typescript-bot added Author: Team For Milestone Bug PRs that fix a bug with a specific milestone labels Jan 18, 2022
@weswigham weswigham requested review from andrewbranch, amcasey and sandersn and removed request for andrewbranch January 18, 2022 21:27
@sandersn
Copy link
Member

Can you add an explanation to the PR description of why 'legacy' is needed -- basically, what you expect to break if everybody were upgraded to 'auto' with no escape hatch.

@weswigham
Copy link
Member Author

weswigham commented Jan 19, 2022

Hm, I alluded to it, but it would break people with global script files using react-jsx or react-jsxdev with jsx tags in them that somehow were post-processing them to actually work (since they didn't by default, but also weren't an error). Hopefully that's a vanishingly small population.

@andrewbranch
Copy link
Member

esm-format files under module: node12+

Does this include .ts files when there’s a package.json with "type": "module"?

@weswigham
Copy link
Member Author

Yep.

src/compiler/parser.ts Outdated Show resolved Hide resolved
Comment on lines +3614 to +3618
* The callback used to set the external module indicator - this is saved to
* be later reused during incremental reparsing, which otherwise lacks the information
* to set this field
*/
/* @internal */ setExternalModuleIndicator?: (file: SourceFile) => void;
Copy link
Member

Choose a reason for hiding this comment

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

which otherwise lacks the information to set this field

Why is this the case now but not before this PR?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because the module indicator now depends on the compiler options the source file was created with, which the source file retains no direct reference to, and small incremental edits (the kind that don't do a full source file rebuild) don't pass in full options, either, they just reuse the old source file's data - so we have to keep the callback around to reuse in that incremental scenario.

Copy link
Member

Choose a reason for hiding this comment

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

Hm, I forgot that parsing in general, much less incremental parsing, doesn’t have access to CompilerOptions; only the (previously very small) subset of them that it needs. I’m a little worried that this will become a footgun whereby we or other API consumers will accidentally close over and retain something they shouldn’t in this function (it you haven’t seen the implementation, you don’t know a reference to it is stored on the SourceFile—the property is even erased from the public API!). What if CreateSourceFileOptions took the moduleDetection, module, and target from CompilerOptions and stored those, such that the function currently returned by getSetExternalModuleIndicator would have everything it needs to run on the SourceFile after creation? It would be more properties to store, but more straightforward. It took me a minute to understand the purpose of this indirection.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hm, while that would be enough for recalculating the correct closure, it doesn't do much to help the impliedNodeFormat field calculation, which needs extensive host availability anyway. I'm disinclined to make these look like CompilerOptions because of that - if you just passed in compiler options, you'd be broken in any nodenext type scenarios.

Copy link
Member

Choose a reason for hiding this comment

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

impliedNodeFormat now gets passed into createSourceFile (and I wasn’t proposing removing that, for the reason you mentioned), and I was assuming it can’t change during updateSourceFile (incremental parsing)—is that right? If so, then incremental parsing doesn’t need to worry about impliedNodeFormat.

tests/baselines/reference/api/typescript.d.ts Show resolved Hide resolved
src/services/documentRegistry.ts Show resolved Hide resolved
@fatcerberus
Copy link

esm-format files under module: node12+ are always parsed as modules

I’m unclear on what constitutes an “esm-format file” if it’s not based on the presence of import/export, seeing as ESM mode is controlled by the runtime, not the file contents. Does this mean node.js semantics, i.e. .mts is always a module, .ts is a module only if "type": "module", etc.?

If the above is the case it sounds like I probably want to use force when targeting ESM in the browser.

@weswigham
Copy link
Member Author

Yes and yes. Esm mode modules are the only ones which transpile to esm output, to whit (cjs mode files transpile to cjs). Honestly, if you're targeting browser modules you probably want moduleDetection: force and module: esnext, rather than nodenext, since you don't want cjs in your compilation at all.

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

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

I think this is a good idea. I made some low-level comments, but I don't understand the higher-level structure enough to make useful comments there.

I'll look at the tests next.

src/compiler/diagnosticMessages.json Outdated Show resolved Hide resolved
src/compiler/parser.ts Show resolved Hide resolved
src/compiler/parser.ts Outdated Show resolved Hide resolved
/**
* This is a somewhat unavoidable full tree walk to locate a JSX tag - `import.meta` requires the same,
* but we avoid that walk (or parts of it) if at all possible using the `PossiblyContainsImportMeta` node flag.
* Unfortunately, there's no `NodeFlag` space to do the same for JSX.
Copy link
Member

Choose a reason for hiding this comment

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

should there be? (there might not be any NodeFlags left in any case)

Copy link
Member Author

Choose a reason for hiding this comment

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

This bails on the first JSX tag you find in a JSX tag, which is usually almost instantly, so.... probably not? Still, I think node.transformFlags & TransformFlags.ContainsJsx probably already tracks the same thing and post-node-factory change should always be set, so... I'll try that out for narrowing the search.

src/compiler/utilities.ts Outdated Show resolved Hide resolved
src/compiler/utilities.ts Outdated Show resolved Hide resolved
src/compiler/utilities.ts Show resolved Hide resolved
src/services/transpile.ts Outdated Show resolved Hide resolved
src/services/types.ts Outdated Show resolved Hide resolved
Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

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

Done looking at the tests, just waiting on answers to earlier questions.

PR Backlog automation moved this from Waiting on reviewers to Waiting on author Mar 9, 2022
@weswigham weswigham requested a review from sandersn March 9, 2022 19:18
PR Backlog automation moved this from Waiting on author to Needs merge Mar 10, 2022
@weswigham weswigham merged commit d1fa945 into microsoft:main Mar 11, 2022
PR Backlog automation moved this from Needs merge to Done Mar 11, 2022
@nayeemrmn
Copy link

In addition, there is a 'force' mode that forces all non-declaration files to be parsed as modules.
(Declaration files are still only modules if they contain a top-level import or export.)

@weswigham Does having a .cts extension override "moduleDetection": "force"?

@weswigham
Copy link
Member Author

A .cts module is still a module, just a cjs format one instead of an esm one. The flag on force disables script mode files, which are like, the old global browser global-type scripts.

@nayeemrmn
Copy link

Ah okay, the script mode stuff doesn't refer to cjs but old-fashioned browser scripts. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team Breaking Change Would introduce errors in existing code For Milestone Bug PRs that fix a bug with a specific milestone
Projects
Archived in project
PR Backlog
  
Done
8 participants