Skip to content

.cts files are should not share the same top-level scope #49207

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

Closed
nicolo-ribaudo opened this issue May 22, 2022 · 13 comments · Fixed by #49268 or #49815
Closed

.cts files are should not share the same top-level scope #49207

nicolo-ribaudo opened this issue May 22, 2022 · 13 comments · Fixed by #49268 or #49815
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue

Comments

@nicolo-ribaudo
Copy link

nicolo-ribaudo commented May 22, 2022

Bug Report

NOTE: There are already multiple (open or closed) issues about this topic. However, this issue is specifically about the behavior in .cts files and not in .ts files.

.ts files disallow using top-level block-scoped variables with the same name because they would conflict when loaded with a <script> tag.

.cts files currently have the same restriction, however:

  • This extension is explicitly documented as referring to CommonJS files. CommonJS doesn't share a single top-level scope, because every file is wrapped in a function. This means that top-level variables with the same name will not conflict.
  • When using CommonJS files, it's common to use the same variable name when importing the same dependency in multiple files. This error makes it impossible to do so.

For these reasons, I think that this restriction should be removed.

A real word example: I'm updating typescript to 4.7 in the Babel repository, and I hoped to be able to convert our remaining JavaScript files to TypeScript. They are still JS because we needed to use the .cjs extension: https://github.com/babel/babel/tree/main/eslint/babel-eslint-parser/src. However, most of those files use the same variables names to import other files, so I'm unable to do so.

🔎 Search Terms

cts top-level Cannot redeclare block-scoped variable

🕗 Version & Regression Information

  • Tested in TS4.7, where .cts has been introduced

⏯ Playground Link

n/a (it requires multiple files)

💻 Code

file1.cts:

const { getVisitorKeys } = require("./ast-info.cjs");

file2.cts:

const { getVisitorKeys } = require("./ast-info.cjs");

🙁 Actual behavior

eslint/babel-eslint-parser/src/worker/maybeParse.cts:3:25 - error TS2451: Cannot redeclare block-scoped variable 'getVisitorKeys'.

3 const { getVisitorKeys } = require("./ast-info.cjs");
          ~~~~~~~~~~~~

  eslint/babel-eslint-parser/src/worker/handle-message.cts:3:25
    3 const { getVisitorKeys } = require("./ast-info.cjs");
              ~~~~~~~~~~~~
    'getTokLabels' was also declared here.

🙂 Expected behavior

No error

@nicolo-ribaudo
Copy link
Author

I didn't realize that CTS files are meant to be authored using ESM syntax, and then TS will properly scope them.

https://twitter.com/atcb/status/1528501621025406976

@nicolo-ribaudo nicolo-ribaudo closed this as not planned Won't fix, can't repro, duplicate, stale May 22, 2022
@robpalme
Copy link

It sounds like the problem here is that the *.cts file is classified as either script or module based on TypeScript's pre-existing disambiguation logic that is also used for *.js files. Meaning you need at least one import or export keyword to have it treated as a module.

That disambiguation is useful for *.js files. But for *.cts & *.cjs files it seems like we already know the intended target is a Node-flavored CommonJS module. And therefore top-level variables should be scoped to the module.

@robpalme
Copy link

I was wrong - the disambiguation is more subtle. The disambiguation logic in *.cjs files is more advanced and detects assignments to module.exports and exports property assignments as enabling module mode. So the incorrect erroring is limited to modules that don't export anything - at least according to static analysis.

// @target: esnext
// @module: nodenext
// @allowJs
// @checkJs

// filename: a.cjs
const same = 1;
//    ^ error TS2451: Cannot redeclare block-scoped variable 'same'.
console.log(same);

// filename: b.cjs
const same = 1;
//    ^ error TS2451: Cannot redeclare block-scoped variable 'same'.
console.log(same);

A (bad) workaround is to add an inert line to each file to trigger this module mode detection, such as:

// Inform TypeScript this file is not a script
("exports" in module) || (module.exports = {});

You have to go pretty far down the rabbit hole to figure this out, so I hope we can get auto-detection for this.

@nicolo-ribaudo
Copy link
Author

I'm seeing the error in this file, which does export something 🤔

https://github.com/nicolo-ribaudo/babel/blob/up-ts/eslint/babel-eslint-parser/src/worker/maybeParse.cts

@robpalme
Copy link

The rule appears to be:

  • in cts you need to have import / export
  • in cjs you need assignment to module.exports / exports

@Josh-Cena
Copy link
Contributor

Josh-Cena commented May 23, 2022

Repro:

// test.cts
const a = 1;

// test2.cts
const a = 1;

It's still warning about redeclaring a. The error is only gone when moduleDetection is force. IMO, it should not error even when moduleDetection is legacy, because this is a new extension and we are free to make new rules.

@fatcerberus
Copy link

IIRC the ".cts is always a module" rule only applies when using module: node16 (or nodenext). Same for .mts

@Josh-Cena
Copy link
Contributor

Tested with "module": "Node16" and "module": "NodeNext" on 4.7.1-rc—when moduleDetection is auto, there are still errors with both.

@DanielRosenwasser DanielRosenwasser added this to the TypeScript 4.7.3 milestone May 25, 2022
@DanielRosenwasser DanielRosenwasser added the Bug A bug in TypeScript label May 25, 2022
@weswigham
Copy link
Member

moduleDetection: force will force module parsing~

@weswigham
Copy link
Member

weswigham commented May 25, 2022

Just a small update: we discussed this a bit today - we'll update auto to always interpret cjs and cts files as modules, but we'll also make module: node16 (and nodenext) imply moduleDetection: force, since in reality nobody actually wants any of their files to be interpreted as scripts when targeting node explicitly. Even though these are technically breaking changes, they probably won't break any real code and we'll probably ship them in the next patch. (Assuming we don't need any API breaks to support them.)

@nicolo-ribaudo
Copy link
Author

Is this supposed to work by default in TC 4.7.3? I'm still getting errors like this if I don't enable "moduleDetection": "force":

'analyze-scope.cts' cannot be compiled under '--isolatedModules' because it is considered a global script file. Add an import, export, or an empty 'export {}' statement to make it a module.

This is my config:

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "lib": ["esnext"],
    "declaration": true,
    "declarationMap": true,
    "emitDeclarationOnly": true,
    "declarationDir": "./dts",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "isolatedModules": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "noImplicitThis": true
  }
}

@RyanCavanaugh
Copy link
Member

@nicolo-ribaudo does it work in nightly? Trying to figure out if the merge went badly or if it's just broken

@nicolo-ribaudo
Copy link
Author

It also fails with 4.8.0-dev.20220611. Are there some debug info to see what module type and why are my files according to tsc?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment