├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── comment-perf.yml ├── .gitignore ├── .mocharc.js ├── .npmignore ├── .nycrc ├── AUTHORS ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── benchmark ├── benchmark.ts ├── compare-performance.ts ├── loadProjectFiles.ts ├── microbenchmark.ts ├── package.json ├── profile.ts ├── sample │ ├── expression.ts │ └── sample.tsx └── yarn.lock ├── bin ├── sucrase └── sucrase-node ├── codecov.yml ├── docs └── PROJECT_VISION.md ├── example-runner ├── example-configs │ ├── apollo-client.patch │ ├── apollo-client.revision │ ├── babel.patch │ ├── babel.revision │ ├── coffee-lex.patch │ ├── coffee-lex.revision │ ├── decaffeinate-parser.patch │ ├── decaffeinate-parser.revision │ ├── decaffeinate.patch │ ├── decaffeinate.revision │ ├── react.patch │ ├── react.revision │ ├── tslint.patch │ └── tslint.revision └── example-runner.ts ├── generator ├── generate.ts ├── generateReadWordTree.ts └── generateTokenTypes.ts ├── integration-test ├── integration-tests.ts ├── package.json ├── test-cases │ ├── cli-cases │ │ ├── respects-disable-es-transforms │ │ │ ├── dist-expected │ │ │ │ └── main.js │ │ │ ├── src │ │ │ │ └── main.ts │ │ │ └── test.json │ │ ├── respects-inject-create-require-for-import-require │ │ │ ├── dist-expected │ │ │ │ └── main.js │ │ │ ├── src │ │ │ │ └── main.ts │ │ │ └── test.json │ │ ├── respects-jsx-defaults │ │ │ ├── dist-expected │ │ │ │ └── main.js │ │ │ ├── src │ │ │ │ └── main.jsx │ │ │ └── test.json │ │ ├── respects-jsx-runtime-automatic │ │ │ ├── dist-expected │ │ │ │ └── main.js │ │ │ ├── src │ │ │ │ └── main.tsx │ │ │ └── test.json │ │ ├── respects-jsx-runtime-classic │ │ │ ├── dist-expected │ │ │ │ └── main.js │ │ │ ├── src │ │ │ │ └── main.jsx │ │ │ └── test.json │ │ ├── respects-jsx-runtime-preserve │ │ │ ├── dist-expected │ │ │ │ └── main.js │ │ │ ├── src │ │ │ │ └── main.tsx │ │ │ └── test.json │ │ ├── respects-keep-unused-imports │ │ │ ├── dist-expected │ │ │ │ └── main.js │ │ │ ├── src │ │ │ │ └── main.ts │ │ │ └── test.json │ │ ├── respects-preserve-dynamic-import │ │ │ ├── dist-expected │ │ │ │ └── main.js │ │ │ ├── src │ │ │ │ └── main.js │ │ │ └── test.json │ │ └── respects-tsconfig │ │ │ ├── dist-expected │ │ │ └── src │ │ │ │ ├── lib.js │ │ │ │ └── main.js │ │ │ ├── src │ │ │ ├── lib-test.ts │ │ │ ├── lib.ts │ │ │ └── main.ts │ │ │ ├── test.json │ │ │ └── tsconfig.json │ ├── jest-cases │ │ ├── allows-dynamic-import-of-esm-from-cjs │ │ │ ├── jest.config.js │ │ │ ├── main.js │ │ │ ├── main.test.ts │ │ │ └── package.json │ │ ├── allows-specifying-sucrase-options │ │ │ ├── jest.config.js │ │ │ └── main.test.tsx │ │ ├── gives-meaningful-errors-on-misconfiguration │ │ │ ├── jest.config.js │ │ │ ├── main.test.tsx │ │ │ └── test.json │ │ └── preserves-imports-in-esm-mode │ │ │ ├── jest.config.js │ │ │ ├── main.js │ │ │ ├── main.test.ts │ │ │ └── package.json │ ├── other-cases │ │ ├── README.md │ │ └── allows-inline-snapshots │ │ │ ├── jest.config.js │ │ │ └── main.test.ts │ ├── register-cases │ │ ├── allows-dynamic-import │ │ │ ├── main.js │ │ │ ├── plain-cjs-file.js │ │ │ └── transpiled-esm-file.js │ │ └── respects-preserve-dynamic-import-option │ │ │ ├── esm-file.mjs │ │ │ ├── main.js │ │ │ └── test.json │ └── ts-node-cases │ │ ├── commonjs-cases │ │ ├── allows-js-files-with-import-syntax │ │ │ ├── file.js │ │ │ ├── main.js │ │ │ └── tsconfig.json │ │ ├── dynamic-import-is-transpiled │ │ │ ├── file.ts │ │ │ └── main.ts │ │ ├── esmoduleinterop-false-is-respected │ │ │ ├── file.js │ │ │ ├── main.ts │ │ │ └── tsconfig.json │ │ ├── package.json │ │ └── tsconfig.json │ │ ├── jsx-cases │ │ ├── allows-automatic-dev-transform │ │ │ ├── main.tsx │ │ │ └── tsconfig.json │ │ ├── allows-automatic-prod-transform │ │ │ ├── main.tsx │ │ │ └── tsconfig.json │ │ ├── jsx-factory-config-is-respected │ │ │ ├── main.tsx │ │ │ └── tsconfig.json │ │ ├── jsx-files-are-transpiled │ │ │ ├── main.jsx │ │ │ └── tsconfig.json │ │ ├── jsx-import-source-is-respected │ │ │ ├── main.tsx │ │ │ └── tsconfig.json │ │ └── tsx-files-allow-jsx-syntax │ │ │ ├── main.tsx │ │ │ └── tsconfig.json │ │ ├── nodenext-cases │ │ ├── cts-can-dynamic-import-mts │ │ │ ├── file.mts │ │ │ └── main.cts │ │ ├── cts-can-import-cts │ │ │ ├── file.cts │ │ │ └── main.cts │ │ ├── cts-runs-as-cjs │ │ │ ├── main.cts │ │ │ └── package.json │ │ ├── mts-can-import-cts │ │ │ ├── file2.cts │ │ │ ├── file3.cjs │ │ │ └── main.mts │ │ ├── mts-can-import-mts │ │ │ ├── main.mts │ │ │ └── other-file.mts │ │ ├── mts-runs-as-esm │ │ │ ├── main.mts │ │ │ └── package.json │ │ ├── ts-infers-to-cjs-from-package-json │ │ │ ├── main.ts │ │ │ └── package.json │ │ ├── ts-infers-to-esm-from-package-json │ │ │ ├── main.ts │ │ │ └── package.json │ │ └── tsconfig.json │ │ └── option-cases │ │ └── respects-verbatim-module-syntax │ │ ├── main.ts │ │ ├── package.json │ │ ├── set-global-to-3.ts │ │ └── tsconfig.json ├── util │ └── assertDirectoriesEqual.ts └── yarn.lock ├── integrations ├── gulp-plugin │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── yarn.lock ├── jest-plugin │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── yarn.lock ├── webpack-loader │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── yarn.lock └── webpack-object-rest-spread-plugin │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ └── index.ts │ ├── tsconfig.json │ └── yarn.lock ├── package.json ├── prettier.config.js ├── register ├── index.js ├── js.js ├── jsx.js ├── ts-legacy-module-interop.js ├── ts.js ├── tsx-legacy-module-interop.js └── tsx.js ├── script ├── build.ts ├── lint.ts ├── mergeDirectoryContents.ts ├── mz.d.ts ├── release.ts ├── run.ts ├── sleep.ts └── util │ └── readFileContents.ts ├── spec-compliance-tests ├── README.md ├── babel-tests │ └── check-babel-tests.ts └── test262 │ ├── run-test262.ts │ └── test262Preprocessor.js ├── src ├── CJSImportProcessor.ts ├── HelperManager.ts ├── NameManager.ts ├── Options-gen-types.ts ├── Options.ts ├── TokenProcessor.ts ├── cli.ts ├── computeSourceMap.ts ├── identifyShadowedGlobals.ts ├── index.ts ├── parser │ ├── index.ts │ ├── plugins │ │ ├── flow.ts │ │ ├── jsx │ │ │ ├── index.ts │ │ │ └── xhtml.ts │ │ ├── types.ts │ │ └── typescript.ts │ ├── tokenizer │ │ ├── index.ts │ │ ├── keywords.ts │ │ ├── readWord.ts │ │ ├── readWordTree.ts │ │ ├── state.ts │ │ └── types.ts │ ├── traverser │ │ ├── base.ts │ │ ├── expression.ts │ │ ├── index.ts │ │ ├── lval.ts │ │ ├── statement.ts │ │ └── util.ts │ └── util │ │ ├── charcodes.ts │ │ ├── identifier.ts │ │ └── whitespace.ts ├── register.ts ├── transformers │ ├── CJSImportTransformer.ts │ ├── ESMImportTransformer.ts │ ├── FlowTransformer.ts │ ├── JSXTransformer.ts │ ├── JestHoistTransformer.ts │ ├── NumericSeparatorTransformer.ts │ ├── OptionalCatchBindingTransformer.ts │ ├── OptionalChainingNullishTransformer.ts │ ├── ReactDisplayNameTransformer.ts │ ├── ReactHotLoaderTransformer.ts │ ├── RootTransformer.ts │ ├── Transformer.ts │ └── TypeScriptTransformer.ts ├── tsconfig.json └── util │ ├── elideImportEquals.ts │ ├── formatTokens.ts │ ├── getClassInfo.ts │ ├── getDeclarationInfo.ts │ ├── getIdentifierNames.ts │ ├── getImportExportSpecifierInfo.ts │ ├── getJSXPragmaInfo.ts │ ├── getNonTypeIdentifiers.ts │ ├── getTSImportedNames.ts │ ├── isAsyncOperation.ts │ ├── isExportFrom.ts │ ├── isIdentifier.ts │ ├── removeMaybeImportAttributes.ts │ └── shouldElideDefaultExport.ts ├── test ├── errors-test.ts ├── flow-test.ts ├── identifyShadowedGlobals-test.ts ├── imports-test.ts ├── index-test.ts ├── jest-test.ts ├── jsx-test.ts ├── prefixes.ts ├── react-display-name-test.ts ├── react-hot-loader-test.ts ├── scopes-test.ts ├── source-maps-test.ts ├── sucrase-test.ts ├── tokens-test.ts ├── types-test.ts ├── typescript-test.ts └── util.ts ├── ts-node-plugin └── index.js ├── tsconfig.json ├── website ├── .eslintrc.js ├── .gitignore ├── README.md ├── config │ ├── env.js │ ├── jest │ │ ├── cssTransform.js │ │ └── fileTransform.js │ ├── paths.js │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ └── webpackDevServer.config.js ├── package.json ├── public │ ├── CNAME │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── index.html │ └── manifest.json ├── scripts │ ├── build.js │ ├── publish.sh │ ├── start.js │ └── test.js ├── src │ ├── App.test.js │ ├── App.tsx │ ├── CheckBox.tsx │ ├── CompareOptionsBox.tsx │ ├── Constants.ts │ ├── DebugOptionsBox.tsx │ ├── Editor.tsx │ ├── EditorWrapper.tsx │ ├── FallbackEditor.tsx │ ├── OptionsBox.tsx │ ├── SimpleSelect.tsx │ ├── SucraseOptionsBox.tsx │ ├── TextInput.tsx │ ├── URLHashState.ts │ ├── Util.ts │ ├── WorkerClient.ts │ ├── WorkerProtocol.ts │ ├── index.css │ ├── index.tsx │ └── worker │ │ ├── Babel.ts │ │ ├── Worker.worker.ts │ │ ├── getSourceMapInfo.ts │ │ └── getTokens.ts ├── tsconfig.json └── yarn.lock └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | benchmark/sample/* 2 | benchmark/node_modules/* 3 | example-runner/example-repos 4 | integration-test/test-cases 5 | spec-compliance-tests/babel-tests/babel-tests-checkout 6 | spec-compliance-tests/test262/test262-checkout 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | benchmark/sample.js linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "All tests" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | lint-and-test: 11 | name: "Lint, core tests, and spec compliance tests" 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: '16' 18 | - run: yarn 19 | - run: yarn build 20 | - run: yarn lint 21 | - run: yarn test-with-coverage && yarn report-coverage 22 | - run: yarn integration-test 23 | - run: yarn test262 24 | - run: yarn check-babel-tests 25 | test-older-node: 26 | name: "Test on older node versions" 27 | runs-on: ubuntu-latest 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | include: 32 | # We support down to node 8, but mocha requires node 14. 33 | - node_version: '14' 34 | steps: 35 | - uses: actions/checkout@v2 36 | - uses: actions/setup-node@v2 37 | with: 38 | node-version: ${{ matrix.node_version }} 39 | - run: yarn 40 | # Do a fast build to avoid trying to run yarn on subprojects. Some 41 | # website dependencies don't support old node versions, so yarn would 42 | # fail for those. 43 | - run: yarn fast-build 44 | - run: yarn test-only 45 | test-examples: 46 | name: "Test example projects" 47 | runs-on: ubuntu-latest 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | include: 52 | - projects: "babel" 53 | - projects: "react decaffeinate decaffeinate-parser coffee-lex" 54 | - projects: "tslint apollo-client" 55 | steps: 56 | - uses: actions/checkout@v2 57 | - uses: actions/setup-node@v2 58 | with: 59 | node-version: '16' 60 | - run: yarn 61 | - run: yarn fast-build 62 | - run: yarn run-examples ${{ matrix.projects }} 63 | perf-benchmark: 64 | name: "Compare performance" 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v2 68 | with: 69 | # Fetch all branches so we can compare with the default branch. 70 | fetch-depth: 0 71 | - uses: actions/setup-node@v2 72 | with: 73 | node-version: '16' 74 | - run: yarn 75 | # Clone the jest repo and run an untimed benchmark first, since the first 76 | # run immediately after cloning tends to be slower. 77 | - run: yarn benchmark jest-dev 78 | - run: yarn benchmark-compare 79 | # The benchmark-compare script leaves a ./.perf-comparison/summary.txt file 80 | # to be forwarded to the comment-perf.yml workflow via an artifact. We 81 | # also need to send the PR number. See that file and 82 | # https://securitylab.github.com/research/github-actions-preventing-pwn-requests 83 | # for details of how the two fit together. 84 | - run: echo ${{ github.event.number }} > ./.perf-comparison/pr-number.txt 85 | - uses: actions/upload-artifact@v2 86 | with: 87 | name: perf-comparison 88 | path: .perf-comparison/ 89 | -------------------------------------------------------------------------------- /.github/workflows/comment-perf.yml: -------------------------------------------------------------------------------- 1 | name: "Post performance comparison comment on PR" 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["All tests"] 6 | types: 7 | - completed 8 | 9 | # This is based off of the code snippets from: 10 | # https://securitylab.github.com/research/github-actions-preventing-pwn-requests 11 | jobs: 12 | post-perf-comparison: 13 | name: "Post performance comparison" 14 | runs-on: ubuntu-latest 15 | if: > 16 | ${{ github.event.workflow_run.event == 'pull_request' && 17 | github.event.workflow_run.conclusion == 'success' }} 18 | steps: 19 | - name: 'Download artifact' 20 | uses: actions/github-script@v3.1.0 21 | with: 22 | script: | 23 | const artifacts = await github.actions.listWorkflowRunArtifacts({ 24 | owner: context.repo.owner, 25 | repo: context.repo.repo, 26 | run_id: ${{github.event.workflow_run.id }}, 27 | }); 28 | const matchArtifact = artifacts.data.artifacts.filter((artifact) => { 29 | return artifact.name == "perf-comparison" 30 | })[0]; 31 | const download = await github.actions.downloadArtifact({ 32 | owner: context.repo.owner, 33 | repo: context.repo.repo, 34 | artifact_id: matchArtifact.id, 35 | archive_format: 'zip', 36 | }); 37 | const fs = require('fs'); 38 | fs.writeFileSync('${{github.workspace}}/perf-comparison.zip', Buffer.from(download.data)); 39 | - run: unzip perf-comparison.zip 40 | 41 | - name: 'Comment on PR' 42 | uses: actions/github-script@v3.1.0 43 | with: 44 | github-token: ${{ secrets.GITHUB_TOKEN }} 45 | script: | 46 | const fs = require('fs'); 47 | 48 | const prNumber = Number(fs.readFileSync('./pr-number.txt')); 49 | if (!prNumber) { 50 | return; 51 | } 52 | 53 | const summary = fs.readFileSync('./summary.txt').toString(); 54 | 55 | const prComments = await github.issues.listComments({ 56 | owner: context.repo.owner, 57 | repo: context.repo.repo, 58 | issue_number: prNumber, 59 | }); 60 | 61 | const existingComment = prComments.data.find( 62 | (comment) => comment.body.includes("## Benchmark results") 63 | ); 64 | 65 | if (existingComment) { 66 | await github.issues.updateComment({ 67 | owner: context.repo.owner, 68 | repo: context.repo.repo, 69 | comment_id: existingComment.id, 70 | body: summary, 71 | }); 72 | } else { 73 | await github.issues.createComment({ 74 | owner: context.repo.owner, 75 | repo: context.repo.repo, 76 | issue_number: prNumber, 77 | body: summary, 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | dist 4 | dist-self-build 5 | dist-types 6 | example-runner/example-repos 7 | integration-test/test-cases/**/dist-actual 8 | spec-compliance-tests/test262/test262-checkout 9 | spec-compliance-tests/babel-tests/babel-tests-checkout 10 | integrations/gulp-plugin/dist 11 | .nyc_output 12 | coverage.lcov 13 | benchmark/sample/jest 14 | .perf-comparison 15 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | require: "sucrase/register", 3 | }; 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !bin/** 3 | !dist/** 4 | !register/** 5 | !ts-node-plugin/** 6 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*" 4 | ], 5 | "extension": [ 6 | ".ts" 7 | ], 8 | "all": true 9 | } 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Direct contributors to the Sucrase project: 2 | Alan Pierce 3 | 4 | Sucrase contains a modified fork of Babylon, which itself was forked from Acorn. 5 | Babel and Babylon are copyright 2014-2018 Sebastian McKenzie 6 | See https://babeljs.io/team for a list of contributors to the Babel project. 7 | 8 | List of Acorn contributors: 9 | Adrian Rakovsky 10 | Alistair Braidwood 11 | Andres Suarez 12 | Aparajita Fishman 13 | Arian Stolwijk 14 | Artem Govorov 15 | Brandon Mills 16 | Charles Hughes 17 | Conrad Irwin 18 | David Bonnet 19 | Forbes Lindesay 20 | Gilad Peleg 21 | impinball 22 | Ingvar Stepanyan 23 | Jesse McCarthy 24 | Jiaxing Wang 25 | Joel Kemp 26 | Johannes Herr 27 | Jürg Lehni 28 | keeyipchan 29 | Kevin Kwok 30 | krator 31 | Marijn Haverbeke 32 | Martin Carlberg 33 | Mathias Bynens 34 | Mathieu 'p01' Henri 35 | Max Schaefer 36 | Max Zerzouri 37 | Mihai Bazon 38 | Mike Rennie 39 | Nick Fitzgerald 40 | Oskar Schöldström 41 | Paul Harper 42 | Peter Rust 43 | PlNG 44 | r-e-d 45 | Rich Harris 46 | Sebastian McKenzie 47 | zsjforcn 48 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at alangpierce@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2018 various contributors (see AUTHORS) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benchmark/loadProjectFiles.ts: -------------------------------------------------------------------------------- 1 | import {readdir, readFile, stat} from "mz/fs"; 2 | import {join} from "path"; 3 | 4 | export interface FileInfo { 5 | path: string; 6 | code: string; 7 | } 8 | 9 | export async function loadProjectFiles(projectPath: string): Promise> { 10 | const results: Array = []; 11 | async function visit(path: string): Promise { 12 | for (const child of await readdir(path)) { 13 | if (["node_modules", ".git"].includes(child)) { 14 | continue; 15 | } 16 | const childPath = join(path, child); 17 | if ((await stat(childPath)).isDirectory()) { 18 | await visit(childPath); 19 | } else if ( 20 | childPath.endsWith(".js") || 21 | childPath.endsWith(".jsx") || 22 | childPath.endsWith(".ts") || 23 | childPath.endsWith(".tsx") 24 | ) { 25 | const code = (await readFile(childPath)).toString(); 26 | results.push({code, path: childPath}); 27 | } 28 | } 29 | } 30 | await visit(projectPath); 31 | return results; 32 | } 33 | -------------------------------------------------------------------------------- /benchmark/microbenchmark.ts: -------------------------------------------------------------------------------- 1 | #!./node_modules/.bin/sucrase-node 2 | /* eslint-disable no-console */ 3 | import {next} from "../src/parser/tokenizer"; 4 | import {initParser} from "../src/parser/traverser/base"; 5 | import {hasPrecedingLineBreak} from "../src/parser/traverser/util"; 6 | 7 | function main(): void { 8 | const benchmark = process.argv[2] || "all"; 9 | console.log(`Running microbenchmark ${benchmark}`); 10 | if (benchmark === "all" || benchmark === "hasPredecingLineBreak") { 11 | initParser("let x\nx++;", false, false, false); 12 | next(); 13 | next(); 14 | next(); 15 | runMicrobenchmark( 16 | "hasPredecingLineBreak", 17 | () => { 18 | hasPrecedingLineBreak(); 19 | hasPrecedingLineBreak(); 20 | hasPrecedingLineBreak(); 21 | hasPrecedingLineBreak(); 22 | hasPrecedingLineBreak(); 23 | }, 24 | 1000000, 25 | ); 26 | } 27 | } 28 | 29 | function runMicrobenchmark(name: string, runTrial: () => void, times: number = 100): void { 30 | // Run before starting the clock to warm up the JIT, caches, etc. 31 | for (let i = 0; i < 10; i++) { 32 | runTrial(); 33 | } 34 | console.time(name); 35 | for (let i = 0; i < times; i++) { 36 | runTrial(); 37 | } 38 | console.timeEnd(name); 39 | } 40 | 41 | main(); 42 | -------------------------------------------------------------------------------- /benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sucrase-benchmark", 3 | "version": "1.0.0", 4 | "private": true, 5 | "devDependencies": { 6 | "@babel/core": "^7.18.6", 7 | "@babel/plugin-proposal-decorators": "^7.18.6", 8 | "@babel/plugin-proposal-export-namespace-from": "^7.18.6", 9 | "@babel/plugin-syntax-top-level-await": "^7.14.5", 10 | "@babel/plugin-transform-modules-commonjs": "^7.18.6", 11 | "@babel/preset-react": "^7.18.6", 12 | "@babel/preset-typescript": "^7.18.6", 13 | "@swc/core": "^1.2.215", 14 | "@types/yargs-parser": "^20.2.0", 15 | "esbuild": "^0.14.49", 16 | "mz": "^2.7.0", 17 | "typescript": "^4.7.4", 18 | "yargs-parser": "^20.2.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /benchmark/profile.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable no-console */ 3 | import * as fs from "fs"; 4 | 5 | import * as sucrase from "../src/index"; 6 | 7 | function main(): void { 8 | const sampleFile = process.argv[2] || "sample.tsx"; 9 | console.log( 10 | `Profiling Sucrase on ${sampleFile}. Make sure you have Chrome DevTools for Node open.`, 11 | ); 12 | const code = fs.readFileSync(`./benchmark/sample/${sampleFile}`).toString(); 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | (console as any).profile("Sucrase"); 15 | for (let i = 0; i < 3000; i++) { 16 | sucrase.transform(code, { 17 | transforms: ["jsx", "imports", "typescript"], 18 | }); 19 | } 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | (console as any).profileEnd("Sucrase"); 22 | } 23 | 24 | main(); 25 | -------------------------------------------------------------------------------- /bin/sucrase: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("../dist/cli").default(); 4 | -------------------------------------------------------------------------------- /bin/sucrase-node: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const Module = require("module"); 3 | const {resolve} = require("path"); 4 | 5 | /* 6 | * Simple wrapper around node that first registers Sucrase with default settings. 7 | * 8 | * This is meant for simple use cases, and doesn't support custom Node/V8 args, 9 | * executing a code snippet, a REPL, or other things that you might find in 10 | * node, babel-node, or ts-node. For more advanced use cases, you can use 11 | * `node -r sucrase/register` or register a require hook programmatically from 12 | * your own code. 13 | */ 14 | require("../register"); 15 | 16 | process.argv.splice(1, 1); 17 | process.argv[1] = resolve(process.argv[1]); 18 | Module.runMain(); 19 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | threshold: 5% 6 | -------------------------------------------------------------------------------- /example-runner/example-configs/apollo-client.revision: -------------------------------------------------------------------------------- 1 | 57b8241c6999b9a34d0d2ca9b7fab4a192f35475 2 | -------------------------------------------------------------------------------- /example-runner/example-configs/babel.revision: -------------------------------------------------------------------------------- 1 | 70c0ed512a5fb2e8f461c7d14e493a3ce131e686 2 | -------------------------------------------------------------------------------- /example-runner/example-configs/coffee-lex.patch: -------------------------------------------------------------------------------- 1 | diff --git a/package.json b/package.json 2 | index 1283c96..f041572 100644 3 | --- a/package.json 4 | +++ b/package.json 5 | @@ -11,7 +11,6 @@ 6 | "lint": "tslint --config tslint.json --project tsconfig.json --type-check", 7 | "prelint-fix": "yarn run reformat", 8 | "lint-fix": "tslint --config tslint.json --project tsconfig.json --type-check --fix", 9 | - "pretest": "yarn run lint", 10 | "test": "mocha" 11 | }, 12 | "repository": { 13 | diff --git a/test/mocha.opts b/test/mocha.opts 14 | index 4b7857d..1074784 100644 15 | --- a/test/mocha.opts 16 | +++ b/test/mocha.opts 17 | @@ -1,3 +1,3 @@ 18 | ---compilers ts:ts-node/register 19 | +--compilers ts:sucrase/register/ts 20 | --recursive 21 | test 22 | -------------------------------------------------------------------------------- /example-runner/example-configs/coffee-lex.revision: -------------------------------------------------------------------------------- 1 | f53a13a82ce8181fd274320d6d8b4bf571f39c3f 2 | -------------------------------------------------------------------------------- /example-runner/example-configs/decaffeinate-parser.patch: -------------------------------------------------------------------------------- 1 | diff --git a/jest.config.js b/jest.config.js 2 | index 87e2017..97f9819 100644 3 | --- a/jest.config.js 4 | +++ b/jest.config.js 5 | @@ -55,11 +55,11 @@ module.exports = { 6 | // globalTeardown: null, 7 | 8 | // A set of global variables that need to be available in all test environments 9 | - globals: { 10 | - 'ts-jest': { 11 | - tsConfig: 'tsconfig.json', 12 | - }, 13 | - }, 14 | + // globals: { 15 | + // 'ts-jest': { 16 | + // tsConfig: 'tsconfig.json', 17 | + // }, 18 | + // }, 19 | 20 | // An array of directory names to be searched recursively up from the requiring module's location 21 | // moduleDirectories: [ 22 | @@ -156,7 +156,7 @@ module.exports = { 23 | 24 | // A map from regular expressions to paths to transformers 25 | transform: { 26 | - '^.+\\.(ts|tsx)$': 'ts-jest', 27 | + '^.+\\.(ts|tsx)$': '@sucrase/jest-plugin', 28 | }, 29 | 30 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 31 | diff --git a/package.json b/package.json 32 | index 4094fa0..dc5e31c 100644 33 | --- a/package.json 34 | +++ b/package.json 35 | @@ -23,7 +23,6 @@ 36 | "build": "./script/build", 37 | "lint": "eslint '{src,test}/**/*.ts'", 38 | "lint:fix": "yarn lint --fix", 39 | - "pretest": "yarn lint", 40 | "test": "jest", 41 | "test:ci": "jest --ci" 42 | }, 43 | -------------------------------------------------------------------------------- /example-runner/example-configs/decaffeinate-parser.revision: -------------------------------------------------------------------------------- 1 | 580589e411e0b5829c3b504fcfe15345dc3153fb 2 | -------------------------------------------------------------------------------- /example-runner/example-configs/decaffeinate.patch: -------------------------------------------------------------------------------- 1 | diff --git a/package.json b/package.json 2 | index aa29f9f..de51dc5 100644 3 | --- a/package.json 4 | +++ b/package.json 5 | @@ -11,11 +11,8 @@ 6 | "scripts": { 7 | "lint": "tslint --config tslint.json --project tsconfig.json --type-check", 8 | "lint-fix": "tslint --config tslint.json --project tsconfig.json --type-check --fix", 9 | - "pretest": "yarn run build", 10 | - "test": "mocha 'test/**/*.ts'", 11 | - "prebuild": "rimraf dist && mkdirp dist && npm run lint", 12 | - "build": "script/build", 13 | - "prepublish": "yarn run build", 14 | + "test": "yarn build && mocha 'test/**/*.ts'", 15 | + "build": "sucrase src -d dist --transforms typescript,imports", 16 | "update-website": "ts-node ./script/update-website.ts" 17 | }, 18 | "repository": { 19 | diff --git a/test/mocha.opts b/test/mocha.opts 20 | index 8ee2787..d433cdb 100644 21 | --- a/test/mocha.opts 22 | +++ b/test/mocha.opts 23 | @@ -1,3 +1,3 @@ 24 | ---require ts-node/register 25 | +--require sucrase/register/ts 26 | --recursive 27 | --timeout 10000 28 | -------------------------------------------------------------------------------- /example-runner/example-configs/decaffeinate.revision: -------------------------------------------------------------------------------- 1 | ed52f42bd8c0cef4f3c77dfd78d233d9ef59059d 2 | -------------------------------------------------------------------------------- /example-runner/example-configs/react.patch: -------------------------------------------------------------------------------- 1 | diff --git a/package.json b/package.json 2 | index e12b9ccd9..d047834e9 100644 3 | --- a/package.json 4 | +++ b/package.json 5 | @@ -106,9 +106,8 @@ 6 | "linc": "node ./scripts/tasks/linc.js", 7 | "lint": "node ./scripts/tasks/eslint.js", 8 | "lint-build": "node ./scripts/rollup/validate/index.js", 9 | - "postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json", 10 | "debug-test": "cross-env NODE_ENV=development node --inspect-brk node_modules/.bin/jest --config ./scripts/jest/config.source.js --runInBand", 11 | - "test": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source.js", 12 | + "test": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source.js --no-cache --runInBand", 13 | "test-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source.js", 14 | "test-prod-build": "yarn test-build-prod", 15 | "test-build": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.build.js", 16 | diff --git a/scripts/babel/__tests__/transform-prevent-infinite-loops-test.js b/scripts/babel/__tests__/transform-prevent-infinite-loops-test.js 17 | index bcc508ad1..41900fa55 100644 18 | --- a/scripts/babel/__tests__/transform-prevent-infinite-loops-test.js 19 | +++ b/scripts/babel/__tests__/transform-prevent-infinite-loops-test.js 20 | @@ -11,7 +11,7 @@ describe('transform-prevent-infinite-loops', () => { 21 | // we assume that it *is* already applied. Since we expect 22 | // it to be applied to all our tests. 23 | 24 | - it('fails the test for `while` loops', () => { 25 | + it.skip('fails the test for `while` loops', () => { 26 | expect(global.infiniteLoopError).toBe(null); 27 | expect(() => { 28 | while (true) { 29 | @@ -24,7 +24,7 @@ describe('transform-prevent-infinite-loops', () => { 30 | global.infiniteLoopError = null; 31 | }); 32 | 33 | - it('fails the test for `for` loops', () => { 34 | + it.skip('fails the test for `for` loops', () => { 35 | expect(global.infiniteLoopError).toBe(null); 36 | expect(() => { 37 | for (;;) { 38 | diff --git a/scripts/jest/preprocessor.js b/scripts/jest/preprocessor.js 39 | index fc2782b0c..c97efb7f9 100644 40 | --- a/scripts/jest/preprocessor.js 41 | +++ b/scripts/jest/preprocessor.js 42 | @@ -2,7 +2,7 @@ 43 | 44 | const path = require('path'); 45 | 46 | -const babel = require('babel-core'); 47 | +const sucrase = require('sucrase'); 48 | const coffee = require('coffee-script'); 49 | 50 | const tsPreprocessor = require('./typescript/preprocessor'); 51 | @@ -52,20 +52,7 @@ module.exports = { 52 | // for test files, we also apply the async-await transform, but we want to 53 | // make sure we don't accidentally apply that transform to product code. 54 | const isTestFile = !!filePath.match(/\/__tests__\//); 55 | - return babel.transform( 56 | - src, 57 | - Object.assign( 58 | - {filename: path.relative(process.cwd(), filePath)}, 59 | - babelOptions, 60 | - isTestFile 61 | - ? { 62 | - plugins: [pathToBabelPluginAsyncToGenerator].concat( 63 | - babelOptions.plugins 64 | - ), 65 | - } 66 | - : {} 67 | - ) 68 | - ).code; 69 | + return sucrase.transform(src, {transforms: ['jsx', 'imports', 'flow'], filePath}) 70 | } 71 | return src; 72 | }, 73 | -------------------------------------------------------------------------------- /example-runner/example-configs/react.revision: -------------------------------------------------------------------------------- 1 | ef8d6d92a28835a8ab89a876f877306a5c3feffe 2 | -------------------------------------------------------------------------------- /example-runner/example-configs/tslint.revision: -------------------------------------------------------------------------------- 1 | 235561e411c58ecbd4d96844fae28e998f0ccbda 2 | -------------------------------------------------------------------------------- /generator/generate.ts: -------------------------------------------------------------------------------- 1 | #!./node_modules/.bin/sucrase-node 2 | /* eslint-disable no-console */ 3 | import {writeFile} from "mz/fs"; 4 | 5 | import run from "../script/run"; 6 | import generateReadWordTree from "./generateReadWordTree"; 7 | import generateTokenTypes from "./generateTokenTypes"; 8 | 9 | /** 10 | * Use code generation. 11 | */ 12 | async function generate(): Promise { 13 | await writeFile("./src/parser/tokenizer/types.ts", generateTokenTypes()); 14 | await run("./node_modules/.bin/prettier --write ./src/parser/tokenizer/types.ts"); 15 | await writeFile("./src/parser/tokenizer/readWordTree.ts", generateReadWordTree()); 16 | await run("./node_modules/.bin/prettier --write ./src/parser/tokenizer/readWordTree.ts"); 17 | await run("./node_modules/.bin/ts-interface-builder src/Options.ts --suffix -gen-types"); 18 | await run("./node_modules/.bin/prettier --write ./src/Options-gen-types.ts"); 19 | console.log("Done with code generation."); 20 | } 21 | 22 | generate().catch((e) => { 23 | console.error("Error during code generation!"); 24 | console.error(e); 25 | process.exitCode = 1; 26 | }); 27 | -------------------------------------------------------------------------------- /generator/generateReadWordTree.ts: -------------------------------------------------------------------------------- 1 | import {charCodes} from "../src/parser/util/charcodes"; 2 | 3 | const KEYWORDS = [ 4 | "break", 5 | "case", 6 | "catch", 7 | "continue", 8 | "debugger", 9 | "default", 10 | "do", 11 | "else", 12 | "finally", 13 | "for", 14 | "function", 15 | "if", 16 | "return", 17 | "switch", 18 | "throw", 19 | "try", 20 | "var", 21 | "while", 22 | "with", 23 | "null", 24 | "true", 25 | "false", 26 | "instanceof", 27 | "typeof", 28 | "void", 29 | "delete", 30 | "new", 31 | "in", 32 | "this", 33 | "let", 34 | "const", 35 | "class", 36 | "extends", 37 | "export", 38 | "import", 39 | "yield", 40 | "super", 41 | ]; 42 | 43 | const CONTEXTUAL_KEYWORDS = [ 44 | "abstract", 45 | "accessor", 46 | "as", 47 | "assert", 48 | "asserts", 49 | "async", 50 | "await", 51 | "checks", 52 | "constructor", 53 | "declare", 54 | "enum", 55 | "exports", 56 | "from", 57 | "get", 58 | "global", 59 | "implements", 60 | "infer", 61 | "interface", 62 | "is", 63 | "keyof", 64 | "mixins", 65 | "module", 66 | "namespace", 67 | "of", 68 | "opaque", 69 | "out", 70 | "override", 71 | "private", 72 | "protected", 73 | "proto", 74 | "public", 75 | "readonly", 76 | "require", 77 | "satisfies", 78 | "set", 79 | "static", 80 | "symbol", 81 | "type", 82 | "unique", 83 | "using", 84 | ]; 85 | 86 | const CODE = `\ 87 | // Generated file, do not edit! Run "yarn generate" to re-generate this file. 88 | import {ContextualKeyword} from "./keywords"; 89 | import {TokenType as tt} from "./types"; 90 | 91 | // prettier-ignore 92 | export const READ_WORD_TREE = new Int32Array([ 93 | $CONTENTS 94 | ]); 95 | `; 96 | 97 | interface Keyword { 98 | name: string; 99 | isContextual: boolean; 100 | } 101 | 102 | interface Node { 103 | prefix: string; 104 | data: Array; 105 | start: number; 106 | } 107 | 108 | const ALL_KEYWORDS: Array = [ 109 | ...KEYWORDS.map((name) => ({name, isContextual: false})), 110 | ...CONTEXTUAL_KEYWORDS.map((name) => ({name, isContextual: true})), 111 | ]; 112 | 113 | export default function generateReadWordTree(): string { 114 | const prefixes = new Set(); 115 | for (const {name} of ALL_KEYWORDS) { 116 | for (let i = 0; i < name.length + 1; i++) { 117 | prefixes.add(name.slice(0, i)); 118 | } 119 | } 120 | 121 | const nodesByPrefix: {[prefix: string]: Node} = {}; 122 | const nodes = [...prefixes].sort().map((prefix, i) => { 123 | const data = []; 124 | for (let j = 0; j < 27; j++) { 125 | data.push(-1); 126 | } 127 | const node = {prefix, data, start: i * 27}; 128 | nodesByPrefix[prefix] = node; 129 | return node; 130 | }); 131 | 132 | for (const {name, isContextual} of ALL_KEYWORDS) { 133 | // Fill in first index. 134 | const keywordNode = nodesByPrefix[name]; 135 | if (isContextual) { 136 | keywordNode.data[0] = `ContextualKeyword._${name} << 1`; 137 | } else { 138 | keywordNode.data[0] = `(tt._${name} << 1) + 1`; 139 | } 140 | 141 | // The later indices are transitions by lowercase letter. 142 | for (let i = 0; i < name.length; i++) { 143 | const node = nodesByPrefix[name.slice(0, i)]; 144 | const nextNode = nodesByPrefix[name.slice(0, i + 1)]; 145 | node.data[name.charCodeAt(i) - charCodes.lowercaseA + 1] = nextNode.start; 146 | } 147 | } 148 | 149 | return CODE.replace( 150 | "$CONTENTS", 151 | nodes 152 | .map( 153 | ({prefix, data}) => `\ 154 | // "${prefix}" 155 | ${data.map((datum) => `${datum},`).join(" ")}`, 156 | ) 157 | .join("\n"), 158 | ); 159 | } 160 | -------------------------------------------------------------------------------- /integration-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sucrase-integration-tests", 3 | "version": "1.0.0", 4 | "private": true, 5 | "devDependencies": { 6 | "jest": "^29.1.2", 7 | "react": "^18.2.0", 8 | "ts-node": "^10.9.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-disable-es-transforms/dist-expected/main.js: -------------------------------------------------------------------------------- 1 | class A { 2 | x = 1; 3 | constructor( y) {;this.y = y;} 4 | } 5 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-disable-es-transforms/src/main.ts: -------------------------------------------------------------------------------- 1 | class A { 2 | x: number = 1; 3 | constructor(readonly y: string) {} 4 | } 5 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-disable-es-transforms/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "cliOptions": "--transforms typescript --disable-es-transforms" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-inject-create-require-for-import-require/dist-expected/main.js: -------------------------------------------------------------------------------- 1 | import {createRequire as _createRequire} from "module"; const _require = _createRequire(import.meta.url); 2 | const A = _require('../A'); 3 | 4 | function foo() { 5 | console.log(A); 6 | } 7 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-inject-create-require-for-import-require/src/main.ts: -------------------------------------------------------------------------------- 1 | 2 | import A = require('../A'); 3 | 4 | function foo(): void { 5 | console.log(A); 6 | } 7 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-inject-create-require-for-import-require/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "cliOptions": "--transforms typescript --inject-create-require-for-import-require" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-jsx-defaults/dist-expected/main.js: -------------------------------------------------------------------------------- 1 | const _jsxFileName = "src/main.jsx";import React from 'react'; 2 | 3 | function Foo() { 4 | return React.createElement(React.Fragment, null, React.createElement('div', {__self: this, __source: {fileName: _jsxFileName, lineNumber: 4}} )); 5 | } 6 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-jsx-defaults/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Foo() { 4 | return <>
; 5 | } 6 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-jsx-defaults/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "cliOptions": "--transforms jsx" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-jsx-runtime-automatic/dist-expected/main.js: -------------------------------------------------------------------------------- 1 | const _jsxFileName = "src/main.tsx";import {jsxDEV as _jsxDEV} from "preact/jsx-dev-runtime"; 2 | function Foo() { 3 | return _jsxDEV('div', {}, void 0, false, {fileName: _jsxFileName, lineNumber: 3}, this ); 4 | } 5 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-jsx-runtime-automatic/src/main.tsx: -------------------------------------------------------------------------------- 1 | 2 | function Foo(): JSX.Element { 3 | return
; 4 | } 5 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-jsx-runtime-automatic/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "cliOptions": "--transforms jsx,typescript --jsx-runtime automatic --jsx-import-source preact" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-jsx-runtime-classic/dist-expected/main.js: -------------------------------------------------------------------------------- 1 | 2 | function Foo() { 3 | return h(React.Fragment, null, h('div', null )); 4 | } 5 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-jsx-runtime-classic/src/main.jsx: -------------------------------------------------------------------------------- 1 | 2 | function Foo() { 3 | return <>
; 4 | } 5 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-jsx-runtime-classic/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "cliOptions": "--transforms jsx --production --jsx-pragma h jsx-fragment-pragma Frag" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-jsx-runtime-preserve/dist-expected/main.js: -------------------------------------------------------------------------------- 1 | function Foo() { 2 | return
; 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-jsx-runtime-preserve/src/main.tsx: -------------------------------------------------------------------------------- 1 | function Foo(): JSX.Element { 2 | return
; 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-jsx-runtime-preserve/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "cliOptions": "--transforms jsx,typescript --jsx-runtime preserve" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-keep-unused-imports/dist-expected/main.js: -------------------------------------------------------------------------------- 1 | import A from '../A'; 2 | import B from '../B'; 3 | 4 | console.log(A); 5 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-keep-unused-imports/src/main.ts: -------------------------------------------------------------------------------- 1 | import A from '../A'; 2 | import B from '../B'; 3 | 4 | console.log(A); 5 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-keep-unused-imports/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "cliOptions": "--transforms typescript --keep-unused-imports" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-preserve-dynamic-import/dist-expected/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }var _A = require('../A'); var _A2 = _interopRequireDefault(_A); 2 | 3 | async function foo() { 4 | const B = (await import("../B")).default; 5 | console.log(_A2.default + B); 6 | } 7 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-preserve-dynamic-import/src/main.js: -------------------------------------------------------------------------------- 1 | import A from "../A"; 2 | 3 | async function foo() { 4 | const B = (await import("../B")).default; 5 | console.log(A + B); 6 | } 7 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-preserve-dynamic-import/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "cliOptions": "--transforms imports --preserve-dynamic-import" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-tsconfig/dist-expected/src/lib.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alangpierce/sucrase/61c05e1e6f29c906c432da57c91ab44660196c8d/integration-test/test-cases/cli-cases/respects-tsconfig/dist-expected/src/lib.js -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-tsconfig/dist-expected/src/main.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alangpierce/sucrase/61c05e1e6f29c906c432da57c91ab44660196c8d/integration-test/test-cases/cli-cases/respects-tsconfig/dist-expected/src/main.js -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-tsconfig/src/lib-test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alangpierce/sucrase/61c05e1e6f29c906c432da57c91ab44660196c8d/integration-test/test-cases/cli-cases/respects-tsconfig/src/lib-test.ts -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-tsconfig/src/lib.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alangpierce/sucrase/61c05e1e6f29c906c432da57c91ab44660196c8d/integration-test/test-cases/cli-cases/respects-tsconfig/src/lib.ts -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-tsconfig/src/main.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alangpierce/sucrase/61c05e1e6f29c906c432da57c91ab44660196c8d/integration-test/test-cases/cli-cases/respects-tsconfig/src/main.ts -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-tsconfig/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "cliOptions": "--project .", 3 | "disableAutomaticInputOutputDirs": true 4 | } 5 | -------------------------------------------------------------------------------- /integration-test/test-cases/cli-cases/respects-tsconfig/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/*[!-test].ts"], 3 | "compilerOptions": { 4 | "outDir": "dist-actual" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /integration-test/test-cases/jest-cases/allows-dynamic-import-of-esm-from-cjs/jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | transform: {"\\.ts$": "@sucrase/jest-plugin"}, 3 | }; 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/jest-cases/allows-dynamic-import-of-esm-from-cjs/main.js: -------------------------------------------------------------------------------- 1 | // This file isn't transpiled, so it remains ESM at runtime. 2 | export const one = 1; 3 | -------------------------------------------------------------------------------- /integration-test/test-cases/jest-cases/allows-dynamic-import-of-esm-from-cjs/main.test.ts: -------------------------------------------------------------------------------- 1 | test("addition", async () => { 2 | // Assert that module is defined to confirm that this is a CJS file. 3 | expect(module).toBeTruthy(); 4 | 5 | // This dynamic import should be preserved, not transformed to require. 6 | const {one} = await import("./main"); 7 | expect(one + one).toBe(2 as number); 8 | }); 9 | -------------------------------------------------------------------------------- /integration-test/test-cases/jest-cases/allows-dynamic-import-of-esm-from-cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/jest-cases/allows-specifying-sucrase-options/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: {"\\.(js|jsx|ts|tsx)$": ["@sucrase/jest-plugin", {jsxRuntime: "automatic"}]}, 3 | }; 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/jest-cases/allows-specifying-sucrase-options/main.test.tsx: -------------------------------------------------------------------------------- 1 | test("jsx elements", () => { 2 | // Test that the automatic runtime is being used by using JSX without a 3 | // React import. 4 | expect(
.type).toBe('div'); 5 | }); 6 | -------------------------------------------------------------------------------- /integration-test/test-cases/jest-cases/gives-meaningful-errors-on-misconfiguration/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: {"\\.(js|jsx|ts|tsx)$": ["@sucrase/jest-plugin", {invalidField: 'test'}]}, 3 | }; 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/jest-cases/gives-meaningful-errors-on-misconfiguration/main.test.tsx: -------------------------------------------------------------------------------- 1 | test("adding numbers", () => { 2 | expect(1 + 1).toBe(2); 3 | }); 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/jest-cases/gives-meaningful-errors-on-misconfiguration/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "expectedError": "invalidField is extraneous" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/jest-cases/preserves-imports-in-esm-mode/jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | transform: {"\\.ts$": "@sucrase/jest-plugin"}, 3 | extensionsToTreatAsEsm: ['.ts'], 4 | }; 5 | -------------------------------------------------------------------------------- /integration-test/test-cases/jest-cases/preserves-imports-in-esm-mode/main.js: -------------------------------------------------------------------------------- 1 | // This file isn't transpiled, so it remains ESM at runtime. 2 | export const one = 1; 3 | -------------------------------------------------------------------------------- /integration-test/test-cases/jest-cases/preserves-imports-in-esm-mode/main.test.ts: -------------------------------------------------------------------------------- 1 | // This static import should be preserved rather than transformed to require. 2 | import {one} from "./main"; 3 | 4 | test("addition", () => { 5 | expect(one + one).toBe(2 as number); 6 | }); 7 | -------------------------------------------------------------------------------- /integration-test/test-cases/jest-cases/preserves-imports-in-esm-mode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/other-cases/README.md: -------------------------------------------------------------------------------- 1 | # Other integration test cases 2 | 3 | Tests in this folder help power test cases that don't fit nicely into the usual 4 | framework of discovering and running each project as a test case. These projects 5 | are referenced directly by tests. 6 | -------------------------------------------------------------------------------- /integration-test/test-cases/other-cases/allows-inline-snapshots/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: {"\\.(js|jsx|ts|tsx)$": "@sucrase/jest-plugin"}, 3 | }; 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/other-cases/allows-inline-snapshots/main.test.ts: -------------------------------------------------------------------------------- 1 | test("fills inline snapshot", () => { 2 | expect(3 as number).toMatchInlineSnapshot(); 3 | }); 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/register-cases/allows-dynamic-import/main.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const plainCJSFile = await import("./plain-cjs-file"); 3 | const transpiledESMFile = await import("./transpiled-esm-file"); 4 | if (plainCJSFile.default !== 15) { 5 | throw new Error(); 6 | } 7 | if (transpiledESMFile.a !== 1) { 8 | throw new Error(); 9 | } 10 | if (transpiledESMFile.default !== 3) { 11 | throw new Error(); 12 | } 13 | } 14 | main(); 15 | -------------------------------------------------------------------------------- /integration-test/test-cases/register-cases/allows-dynamic-import/plain-cjs-file.js: -------------------------------------------------------------------------------- 1 | module.exports = 15; 2 | -------------------------------------------------------------------------------- /integration-test/test-cases/register-cases/allows-dynamic-import/transpiled-esm-file.js: -------------------------------------------------------------------------------- 1 | export const a = 1; 2 | export default 3; 3 | -------------------------------------------------------------------------------- /integration-test/test-cases/register-cases/respects-preserve-dynamic-import-option/esm-file.mjs: -------------------------------------------------------------------------------- 1 | export const foo = 3; 2 | -------------------------------------------------------------------------------- /integration-test/test-cases/register-cases/respects-preserve-dynamic-import-option/main.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const plainESMFile = await import("./esm-file.mjs"); 3 | if (plainESMFile.foo !== 3) { 4 | throw new Error(); 5 | } 6 | } 7 | main(); 8 | -------------------------------------------------------------------------------- /integration-test/test-cases/register-cases/respects-preserve-dynamic-import-option/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "sucraseOptions": { 3 | "preserveDynamicImport": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/commonjs-cases/allows-js-files-with-import-syntax/file.js: -------------------------------------------------------------------------------- 1 | export const x = 4; 2 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/commonjs-cases/allows-js-files-with-import-syntax/main.js: -------------------------------------------------------------------------------- 1 | import {x} from './file'; 2 | 3 | if (x !== 4) { 4 | throw new Error(); 5 | } 6 | 7 | const a = 1; 8 | // This snippet confirms that we're running in JS, not TS. In TS, it is parsed 9 | // as a function call, and in JS, it is parsed as comparison operators. 10 | const comparisonResult = a<2>(3); 11 | if (comparisonResult !== false) { 12 | throw new Error(); 13 | } 14 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/commonjs-cases/allows-js-files-with-import-syntax/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ESNext", 5 | "esModuleInterop": true, 6 | "allowJs": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/commonjs-cases/dynamic-import-is-transpiled/file.ts: -------------------------------------------------------------------------------- 1 | export const x = 7; 2 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/commonjs-cases/dynamic-import-is-transpiled/main.ts: -------------------------------------------------------------------------------- 1 | async function foo() { 2 | let calledFakeRequire = false; 3 | const require = () => { 4 | calledFakeRequire = true; 5 | } 6 | // Import should become require, which will end up calling our shadowed 7 | // declaration of require. This is different behavior from nodenext, where 8 | // import should be a true ESM import. 9 | const OtherFile = await import('./file'); 10 | if (!calledFakeRequire) { 11 | throw new Error(); 12 | } 13 | } 14 | foo(); 15 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/commonjs-cases/esmoduleinterop-false-is-respected/file.js: -------------------------------------------------------------------------------- 1 | module.exports = function twelve() { 2 | return 12; 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/commonjs-cases/esmoduleinterop-false-is-respected/main.ts: -------------------------------------------------------------------------------- 1 | import * as twelve from './file'; 2 | 3 | if (twelve() !== 12) { 4 | throw new Error(); 5 | } 6 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/commonjs-cases/esmoduleinterop-false-is-respected/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ESNext", 5 | "esModuleInterop": false 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/commonjs-cases/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/commonjs-cases/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ESNext", 5 | "esModuleInterop": true 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/jsx-cases/allows-automatic-dev-transform/main.tsx: -------------------------------------------------------------------------------- 1 | let wasCalled: boolean = false; 2 | function require(path) { 3 | if (path !== "react/jsx-dev-runtime") { 4 | throw new Error(); 5 | } 6 | return { 7 | jsxDEV: () => { 8 | wasCalled = true; 9 | } 10 | }; 11 | } 12 | 13 | const elem =
; 14 | if (!wasCalled) { 15 | throw new Error(); 16 | } 17 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/jsx-cases/allows-automatic-dev-transform/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "target": "ESNext", 5 | "esModuleInterop": true, 6 | "jsx": "react-jsxdev", 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/jsx-cases/allows-automatic-prod-transform/main.tsx: -------------------------------------------------------------------------------- 1 | let wasCalled: boolean = false; 2 | function require(path) { 3 | if (path !== "react/jsx-runtime") { 4 | throw new Error(); 5 | } 6 | return { 7 | jsx: () => { 8 | wasCalled = true; 9 | } 10 | }; 11 | } 12 | 13 | const elem =
; 14 | if (!wasCalled) { 15 | throw new Error(); 16 | } 17 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/jsx-cases/allows-automatic-prod-transform/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "target": "ESNext", 5 | "esModuleInterop": true, 6 | "jsx": "react-jsx", 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/jsx-cases/jsx-factory-config-is-respected/main.tsx: -------------------------------------------------------------------------------- 1 | let hWasCalledWithDiv = false; 2 | let hWasCalledWithFragment = false; 3 | const Fragment = {}; 4 | 5 | function h(tag) { 6 | if (tag === 'div') { 7 | hWasCalledWithDiv = true; 8 | } else if (tag === Fragment) { 9 | hWasCalledWithFragment = true; 10 | } 11 | } 12 | const elem1 =
; 13 | if (!hWasCalledWithDiv) { 14 | throw new Error(); 15 | } 16 | 17 | const elem2 = <>hello; 18 | if (!hWasCalledWithFragment) { 19 | throw new Error(); 20 | } 21 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/jsx-cases/jsx-factory-config-is-respected/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "target": "ESNext", 5 | "esModuleInterop": true, 6 | "jsx": "react", 7 | "jsxFactory": "h", 8 | "jsxFragmentFactory": "Fragment" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/jsx-cases/jsx-files-are-transpiled/main.jsx: -------------------------------------------------------------------------------- 1 | let wasCalled = false; 2 | const React = { 3 | createElement() { 4 | wasCalled = true; 5 | } 6 | } 7 | const elem =
; 8 | if (!wasCalled) { 9 | throw new Error(); 10 | } 11 | 12 | const a = 1; 13 | // This snippet confirms that we're running in JS, not TS. In TS, it is parsed 14 | // as a function call, and in JS, it is parsed as comparison operators. 15 | const comparisonResult = a<2>(3); 16 | if (comparisonResult !== false) { 17 | throw new Error(); 18 | } 19 | {} 20 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/jsx-cases/jsx-files-are-transpiled/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "target": "ESNext", 5 | "esModuleInterop": true, 6 | "jsx": "react", 7 | "allowJs": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/jsx-cases/jsx-import-source-is-respected/main.tsx: -------------------------------------------------------------------------------- 1 | let wasCalled: boolean = false; 2 | function require(path) { 3 | if (path !== "my-library/jsx-runtime") { 4 | throw new Error(); 5 | } 6 | return { 7 | jsx: () => { 8 | wasCalled = true; 9 | } 10 | }; 11 | } 12 | 13 | const elem =
; 14 | if (!wasCalled) { 15 | throw new Error(); 16 | } 17 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/jsx-cases/jsx-import-source-is-respected/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "target": "ESNext", 5 | "esModuleInterop": true, 6 | "jsx": "react-jsx", 7 | "jsxImportSource": "my-library" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/jsx-cases/tsx-files-allow-jsx-syntax/main.tsx: -------------------------------------------------------------------------------- 1 | let wasCalled: boolean = false; 2 | const React = { 3 | createElement(): void { 4 | wasCalled = true; 5 | } 6 | } 7 | const elem =
; 8 | if (!wasCalled) { 9 | throw new Error(); 10 | } 11 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/jsx-cases/tsx-files-allow-jsx-syntax/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "target": "ESNext", 5 | "esModuleInterop": true, 6 | "jsx": "react", 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/cts-can-dynamic-import-mts/file.mts: -------------------------------------------------------------------------------- 1 | // Crashes if run as CJS 2 | console.log(import.meta.url); 3 | export const x = 3; 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/cts-can-dynamic-import-mts/main.cts: -------------------------------------------------------------------------------- 1 | async function foo() { 2 | // Dynamic import is expected to be preserved so that it's possible to import 3 | // mjs files. 4 | const result = await import('./file.mjs'); 5 | if (result.x !== 3) { 6 | throw new Error(); 7 | } 8 | } 9 | foo(); 10 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/cts-can-import-cts/file.cts: -------------------------------------------------------------------------------- 1 | export const two = 2; 2 | export default 1; 3 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/cts-can-import-cts/main.cts: -------------------------------------------------------------------------------- 1 | import one, {two} from './file.cjs' 2 | 3 | if (one + two !== 3) { 4 | throw new Error(); 5 | } 6 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/cts-runs-as-cjs/main.cts: -------------------------------------------------------------------------------- 1 | // Crashes if run as ESM 2 | console.log(__dirname); 3 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/cts-runs-as-cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/mts-can-import-cts/file2.cts: -------------------------------------------------------------------------------- 1 | export = 2; 2 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/mts-can-import-cts/file3.cjs: -------------------------------------------------------------------------------- 1 | module.exports = 3; 2 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/mts-can-import-cts/main.mts: -------------------------------------------------------------------------------- 1 | import File2 from './file2.cjs'; 2 | import File3 = require("./file3.cjs"); 3 | if (File2 !== 2) { 4 | throw new Error(); 5 | } 6 | if (File3 !== 3) { 7 | throw new Error(); 8 | } 9 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/mts-can-import-mts/main.mts: -------------------------------------------------------------------------------- 1 | import {x} from './other-file.mjs'; 2 | 3 | if (x !== 3) { 4 | throw new Error(); 5 | } 6 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/mts-can-import-mts/other-file.mts: -------------------------------------------------------------------------------- 1 | export const x = 3; 2 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/mts-runs-as-esm/main.mts: -------------------------------------------------------------------------------- 1 | // Crashes if run as CJS 2 | console.log(import.meta.url); 3 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/mts-runs-as-esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/ts-infers-to-cjs-from-package-json/main.ts: -------------------------------------------------------------------------------- 1 | // Crashes if run as ESM 2 | console.log(__dirname); 3 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/ts-infers-to-cjs-from-package-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/ts-infers-to-esm-from-package-json/main.ts: -------------------------------------------------------------------------------- 1 | // Crashes if run as CJS 2 | console.log(import.meta.url); 3 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/ts-infers-to-esm-from-package-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/nodenext-cases/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "target": "ESNext", 5 | "esModuleInterop": true 6 | }, 7 | "ts-node": { 8 | "experimentalResolver": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/option-cases/respects-verbatim-module-syntax/main.ts: -------------------------------------------------------------------------------- 1 | // This import should be preserved. 2 | import A from './set-global-to-3.js' 3 | 4 | if (global.testValue !== 3) { 5 | throw new Error(); 6 | } 7 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/option-cases/respects-verbatim-module-syntax/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/option-cases/respects-verbatim-module-syntax/set-global-to-3.ts: -------------------------------------------------------------------------------- 1 | global.testValue = 3; 2 | export default 2; 3 | -------------------------------------------------------------------------------- /integration-test/test-cases/ts-node-cases/option-cases/respects-verbatim-module-syntax/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "target": "ESNext", 5 | "esModuleInterop": true, 6 | "verbatimModuleSyntax": true 7 | }, 8 | "ts-node": { 9 | "experimentalResolver": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /integration-test/util/assertDirectoriesEqual.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import {readdir, readFile, stat} from "fs/promises"; 3 | import {join} from "path"; 4 | 5 | export default async function assertDirectoriesEqual(dir1: string, dir2: string): Promise { 6 | const dir1Files = (await readdir(dir1)).sort(); 7 | const dir2Files = (await readdir(dir2)).sort(); 8 | assert.strictEqual( 9 | dir1Files.join(", "), 10 | dir2Files.join(", "), 11 | `Unexpected different lists of files for directories:\n${dir1}\n${dir2}`, 12 | ); 13 | for (const filename of dir1Files) { 14 | const path1 = join(dir1, filename); 15 | const path2 = join(dir2, filename); 16 | const isDir1 = (await stat(path1)).isDirectory(); 17 | const isDir2 = (await stat(path2)).isDirectory(); 18 | assert.strictEqual(isDir1, isDir2, `Paths are different types:\n${path1}\n${path2}`); 19 | if (isDir1) { 20 | await assertDirectoriesEqual(path1, path2); 21 | } else { 22 | const contents1 = (await readFile(path1)).toString(); 23 | const contents2 = (await readFile(path2)).toString(); 24 | assert.strictEqual(contents1, contents2, `File contents differed:\n${path1}\n${path2}`); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /integrations/gulp-plugin/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | project: `${__dirname}/tsconfig.json`, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /integrations/gulp-plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.0 (2018-06-10) 2 | 3 | * Switch Sucrase peer dependency to `^3`, update code to use new return type. 4 | -------------------------------------------------------------------------------- /integrations/gulp-plugin/README.md: -------------------------------------------------------------------------------- 1 | # Sucrase Gulp plugin 2 | 3 | [![npm version](https://badge.fury.io/js/@sucrase%2Fgulp-plugin.svg)](https://www.npmjs.com/package/@sucrase/gulp-plugin) 4 | [![MIT License](https://img.shields.io/npm/l/express.svg?maxAge=2592000)](LICENSE) 5 | 6 | This is a simple Gulp plugin that makes it easy to tie 7 | [Sucrase](https://github.com/alangpierce/sucrase) into your build. 8 | 9 | ## Usage 10 | 11 | Sucrase is a peer dependency, so you'll need to install it as well: 12 | ``` 13 | yarn add --dev @sucrase/gulp-plugin sucrase 14 | ``` 15 | 16 | In your gulpfile, you can add it like this: 17 | 18 | ``` 19 | const sucrase = require('@sucrase/gulp-plugin'); 20 | 21 | ... 22 | .pipe(sucrase({transforms: ['imports', 'jsx']})) 23 | ... 24 | ``` 25 | -------------------------------------------------------------------------------- /integrations/gulp-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sucrase/gulp-plugin", 3 | "version": "2.0.0", 4 | "description": "Gulp plugin for Sucrase", 5 | "author": "Alan Pierce ", 6 | "license": "MIT", 7 | "main": "dist/index.js", 8 | "repository": "https://github.com/alangpierce/sucrase/tree/main/integrations/gulp-plugin", 9 | "files": [ 10 | "dist" 11 | ], 12 | "peerDependencies": { 13 | "sucrase": "^3" 14 | }, 15 | "devDependencies": { 16 | "@types/plugin-error": "^0.1.1", 17 | "@types/replace-ext": "^0.0.27", 18 | "@types/through2": "^2.0.34", 19 | "sucrase": "^3.10.1" 20 | }, 21 | "dependencies": { 22 | "plugin-error": "^1.0.1", 23 | "replace-ext": "^1.0.0", 24 | "through2": "^3.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /integrations/gulp-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/first */ 2 | import PluginError = require("plugin-error"); 3 | import replaceExt = require("replace-ext"); 4 | // TODO: ESLint now rightfully complains about ESM syntax in a CJS file. 5 | // Keeping as-is to avoid unintended issues, but this could be resolved 6 | // when revisiting this plugin. 7 | // eslint-disable-next-line import/no-import-module-exports 8 | import {type Transform} from "stream"; 9 | // eslint-disable-next-line import/no-import-module-exports 10 | import {type Options, transform} from "sucrase"; 11 | import through = require("through2"); 12 | 13 | const PLUGIN_NAME = "@sucrase/gulp-plugin"; 14 | 15 | function gulpSucrase(options: Options): Transform { 16 | return through.obj(function ( 17 | this: Transform, 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | file: any, 20 | enc: string, 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | cb: (err?: any, data?: any) => void, 23 | ): void { 24 | if (file.isNull()) { 25 | cb(null, file); 26 | return; 27 | } 28 | if (file.isStream()) { 29 | cb(new PluginError(PLUGIN_NAME, "Streaming is not supported.")); 30 | return; 31 | } 32 | 33 | try { 34 | const resultCode = transform(file.contents.toString(), { 35 | filePath: file.path, 36 | ...options, 37 | }).code; 38 | file.contents = Buffer.from(resultCode); 39 | file.path = replaceExt(file.path, ".js"); 40 | this.push(file); 41 | } catch (e) { 42 | e.message = `Error when processing file ${file.path}: ${e.message}`; 43 | this.emit("error", new PluginError(PLUGIN_NAME, e)); 44 | } 45 | 46 | cb(); 47 | }); 48 | } 49 | 50 | module.exports = gulpSucrase; 51 | -------------------------------------------------------------------------------- /integrations/gulp-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true, 4 | "target": "es2017", 5 | "module": "commonjs", 6 | "typeRoots": ["node_modules/@types"], 7 | "experimentalDecorators": true, 8 | "isolatedModules": true, 9 | "noImplicitAny": true, 10 | "noImplicitReturns": true, 11 | "downlevelIteration": true, 12 | "noEmitHelpers": true, 13 | "importHelpers": true, 14 | }, 15 | "exclude": [ 16 | "node_modules", 17 | "dist/**/*", 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /integrations/jest-plugin/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | project: `${__dirname}/tsconfig.json`, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /integrations/jest-plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.0.0 (2022-10-08) 2 | 3 | * BREAKING: Switch `sucrase` to a peer dependency instead of a regular dependency. 4 | Please install `sucrase` separately as part of updating to this version. ([#756]) 5 | * BREAKING: Require `jest >= 27` and and `sucrase >= 3.25`. ([#749], [#756]) 6 | * BREAKING: Specify `disableESTransforms` by default, since it's no longer necessary 7 | for supported Node versions. ([#754]) 8 | * Add support for specifying Sucrase options in Jest config. ([#749]) (Jan Aagaard Meier; also implemented by stagas in [#663]) 9 | * Add support for using Jest in ESM mode, automatically configuring Sucrase accordingly. ([#752]) 10 | * Fix unhelpful error messages when an error is thrown from Sucrase. ([#750]) 11 | 12 | # 2.2.1 (2022-04-28) 13 | 14 | * Add support for Jest 28. ([#695]) (Viktor Luft) 15 | 16 | # 2.2.0 (2021-10-24) 17 | 18 | * Include inline source maps when transforming code. This fixes debugging in 19 | WebStorm. ([#661]) 20 | 21 | # 2.1.1 (2021-08-09) 22 | 23 | * Add support for Jest 27. ([#640]) (Victor Pontis) 24 | 25 | # 2.1.0 (2021-04-11) 26 | 27 | * Enable Sucrase's new `jest` transform to hoist `jest.mock` calls. 28 | 29 | # 2.0.0 (2018-06-10) 30 | 31 | * Switch Sucrase dependency to `^3`, update code to use new return type. 32 | 33 | [#640]: https://github.com/alangpierce/sucrase/pull/640 34 | [#661]: https://github.com/alangpierce/sucrase/pull/661 35 | [#663]: https://github.com/alangpierce/sucrase/pull/663 36 | [#695]: https://github.com/alangpierce/sucrase/pull/695 37 | [#749]: https://github.com/alangpierce/sucrase/pull/749 38 | [#750]: https://github.com/alangpierce/sucrase/pull/750 39 | [#752]: https://github.com/alangpierce/sucrase/pull/752 40 | [#754]: https://github.com/alangpierce/sucrase/pull/754 41 | [#756]: https://github.com/alangpierce/sucrase/pull/756 42 | -------------------------------------------------------------------------------- /integrations/jest-plugin/README.md: -------------------------------------------------------------------------------- 1 | # Sucrase Jest plugin 2 | 3 | [![npm version](https://badge.fury.io/js/@sucrase%2Fjest-plugin.svg)](https://www.npmjs.com/package/@sucrase/jest-plugin) 4 | [![MIT License](https://img.shields.io/npm/l/express.svg?maxAge=2592000)](LICENSE) 5 | 6 | This is a simple Jest plugin that makes it easy to use 7 | [Sucrase](https://github.com/alangpierce/sucrase) when running Jest tests. 8 | 9 | ## Usage 10 | 11 | First install the package and `sucrase` as a dev dependency: 12 | ``` 13 | yarn add --dev @sucrase/jest-plugin sucrase 14 | ``` 15 | 16 | Then change the default transform in jest.config.js file: 17 | ```ts 18 | ... 19 | transform: { "\\.(js|jsx|ts|tsx)$": "@sucrase/jest-plugin" }, 20 | ... 21 | ``` 22 | 23 | You can specify additional transformation options to Sucrase by passing an object. For example, to enable automatic react transforms: 24 | 25 | ```ts 26 | ... 27 | transform: { "\\.(js|jsx|ts|tsx)$": ["@sucrase/jest-plugin", { jsxRuntime: 'automatic' }] }, 28 | ... 29 | ``` 30 | 31 | By default, the `transforms` option is automatically detected based on file type and Jest mode. 32 | If you pass a `transforms` array in the options, it will apply to all files, regardless of extension. 33 | -------------------------------------------------------------------------------- /integrations/jest-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sucrase/jest-plugin", 3 | "version": "3.0.0", 4 | "description": "Jest plugin for Sucrase", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/alangpierce/sucrase/tree/main/integrations/jest-plugin", 7 | "author": "Alan Pierce ", 8 | "license": "MIT", 9 | "devDependencies": { 10 | "@jest/transform": "^29.1.2", 11 | "@types/node": "^16.11.4" 12 | }, 13 | "peerDependencies": { 14 | "jest": ">=27", 15 | "sucrase": ">=3.25.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /integrations/jest-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import type {TransformOptions} from "@jest/transform"; 2 | import {extname} from "path"; 3 | import {type Transform, transform} from "sucrase"; 4 | 5 | import type {Options} from "../../../src/Options"; 6 | 7 | function getTransforms(filename: string, supportsStaticESM: boolean): Array | null { 8 | const extension = extname(filename); 9 | const maybeImports: Array = supportsStaticESM ? [] : ["imports"]; 10 | if ([".js", ".jsx", ".mjs", ".cjs"].includes(extension)) { 11 | return [...maybeImports, "flow", "jsx", "jest"]; 12 | } else if (extension === ".ts") { 13 | return [...maybeImports, "typescript", "jest"]; 14 | } else if ([".tsx", ".mts", ".cts"].includes(extension)) { 15 | return [...maybeImports, "typescript", "jsx", "jest"]; 16 | } 17 | return null; 18 | } 19 | 20 | // this is compatible to the one that is required by Jest, using the type from here: 21 | // https://github.com/mozilla/source-map/blob/0.6.1/source-map.d.ts#L6-L12 22 | type RawSourceMap = ReturnType["sourceMap"]; 23 | 24 | export function process( 25 | src: string, 26 | filename: string, 27 | options: TransformOptions>, 28 | ): {code: string; map?: RawSourceMap | string | null} { 29 | const transforms = getTransforms(filename, options.supportsStaticESM); 30 | if (transforms !== null) { 31 | const {code, sourceMap} = transform(src, { 32 | transforms, 33 | disableESTransforms: true, 34 | preserveDynamicImport: options.supportsDynamicImport, 35 | ...options.transformerConfig, 36 | sourceMapOptions: {compiledFilename: filename}, 37 | filePath: filename, 38 | }); 39 | const mapBase64 = Buffer.from(JSON.stringify(sourceMap)).toString("base64"); 40 | // Split the source map comment across two strings so that tools like 41 | // source-map-support don't accidentally interpret it as a source map 42 | // comment for this file. 43 | let suffix = "//# sourceMapping"; 44 | suffix += `URL=data:application/json;charset=utf-8;base64,${mapBase64}`; 45 | // sourceMappingURL is necessary for breakpoints to work in WebStorm, so 46 | // include it in addition to specifying the source map normally. 47 | return {code: `${code}\n${suffix}`, map: sourceMap}; 48 | } else { 49 | return {code: src}; 50 | } 51 | } 52 | 53 | export default {process}; 54 | -------------------------------------------------------------------------------- /integrations/jest-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true, 4 | "target": "es2017", 5 | "module": "commonjs", 6 | "typeRoots": ["node_modules/@types"], 7 | "experimentalDecorators": true, 8 | "isolatedModules": true, 9 | "noImplicitAny": true, 10 | "noImplicitReturns": true, 11 | "downlevelIteration": true, 12 | "noEmitHelpers": true, 13 | "importHelpers": true, 14 | }, 15 | "exclude": [ 16 | "node_modules", 17 | "dist/**/*", 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /integrations/webpack-loader/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | project: `${__dirname}/tsconfig.json`, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /integrations/webpack-loader/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.0 (2018-06-10) 2 | 3 | * Switch Sucrase peer dependency to `^3`, update code to use new return type. 4 | -------------------------------------------------------------------------------- /integrations/webpack-loader/README.md: -------------------------------------------------------------------------------- 1 | # Sucrase Webpack loader 2 | 3 | [![npm version](https://badge.fury.io/js/@sucrase%2Fwebpack-loader.svg)](https://www.npmjs.com/package/@sucrase/webpack-loader) 4 | [![MIT License](https://img.shields.io/npm/l/express.svg?maxAge=2592000)](LICENSE) 5 | 6 | This is a simple Webpack loader that makes it easy to use 7 | [Sucrase](https://github.com/alangpierce/sucrase) in your build. 8 | 9 | **Note: Object rest/spread syntax (e.g. `{...a, b: c}`) requires Webpack 4. For 10 | earlier Webpack versions, you can use 11 | [webpack-object-rest-spread-plugin](https://github.com/alangpierce/sucrase/tree/main/integrations/webpack-object-rest-spread-plugin) 12 | alongside this loader.** 13 | 14 | ## Usage 15 | 16 | First install the package and Sucrase as a dev dependency: 17 | ``` 18 | yarn add --dev @sucrase/webpack-loader sucrase 19 | ``` 20 | 21 | Then add it as a loader to your webpack config: 22 | ``` 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.js$/, 27 | use: { 28 | loader: '@sucrase/webpack-loader', 29 | options: { 30 | transforms: ['jsx'] 31 | } 32 | } 33 | } 34 | ] 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /integrations/webpack-loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sucrase/webpack-loader", 3 | "version": "2.0.0", 4 | "description": "Webpack loader for Sucrase", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/alangpierce/sucrase/tree/main/integrations/webpack-loader", 7 | "author": "Alan Pierce ", 8 | "license": "MIT", 9 | "peerDependencies": { 10 | "sucrase": "^3" 11 | }, 12 | "devDependencies": { 13 | "@types/loader-utils": "^1.1.3", 14 | "sucrase": "^3.10.1" 15 | }, 16 | "dependencies": { 17 | "loader-utils": "^1.2.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /integrations/webpack-loader/src/index.ts: -------------------------------------------------------------------------------- 1 | import {getOptions, getRemainingRequest} from "loader-utils"; 2 | import {type Options, transform} from "sucrase"; 3 | 4 | function loader(code: string): string { 5 | const webpackRemainingChain = getRemainingRequest(this).split("!"); 6 | const filePath = webpackRemainingChain[webpackRemainingChain.length - 1]; 7 | const options: Options = getOptions(this) as Options; 8 | return transform(code, {filePath, ...options}).code; 9 | } 10 | 11 | export = loader; 12 | -------------------------------------------------------------------------------- /integrations/webpack-loader/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true, 4 | "target": "es2017", 5 | "module": "commonjs", 6 | "typeRoots": ["node_modules/@types"], 7 | "experimentalDecorators": true, 8 | "isolatedModules": true, 9 | "noImplicitAny": true, 10 | "noImplicitReturns": true, 11 | "downlevelIteration": true, 12 | "noEmitHelpers": true, 13 | "importHelpers": true, 14 | }, 15 | "exclude": [ 16 | "node_modules", 17 | "dist/**/*", 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /integrations/webpack-object-rest-spread-plugin/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | project: `${__dirname}/tsconfig.json`, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /integrations/webpack-object-rest-spread-plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.1 (2018-06-09) 2 | 3 | * Add webpack peer dependency to indicate that webpack 4 is unsupported. 4 | 5 | # 1.0.0 6 | 7 | Initial release 8 | -------------------------------------------------------------------------------- /integrations/webpack-object-rest-spread-plugin/README.md: -------------------------------------------------------------------------------- 1 | # Webpack object rest/spread plugin 2 | 3 | [![npm version](https://badge.fury.io/js/@sucrase%2Fwebpack-object-rest-spread-plugin.svg)](https://www.npmjs.com/package/@sucrase/webpack-object-rest-spread-plugin) 4 | [![MIT License](https://img.shields.io/npm/l/express.svg?maxAge=2592000)](LICENSE) 5 | 6 | This is a Webpack plugin that hacks the Webpack parser to allow object 7 | rest/spread syntax (e.g. `{...a, b: c}`) in Webpack 3 and earlier. 8 | 9 | **Note: This plugin is only necessary when using Webpack 3 or earlier. Webpack 4 10 | natively supports object rest/spread. See https://github.com/webpack/webpack/issues/5548 11 | for troubleshooting tips.** 12 | 13 | ## Usage 14 | 15 | First install the package as a dev dependency: 16 | ``` 17 | yarn add --dev @sucrase/webpack-object-rest-spread-plugin 18 | ``` 19 | 20 | Then add it as a plugin to your webpack config: 21 | ``` 22 | const ObjectRestSpreadPlugin = require('@sucrase/webpack-object-rest-spread-plugin'); 23 | 24 | ... 25 | 26 | plugins: [ 27 | new ObjectRestSpreadPlugin(), 28 | ], 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /integrations/webpack-object-rest-spread-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sucrase/webpack-object-rest-spread-plugin", 3 | "version": "1.0.1", 4 | "description": "Webpack plugin to enable object rest/spread syntax", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/alangpierce/sucrase/tree/main/integrations/webpack-object-rest-spread-plugin", 7 | "author": "Alan Pierce ", 8 | "license": "MIT", 9 | "private": false, 10 | "peerDependencies": { 11 | "webpack": "<4" 12 | }, 13 | "dependencies": { 14 | "acorn": "^5.3.0", 15 | "acorn-dynamic-import": "^3.0.0", 16 | "acorn-object-rest-spread": "^1.1.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /integrations/webpack-object-rest-spread-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true, 4 | "target": "es2017", 5 | "module": "commonjs", 6 | "typeRoots": ["node_modules/@types"], 7 | "experimentalDecorators": true, 8 | "isolatedModules": true, 9 | "noImplicitAny": true, 10 | "noImplicitReturns": true, 11 | "downlevelIteration": true, 12 | "noEmitHelpers": true, 13 | "importHelpers": true, 14 | }, 15 | "exclude": [ 16 | "node_modules", 17 | "dist/**/*", 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /integrations/webpack-object-rest-spread-plugin/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | acorn-dynamic-import@^3.0.0: 6 | version "3.0.0" 7 | resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz#901ceee4c7faaef7e07ad2a47e890675da50a278" 8 | integrity sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg== 9 | dependencies: 10 | acorn "^5.0.0" 11 | 12 | acorn-object-rest-spread@^1.1.0: 13 | version "1.1.0" 14 | resolved "https://registry.yarnpkg.com/acorn-object-rest-spread/-/acorn-object-rest-spread-1.1.0.tgz#78699aefdd18ec3182caadadf52e2697c048f476" 15 | integrity sha512-v/lolbMJxMB2tm66GjxhNSYEz2I+a8S99emxJu2mhqhDE348dJkxyliJ3pMV79tbTQEzGJA8pyRvC5qoSbjJAA== 16 | dependencies: 17 | acorn "^5.0.3" 18 | 19 | acorn@^5.0.0, acorn@^5.0.3, acorn@^5.3.0: 20 | version "5.7.4" 21 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" 22 | integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sucrase", 3 | "version": "3.35.0", 4 | "description": "Super-fast alternative to Babel for when you can target modern JS runtimes", 5 | "author": "Alan Pierce ", 6 | "license": "MIT", 7 | "main": "dist/index.js", 8 | "module": "dist/esm/index.js", 9 | "types": "dist/types/index.d.ts", 10 | "bin": { 11 | "sucrase": "./bin/sucrase", 12 | "sucrase-node": "./bin/sucrase-node" 13 | }, 14 | "scripts": { 15 | "build": "sucrase-node script/build.ts", 16 | "fast-build": "sucrase-node script/build.ts --fast", 17 | "clean": "rm -rf ./build ./dist ./dist-self-build ./dist-types ./example-runner/example-repos ./spec-compliance-tests/test262/test262-checkout ./spec-compliance-tests/babel-tests/babel-tests-checkout", 18 | "generate": "sucrase-node generator/generate.ts", 19 | "benchmark": "cd benchmark && yarn && sucrase-node ./benchmark.ts", 20 | "benchmark-compare": "sucrase-node ./benchmark/compare-performance.ts", 21 | "microbenchmark": "sucrase-node benchmark/microbenchmark.ts", 22 | "lint": "sucrase-node script/lint.ts", 23 | "lint-fix": "sucrase-node script/lint.ts --fix", 24 | "profile": "node --inspect-brk ./node_modules/.bin/sucrase-node ./benchmark/profile", 25 | "profile-project": "node --inspect-brk ./node_modules/.bin/sucrase-node ./benchmark/benchmark-project.ts --profile", 26 | "prepublishOnly": "yarn clean && yarn build", 27 | "release": "sucrase-node script/release.ts", 28 | "run-examples": "sucrase-node example-runner/example-runner.ts", 29 | "test": "yarn lint && yarn test-only", 30 | "test-only": "mocha './test/**/*.ts'", 31 | "integration-test": "cd integration-test && yarn && yarn link @sucrase/jest-plugin && mocha --timeout 10000 ./integration-tests.ts", 32 | "test262": "sucrase-node spec-compliance-tests/test262/run-test262.ts", 33 | "check-babel-tests": "sucrase-node spec-compliance-tests/babel-tests/check-babel-tests.ts", 34 | "test-with-coverage": "nyc mocha './test/**/*.ts'", 35 | "report-coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/alangpierce/sucrase.git" 40 | }, 41 | "keywords": [ 42 | "babel", 43 | "jsx", 44 | "typescript", 45 | "flow" 46 | ], 47 | "bugs": { 48 | "url": "https://github.com/alangpierce/sucrase/issues" 49 | }, 50 | "devDependencies": { 51 | "@babel/core": "^7.22.5", 52 | "@jridgewell/trace-mapping": "^0.3.18", 53 | "@types/mocha": "^9.1.1", 54 | "@types/mz": "^2.7.4", 55 | "@types/node": "^20.3.2", 56 | "@typescript-eslint/eslint-plugin": "^5.60.1", 57 | "@typescript-eslint/parser": "^5.60.1", 58 | "chalk": "^4", 59 | "codecov": "^3.8.3", 60 | "eslint": "^8.43.0", 61 | "eslint-config-airbnb-base": "^15.0.0", 62 | "eslint-config-prettier": "^8.8.0", 63 | "eslint-plugin-import": "~2.26", 64 | "eslint-plugin-prettier": "^4.2.1", 65 | "mocha": "^10.2.0", 66 | "nyc": "^15.1.0", 67 | "prettier": "^2.8.8", 68 | "sucrase": "^3.35.0", 69 | "test262-harness": "^10.0.0", 70 | "ts-interface-builder": "^0.3.3", 71 | "typescript": "~5.0" 72 | }, 73 | "dependencies": { 74 | "@jridgewell/gen-mapping": "^0.3.2", 75 | "commander": "^4.0.0", 76 | "glob": "^10.3.10", 77 | "lines-and-columns": "^1.1.6", 78 | "mz": "^2.7.0", 79 | "pirates": "^4.0.1", 80 | "ts-interface-checker": "^0.1.9" 81 | }, 82 | "engines": { 83 | "node": ">=16 || 14 >=14.17" 84 | }, 85 | "resolutions": { 86 | "**/eshost/socket.io": "4.7.0" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: false, 3 | parser: "typescript", 4 | printWidth: 100, 5 | trailingComma: "all", 6 | }; 7 | -------------------------------------------------------------------------------- /register/index.js: -------------------------------------------------------------------------------- 1 | require("../dist/register").registerAll(); 2 | -------------------------------------------------------------------------------- /register/js.js: -------------------------------------------------------------------------------- 1 | require("../dist/register").registerJS(); 2 | -------------------------------------------------------------------------------- /register/jsx.js: -------------------------------------------------------------------------------- 1 | require("../dist/register").registerJSX(); 2 | -------------------------------------------------------------------------------- /register/ts-legacy-module-interop.js: -------------------------------------------------------------------------------- 1 | require("../dist/register").registerTSLegacyModuleInterop(); 2 | -------------------------------------------------------------------------------- /register/ts.js: -------------------------------------------------------------------------------- 1 | require("../dist/register").registerTS(); 2 | -------------------------------------------------------------------------------- /register/tsx-legacy-module-interop.js: -------------------------------------------------------------------------------- 1 | require("../dist/register").registerTSXLegacyModuleInterop(); 2 | -------------------------------------------------------------------------------- /register/tsx.js: -------------------------------------------------------------------------------- 1 | require("../dist/register").registerTSX(); 2 | -------------------------------------------------------------------------------- /script/lint.ts: -------------------------------------------------------------------------------- 1 | #!./node_modules/.bin/sucrase-node 2 | /* eslint-disable no-console */ 3 | import {exists} from "mz/fs"; 4 | 5 | import run from "./run"; 6 | 7 | const TSC = "./node_modules/.bin/tsc"; 8 | const ESLINT = "./node_modules/.bin/eslint"; 9 | 10 | function isFix(): boolean { 11 | return process.argv.includes("--fix"); 12 | } 13 | 14 | async function main(): Promise { 15 | // Linting sub-projects requires the latest Sucrase types, so require a build first. 16 | if (!(await exists("./dist"))) { 17 | console.log("Must run build before lint, running build..."); 18 | await run("yarn build"); 19 | } 20 | await Promise.all([ 21 | checkSucrase(), 22 | checkProject("./integrations/gulp-plugin"), 23 | checkProject("./integrations/jest-plugin"), 24 | checkProject("./integrations/webpack-loader"), 25 | checkProject("./integrations/webpack-object-rest-spread-plugin"), 26 | checkProject("./website"), 27 | ]); 28 | } 29 | 30 | async function checkSucrase(): Promise { 31 | await Promise.all([ 32 | run(`${TSC} --project . --noEmit`), 33 | run( 34 | `${ESLINT} ${isFix() ? "--fix" : ""} ${[ 35 | "benchmark", 36 | "example-runner", 37 | "generator", 38 | "integration-test", 39 | "script", 40 | "spec-compliance-tests", 41 | "src", 42 | "test", 43 | ] 44 | .map((dir) => `'${dir}/**/*.ts'`) 45 | .join(" ")}`, 46 | ), 47 | ]); 48 | } 49 | 50 | async function checkProject(path: string): Promise { 51 | await Promise.all([ 52 | run(`${TSC} --project ${path} --noEmit`), 53 | run(`${ESLINT} ${isFix() ? "--fix" : ""} '${path}/src/**/*.{ts,tsx}'`), 54 | ]); 55 | } 56 | 57 | main().catch((e) => { 58 | console.error("Unhandled error:"); 59 | console.error(e); 60 | process.exitCode = 1; 61 | }); 62 | -------------------------------------------------------------------------------- /script/mergeDirectoryContents.ts: -------------------------------------------------------------------------------- 1 | import {copyFile, exists, mkdir, readdir, stat} from "mz/fs"; 2 | import {join} from "path"; 3 | 4 | export default async function mergeDirectoryContents( 5 | srcDirPath: string, 6 | destDirPath: string, 7 | ): Promise { 8 | if (!(await exists(destDirPath))) { 9 | await mkdir(destDirPath); 10 | } 11 | for (const child of await readdir(srcDirPath)) { 12 | const srcChildPath = join(srcDirPath, child); 13 | const destChildPath = join(destDirPath, child); 14 | if ((await stat(srcChildPath)).isDirectory()) { 15 | await mergeDirectoryContents(srcChildPath, destChildPath); 16 | } else { 17 | await copyFile(srcChildPath, destChildPath); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /script/mz.d.ts: -------------------------------------------------------------------------------- 1 | import "mz/fs"; 2 | 3 | declare module "mz/fs" { 4 | // copyFile isn't in the typedefs yet, so add it manually. 5 | export function copyFile(oldPath: string, newPath: string): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /script/release.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import {readFile} from "mz/fs"; 3 | 4 | import run from "./run"; 5 | import sleep from "./sleep"; 6 | 7 | /** 8 | * This script releases the core sucrase package (NOT any integrations). To use: 9 | * 1.) Update package.json to the new version without committing. 10 | * 2.) Add changelog notes for the new version without committing. 11 | * 3.) Update getVersion in index.ts without committing. 12 | * 4.) Run this script with "yarn release" from the root directory. 13 | * 5.) Verify that everything in the commit looks right and push. 14 | * 15 | * To release an integration, take the equivalent manual steps: 16 | * 1.) Update the version in the integration's package.json. 17 | * 2.) Add changelog notes in the integration's CHANGELOG.md. 18 | * 3.) Run "yarn build" at the top level to make sure the integration is fully 19 | * built. 20 | * 4.) Run "yarn publish" in the integration directory. 21 | * 5.) Commit and push the change. 22 | */ 23 | async function main(): Promise { 24 | const packageJSON = JSON.parse((await readFile("./package.json")).toString()); 25 | const version = packageJSON.version; 26 | const changelogText = await readFile("./CHANGELOG.md"); 27 | if (!changelogText.includes(version)) { 28 | throw new Error("Release notes must be already added to changelog."); 29 | } 30 | const indexTSText = await readFile("./src/index.ts"); 31 | if (!indexTSText.includes(version)) { 32 | throw new Error("getVersion must be updated to the new version."); 33 | } 34 | console.log(`Version ${version} is valid and found in changelog.`); 35 | try { 36 | await run("npm whoami"); 37 | } catch (e) { 38 | await run("npm login"); 39 | } 40 | await run("npm publish"); 41 | console.log("Taking a quick nap to make sure we update with the right version."); 42 | await sleep(30000); 43 | await run("yarn add sucrase@latest"); 44 | process.chdir("./website"); 45 | await run("yarn add sucrase@latest"); 46 | await run("yarn publish-website"); 47 | process.chdir(".."); 48 | await run(`git commit -a -m "v${version}"`); 49 | console.log("Done! Please sanity-check the commit, then push."); 50 | } 51 | 52 | main().catch((e) => { 53 | console.error("Unhandled error:"); 54 | console.error(e); 55 | process.exitCode = 1; 56 | }); 57 | -------------------------------------------------------------------------------- /script/run.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import {spawn} from "child_process"; 3 | 4 | /** 5 | * Variant of exec that connects stdout, stderr, and stdin, mostly so console 6 | * output is shown continuously. As with the mz version of exec, this returns a 7 | * promise that resolves when the shell command finishes. 8 | * 9 | * Taken directly from run in decaffeinate-examples. 10 | */ 11 | export default function run(command: string): Promise { 12 | console.log(`> ${command}`); 13 | return new Promise((resolve, reject) => { 14 | const childProcess = spawn("/bin/bash", ["-c", command], {stdio: "inherit"}); 15 | childProcess.on("close", (code) => { 16 | if (code === 0) { 17 | resolve(); 18 | } else { 19 | reject(new Error(`Command failed: ${command}`)); 20 | } 21 | }); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /script/sleep.ts: -------------------------------------------------------------------------------- 1 | export default async function sleep(timeMs: number): Promise { 2 | return new Promise((resolve) => { 3 | setTimeout(resolve, timeMs); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /script/util/readFileContents.ts: -------------------------------------------------------------------------------- 1 | import {readFile} from "fs/promises"; 2 | 3 | export async function readFileContents(path: string): Promise { 4 | return (await readFile(path)).toString(); 5 | } 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | export async function readJSONFileContents(path: string): Promise { 8 | return JSON.parse(await readFileContents(path)); 9 | } 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | export async function readJSONFileContentsIfExists(path: string): Promise { 13 | try { 14 | return JSON.parse(await readFileContents(path)); 15 | } catch (e) { 16 | if ((e as {code: string}).code === "ENOENT") { 17 | return null; 18 | } else { 19 | throw e; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /spec-compliance-tests/README.md: -------------------------------------------------------------------------------- 1 | # Spec compliance tests 2 | 3 | This directory consists of integrations with externally-written test suites that 4 | are designed to surface tricky cases. This is in contrast to the example-runner 5 | directory, which tests Sucrase on realistic codebases, and the test directory, 6 | which is the core test suite for Sucrase. 7 | -------------------------------------------------------------------------------- /spec-compliance-tests/test262/run-test262.ts: -------------------------------------------------------------------------------- 1 | #!./node_modules/.bin/sucrase-node 2 | /* eslint-disable no-console */ 3 | import chalk from "chalk"; 4 | import {exec} from "mz/child_process"; 5 | import {exists} from "mz/fs"; 6 | 7 | import run from "../../script/run"; 8 | 9 | const TEST262_HARNESS = "./node_modules/.bin/test262-harness"; 10 | const TEST262_DIR = "./spec-compliance-tests/test262/test262-checkout"; 11 | const TEST262_REPO_URL = "https://github.com/tc39/test262.git"; 12 | const TEST262_REVISION = "157b18d16b5d52501c4d75ac422d3a80bfad1c17"; 13 | 14 | const SKIPPED_TESTS = [ 15 | // This test fails due to an unhandled promise rejection that seems to be unrelated to the use of 16 | // optional chaining. 17 | "language/expressions/optional-chaining/member-expression-async-identifier.js", 18 | // This file tests a number of cases like (a?.b)(), which are currently considered out of scope. 19 | // See tech plan at: 20 | // https://github.com/alangpierce/sucrase/wiki/Sucrase-Optional-Chaining-and-Nullish-Coalescing-Technical-Plan 21 | "language/expressions/optional-chaining/optional-call-preserves-this.js", 22 | ]; 23 | 24 | /** 25 | * Run the test262 suite on some tests that we know are useful. 26 | */ 27 | async function main(): Promise { 28 | if (!(await exists(TEST262_DIR))) { 29 | console.log(`Directory ${TEST262_DIR} not found, cloning a new one.`); 30 | await run(`git clone ${TEST262_REPO_URL} ${TEST262_DIR}`); 31 | } 32 | 33 | // Force a specific revision so we don't get a breakage from changes to the main branch. 34 | const originalCwd = process.cwd(); 35 | try { 36 | process.chdir(TEST262_DIR); 37 | await run(`git reset --hard ${TEST262_REVISION}`); 38 | await run(`git clean -f`); 39 | } catch (e) { 40 | await run("git fetch"); 41 | await run(`git reset --hard ${TEST262_REVISION}`); 42 | await run(`git clean -f`); 43 | } finally { 44 | process.chdir(originalCwd); 45 | } 46 | 47 | console.log("Running test262..."); 48 | const harnessStdout = ( 49 | await exec(`${TEST262_HARNESS} \ 50 | --preprocessor "./spec-compliance-tests/test262/test262Preprocessor.js" \ 51 | --reporter "json" \ 52 | "${TEST262_DIR}/test/language/expressions/coalesce/**/*.js" \ 53 | "${TEST262_DIR}/test/language/expressions/optional-chaining/**/*.js"`) 54 | )[0].toString(); 55 | 56 | const harnessOutput = JSON.parse(harnessStdout); 57 | let numPassed = 0; 58 | let numFailed = 0; 59 | let numSkipped = 0; 60 | for (const result of harnessOutput) { 61 | if (!result.result.pass) { 62 | if (SKIPPED_TESTS.includes(result.relative)) { 63 | numSkipped++; 64 | console.error(`${chalk.bold(chalk.bgYellow("SKIP"))} ${chalk.bold(result.file)}`); 65 | console.error(); 66 | } else { 67 | numFailed++; 68 | console.error(`${chalk.bold(chalk.bgRed("FAIL"))} ${chalk.bold(result.file)}`); 69 | console.error(result.result.message); 70 | console.error(`stdout:\n${result.rawResult.stdout}`); 71 | console.error(`stderr:\n${result.rawResult.stderr}`); 72 | console.error(); 73 | } 74 | } else { 75 | numPassed++; 76 | } 77 | } 78 | console.log(`${numPassed} passed, ${numFailed} failed, ${numSkipped} skipped`); 79 | if (numFailed > 0) { 80 | process.exitCode = 1; 81 | } 82 | } 83 | 84 | main().catch((e) => { 85 | console.error("Unhandled error:"); 86 | console.error(e); 87 | process.exitCode = 1; 88 | }); 89 | -------------------------------------------------------------------------------- /spec-compliance-tests/test262/test262Preprocessor.js: -------------------------------------------------------------------------------- 1 | const sucrase = require("../.."); 2 | 3 | /** 4 | * test262-harness preprocessor documented here: 5 | https://github.com/bterlson/test262-harness#preprocessor 6 | */ 7 | module.exports = function (test) { 8 | // Sucrase doesn't attempt to throw SyntaxError on bad syntax, so skip those tests. 9 | if (test.attrs.negative) { 10 | return null; 11 | } 12 | 13 | // Sucrase assumes strict mode, so skip sloppy mode tests. 14 | if (test.scenario === "default") { 15 | return null; 16 | } 17 | 18 | // TCO tests seem to fail in V8 normally, so skip those. 19 | if (test.attrs.features.includes("tail-call-optimization")) { 20 | return null; 21 | } 22 | 23 | try { 24 | test.contents = sucrase.transform(test.contents, {transforms: []}).code; 25 | } catch (error) { 26 | test.result = { 27 | stderr: `${error.name}: ${error.message}\n`, 28 | stdout: "", 29 | error, 30 | }; 31 | } 32 | 33 | return test; 34 | }; 35 | -------------------------------------------------------------------------------- /src/NameManager.ts: -------------------------------------------------------------------------------- 1 | import type {Token} from "./parser/tokenizer"; 2 | import getIdentifierNames from "./util/getIdentifierNames"; 3 | 4 | export default class NameManager { 5 | private readonly usedNames: Set = new Set(); 6 | 7 | constructor(code: string, tokens: Array) { 8 | this.usedNames = new Set(getIdentifierNames(code, tokens)); 9 | } 10 | 11 | claimFreeName(name: string): string { 12 | const newName = this.findFreeName(name); 13 | this.usedNames.add(newName); 14 | return newName; 15 | } 16 | 17 | findFreeName(name: string): string { 18 | if (!this.usedNames.has(name)) { 19 | return name; 20 | } 21 | let suffixNum = 2; 22 | while (this.usedNames.has(name + String(suffixNum))) { 23 | suffixNum++; 24 | } 25 | return name + String(suffixNum); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Options-gen-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This module was automatically generated by `ts-interface-builder` 3 | */ 4 | import * as t from "ts-interface-checker"; 5 | // tslint:disable:object-literal-key-quotes 6 | 7 | export const Transform = t.union( 8 | t.lit("jsx"), 9 | t.lit("typescript"), 10 | t.lit("flow"), 11 | t.lit("imports"), 12 | t.lit("react-hot-loader"), 13 | t.lit("jest"), 14 | ); 15 | 16 | export const SourceMapOptions = t.iface([], { 17 | compiledFilename: "string", 18 | }); 19 | 20 | export const Options = t.iface([], { 21 | transforms: t.array("Transform"), 22 | disableESTransforms: t.opt("boolean"), 23 | jsxRuntime: t.opt(t.union(t.lit("classic"), t.lit("automatic"), t.lit("preserve"))), 24 | production: t.opt("boolean"), 25 | jsxImportSource: t.opt("string"), 26 | jsxPragma: t.opt("string"), 27 | jsxFragmentPragma: t.opt("string"), 28 | keepUnusedImports: t.opt("boolean"), 29 | preserveDynamicImport: t.opt("boolean"), 30 | injectCreateRequireForImportRequire: t.opt("boolean"), 31 | enableLegacyTypeScriptModuleInterop: t.opt("boolean"), 32 | enableLegacyBabel5ModuleInterop: t.opt("boolean"), 33 | sourceMapOptions: t.opt("SourceMapOptions"), 34 | filePath: t.opt("string"), 35 | }); 36 | 37 | const exportedTypeSuite: t.ITypeSuite = { 38 | Transform, 39 | SourceMapOptions, 40 | Options, 41 | }; 42 | export default exportedTypeSuite; 43 | -------------------------------------------------------------------------------- /src/Options.ts: -------------------------------------------------------------------------------- 1 | import {createCheckers} from "ts-interface-checker"; 2 | 3 | import OptionsGenTypes from "./Options-gen-types"; 4 | 5 | const {Options: OptionsChecker} = createCheckers(OptionsGenTypes); 6 | 7 | export type Transform = "jsx" | "typescript" | "flow" | "imports" | "react-hot-loader" | "jest"; 8 | 9 | export interface SourceMapOptions { 10 | /** 11 | * The name to use in the "file" field of the source map. This should be the name of the compiled 12 | * file. 13 | */ 14 | compiledFilename: string; 15 | } 16 | 17 | export interface Options { 18 | /** 19 | * Unordered array of transform names describing both the allowed syntax 20 | * (where applicable) and the transformation behavior. 21 | */ 22 | transforms: Array; 23 | /** 24 | * Opts out of all ES syntax transformations: optional chaining, nullish 25 | * coalescing, class fields, numeric separators, optional catch binding. 26 | */ 27 | disableESTransforms?: boolean; 28 | /** 29 | * Transformation mode for the JSX transform. 30 | * - "classic" refers to the original behavior using `React.createElement`. 31 | * - "automatic" refers to the transform behavior released with React 17, 32 | * where the `jsx` function (or a variation) is automatically imported. 33 | * - "preserve" leaves the JSX as-is. 34 | * 35 | * Default value: "classic". 36 | */ 37 | jsxRuntime?: "classic" | "automatic" | "preserve"; 38 | /** 39 | * Compile code for production use. Currently only applies to the JSX 40 | * transform. 41 | */ 42 | production?: boolean; 43 | /** 44 | * If specified, import path prefix to use in place of "react" when compiling 45 | * JSX with the automatic runtime. 46 | */ 47 | jsxImportSource?: string; 48 | /** 49 | * If specified, function name to use in place of React.createClass when 50 | * compiling JSX with the classic runtime. 51 | */ 52 | jsxPragma?: string; 53 | /** 54 | * If specified, function name to use in place of React.Fragment when 55 | * compiling JSX with the classic runtime. 56 | */ 57 | jsxFragmentPragma?: string; 58 | /** 59 | * If specified, disable automatic removal of type-only import and export 60 | * statements and names. Only statements and names that explicitly use the 61 | * `type` keyword are removed. 62 | */ 63 | keepUnusedImports?: boolean; 64 | /** 65 | * If specified, the imports transform does not attempt to change dynamic 66 | * import() expressions into require() calls. 67 | */ 68 | preserveDynamicImport?: boolean; 69 | /** 70 | * Only relevant when targeting ESM (i.e. when the imports transform is *not* 71 | * specified). This flag changes the behavior of TS require imports: 72 | * 73 | * import Foo = require("foo"); 74 | * 75 | * to import createRequire, create a require function, and use that function. 76 | * This is the TS behavior with module: nodenext and makes it easier for the 77 | * same code to target ESM and CJS. 78 | */ 79 | injectCreateRequireForImportRequire?: boolean; 80 | /** 81 | * If true, replicate the import behavior of TypeScript's esModuleInterop: false. 82 | */ 83 | enableLegacyTypeScriptModuleInterop?: boolean; 84 | /** 85 | * If true, replicate the import behavior Babel 5 and babel-plugin-add-module-exports. 86 | */ 87 | enableLegacyBabel5ModuleInterop?: boolean; 88 | /** 89 | * If specified, we also return a RawSourceMap object alongside the code. 90 | * filePath must be specified if this option is enabled. 91 | */ 92 | sourceMapOptions?: SourceMapOptions; 93 | /** 94 | * File path to use in error messages, React display names, and source maps. 95 | */ 96 | filePath?: string; 97 | } 98 | 99 | export function validateOptions(options: Options): void { 100 | OptionsChecker.strictCheck(options); 101 | } 102 | -------------------------------------------------------------------------------- /src/computeSourceMap.ts: -------------------------------------------------------------------------------- 1 | import {GenMapping, maybeAddSegment, toEncodedMap} from "@jridgewell/gen-mapping"; 2 | 3 | import type {SourceMapOptions} from "./index"; 4 | import type {Token} from "./parser/tokenizer"; 5 | import {charCodes} from "./parser/util/charcodes"; 6 | import type {RootTransformerResult} from "./transformers/RootTransformer"; 7 | 8 | export interface RawSourceMap { 9 | version: number; 10 | file: string; 11 | sources: Array; 12 | sourceRoot?: string; 13 | sourcesContent?: Array; 14 | mappings: string; 15 | names: Array; 16 | } 17 | 18 | /** 19 | * Generate a source map indicating that each line maps directly to the original line, 20 | * with the tokens in their new positions. 21 | */ 22 | export default function computeSourceMap( 23 | {code: generatedCode, mappings: rawMappings}: RootTransformerResult, 24 | filePath: string, 25 | options: SourceMapOptions, 26 | source: string, 27 | tokens: Array, 28 | ): RawSourceMap { 29 | const sourceColumns = computeSourceColumns(source, tokens); 30 | const map = new GenMapping({file: options.compiledFilename}); 31 | let tokenIndex = 0; 32 | // currentMapping is the output source index for the current input token being 33 | // considered. 34 | let currentMapping = rawMappings[0]; 35 | while (currentMapping === undefined && tokenIndex < rawMappings.length - 1) { 36 | tokenIndex++; 37 | currentMapping = rawMappings[tokenIndex]; 38 | } 39 | let line = 0; 40 | let lineStart = 0; 41 | if (currentMapping !== lineStart) { 42 | maybeAddSegment(map, line, 0, filePath, line, 0); 43 | } 44 | for (let i = 0; i < generatedCode.length; i++) { 45 | if (i === currentMapping) { 46 | const genColumn = currentMapping - lineStart; 47 | const sourceColumn = sourceColumns[tokenIndex]; 48 | maybeAddSegment(map, line, genColumn, filePath, line, sourceColumn); 49 | while ( 50 | (currentMapping === i || currentMapping === undefined) && 51 | tokenIndex < rawMappings.length - 1 52 | ) { 53 | tokenIndex++; 54 | currentMapping = rawMappings[tokenIndex]; 55 | } 56 | } 57 | if (generatedCode.charCodeAt(i) === charCodes.lineFeed) { 58 | line++; 59 | lineStart = i + 1; 60 | if (currentMapping !== lineStart) { 61 | maybeAddSegment(map, line, 0, filePath, line, 0); 62 | } 63 | } 64 | } 65 | const {sourceRoot, sourcesContent, ...sourceMap} = toEncodedMap(map); 66 | return sourceMap as RawSourceMap; 67 | } 68 | 69 | /** 70 | * Create an array mapping each token index to the 0-based column of the start 71 | * position of the token. 72 | */ 73 | function computeSourceColumns(code: string, tokens: Array): Array { 74 | const sourceColumns: Array = new Array(tokens.length); 75 | let tokenIndex = 0; 76 | let currentMapping = tokens[tokenIndex].start; 77 | let lineStart = 0; 78 | for (let i = 0; i < code.length; i++) { 79 | if (i === currentMapping) { 80 | sourceColumns[tokenIndex] = currentMapping - lineStart; 81 | tokenIndex++; 82 | currentMapping = tokens[tokenIndex].start; 83 | } 84 | if (code.charCodeAt(i) === charCodes.lineFeed) { 85 | lineStart = i + 1; 86 | } 87 | } 88 | return sourceColumns; 89 | } 90 | -------------------------------------------------------------------------------- /src/identifyShadowedGlobals.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isBlockScopedDeclaration, 3 | isFunctionScopedDeclaration, 4 | isNonTopLevelDeclaration, 5 | } from "./parser/tokenizer"; 6 | import type {Scope} from "./parser/tokenizer/state"; 7 | import {TokenType as tt} from "./parser/tokenizer/types"; 8 | import type TokenProcessor from "./TokenProcessor"; 9 | 10 | /** 11 | * Traverse the given tokens and modify them if necessary to indicate that some names shadow global 12 | * variables. 13 | */ 14 | export default function identifyShadowedGlobals( 15 | tokens: TokenProcessor, 16 | scopes: Array, 17 | globalNames: Set, 18 | ): void { 19 | if (!hasShadowedGlobals(tokens, globalNames)) { 20 | return; 21 | } 22 | markShadowedGlobals(tokens, scopes, globalNames); 23 | } 24 | 25 | /** 26 | * We can do a fast up-front check to see if there are any declarations to global names. If not, 27 | * then there's no point in computing scope assignments. 28 | */ 29 | // Exported for testing. 30 | export function hasShadowedGlobals(tokens: TokenProcessor, globalNames: Set): boolean { 31 | for (const token of tokens.tokens) { 32 | if ( 33 | token.type === tt.name && 34 | !token.isType && 35 | isNonTopLevelDeclaration(token) && 36 | globalNames.has(tokens.identifierNameForToken(token)) 37 | ) { 38 | return true; 39 | } 40 | } 41 | return false; 42 | } 43 | 44 | function markShadowedGlobals( 45 | tokens: TokenProcessor, 46 | scopes: Array, 47 | globalNames: Set, 48 | ): void { 49 | const scopeStack = []; 50 | let scopeIndex = scopes.length - 1; 51 | // Scopes were generated at completion time, so they're sorted by end index, so we can maintain a 52 | // good stack by going backwards through them. 53 | for (let i = tokens.tokens.length - 1; ; i--) { 54 | while (scopeStack.length > 0 && scopeStack[scopeStack.length - 1].startTokenIndex === i + 1) { 55 | scopeStack.pop(); 56 | } 57 | while (scopeIndex >= 0 && scopes[scopeIndex].endTokenIndex === i + 1) { 58 | scopeStack.push(scopes[scopeIndex]); 59 | scopeIndex--; 60 | } 61 | // Process scopes after the last iteration so we can make sure we pop all of them. 62 | if (i < 0) { 63 | break; 64 | } 65 | 66 | const token = tokens.tokens[i]; 67 | const name = tokens.identifierNameForToken(token); 68 | if (scopeStack.length > 1 && !token.isType && token.type === tt.name && globalNames.has(name)) { 69 | if (isBlockScopedDeclaration(token)) { 70 | markShadowedForScope(scopeStack[scopeStack.length - 1], tokens, name); 71 | } else if (isFunctionScopedDeclaration(token)) { 72 | let stackIndex = scopeStack.length - 1; 73 | while (stackIndex > 0 && !scopeStack[stackIndex].isFunctionScope) { 74 | stackIndex--; 75 | } 76 | if (stackIndex < 0) { 77 | throw new Error("Did not find parent function scope."); 78 | } 79 | markShadowedForScope(scopeStack[stackIndex], tokens, name); 80 | } 81 | } 82 | } 83 | if (scopeStack.length > 0) { 84 | throw new Error("Expected empty scope stack after processing file."); 85 | } 86 | } 87 | 88 | function markShadowedForScope(scope: Scope, tokens: TokenProcessor, name: string): void { 89 | for (let i = scope.startTokenIndex; i < scope.endTokenIndex; i++) { 90 | const token = tokens.tokens[i]; 91 | if ( 92 | (token.type === tt.name || token.type === tt.jsxName) && 93 | tokens.identifierNameForToken(token) === name 94 | ) { 95 | token.shadowsGlobal = true; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/parser/index.ts: -------------------------------------------------------------------------------- 1 | import type {Token} from "./tokenizer/index"; 2 | import type {Scope} from "./tokenizer/state"; 3 | import {augmentError, initParser, state} from "./traverser/base"; 4 | import {parseFile} from "./traverser/index"; 5 | 6 | export class File { 7 | tokens: Array; 8 | scopes: Array; 9 | 10 | constructor(tokens: Array, scopes: Array) { 11 | this.tokens = tokens; 12 | this.scopes = scopes; 13 | } 14 | } 15 | 16 | export function parse( 17 | input: string, 18 | isJSXEnabled: boolean, 19 | isTypeScriptEnabled: boolean, 20 | isFlowEnabled: boolean, 21 | ): File { 22 | if (isFlowEnabled && isTypeScriptEnabled) { 23 | throw new Error("Cannot combine flow and typescript plugins."); 24 | } 25 | initParser(input, isJSXEnabled, isTypeScriptEnabled, isFlowEnabled); 26 | const result = parseFile(); 27 | if (state.error) { 28 | throw augmentError(state.error); 29 | } 30 | return result; 31 | } 32 | -------------------------------------------------------------------------------- /src/parser/plugins/types.ts: -------------------------------------------------------------------------------- 1 | import {eatTypeToken, lookaheadType, match} from "../tokenizer/index"; 2 | import {TokenType as tt} from "../tokenizer/types"; 3 | import {isFlowEnabled, isTypeScriptEnabled} from "../traverser/base"; 4 | import {baseParseConditional} from "../traverser/expression"; 5 | import {flowParseTypeAnnotation} from "./flow"; 6 | import {tsParseTypeAnnotation} from "./typescript"; 7 | 8 | /** 9 | * Common parser code for TypeScript and Flow. 10 | */ 11 | 12 | // An apparent conditional expression could actually be an optional parameter in an arrow function. 13 | export function typedParseConditional(noIn: boolean): void { 14 | // If we see ?:, this can't possibly be a valid conditional. typedParseParenItem will be called 15 | // later to finish off the arrow parameter. We also need to handle bare ? tokens for optional 16 | // parameters without type annotations, i.e. ?, and ?) . 17 | if (match(tt.question)) { 18 | const nextType = lookaheadType(); 19 | if (nextType === tt.colon || nextType === tt.comma || nextType === tt.parenR) { 20 | return; 21 | } 22 | } 23 | baseParseConditional(noIn); 24 | } 25 | 26 | // Note: These "type casts" are *not* valid TS expressions. 27 | // But we parse them here and change them when completing the arrow function. 28 | export function typedParseParenItem(): void { 29 | eatTypeToken(tt.question); 30 | if (match(tt.colon)) { 31 | if (isTypeScriptEnabled) { 32 | tsParseTypeAnnotation(); 33 | } else if (isFlowEnabled) { 34 | flowParseTypeAnnotation(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/parser/tokenizer/keywords.ts: -------------------------------------------------------------------------------- 1 | export enum ContextualKeyword { 2 | NONE, 3 | _abstract, 4 | _accessor, 5 | _as, 6 | _assert, 7 | _asserts, 8 | _async, 9 | _await, 10 | _checks, 11 | _constructor, 12 | _declare, 13 | _enum, 14 | _exports, 15 | _from, 16 | _get, 17 | _global, 18 | _implements, 19 | _infer, 20 | _interface, 21 | _is, 22 | _keyof, 23 | _mixins, 24 | _module, 25 | _namespace, 26 | _of, 27 | _opaque, 28 | _out, 29 | _override, 30 | _private, 31 | _protected, 32 | _proto, 33 | _public, 34 | _readonly, 35 | _require, 36 | _satisfies, 37 | _set, 38 | _static, 39 | _symbol, 40 | _type, 41 | _unique, 42 | _using, 43 | } 44 | -------------------------------------------------------------------------------- /src/parser/tokenizer/readWord.ts: -------------------------------------------------------------------------------- 1 | import {input, state} from "../traverser/base"; 2 | import {charCodes} from "../util/charcodes"; 3 | import {IS_IDENTIFIER_CHAR} from "../util/identifier"; 4 | import {finishToken} from "./index"; 5 | import {READ_WORD_TREE} from "./readWordTree"; 6 | import {TokenType as tt} from "./types"; 7 | 8 | /** 9 | * Read an identifier, producing either a name token or matching on one of the existing keywords. 10 | * For performance, we pre-generate big decision tree that we traverse. Each node represents a 11 | * prefix and has 27 values, where the first value is the token or contextual token, if any (-1 if 12 | * not), and the other 26 values are the transitions to other nodes, or -1 to stop. 13 | */ 14 | export default function readWord(): void { 15 | let treePos = 0; 16 | let code = 0; 17 | let pos = state.pos; 18 | while (pos < input.length) { 19 | code = input.charCodeAt(pos); 20 | if (code < charCodes.lowercaseA || code > charCodes.lowercaseZ) { 21 | break; 22 | } 23 | const next = READ_WORD_TREE[treePos + (code - charCodes.lowercaseA) + 1]; 24 | if (next === -1) { 25 | break; 26 | } else { 27 | treePos = next; 28 | pos++; 29 | } 30 | } 31 | 32 | const keywordValue = READ_WORD_TREE[treePos]; 33 | if (keywordValue > -1 && !IS_IDENTIFIER_CHAR[code]) { 34 | state.pos = pos; 35 | if (keywordValue & 1) { 36 | finishToken(keywordValue >>> 1); 37 | } else { 38 | finishToken(tt.name, keywordValue >>> 1); 39 | } 40 | return; 41 | } 42 | 43 | while (pos < input.length) { 44 | const ch = input.charCodeAt(pos); 45 | if (IS_IDENTIFIER_CHAR[ch]) { 46 | pos++; 47 | } else if (ch === charCodes.backslash) { 48 | // \u 49 | pos += 2; 50 | if (input.charCodeAt(pos) === charCodes.leftCurlyBrace) { 51 | while (pos < input.length && input.charCodeAt(pos) !== charCodes.rightCurlyBrace) { 52 | pos++; 53 | } 54 | pos++; 55 | } 56 | } else if (ch === charCodes.atSign && input.charCodeAt(pos + 1) === charCodes.atSign) { 57 | pos += 2; 58 | } else { 59 | break; 60 | } 61 | } 62 | state.pos = pos; 63 | finishToken(tt.name); 64 | } 65 | -------------------------------------------------------------------------------- /src/parser/tokenizer/state.ts: -------------------------------------------------------------------------------- 1 | import type {Token} from "./index"; 2 | import {ContextualKeyword} from "./keywords"; 3 | import {type TokenType, TokenType as tt} from "./types"; 4 | 5 | export class Scope { 6 | startTokenIndex: number; 7 | endTokenIndex: number; 8 | isFunctionScope: boolean; 9 | 10 | constructor(startTokenIndex: number, endTokenIndex: number, isFunctionScope: boolean) { 11 | this.startTokenIndex = startTokenIndex; 12 | this.endTokenIndex = endTokenIndex; 13 | this.isFunctionScope = isFunctionScope; 14 | } 15 | } 16 | 17 | export class StateSnapshot { 18 | constructor( 19 | readonly potentialArrowAt: number, 20 | readonly noAnonFunctionType: boolean, 21 | readonly inDisallowConditionalTypesContext: boolean, 22 | readonly tokensLength: number, 23 | readonly scopesLength: number, 24 | readonly pos: number, 25 | readonly type: TokenType, 26 | readonly contextualKeyword: ContextualKeyword, 27 | readonly start: number, 28 | readonly end: number, 29 | readonly isType: boolean, 30 | readonly scopeDepth: number, 31 | readonly error: Error | null, 32 | ) {} 33 | } 34 | 35 | export default class State { 36 | // Used to signify the start of a potential arrow function 37 | potentialArrowAt: number = -1; 38 | 39 | // Used by Flow to handle an edge case involving function type parsing. 40 | noAnonFunctionType: boolean = false; 41 | 42 | // Used by TypeScript to handle ambiguities when parsing conditional types. 43 | inDisallowConditionalTypesContext: boolean = false; 44 | 45 | // Token store. 46 | tokens: Array = []; 47 | 48 | // Array of all observed scopes, ordered by their ending position. 49 | scopes: Array = []; 50 | 51 | // The current position of the tokenizer in the input. 52 | pos: number = 0; 53 | 54 | // Information about the current token. 55 | type: TokenType = tt.eof; 56 | contextualKeyword: ContextualKeyword = ContextualKeyword.NONE; 57 | start: number = 0; 58 | end: number = 0; 59 | 60 | isType: boolean = false; 61 | scopeDepth: number = 0; 62 | 63 | /** 64 | * If the parser is in an error state, then the token is always tt.eof and all functions can 65 | * keep executing but should be written so they don't get into an infinite loop in this situation. 66 | * 67 | * This approach, combined with the ability to snapshot and restore state, allows us to implement 68 | * backtracking without exceptions and without needing to explicitly propagate error states 69 | * everywhere. 70 | */ 71 | error: Error | null = null; 72 | 73 | snapshot(): StateSnapshot { 74 | return new StateSnapshot( 75 | this.potentialArrowAt, 76 | this.noAnonFunctionType, 77 | this.inDisallowConditionalTypesContext, 78 | this.tokens.length, 79 | this.scopes.length, 80 | this.pos, 81 | this.type, 82 | this.contextualKeyword, 83 | this.start, 84 | this.end, 85 | this.isType, 86 | this.scopeDepth, 87 | this.error, 88 | ); 89 | } 90 | 91 | restoreFromSnapshot(snapshot: StateSnapshot): void { 92 | this.potentialArrowAt = snapshot.potentialArrowAt; 93 | this.noAnonFunctionType = snapshot.noAnonFunctionType; 94 | this.inDisallowConditionalTypesContext = snapshot.inDisallowConditionalTypesContext; 95 | this.tokens.length = snapshot.tokensLength; 96 | this.scopes.length = snapshot.scopesLength; 97 | this.pos = snapshot.pos; 98 | this.type = snapshot.type; 99 | this.contextualKeyword = snapshot.contextualKeyword; 100 | this.start = snapshot.start; 101 | this.end = snapshot.end; 102 | this.isType = snapshot.isType; 103 | this.scopeDepth = snapshot.scopeDepth; 104 | this.error = snapshot.error; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/parser/traverser/base.ts: -------------------------------------------------------------------------------- 1 | import State from "../tokenizer/state"; 2 | import {charCodes} from "../util/charcodes"; 3 | 4 | export let isJSXEnabled: boolean; 5 | export let isTypeScriptEnabled: boolean; 6 | export let isFlowEnabled: boolean; 7 | export let state: State; 8 | export let input: string; 9 | export let nextContextId: number; 10 | 11 | export function getNextContextId(): number { 12 | return nextContextId++; 13 | } 14 | 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | export function augmentError(error: any): any { 17 | if ("pos" in error) { 18 | const loc = locationForIndex(error.pos); 19 | error.message += ` (${loc.line}:${loc.column})`; 20 | error.loc = loc; 21 | } 22 | return error; 23 | } 24 | 25 | export class Loc { 26 | line: number; 27 | column: number; 28 | constructor(line: number, column: number) { 29 | this.line = line; 30 | this.column = column; 31 | } 32 | } 33 | 34 | export function locationForIndex(pos: number): Loc { 35 | let line = 1; 36 | let column = 1; 37 | for (let i = 0; i < pos; i++) { 38 | if (input.charCodeAt(i) === charCodes.lineFeed) { 39 | line++; 40 | column = 1; 41 | } else { 42 | column++; 43 | } 44 | } 45 | return new Loc(line, column); 46 | } 47 | 48 | export function initParser( 49 | inputCode: string, 50 | isJSXEnabledArg: boolean, 51 | isTypeScriptEnabledArg: boolean, 52 | isFlowEnabledArg: boolean, 53 | ): void { 54 | input = inputCode; 55 | state = new State(); 56 | nextContextId = 1; 57 | isJSXEnabled = isJSXEnabledArg; 58 | isTypeScriptEnabled = isTypeScriptEnabledArg; 59 | isFlowEnabled = isFlowEnabledArg; 60 | } 61 | -------------------------------------------------------------------------------- /src/parser/traverser/index.ts: -------------------------------------------------------------------------------- 1 | import type {File} from "../index"; 2 | import {nextToken, skipLineComment} from "../tokenizer/index"; 3 | import {charCodes} from "../util/charcodes"; 4 | import {input, state} from "./base"; 5 | import {parseTopLevel} from "./statement"; 6 | 7 | export function parseFile(): File { 8 | // If enabled, skip leading hashbang line. 9 | if ( 10 | state.pos === 0 && 11 | input.charCodeAt(0) === charCodes.numberSign && 12 | input.charCodeAt(1) === charCodes.exclamationMark 13 | ) { 14 | skipLineComment(2); 15 | } 16 | nextToken(); 17 | return parseTopLevel(); 18 | } 19 | -------------------------------------------------------------------------------- /src/parser/traverser/util.ts: -------------------------------------------------------------------------------- 1 | import {eat, finishToken, lookaheadTypeAndKeyword, match, nextTokenStart} from "../tokenizer/index"; 2 | import type {ContextualKeyword} from "../tokenizer/keywords"; 3 | import {formatTokenType, type TokenType, TokenType as tt} from "../tokenizer/types"; 4 | import {charCodes} from "../util/charcodes"; 5 | import {input, state} from "./base"; 6 | 7 | // ## Parser utilities 8 | 9 | // Tests whether parsed token is a contextual keyword. 10 | export function isContextual(contextualKeyword: ContextualKeyword): boolean { 11 | return state.contextualKeyword === contextualKeyword; 12 | } 13 | 14 | export function isLookaheadContextual(contextualKeyword: ContextualKeyword): boolean { 15 | const l = lookaheadTypeAndKeyword(); 16 | return l.type === tt.name && l.contextualKeyword === contextualKeyword; 17 | } 18 | 19 | // Consumes contextual keyword if possible. 20 | export function eatContextual(contextualKeyword: ContextualKeyword): boolean { 21 | return state.contextualKeyword === contextualKeyword && eat(tt.name); 22 | } 23 | 24 | // Asserts that following token is given contextual keyword. 25 | export function expectContextual(contextualKeyword: ContextualKeyword): void { 26 | if (!eatContextual(contextualKeyword)) { 27 | unexpected(); 28 | } 29 | } 30 | 31 | // Test whether a semicolon can be inserted at the current position. 32 | export function canInsertSemicolon(): boolean { 33 | return match(tt.eof) || match(tt.braceR) || hasPrecedingLineBreak(); 34 | } 35 | 36 | export function hasPrecedingLineBreak(): boolean { 37 | const prevToken = state.tokens[state.tokens.length - 1]; 38 | const lastTokEnd = prevToken ? prevToken.end : 0; 39 | for (let i = lastTokEnd; i < state.start; i++) { 40 | const code = input.charCodeAt(i); 41 | if ( 42 | code === charCodes.lineFeed || 43 | code === charCodes.carriageReturn || 44 | code === 0x2028 || 45 | code === 0x2029 46 | ) { 47 | return true; 48 | } 49 | } 50 | return false; 51 | } 52 | 53 | export function hasFollowingLineBreak(): boolean { 54 | const nextStart = nextTokenStart(); 55 | for (let i = state.end; i < nextStart; i++) { 56 | const code = input.charCodeAt(i); 57 | if ( 58 | code === charCodes.lineFeed || 59 | code === charCodes.carriageReturn || 60 | code === 0x2028 || 61 | code === 0x2029 62 | ) { 63 | return true; 64 | } 65 | } 66 | return false; 67 | } 68 | 69 | export function isLineTerminator(): boolean { 70 | return eat(tt.semi) || canInsertSemicolon(); 71 | } 72 | 73 | // Consume a semicolon, or, failing that, see if we are allowed to 74 | // pretend that there is a semicolon at this position. 75 | export function semicolon(): void { 76 | if (!isLineTerminator()) { 77 | unexpected('Unexpected token, expected ";"'); 78 | } 79 | } 80 | 81 | // Expect a token of a given type. If found, consume it, otherwise, 82 | // raise an unexpected token error at given pos. 83 | export function expect(type: TokenType): void { 84 | const matched = eat(type); 85 | if (!matched) { 86 | unexpected(`Unexpected token, expected "${formatTokenType(type)}"`); 87 | } 88 | } 89 | 90 | /** 91 | * Transition the parser to an error state. All code needs to be written to naturally unwind in this 92 | * state, which allows us to backtrack without exceptions and without error plumbing everywhere. 93 | */ 94 | export function unexpected(message: string = "Unexpected token", pos: number = state.start): void { 95 | if (state.error) { 96 | return; 97 | } 98 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 99 | const err: any = new SyntaxError(message); 100 | err.pos = pos; 101 | state.error = err; 102 | state.pos = input.length; 103 | finishToken(tt.eof); 104 | } 105 | -------------------------------------------------------------------------------- /src/parser/util/charcodes.ts: -------------------------------------------------------------------------------- 1 | export enum charCodes { 2 | backSpace = 8, 3 | lineFeed = 10, // '\n' 4 | tab = 9, // '\t' 5 | carriageReturn = 13, // '\r' 6 | shiftOut = 14, 7 | space = 32, 8 | exclamationMark = 33, // '!' 9 | quotationMark = 34, // '"' 10 | numberSign = 35, // '#' 11 | dollarSign = 36, // '$' 12 | percentSign = 37, // '%' 13 | ampersand = 38, // '&' 14 | apostrophe = 39, // ''' 15 | leftParenthesis = 40, // '(' 16 | rightParenthesis = 41, // ')' 17 | asterisk = 42, // '*' 18 | plusSign = 43, // '+' 19 | comma = 44, // ',' 20 | dash = 45, // '-' 21 | dot = 46, // '.' 22 | slash = 47, // '/' 23 | digit0 = 48, // '0' 24 | digit1 = 49, // '1' 25 | digit2 = 50, // '2' 26 | digit3 = 51, // '3' 27 | digit4 = 52, // '4' 28 | digit5 = 53, // '5' 29 | digit6 = 54, // '6' 30 | digit7 = 55, // '7' 31 | digit8 = 56, // '8' 32 | digit9 = 57, // '9' 33 | colon = 58, // ':' 34 | semicolon = 59, // ';' 35 | lessThan = 60, // '<' 36 | equalsTo = 61, // '=' 37 | greaterThan = 62, // '>' 38 | questionMark = 63, // '?' 39 | atSign = 64, // '@' 40 | uppercaseA = 65, // 'A' 41 | uppercaseB = 66, // 'B' 42 | uppercaseC = 67, // 'C' 43 | uppercaseD = 68, // 'D' 44 | uppercaseE = 69, // 'E' 45 | uppercaseF = 70, // 'F' 46 | uppercaseG = 71, // 'G' 47 | uppercaseH = 72, // 'H' 48 | uppercaseI = 73, // 'I' 49 | uppercaseJ = 74, // 'J' 50 | uppercaseK = 75, // 'K' 51 | uppercaseL = 76, // 'L' 52 | uppercaseM = 77, // 'M' 53 | uppercaseN = 78, // 'N' 54 | uppercaseO = 79, // 'O' 55 | uppercaseP = 80, // 'P' 56 | uppercaseQ = 81, // 'Q' 57 | uppercaseR = 82, // 'R' 58 | uppercaseS = 83, // 'S' 59 | uppercaseT = 84, // 'T' 60 | uppercaseU = 85, // 'U' 61 | uppercaseV = 86, // 'V' 62 | uppercaseW = 87, // 'W' 63 | uppercaseX = 88, // 'X' 64 | uppercaseY = 89, // 'Y' 65 | uppercaseZ = 90, // 'Z' 66 | leftSquareBracket = 91, // '[' 67 | backslash = 92, // '\ ' 68 | rightSquareBracket = 93, // ']' 69 | caret = 94, // '^' 70 | underscore = 95, // '_' 71 | graveAccent = 96, // '`' 72 | lowercaseA = 97, // 'a' 73 | lowercaseB = 98, // 'b' 74 | lowercaseC = 99, // 'c' 75 | lowercaseD = 100, // 'd' 76 | lowercaseE = 101, // 'e' 77 | lowercaseF = 102, // 'f' 78 | lowercaseG = 103, // 'g' 79 | lowercaseH = 104, // 'h' 80 | lowercaseI = 105, // 'i' 81 | lowercaseJ = 106, // 'j' 82 | lowercaseK = 107, // 'k' 83 | lowercaseL = 108, // 'l' 84 | lowercaseM = 109, // 'm' 85 | lowercaseN = 110, // 'n' 86 | lowercaseO = 111, // 'o' 87 | lowercaseP = 112, // 'p' 88 | lowercaseQ = 113, // 'q' 89 | lowercaseR = 114, // 'r' 90 | lowercaseS = 115, // 's' 91 | lowercaseT = 116, // 't' 92 | lowercaseU = 117, // 'u' 93 | lowercaseV = 118, // 'v' 94 | lowercaseW = 119, // 'w' 95 | lowercaseX = 120, // 'x' 96 | lowercaseY = 121, // 'y' 97 | lowercaseZ = 122, // 'z' 98 | leftCurlyBrace = 123, // '{' 99 | verticalBar = 124, // '|' 100 | rightCurlyBrace = 125, // '}' 101 | tilde = 126, // '~' 102 | nonBreakingSpace = 160, 103 | // eslint-disable-next-line no-irregular-whitespace 104 | oghamSpaceMark = 5760, // ' ' 105 | lineSeparator = 8232, 106 | paragraphSeparator = 8233, 107 | } 108 | 109 | export function isDigit(code: number): boolean { 110 | return ( 111 | (code >= charCodes.digit0 && code <= charCodes.digit9) || 112 | (code >= charCodes.lowercaseA && code <= charCodes.lowercaseF) || 113 | (code >= charCodes.uppercaseA && code <= charCodes.uppercaseF) 114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /src/parser/util/identifier.ts: -------------------------------------------------------------------------------- 1 | import {charCodes} from "./charcodes"; 2 | import {WHITESPACE_CHARS} from "./whitespace"; 3 | 4 | function computeIsIdentifierChar(code: number): boolean { 5 | if (code < 48) return code === 36; 6 | if (code < 58) return true; 7 | if (code < 65) return false; 8 | if (code < 91) return true; 9 | if (code < 97) return code === 95; 10 | if (code < 123) return true; 11 | if (code < 128) return false; 12 | throw new Error("Should not be called with non-ASCII char code."); 13 | } 14 | 15 | export const IS_IDENTIFIER_CHAR = new Uint8Array(65536); 16 | for (let i = 0; i < 128; i++) { 17 | IS_IDENTIFIER_CHAR[i] = computeIsIdentifierChar(i) ? 1 : 0; 18 | } 19 | for (let i = 128; i < 65536; i++) { 20 | IS_IDENTIFIER_CHAR[i] = 1; 21 | } 22 | // Aside from whitespace and newlines, all characters outside the ASCII space are either 23 | // identifier characters or invalid. Since we're not performing code validation, we can just 24 | // treat all invalid characters as identifier characters. 25 | for (const whitespaceChar of WHITESPACE_CHARS) { 26 | IS_IDENTIFIER_CHAR[whitespaceChar] = 0; 27 | } 28 | IS_IDENTIFIER_CHAR[0x2028] = 0; 29 | IS_IDENTIFIER_CHAR[0x2029] = 0; 30 | 31 | export const IS_IDENTIFIER_START = IS_IDENTIFIER_CHAR.slice(); 32 | for (let numChar = charCodes.digit0; numChar <= charCodes.digit9; numChar++) { 33 | IS_IDENTIFIER_START[numChar] = 0; 34 | } 35 | -------------------------------------------------------------------------------- /src/parser/util/whitespace.ts: -------------------------------------------------------------------------------- 1 | import {charCodes} from "./charcodes"; 2 | 3 | // https://tc39.github.io/ecma262/#sec-white-space 4 | export const WHITESPACE_CHARS: Array = [ 5 | 0x0009, 6 | 0x000b, 7 | 0x000c, 8 | charCodes.space, 9 | charCodes.nonBreakingSpace, 10 | charCodes.oghamSpaceMark, 11 | 0x2000, // EN QUAD 12 | 0x2001, // EM QUAD 13 | 0x2002, // EN SPACE 14 | 0x2003, // EM SPACE 15 | 0x2004, // THREE-PER-EM SPACE 16 | 0x2005, // FOUR-PER-EM SPACE 17 | 0x2006, // SIX-PER-EM SPACE 18 | 0x2007, // FIGURE SPACE 19 | 0x2008, // PUNCTUATION SPACE 20 | 0x2009, // THIN SPACE 21 | 0x200a, // HAIR SPACE 22 | 0x202f, // NARROW NO-BREAK SPACE 23 | 0x205f, // MEDIUM MATHEMATICAL SPACE 24 | 0x3000, // IDEOGRAPHIC SPACE 25 | 0xfeff, // ZERO WIDTH NO-BREAK SPACE 26 | ]; 27 | 28 | export const skipWhiteSpace = /(?:\s|\/\/.*|\/\*[^]*?\*\/)*/g; 29 | 30 | export const IS_WHITESPACE = new Uint8Array(65536); 31 | for (const char of WHITESPACE_CHARS) { 32 | IS_WHITESPACE[char] = 1; 33 | } 34 | -------------------------------------------------------------------------------- /src/register.ts: -------------------------------------------------------------------------------- 1 | import * as pirates from "pirates"; 2 | 3 | import {type Options, transform} from "./index"; 4 | 5 | export interface HookOptions { 6 | matcher?: (code: string) => boolean; 7 | ignoreNodeModules?: boolean; 8 | } 9 | 10 | export type RevertFunction = () => void; 11 | 12 | export function addHook( 13 | extension: string, 14 | sucraseOptions: Options, 15 | hookOptions?: HookOptions, 16 | ): RevertFunction { 17 | let mergedSucraseOptions = sucraseOptions; 18 | const sucraseOptionsEnvJSON = process.env.SUCRASE_OPTIONS; 19 | if (sucraseOptionsEnvJSON) { 20 | mergedSucraseOptions = {...mergedSucraseOptions, ...JSON.parse(sucraseOptionsEnvJSON)}; 21 | } 22 | return pirates.addHook( 23 | (code: string, filePath: string): string => { 24 | const {code: transformedCode, sourceMap} = transform(code, { 25 | ...mergedSucraseOptions, 26 | sourceMapOptions: {compiledFilename: filePath}, 27 | filePath, 28 | }); 29 | const mapBase64 = Buffer.from(JSON.stringify(sourceMap)).toString("base64"); 30 | const suffix = `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${mapBase64}`; 31 | return `${transformedCode}\n${suffix}`; 32 | }, 33 | {...hookOptions, exts: [extension]}, 34 | ); 35 | } 36 | 37 | export function registerJS(hookOptions?: HookOptions): RevertFunction { 38 | return addHook(".js", {transforms: ["imports", "flow", "jsx"]}, hookOptions); 39 | } 40 | 41 | export function registerJSX(hookOptions?: HookOptions): RevertFunction { 42 | return addHook(".jsx", {transforms: ["imports", "flow", "jsx"]}, hookOptions); 43 | } 44 | 45 | export function registerTS(hookOptions?: HookOptions): RevertFunction { 46 | return addHook(".ts", {transforms: ["imports", "typescript"]}, hookOptions); 47 | } 48 | 49 | export function registerTSX(hookOptions?: HookOptions): RevertFunction { 50 | return addHook(".tsx", {transforms: ["imports", "typescript", "jsx"]}, hookOptions); 51 | } 52 | 53 | export function registerTSLegacyModuleInterop(hookOptions?: HookOptions): RevertFunction { 54 | return addHook( 55 | ".ts", 56 | { 57 | transforms: ["imports", "typescript"], 58 | enableLegacyTypeScriptModuleInterop: true, 59 | }, 60 | hookOptions, 61 | ); 62 | } 63 | 64 | export function registerTSXLegacyModuleInterop(hookOptions?: HookOptions): RevertFunction { 65 | return addHook( 66 | ".tsx", 67 | { 68 | transforms: ["imports", "typescript", "jsx"], 69 | enableLegacyTypeScriptModuleInterop: true, 70 | }, 71 | hookOptions, 72 | ); 73 | } 74 | 75 | export function registerAll(hookOptions?: HookOptions): RevertFunction { 76 | const reverts = [ 77 | registerJS(hookOptions), 78 | registerJSX(hookOptions), 79 | registerTS(hookOptions), 80 | registerTSX(hookOptions), 81 | ]; 82 | 83 | return () => { 84 | for (const fn of reverts) { 85 | fn(); 86 | } 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /src/transformers/NumericSeparatorTransformer.ts: -------------------------------------------------------------------------------- 1 | import {TokenType as tt} from "../parser/tokenizer/types"; 2 | import type TokenProcessor from "../TokenProcessor"; 3 | import Transformer from "./Transformer"; 4 | 5 | export default class NumericSeparatorTransformer extends Transformer { 6 | constructor(readonly tokens: TokenProcessor) { 7 | super(); 8 | } 9 | 10 | process(): boolean { 11 | if (this.tokens.matches1(tt.num)) { 12 | const code = this.tokens.currentTokenCode(); 13 | if (code.includes("_")) { 14 | this.tokens.replaceToken(code.replace(/_/g, "")); 15 | return true; 16 | } 17 | } 18 | return false; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/transformers/OptionalCatchBindingTransformer.ts: -------------------------------------------------------------------------------- 1 | import type NameManager from "../NameManager"; 2 | import {TokenType as tt} from "../parser/tokenizer/types"; 3 | import type TokenProcessor from "../TokenProcessor"; 4 | import Transformer from "./Transformer"; 5 | 6 | export default class OptionalCatchBindingTransformer extends Transformer { 7 | constructor(readonly tokens: TokenProcessor, readonly nameManager: NameManager) { 8 | super(); 9 | } 10 | 11 | process(): boolean { 12 | if (this.tokens.matches2(tt._catch, tt.braceL)) { 13 | this.tokens.copyToken(); 14 | this.tokens.appendCode(` (${this.nameManager.claimFreeName("e")})`); 15 | return true; 16 | } 17 | return false; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/transformers/ReactHotLoaderTransformer.ts: -------------------------------------------------------------------------------- 1 | import {IdentifierRole, isTopLevelDeclaration} from "../parser/tokenizer"; 2 | import type TokenProcessor from "../TokenProcessor"; 3 | import Transformer from "./Transformer"; 4 | 5 | export default class ReactHotLoaderTransformer extends Transformer { 6 | private extractedDefaultExportName: string | null = null; 7 | 8 | constructor(readonly tokens: TokenProcessor, readonly filePath: string) { 9 | super(); 10 | } 11 | 12 | setExtractedDefaultExportName(extractedDefaultExportName: string): void { 13 | this.extractedDefaultExportName = extractedDefaultExportName; 14 | } 15 | 16 | getPrefixCode(): string { 17 | return ` 18 | (function () { 19 | var enterModule = require('react-hot-loader').enterModule; 20 | enterModule && enterModule(module); 21 | })();` 22 | .replace(/\s+/g, " ") 23 | .trim(); 24 | } 25 | 26 | getSuffixCode(): string { 27 | const topLevelNames = new Set(); 28 | for (const token of this.tokens.tokens) { 29 | if ( 30 | !token.isType && 31 | isTopLevelDeclaration(token) && 32 | token.identifierRole !== IdentifierRole.ImportDeclaration 33 | ) { 34 | topLevelNames.add(this.tokens.identifierNameForToken(token)); 35 | } 36 | } 37 | const namesToRegister = Array.from(topLevelNames).map((name) => ({ 38 | variableName: name, 39 | uniqueLocalName: name, 40 | })); 41 | if (this.extractedDefaultExportName) { 42 | namesToRegister.push({ 43 | variableName: this.extractedDefaultExportName, 44 | uniqueLocalName: "default", 45 | }); 46 | } 47 | return ` 48 | ;(function () { 49 | var reactHotLoader = require('react-hot-loader').default; 50 | var leaveModule = require('react-hot-loader').leaveModule; 51 | if (!reactHotLoader) { 52 | return; 53 | } 54 | ${namesToRegister 55 | .map( 56 | ({variableName, uniqueLocalName}) => 57 | ` reactHotLoader.register(${variableName}, "${uniqueLocalName}", ${JSON.stringify( 58 | this.filePath || "", 59 | )});`, 60 | ) 61 | .join("\n")} 62 | leaveModule(module); 63 | })();`; 64 | } 65 | 66 | process(): boolean { 67 | return false; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/transformers/Transformer.ts: -------------------------------------------------------------------------------- 1 | export default abstract class Transformer { 2 | // Return true if anything was processed, false otherwise. 3 | abstract process(): boolean; 4 | 5 | getPrefixCode(): string { 6 | return ""; 7 | } 8 | 9 | getHoistedCode(): string { 10 | return ""; 11 | } 12 | 13 | getSuffixCode(): string { 14 | return ""; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | // Special TS project just for source files so we can build types for those. 2 | { 3 | "extends": "../tsconfig.json", 4 | "compilerOptions": { 5 | "declaration": true, 6 | "emitDeclarationOnly": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/util/elideImportEquals.ts: -------------------------------------------------------------------------------- 1 | import {TokenType as tt} from "../parser/tokenizer/types"; 2 | import type TokenProcessor from "../TokenProcessor"; 3 | 4 | export default function elideImportEquals(tokens: TokenProcessor): void { 5 | // import 6 | tokens.removeInitialToken(); 7 | // name 8 | tokens.removeToken(); 9 | // = 10 | tokens.removeToken(); 11 | // name or require 12 | tokens.removeToken(); 13 | // Handle either `import A = require('A')` or `import A = B.C.D`. 14 | if (tokens.matches1(tt.parenL)) { 15 | // ( 16 | tokens.removeToken(); 17 | // path string 18 | tokens.removeToken(); 19 | // ) 20 | tokens.removeToken(); 21 | } else { 22 | while (tokens.matches1(tt.dot)) { 23 | // . 24 | tokens.removeToken(); 25 | // name 26 | tokens.removeToken(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/util/formatTokens.ts: -------------------------------------------------------------------------------- 1 | import LinesAndColumns from "lines-and-columns"; 2 | 3 | import type {Token} from "../parser/tokenizer"; 4 | import {formatTokenType} from "../parser/tokenizer/types"; 5 | 6 | export default function formatTokens(code: string, tokens: Array): string { 7 | if (tokens.length === 0) { 8 | return ""; 9 | } 10 | 11 | const tokenKeys = Object.keys(tokens[0]).filter( 12 | (k) => k !== "type" && k !== "value" && k !== "start" && k !== "end" && k !== "loc", 13 | ); 14 | const typeKeys = Object.keys(tokens[0].type).filter((k) => k !== "label" && k !== "keyword"); 15 | 16 | const headings = ["Location", "Label", "Raw", ...tokenKeys, ...typeKeys]; 17 | 18 | const lines = new LinesAndColumns(code); 19 | const rows = [headings, ...tokens.map(getTokenComponents)]; 20 | const padding = headings.map(() => 0); 21 | for (const components of rows) { 22 | for (let i = 0; i < components.length; i++) { 23 | padding[i] = Math.max(padding[i], components[i].length); 24 | } 25 | } 26 | return rows 27 | .map((components) => components.map((component, i) => component.padEnd(padding[i])).join(" ")) 28 | .join("\n"); 29 | 30 | function getTokenComponents(token: Token): Array { 31 | const raw = code.slice(token.start, token.end); 32 | return [ 33 | formatRange(token.start, token.end), 34 | formatTokenType(token.type), 35 | truncate(String(raw), 14), 36 | // @ts-ignore: Intentional dynamic access by key. 37 | ...tokenKeys.map((key) => formatValue(token[key], key)), 38 | // @ts-ignore: Intentional dynamic access by key. 39 | ...typeKeys.map((key) => formatValue(token.type[key], key)), 40 | ]; 41 | } 42 | 43 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 44 | function formatValue(value: any, key: string): string { 45 | if (value === true) { 46 | return key; 47 | } else if (value === false || value === null) { 48 | return ""; 49 | } else { 50 | return String(value); 51 | } 52 | } 53 | 54 | function formatRange(start: number, end: number): string { 55 | return `${formatPos(start)}-${formatPos(end)}`; 56 | } 57 | 58 | function formatPos(pos: number): string { 59 | const location = lines.locationForIndex(pos); 60 | if (!location) { 61 | return "Unknown"; 62 | } else { 63 | return `${location.line + 1}:${location.column + 1}`; 64 | } 65 | } 66 | } 67 | 68 | function truncate(s: string, length: number): string { 69 | if (s.length > length) { 70 | return `${s.slice(0, length - 3)}...`; 71 | } else { 72 | return s; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/util/getDeclarationInfo.ts: -------------------------------------------------------------------------------- 1 | import {isTopLevelDeclaration} from "../parser/tokenizer"; 2 | import {TokenType as tt} from "../parser/tokenizer/types"; 3 | import type TokenProcessor from "../TokenProcessor"; 4 | 5 | export interface DeclarationInfo { 6 | typeDeclarations: Set; 7 | valueDeclarations: Set; 8 | } 9 | 10 | export const EMPTY_DECLARATION_INFO: DeclarationInfo = { 11 | typeDeclarations: new Set(), 12 | valueDeclarations: new Set(), 13 | }; 14 | 15 | /** 16 | * Get all top-level identifiers that should be preserved when exported in TypeScript. 17 | * 18 | * Examples: 19 | * - If an identifier is declared as `const x`, then `export {x}` should be preserved. 20 | * - If it's declared as `type x`, then `export {x}` should be removed. 21 | * - If it's declared as both `const x` and `type x`, then the export should be preserved. 22 | * - Classes and enums should be preserved (even though they also introduce types). 23 | * - Imported identifiers should be preserved since we don't have enough information to 24 | * rule them out. --isolatedModules disallows re-exports, which catches errors here. 25 | */ 26 | export default function getDeclarationInfo(tokens: TokenProcessor): DeclarationInfo { 27 | const typeDeclarations: Set = new Set(); 28 | const valueDeclarations: Set = new Set(); 29 | for (let i = 0; i < tokens.tokens.length; i++) { 30 | const token = tokens.tokens[i]; 31 | if (token.type === tt.name && isTopLevelDeclaration(token)) { 32 | if (token.isType) { 33 | typeDeclarations.add(tokens.identifierNameForToken(token)); 34 | } else { 35 | valueDeclarations.add(tokens.identifierNameForToken(token)); 36 | } 37 | } 38 | } 39 | return {typeDeclarations, valueDeclarations}; 40 | } 41 | -------------------------------------------------------------------------------- /src/util/getIdentifierNames.ts: -------------------------------------------------------------------------------- 1 | import type {Token} from "../parser/tokenizer"; 2 | import {TokenType as tt} from "../parser/tokenizer/types"; 3 | 4 | /** 5 | * Get all identifier names in the code, in order, including duplicates. 6 | */ 7 | export default function getIdentifierNames(code: string, tokens: Array): Array { 8 | const names = []; 9 | for (const token of tokens) { 10 | if (token.type === tt.name) { 11 | names.push(code.slice(token.start, token.end)); 12 | } 13 | } 14 | return names; 15 | } 16 | -------------------------------------------------------------------------------- /src/util/getImportExportSpecifierInfo.ts: -------------------------------------------------------------------------------- 1 | import {TokenType as tt} from "../parser/tokenizer/types"; 2 | import type TokenProcessor from "../TokenProcessor"; 3 | 4 | export type ImportExportSpecifierInfo = 5 | | { 6 | isType: false; 7 | leftName: string; 8 | rightName: string; 9 | endIndex: number; 10 | } 11 | | { 12 | isType: true; 13 | leftName: null; 14 | rightName: null; 15 | endIndex: number; 16 | }; 17 | 18 | /** 19 | * Determine information about this named import or named export specifier. 20 | * 21 | * This syntax is the `a` from statements like these: 22 | * import {A} from "./foo"; 23 | * export {A}; 24 | * export {A} from "./foo"; 25 | * 26 | * As it turns out, we can exactly characterize the syntax meaning by simply 27 | * counting the number of tokens, which can be from 1 to 4: 28 | * {A} 29 | * {type A} 30 | * {A as B} 31 | * {type A as B} 32 | * 33 | * In the type case, we never actually need the names in practice, so don't get 34 | * them. 35 | * 36 | * TODO: There's some redundancy with the type detection here and the isType 37 | * flag that's already present on tokens in TS mode. This function could 38 | * potentially be simplified and/or pushed to the call sites to avoid the object 39 | * allocation. 40 | */ 41 | export default function getImportExportSpecifierInfo( 42 | tokens: TokenProcessor, 43 | index: number = tokens.currentIndex(), 44 | ): ImportExportSpecifierInfo { 45 | let endIndex = index + 1; 46 | if (isSpecifierEnd(tokens, endIndex)) { 47 | // import {A} 48 | const name = tokens.identifierNameAtIndex(index); 49 | return { 50 | isType: false, 51 | leftName: name, 52 | rightName: name, 53 | endIndex, 54 | }; 55 | } 56 | endIndex++; 57 | if (isSpecifierEnd(tokens, endIndex)) { 58 | // import {type A} 59 | return { 60 | isType: true, 61 | leftName: null, 62 | rightName: null, 63 | endIndex, 64 | }; 65 | } 66 | endIndex++; 67 | if (isSpecifierEnd(tokens, endIndex)) { 68 | // import {A as B} 69 | return { 70 | isType: false, 71 | leftName: tokens.identifierNameAtIndex(index), 72 | rightName: tokens.identifierNameAtIndex(index + 2), 73 | endIndex, 74 | }; 75 | } 76 | endIndex++; 77 | if (isSpecifierEnd(tokens, endIndex)) { 78 | // import {type A as B} 79 | return { 80 | isType: true, 81 | leftName: null, 82 | rightName: null, 83 | endIndex, 84 | }; 85 | } 86 | throw new Error(`Unexpected import/export specifier at ${index}`); 87 | } 88 | 89 | function isSpecifierEnd(tokens: TokenProcessor, index: number): boolean { 90 | const token = tokens.tokens[index]; 91 | return token.type === tt.braceR || token.type === tt.comma; 92 | } 93 | -------------------------------------------------------------------------------- /src/util/getJSXPragmaInfo.ts: -------------------------------------------------------------------------------- 1 | import type {Options} from "../index"; 2 | 3 | export interface JSXPragmaInfo { 4 | base: string; 5 | suffix: string; 6 | fragmentBase: string; 7 | fragmentSuffix: string; 8 | } 9 | 10 | export default function getJSXPragmaInfo(options: Options): JSXPragmaInfo { 11 | const [base, suffix] = splitPragma(options.jsxPragma || "React.createElement"); 12 | const [fragmentBase, fragmentSuffix] = splitPragma(options.jsxFragmentPragma || "React.Fragment"); 13 | return {base, suffix, fragmentBase, fragmentSuffix}; 14 | } 15 | 16 | function splitPragma(pragma: string): [string, string] { 17 | let dotIndex = pragma.indexOf("."); 18 | if (dotIndex === -1) { 19 | dotIndex = pragma.length; 20 | } 21 | return [pragma.slice(0, dotIndex), pragma.slice(dotIndex)]; 22 | } 23 | -------------------------------------------------------------------------------- /src/util/getNonTypeIdentifiers.ts: -------------------------------------------------------------------------------- 1 | import type {Options} from "../index"; 2 | import {IdentifierRole} from "../parser/tokenizer"; 3 | import {TokenType, TokenType as tt} from "../parser/tokenizer/types"; 4 | import type TokenProcessor from "../TokenProcessor"; 5 | import {startsWithLowerCase} from "../transformers/JSXTransformer"; 6 | import getJSXPragmaInfo from "./getJSXPragmaInfo"; 7 | 8 | export function getNonTypeIdentifiers(tokens: TokenProcessor, options: Options): Set { 9 | const jsxPragmaInfo = getJSXPragmaInfo(options); 10 | const nonTypeIdentifiers: Set = new Set(); 11 | for (let i = 0; i < tokens.tokens.length; i++) { 12 | const token = tokens.tokens[i]; 13 | if ( 14 | token.type === tt.name && 15 | !token.isType && 16 | (token.identifierRole === IdentifierRole.Access || 17 | token.identifierRole === IdentifierRole.ObjectShorthand || 18 | token.identifierRole === IdentifierRole.ExportAccess) && 19 | !token.shadowsGlobal 20 | ) { 21 | nonTypeIdentifiers.add(tokens.identifierNameForToken(token)); 22 | } 23 | if (token.type === tt.jsxTagStart) { 24 | nonTypeIdentifiers.add(jsxPragmaInfo.base); 25 | } 26 | if ( 27 | token.type === tt.jsxTagStart && 28 | i + 1 < tokens.tokens.length && 29 | tokens.tokens[i + 1].type === tt.jsxTagEnd 30 | ) { 31 | nonTypeIdentifiers.add(jsxPragmaInfo.base); 32 | nonTypeIdentifiers.add(jsxPragmaInfo.fragmentBase); 33 | } 34 | if (token.type === tt.jsxName && token.identifierRole === IdentifierRole.Access) { 35 | const identifierName = tokens.identifierNameForToken(token); 36 | // Lower-case single-component tag names like "div" don't count. 37 | if (!startsWithLowerCase(identifierName) || tokens.tokens[i + 1].type === TokenType.dot) { 38 | nonTypeIdentifiers.add(tokens.identifierNameForToken(token)); 39 | } 40 | } 41 | } 42 | return nonTypeIdentifiers; 43 | } 44 | -------------------------------------------------------------------------------- /src/util/getTSImportedNames.ts: -------------------------------------------------------------------------------- 1 | import {TokenType as tt} from "../parser/tokenizer/types"; 2 | import type TokenProcessor from "../TokenProcessor"; 3 | import getImportExportSpecifierInfo from "./getImportExportSpecifierInfo"; 4 | 5 | /** 6 | * Special case code to scan for imported names in ESM TypeScript. We need to do this so we can 7 | * properly get globals so we can compute shadowed globals. 8 | * 9 | * This is similar to logic in CJSImportProcessor, but trimmed down to avoid logic with CJS 10 | * replacement and flow type imports. 11 | */ 12 | export default function getTSImportedNames(tokens: TokenProcessor): Set { 13 | const importedNames = new Set(); 14 | for (let i = 0; i < tokens.tokens.length; i++) { 15 | if ( 16 | tokens.matches1AtIndex(i, tt._import) && 17 | !tokens.matches3AtIndex(i, tt._import, tt.name, tt.eq) 18 | ) { 19 | collectNamesForImport(tokens, i, importedNames); 20 | } 21 | } 22 | return importedNames; 23 | } 24 | 25 | function collectNamesForImport( 26 | tokens: TokenProcessor, 27 | index: number, 28 | importedNames: Set, 29 | ): void { 30 | index++; 31 | 32 | if (tokens.matches1AtIndex(index, tt.parenL)) { 33 | // Dynamic import, so nothing to do 34 | return; 35 | } 36 | 37 | if (tokens.matches1AtIndex(index, tt.name)) { 38 | importedNames.add(tokens.identifierNameAtIndex(index)); 39 | index++; 40 | if (tokens.matches1AtIndex(index, tt.comma)) { 41 | index++; 42 | } 43 | } 44 | 45 | if (tokens.matches1AtIndex(index, tt.star)) { 46 | // * as 47 | index += 2; 48 | importedNames.add(tokens.identifierNameAtIndex(index)); 49 | index++; 50 | } 51 | 52 | if (tokens.matches1AtIndex(index, tt.braceL)) { 53 | index++; 54 | collectNamesForNamedImport(tokens, index, importedNames); 55 | } 56 | } 57 | 58 | function collectNamesForNamedImport( 59 | tokens: TokenProcessor, 60 | index: number, 61 | importedNames: Set, 62 | ): void { 63 | while (true) { 64 | if (tokens.matches1AtIndex(index, tt.braceR)) { 65 | return; 66 | } 67 | 68 | const specifierInfo = getImportExportSpecifierInfo(tokens, index); 69 | index = specifierInfo.endIndex; 70 | if (!specifierInfo.isType) { 71 | importedNames.add(specifierInfo.rightName); 72 | } 73 | 74 | if (tokens.matches2AtIndex(index, tt.comma, tt.braceR)) { 75 | return; 76 | } else if (tokens.matches1AtIndex(index, tt.braceR)) { 77 | return; 78 | } else if (tokens.matches1AtIndex(index, tt.comma)) { 79 | index++; 80 | } else { 81 | throw new Error(`Unexpected token: ${JSON.stringify(tokens.tokens[index])}`); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/util/isAsyncOperation.ts: -------------------------------------------------------------------------------- 1 | import {ContextualKeyword} from "../parser/tokenizer/keywords"; 2 | import type TokenProcessor from "../TokenProcessor"; 3 | 4 | /** 5 | * Determine whether this optional chain or nullish coalescing operation has any await statements in 6 | * it. If so, we'll need to transpile to an async operation. 7 | * 8 | * We compute this by walking the length of the operation and returning true if we see an await 9 | * keyword used as a real await (rather than an object key or property access). Nested optional 10 | * chain/nullish operations need to be tracked but don't silence await, but a nested async function 11 | * (or any other nested scope) will make the await not count. 12 | */ 13 | export default function isAsyncOperation(tokens: TokenProcessor): boolean { 14 | let index = tokens.currentIndex(); 15 | let depth = 0; 16 | const startToken = tokens.currentToken(); 17 | do { 18 | const token = tokens.tokens[index]; 19 | if (token.isOptionalChainStart) { 20 | depth++; 21 | } 22 | if (token.isOptionalChainEnd) { 23 | depth--; 24 | } 25 | depth += token.numNullishCoalesceStarts; 26 | depth -= token.numNullishCoalesceEnds; 27 | 28 | if ( 29 | token.contextualKeyword === ContextualKeyword._await && 30 | token.identifierRole == null && 31 | token.scopeDepth === startToken.scopeDepth 32 | ) { 33 | return true; 34 | } 35 | index += 1; 36 | } while (depth > 0 && index < tokens.tokens.length); 37 | return false; 38 | } 39 | -------------------------------------------------------------------------------- /src/util/isExportFrom.ts: -------------------------------------------------------------------------------- 1 | import {ContextualKeyword} from "../parser/tokenizer/keywords"; 2 | import {TokenType as tt} from "../parser/tokenizer/types"; 3 | import type TokenProcessor from "../TokenProcessor"; 4 | 5 | /** 6 | * Starting at `export {`, look ahead and return `true` if this is an 7 | * `export {...} from` statement and `false` if this is a plain multi-export. 8 | */ 9 | export default function isExportFrom(tokens: TokenProcessor): boolean { 10 | let closeBraceIndex = tokens.currentIndex(); 11 | while (!tokens.matches1AtIndex(closeBraceIndex, tt.braceR)) { 12 | closeBraceIndex++; 13 | } 14 | return ( 15 | tokens.matchesContextualAtIndex(closeBraceIndex + 1, ContextualKeyword._from) && 16 | tokens.matches1AtIndex(closeBraceIndex + 2, tt.string) 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/util/isIdentifier.ts: -------------------------------------------------------------------------------- 1 | import {IS_IDENTIFIER_CHAR, IS_IDENTIFIER_START} from "../parser/util/identifier"; 2 | 3 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar 4 | // Hard-code a list of reserved words rather than trying to use keywords or contextual keywords 5 | // from the parser, since currently there are various exceptions, like `package` being reserved 6 | // but unused and various contextual keywords being reserved. Note that we assume that all code 7 | // compiled by Sucrase is in a module, so strict mode words and await are all considered reserved 8 | // here. 9 | const RESERVED_WORDS = new Set([ 10 | // Reserved keywords as of ECMAScript 2015 11 | "break", 12 | "case", 13 | "catch", 14 | "class", 15 | "const", 16 | "continue", 17 | "debugger", 18 | "default", 19 | "delete", 20 | "do", 21 | "else", 22 | "export", 23 | "extends", 24 | "finally", 25 | "for", 26 | "function", 27 | "if", 28 | "import", 29 | "in", 30 | "instanceof", 31 | "new", 32 | "return", 33 | "super", 34 | "switch", 35 | "this", 36 | "throw", 37 | "try", 38 | "typeof", 39 | "var", 40 | "void", 41 | "while", 42 | "with", 43 | "yield", 44 | // Future reserved keywords 45 | "enum", 46 | "implements", 47 | "interface", 48 | "let", 49 | "package", 50 | "private", 51 | "protected", 52 | "public", 53 | "static", 54 | "await", 55 | // Literals that cannot be used as identifiers 56 | "false", 57 | "null", 58 | "true", 59 | ]); 60 | 61 | /** 62 | * Determine if the given name is a legal variable name. 63 | * 64 | * This is needed when transforming TypeScript enums; if an enum key is a valid 65 | * variable name, it might be referenced later in the enum, so we need to 66 | * declare a variable. 67 | */ 68 | export default function isIdentifier(name: string): boolean { 69 | if (name.length === 0) { 70 | return false; 71 | } 72 | if (!IS_IDENTIFIER_START[name.charCodeAt(0)]) { 73 | return false; 74 | } 75 | for (let i = 1; i < name.length; i++) { 76 | if (!IS_IDENTIFIER_CHAR[name.charCodeAt(i)]) { 77 | return false; 78 | } 79 | } 80 | return !RESERVED_WORDS.has(name); 81 | } 82 | -------------------------------------------------------------------------------- /src/util/removeMaybeImportAttributes.ts: -------------------------------------------------------------------------------- 1 | import {ContextualKeyword} from "../parser/tokenizer/keywords"; 2 | import {TokenType as tt} from "../parser/tokenizer/types"; 3 | import type TokenProcessor from "../TokenProcessor"; 4 | 5 | /** 6 | * Starting at a potential `with` or (legacy) `assert` token, remove the import 7 | * attributes if they exist. 8 | */ 9 | export function removeMaybeImportAttributes(tokens: TokenProcessor): void { 10 | if ( 11 | tokens.matches2(tt._with, tt.braceL) || 12 | (tokens.matches2(tt.name, tt.braceL) && tokens.matchesContextual(ContextualKeyword._assert)) 13 | ) { 14 | // assert 15 | tokens.removeToken(); 16 | // { 17 | tokens.removeToken(); 18 | tokens.removeBalancedCode(); 19 | // } 20 | tokens.removeToken(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/util/shouldElideDefaultExport.ts: -------------------------------------------------------------------------------- 1 | import {TokenType as tt} from "../parser/tokenizer/types"; 2 | import type TokenProcessor from "../TokenProcessor"; 3 | import type {DeclarationInfo} from "./getDeclarationInfo"; 4 | 5 | /** 6 | * Common method sharing code between CJS and ESM cases, since they're the same here. 7 | */ 8 | export default function shouldElideDefaultExport( 9 | isTypeScriptTransformEnabled: boolean, 10 | keepUnusedImports: boolean, 11 | tokens: TokenProcessor, 12 | declarationInfo: DeclarationInfo, 13 | ): boolean { 14 | if (!isTypeScriptTransformEnabled || keepUnusedImports) { 15 | return false; 16 | } 17 | const exportToken = tokens.currentToken(); 18 | if (exportToken.rhsEndIndex == null) { 19 | throw new Error("Expected non-null rhsEndIndex on export token."); 20 | } 21 | // The export must be of the form `export default a` or `export default a;`. 22 | const numTokens = exportToken.rhsEndIndex - tokens.currentIndex(); 23 | if ( 24 | numTokens !== 3 && 25 | !(numTokens === 4 && tokens.matches1AtIndex(exportToken.rhsEndIndex - 1, tt.semi)) 26 | ) { 27 | return false; 28 | } 29 | const identifierToken = tokens.tokenAtRelativeIndex(2); 30 | if (identifierToken.type !== tt.name) { 31 | return false; 32 | } 33 | const exportedName = tokens.identifierNameForToken(identifierToken); 34 | return ( 35 | declarationInfo.typeDeclarations.has(exportedName) && 36 | !declarationInfo.valueDeclarations.has(exportedName) 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /test/errors-test.ts: -------------------------------------------------------------------------------- 1 | import {throws} from "assert"; 2 | 3 | import {transform} from "../src"; 4 | 5 | describe("errors", () => { 6 | it("gives proper line numbers in syntax errors", () => { 7 | throws( 8 | () => transform("const x = 1;\nconst y = )\n", {transforms: []}), 9 | /SyntaxError: Unexpected token \(2:11\)/, 10 | ); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/identifyShadowedGlobals-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | import CJSImportProcessor from "../src/CJSImportProcessor"; 4 | import {HelperManager} from "../src/HelperManager"; 5 | import {hasShadowedGlobals} from "../src/identifyShadowedGlobals"; 6 | import NameManager from "../src/NameManager"; 7 | import {parse} from "../src/parser"; 8 | import TokenProcessor from "../src/TokenProcessor"; 9 | 10 | function assertHasShadowedGlobals(code: string, expected: boolean): void { 11 | const file = parse(code, false, true, false); 12 | const nameManager = new NameManager(code, file.tokens); 13 | const helperManager = new HelperManager(nameManager); 14 | const tokenProcessor = new TokenProcessor(code, file.tokens, false, false, helperManager); 15 | const importProcessor = new CJSImportProcessor( 16 | nameManager, 17 | tokenProcessor, 18 | false, 19 | { 20 | transforms: [], 21 | }, 22 | false, 23 | false, 24 | helperManager, 25 | ); 26 | importProcessor.preprocessTokens(); 27 | assert.strictEqual( 28 | hasShadowedGlobals(tokenProcessor, importProcessor.getGlobalNames()), 29 | expected, 30 | ); 31 | } 32 | 33 | describe("identifyShadowedGlobals", () => { 34 | it("properly does an up-front check that there are any shadowed globals", () => { 35 | assertHasShadowedGlobals( 36 | ` 37 | import a from 'a'; 38 | function foo() { 39 | const a = 3; 40 | console.log(a); 41 | } 42 | `, 43 | true, 44 | ); 45 | }); 46 | 47 | it("properly detects when there are no shadowed globals", () => { 48 | assertHasShadowedGlobals( 49 | ` 50 | import a from 'a'; 51 | 52 | export const b = 3; 53 | `, 54 | false, 55 | ); 56 | }); 57 | 58 | it("does not treat parameters within types as real declarations", () => { 59 | assertHasShadowedGlobals( 60 | ` 61 | import a from 'a'; 62 | 63 | function foo(f: (a: number) => void) { 64 | console.log(a); 65 | } 66 | `, 67 | false, 68 | ); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/prefixes.ts: -------------------------------------------------------------------------------- 1 | export const JSX_PREFIX = 'const _jsxFileName = "";'; 2 | export const CREATE_REQUIRE_PREFIX = ` import {createRequire as _createRequire} from "module"; \ 3 | const _require = _createRequire(import.meta.url);`; 4 | export const IMPORT_DEFAULT_PREFIX = ` function _interopRequireDefault(obj) { \ 5 | return obj && obj.__esModule ? obj : { default: obj }; }`; 6 | export const IMPORT_WILDCARD_PREFIX = ` function _interopRequireWildcard(obj) { \ 7 | if (obj && obj.__esModule) { return obj; } else { var newObj = {}; \ 8 | if (obj != null) { for (var key in obj) { \ 9 | if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } \ 10 | newObj.default = obj; return newObj; } }`; 11 | export const CREATE_NAMED_EXPORT_FROM_PREFIX = ` function _createNamedExportFrom(obj, \ 12 | localName, importedName) { Object.defineProperty(exports, localName, \ 13 | {enumerable: true, configurable: true, get: () => obj[importedName]}); }`; 14 | export const CREATE_STAR_EXPORT_PREFIX = ` function _createStarExport(obj) { \ 15 | Object.keys(obj) .filter((key) => key !== "default" && key !== "__esModule") \ 16 | .forEach((key) => { if (exports.hasOwnProperty(key)) { return; } \ 17 | Object.defineProperty(exports, key, {enumerable: true, configurable: true, get: () => obj[key]}); }); }`; 18 | export const ESMODULE_PREFIX = 'Object.defineProperty(exports, "__esModule", {value: true});'; 19 | export const RHL_PREFIX = `(function () { \ 20 | var enterModule = require('react-hot-loader').enterModule; enterModule && enterModule(module); \ 21 | })();`; 22 | export const NULLISH_COALESCE_PREFIX = ` function _nullishCoalesce(lhs, rhsFn) { \ 23 | if (lhs != null) { return lhs; } else { return rhsFn(); } }`; 24 | export const ASYNC_NULLISH_COALESCE_PREFIX = ` async function _asyncNullishCoalesce(lhs, rhsFn) { \ 25 | if (lhs != null) { return lhs; } else { return await rhsFn(); } }`; 26 | export const OPTIONAL_CHAIN_PREFIX = ` function _optionalChain(ops) { \ 27 | let lastAccessLHS = undefined; let value = ops[0]; let i = 1; \ 28 | while (i < ops.length) { \ 29 | const op = ops[i]; const fn = ops[i + 1]; i += 2; \ 30 | if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } \ 31 | if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } \ 32 | else if (op === 'call' || op === 'optionalCall') { \ 33 | value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; \ 34 | } } return value; }`; 35 | export const ASYNC_OPTIONAL_CHAIN_PREFIX = ` async function _asyncOptionalChain(ops) { \ 36 | let lastAccessLHS = undefined; let value = ops[0]; let i = 1; \ 37 | while (i < ops.length) { \ 38 | const op = ops[i]; const fn = ops[i + 1]; i += 2; \ 39 | if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } \ 40 | if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = await fn(value); } \ 41 | else if (op === 'call' || op === 'optionalCall') { \ 42 | value = await fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; \ 43 | } } return value; }`; 44 | export const OPTIONAL_CHAIN_DELETE_PREFIX = ` function _optionalChainDelete(ops) { \ 45 | const result = _optionalChain(ops); return result == null ? true : result; }`; 46 | export const ASYNC_OPTIONAL_CHAIN_DELETE_PREFIX = ` async function _asyncOptionalChainDelete(ops) { \ 47 | const result = await _asyncOptionalChain(ops); return result == null ? true : result; }`; 48 | -------------------------------------------------------------------------------- /test/source-maps-test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | eachMapping, 3 | TraceMap, 4 | type SourceMapInput, 5 | type EachMapping, 6 | } from "@jridgewell/trace-mapping"; 7 | import * as assert from "assert"; 8 | 9 | import {transform} from "../src"; 10 | 11 | describe("source maps", () => { 12 | it("generates a detailed line-based source map", () => { 13 | const source = `\ 14 | import a from "./a"; 15 | const x: number = a; 16 | console.log(x + 1); 17 | `; 18 | const result = transform(source, { 19 | transforms: ["imports", "typescript"], 20 | sourceMapOptions: {compiledFilename: "test.js"}, 21 | filePath: "test.ts", 22 | }); 23 | assert.equal( 24 | result.code, 25 | `"use strict"; function _interopRequireDefault(obj) { \ 26 | return obj && obj.__esModule ? obj : { default: obj }; } \ 27 | var _a = require('./a'); var _a2 = _interopRequireDefault(_a); 28 | const x = _a2.default; 29 | console.log(x + 1); 30 | `, 31 | ); 32 | assert.deepEqual(result.sourceMap, { 33 | version: 3, 34 | sources: ["test.ts"], 35 | names: [], 36 | mappings: `AAAA,mHAAM,8DAAmB;AACzB,MAAM,MAAM,CAAC,CAAS,EAAE,WAAC;AACzB,\ 37 | MAAM,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;AACxB`, 38 | file: "test.js", 39 | }); 40 | const traceMap = new TraceMap(result.sourceMap as SourceMapInput); 41 | const mappings: Array< 42 | Pick 43 | > = []; 44 | eachMapping(traceMap, ({generatedLine, generatedColumn, originalLine, originalColumn}) => { 45 | mappings.push({generatedLine, generatedColumn, originalLine, originalColumn}); 46 | }); 47 | assert.deepEqual( 48 | mappings, 49 | [ 50 | {generatedLine: 1, generatedColumn: 0, originalLine: 1, originalColumn: 0}, 51 | {generatedLine: 1, generatedColumn: 115, originalLine: 1, originalColumn: 6}, 52 | {generatedLine: 1, generatedColumn: 177, originalLine: 1, originalColumn: 25}, 53 | {generatedLine: 2, generatedColumn: 0, originalLine: 2, originalColumn: 0}, 54 | {generatedLine: 2, generatedColumn: 6, originalLine: 2, originalColumn: 6}, 55 | {generatedLine: 2, generatedColumn: 12, originalLine: 2, originalColumn: 12}, 56 | {generatedLine: 2, generatedColumn: 13, originalLine: 2, originalColumn: 13}, 57 | {generatedLine: 2, generatedColumn: 14, originalLine: 2, originalColumn: 22}, 58 | {generatedLine: 2, generatedColumn: 16, originalLine: 2, originalColumn: 24}, 59 | {generatedLine: 2, generatedColumn: 27, originalLine: 2, originalColumn: 25}, 60 | {generatedLine: 3, generatedColumn: 0, originalLine: 3, originalColumn: 0}, 61 | {generatedLine: 3, generatedColumn: 6, originalLine: 3, originalColumn: 6}, 62 | {generatedLine: 3, generatedColumn: 13, originalLine: 3, originalColumn: 13}, 63 | {generatedLine: 3, generatedColumn: 14, originalLine: 3, originalColumn: 14}, 64 | {generatedLine: 3, generatedColumn: 17, originalLine: 3, originalColumn: 17}, 65 | {generatedLine: 3, generatedColumn: 18, originalLine: 3, originalColumn: 18}, 66 | {generatedLine: 3, generatedColumn: 20, originalLine: 3, originalColumn: 20}, 67 | {generatedLine: 3, generatedColumn: 22, originalLine: 3, originalColumn: 22}, 68 | {generatedLine: 3, generatedColumn: 23, originalLine: 3, originalColumn: 23}, 69 | {generatedLine: 3, generatedColumn: 24, originalLine: 3, originalColumn: 24}, 70 | {generatedLine: 4, generatedColumn: 0, originalLine: 4, originalColumn: 0}, 71 | ], 72 | `Expected:\n${mappings.map((m) => `${JSON.stringify(m)},`).join("\n")}`, 73 | ); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/util.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import vm from "vm"; 3 | 4 | import {type Options, transform} from "../src"; 5 | 6 | export interface Expectations { 7 | expectedResult?: string; 8 | expectedOutput?: unknown; 9 | } 10 | 11 | export function assertExpectations( 12 | code: string, 13 | expectations: Expectations, 14 | options: Options, 15 | message?: string, 16 | ): void { 17 | const resultCode = transform(code, options).code; 18 | if ("expectedResult" in expectations) { 19 | assert.strictEqual(resultCode, expectations.expectedResult, message); 20 | } 21 | if ("expectedOutput" in expectations) { 22 | const outputs: Array = []; 23 | vm.runInNewContext(resultCode, { 24 | // Convert to JSON and back so that nested objects have the right prototypes. Unfortunately, 25 | // this limits us to using JSON-like values (particularly, never using undefined). 26 | setOutput: (value: unknown) => outputs.push(JSON.parse(JSON.stringify(value))), 27 | }); 28 | assert.strictEqual(outputs.length, 1, "setOutput should be called exactly once"); 29 | assert.deepStrictEqual(outputs[0], expectations.expectedOutput, message); 30 | } 31 | } 32 | 33 | export function assertResult( 34 | code: string, 35 | expectedResult: string, 36 | options: Options = {transforms: ["jsx", "imports"]}, 37 | message: string | undefined = undefined, 38 | ): void { 39 | assertExpectations(code, {expectedResult}, options, message); 40 | } 41 | 42 | export function assertOutput( 43 | code: string, 44 | expectedOutput: unknown, 45 | options: Options = {transforms: ["jsx", "imports"]}, 46 | ): void { 47 | assertExpectations(code, {expectedOutput}, options); 48 | } 49 | 50 | /** 51 | * Dev-specific props when using the createElement function, either in the 52 | * classic runtime or in the fallback case for the automatic runtime. 53 | */ 54 | export function devProps(lineNumber: number): string { 55 | return `__self: this, __source: {fileName: _jsxFileName, lineNumber: ${lineNumber}}`; 56 | } 57 | 58 | /** 59 | * Dev-specific args to the jsxDEV function in the automatic runtime. 60 | */ 61 | export function jsxDevArgs(lineNumber: number): string { 62 | return `{fileName: _jsxFileName, lineNumber: ${lineNumber}}, this`; 63 | } 64 | 65 | /** 66 | * Given a mapping from filename to code, compiles each file and runs the file called "main" 67 | * under normal CJS semantics (require and exports). The main module should export a value 68 | * called `output`, and we assert that it's equal to expectedOutput. 69 | */ 70 | export function assertMultiFileOutput( 71 | codeByFilename: {[filename: string]: string}, 72 | expectedOutput: unknown, 73 | ): void { 74 | const mainResult = new FakeModuleResolver(codeByFilename).evaluateModule("main"); 75 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 76 | const output = (mainResult as any).output; 77 | assert.deepStrictEqual(output, expectedOutput); 78 | } 79 | 80 | class FakeModuleResolver { 81 | moduleExportsCache: {[filename: string]: unknown} = {}; 82 | 83 | constructor(readonly codeByFilename: {[filename: string]: string}) {} 84 | 85 | evaluateModule(filename: string): unknown { 86 | if (filename in this.moduleExportsCache) { 87 | return this.moduleExportsCache[filename]; 88 | } 89 | const exports = {}; 90 | this.moduleExportsCache[filename] = exports; 91 | const code = this.codeByFilename[filename]; 92 | if (!code) { 93 | throw new Error(`Did not find file ${filename}`); 94 | } 95 | const compiledCode = transform(code, {transforms: ["imports"]}).code; 96 | vm.runInNewContext(compiledCode, {require: this.evaluateModule.bind(this), exports}); 97 | return exports; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ts-node-plugin/index.js: -------------------------------------------------------------------------------- 1 | const {transform} = require("../dist"); 2 | 3 | // Enum constants taken from the TypeScript codebase. 4 | const ModuleKindCommonJS = 1; 5 | 6 | const JsxEmitReactJSX = 4; 7 | const JsxEmitReactJSXDev = 5; 8 | 9 | /** 10 | * ts-node transpiler plugin 11 | * 12 | * This plugin hooks into ts-node so that Sucrase can handle all TS-to-JS 13 | * conversion while ts-node handles the ESM loader, require hook, REPL 14 | * integration, etc. ts-node automatically discovers the relevant tsconfig file, 15 | * so the main logic in this integration is translating tsconfig options to the 16 | * corresponding Sucrase options. 17 | * 18 | * Any tsconfig options relevant to Sucrase are translated, but some config 19 | * options outside the scope of Sucrase are ignored. For example, we assume the 20 | * isolatedModules option, and we ignore target because Sucrase doesn't provide 21 | * JS syntax downleveling (at least not in a way that is useful for Node). 22 | * 23 | * One notable caveat is that importsNotUsedAsValues and preserveValueImports 24 | * are ignored right now, since they are deprecated and don't have exact Sucrase 25 | * equivalents. To preserve imports and exports, use verbatimModuleSyntax. 26 | */ 27 | function create(createOptions) { 28 | const {nodeModuleEmitKind} = createOptions; 29 | const { 30 | module, 31 | jsx, 32 | jsxFactory, 33 | jsxFragmentFactory, 34 | jsxImportSource, 35 | esModuleInterop, 36 | verbatimModuleSyntax, 37 | } = createOptions.service.config.options; 38 | 39 | return { 40 | transpile(input, transpileOptions) { 41 | const {fileName} = transpileOptions; 42 | const transforms = []; 43 | // Detect JS rather than TS so we bias toward including the typescript 44 | // transform, since almost always it doesn't hurt to include. 45 | const isJS = 46 | fileName.endsWith(".js") || 47 | fileName.endsWith(".jsx") || 48 | fileName.endsWith(".mjs") || 49 | fileName.endsWith(".cjs"); 50 | if (!isJS) { 51 | transforms.push("typescript"); 52 | } 53 | if (module === ModuleKindCommonJS || nodeModuleEmitKind === "nodecjs") { 54 | transforms.push("imports"); 55 | } 56 | if (fileName.endsWith(".tsx") || fileName.endsWith(".jsx")) { 57 | transforms.push("jsx"); 58 | } 59 | 60 | const {code, sourceMap} = transform(input, { 61 | transforms, 62 | disableESTransforms: true, 63 | jsxRuntime: jsx === JsxEmitReactJSX || jsx === JsxEmitReactJSXDev ? "automatic" : "classic", 64 | production: jsx === JsxEmitReactJSX, 65 | jsxImportSource, 66 | jsxPragma: jsxFactory, 67 | jsxFragmentPragma: jsxFragmentFactory, 68 | keepUnusedImports: verbatimModuleSyntax, 69 | preserveDynamicImport: nodeModuleEmitKind === "nodecjs", 70 | injectCreateRequireForImportRequire: nodeModuleEmitKind === "nodeesm", 71 | enableLegacyTypeScriptModuleInterop: !esModuleInterop, 72 | sourceMapOptions: {compiledFilename: fileName}, 73 | filePath: fileName, 74 | }); 75 | return { 76 | outputText: code, 77 | sourceMapText: JSON.stringify(sourceMap), 78 | }; 79 | }, 80 | }; 81 | } 82 | 83 | exports.create = create; 84 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // We never emit JS code (only type declarations) from TSC, so ignore those 4 | // options. 5 | "strict": true, 6 | "target": "es2020", 7 | "module": "commonjs", 8 | "isolatedModules": true, 9 | "esModuleInterop": true, 10 | "noImplicitReturns": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "useDefineForClassFields": true, 13 | "lib": ["es2020", "dom"] 14 | }, 15 | "include": [ 16 | "benchmark", 17 | "example-runner", 18 | "generator", 19 | "integration-test", 20 | "script", 21 | "spec-compliance-tests", 22 | "src", 23 | "test", 24 | ], 25 | "exclude": [ 26 | "benchmark/sample", 27 | "example-runner/example-repos", 28 | "integration-test/test-cases", 29 | "spec-compliance-tests/test262/test262-checkout", 30 | "spec-compliance-tests/babel-tests/babel-tests-checkout" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /website/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["plugin:react-hooks/recommended"], 3 | parserOptions: { 4 | project: `${__dirname}/tsconfig.json`, 5 | }, 6 | rules: { 7 | "react-hooks/exhaustive-deps": "error", 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | gh-pages 24 | -------------------------------------------------------------------------------- /website/config/env.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Make sure that including paths.js after env.js will read .env variables. 4 | delete require.cache[require.resolve("./paths")]; 5 | 6 | const NODE_ENV = process.env.NODE_ENV; 7 | if (!NODE_ENV) { 8 | throw new Error("The NODE_ENV environment variable is required but was not specified."); 9 | } 10 | 11 | function getClientEnvironment(publicUrl) { 12 | const raw = { 13 | // Useful for determining whether we’re running in production mode. 14 | // Most importantly, it switches React into the correct mode. 15 | NODE_ENV: process.env.NODE_ENV || "development", 16 | // Useful for resolving the correct path to static assets in `public`. 17 | // For example, . 18 | // This should only be used as an escape hatch. Normally you would put 19 | // images into the `src` and `import` them in code to get their paths. 20 | PUBLIC_URL: publicUrl, 21 | // eslint-disable-next-line global-require 22 | SUCRASE_VERSION: require("sucrase").getVersion(), 23 | }; 24 | // Stringify all values so we can feed into Webpack DefinePlugin 25 | const stringified = { 26 | "process.env": Object.keys(raw).reduce((env, key) => { 27 | env[key] = JSON.stringify(raw[key]); 28 | return env; 29 | }, {}), 30 | }; 31 | 32 | return {raw, stringified}; 33 | } 34 | 35 | module.exports = getClientEnvironment; 36 | -------------------------------------------------------------------------------- /website/config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /website/config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /website/config/paths.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const fs = require("fs"); 5 | 6 | // Make sure any symlinks in the project folder are resolved: 7 | // https://github.com/facebookincubator/create-react-app/issues/637 8 | const appDirectory = fs.realpathSync(process.cwd()); 9 | const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath); 10 | 11 | const PUBLIC_URL = "https://sucrase.io"; 12 | 13 | module.exports = { 14 | dotenv: resolveApp(".env"), 15 | appBuild: resolveApp("build"), 16 | appPublic: resolveApp("public"), 17 | appHtml: resolveApp("public/index.html"), 18 | appIndexJs: resolveApp("src/index.tsx"), 19 | appPackageJson: resolveApp("package.json"), 20 | appSrc: resolveApp("src"), 21 | yarnLockFile: resolveApp("yarn.lock"), 22 | testsSetup: resolveApp("src/setupTests.js"), 23 | appNodeModules: resolveApp("node_modules"), 24 | publicUrl: PUBLIC_URL, 25 | servedPath: "/", 26 | }; 27 | -------------------------------------------------------------------------------- /website/config/webpackDevServer.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | client: { 5 | overlay: { 6 | // Silence warning: 7 | // "new Worker() will only be bundled if passed a String." 8 | // which appears to be coming from monaco. 9 | warnings: false, 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sucrase-website", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://sucrase.io", 6 | "dependencies": { 7 | "@babel/standalone": "^7.22.5", 8 | "@jridgewell/trace-mapping": "^0.3.17", 9 | "@sucrase/webpack-loader": "^2.0.0", 10 | "@types/base64-js": "^1.2.5", 11 | "@types/gzip-js": "^0.3.1", 12 | "@types/react": "^16", 13 | "@types/react-dom": "^16.9.4", 14 | "aphrodite": "^2.4.0", 15 | "babel-plugin-dynamic-import-node": "^2.3.0", 16 | "babel-plugin-jest-hoist": "^28.1.1", 17 | "babel-plugin-transform-flow-enums": "^0.0.2", 18 | "base64-js": "^1.3.1", 19 | "case-sensitive-paths-webpack-plugin": "^2.2.0", 20 | "chalk": "^3.0.0", 21 | "css-loader": "^5.2.6", 22 | "file-loader": "^4.2.0", 23 | "fs-extra": "^8.1.0", 24 | "gzip-js": "^0.3.2", 25 | "html-webpack-plugin": "^4.0.0-beta.5", 26 | "immer": "^9.0.15", 27 | "jest": "^26.6.3", 28 | "monaco-editor": "^0.33.0", 29 | "monaco-editor-webpack-plugin": "^7.0.1", 30 | "process": "^0.11.10", 31 | "react": "^18.2.0", 32 | "react-dev-utils": "^12.0.0-next.47", 33 | "react-dom": "^18.2.0", 34 | "react-hot-loader": "^4.12.16", 35 | "react-monaco-editor": "^0.48.0", 36 | "react-virtualized-auto-sizer": "^1.0.6", 37 | "style-loader": "^1.0.0", 38 | "sucrase": "^3.35.0", 39 | "typescript": "^5.1.6", 40 | "url-loader": "^2.2.0", 41 | "webpack": "^5.73.0", 42 | "webpack-dev-server": "^4.9.2" 43 | }, 44 | "scripts": { 45 | "start": "node scripts/start.js", 46 | "build": "node --max-old-space-size=8192 scripts/build.js", 47 | "test": "node scripts/test.js --env=jsdom", 48 | "publish-website": "scripts/publish.sh" 49 | }, 50 | "resolutions": { 51 | "**/@types/react": "16.14.5", 52 | "react-dev-utils/browserslist": "^4.16.5", 53 | "**/set-value": "4.0.1" 54 | }, 55 | "jest": { 56 | "collectCoverageFrom": [ 57 | "src/**/*.{js,jsx}" 58 | ], 59 | "testMatch": [ 60 | "/src/**/__tests__/**/*.js?(x)", 61 | "/src/**/?(*.)(spec|test).js?(x)" 62 | ], 63 | "testEnvironment": "node", 64 | "testURL": "http://localhost", 65 | "transform": { 66 | "^.+\\.(js|jsx)$": "/node_modules/babel-jest", 67 | "^.+\\.css$": "/config/jest/cssTransform.js", 68 | "^(?!.*\\.(js|jsx|css|json)$)": "/config/jest/fileTransform.js" 69 | }, 70 | "transformIgnorePatterns": [ 71 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$" 72 | ], 73 | "moduleFileExtensions": [ 74 | "web.js", 75 | "js", 76 | "json", 77 | "web.jsx", 78 | "jsx", 79 | "node" 80 | ] 81 | }, 82 | "devDependencies": { 83 | "eslint-plugin-react-hooks": "^4.6.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /website/public/CNAME: -------------------------------------------------------------------------------- 1 | sucrase.io 2 | -------------------------------------------------------------------------------- /website/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alangpierce/sucrase/61c05e1e6f29c906c432da57c91ab44660196c8d/website/public/favicon-16x16.png -------------------------------------------------------------------------------- /website/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alangpierce/sucrase/61c05e1e6f29c906c432da57c91ab44660196c8d/website/public/favicon-32x32.png -------------------------------------------------------------------------------- /website/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Sucrase 12 | 13 | 14 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /website/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Sucrase", 3 | "name": "Sucrase", 4 | "start_url": "./index.html", 5 | "display": "standalone", 6 | "theme_color": "#ffffff", 7 | "background_color": "#222222" 8 | } 9 | -------------------------------------------------------------------------------- /website/scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | yarn 6 | yarn build 7 | rm -rf ./gh-pages 8 | mkdir ./gh-pages 9 | cd gh-pages 10 | git init 11 | git remote add origin git@github.com:alangpierce/sucrase.git 12 | cp -r ../build/* . 13 | git add -A 14 | git commit -m 'Update website' 15 | git push origin HEAD:gh-pages -f 16 | -------------------------------------------------------------------------------- /website/scripts/start.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.NODE_ENV = "development"; 5 | 6 | // Makes the script crash on unhandled rejections instead of silently 7 | // ignoring them. In the future, promise rejections that are not handled will 8 | // terminate the Node.js process with a non-zero exit code. 9 | process.on("unhandledRejection", (err) => { 10 | throw err; 11 | }); 12 | 13 | // Ensure environment variables are read. 14 | require("../config/env"); 15 | 16 | const chalk = require("chalk"); 17 | const webpack = require("webpack"); 18 | const WebpackDevServer = require("webpack-dev-server"); 19 | const checkRequiredFiles = require("react-dev-utils/checkRequiredFiles"); 20 | const {choosePort, prepareUrls} = require("react-dev-utils/WebpackDevServerUtils"); 21 | const openBrowser = require("react-dev-utils/openBrowser"); 22 | const paths = require("../config/paths"); 23 | const config = require("../config/webpack.config.dev"); 24 | const devServerConfig = require("../config/webpackDevServer.config"); 25 | 26 | // Warn and crash if required files are missing 27 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 28 | process.exit(1); 29 | } 30 | 31 | // Tools like Cloud9 rely on this. 32 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 33 | const HOST = process.env.HOST || "0.0.0.0"; 34 | 35 | // We attempt to use the default port but if it is busy, we offer the user to 36 | // run on a different port. `detect()` Promise resolves to the next free port. 37 | choosePort(HOST, DEFAULT_PORT) 38 | .then((port) => { 39 | if (port == null) { 40 | // We have not found a port. 41 | return; 42 | } 43 | const protocol = process.env.HTTPS === "true" ? "https" : "http"; 44 | const urls = prepareUrls(protocol, HOST, port); 45 | const compiler = webpack(config); 46 | // Serve webpack assets generated by the compiler over a web sever. 47 | const serverConfig = devServerConfig; 48 | const devServer = new WebpackDevServer(compiler, serverConfig); 49 | // Launch WebpackDevServer. 50 | devServer.listen(port, HOST, (err) => { 51 | if (err) { 52 | return console.log(err); 53 | } 54 | console.log(chalk.cyan("Starting the development server...\n")); 55 | openBrowser(urls.localUrlForBrowser); 56 | }); 57 | 58 | ["SIGINT", "SIGTERM"].forEach(function (sig) { 59 | process.on(sig, function () { 60 | devServer.close(); 61 | process.exit(); 62 | }); 63 | }); 64 | }) 65 | .catch((err) => { 66 | if (err && err.message) { 67 | console.log(err.message); 68 | } 69 | process.exit(1); 70 | }); 71 | -------------------------------------------------------------------------------- /website/scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.NODE_ENV = 'test'; 5 | process.env.PUBLIC_URL = ''; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const jest = require('jest'); 18 | const argv = process.argv.slice(2); 19 | 20 | // Watch unless on CI or in coverage mode 21 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 22 | argv.push('--watch'); 23 | } 24 | 25 | 26 | jest.run(argv); 27 | -------------------------------------------------------------------------------- /website/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /website/src/CheckBox.tsx: -------------------------------------------------------------------------------- 1 | import {css, StyleSheet} from "aphrodite"; 2 | import type {ReactNode} from "react"; 3 | 4 | interface CheckBoxProps { 5 | label: ReactNode; 6 | checked: boolean; 7 | onChange: (checked: boolean) => void; 8 | } 9 | 10 | export default function CheckBox({label, checked, onChange}: CheckBoxProps): JSX.Element { 11 | return ( 12 | 23 | ); 24 | } 25 | 26 | const styles = StyleSheet.create({ 27 | label: { 28 | display: "flex", 29 | alignItems: "center", 30 | marginLeft: 6, 31 | marginRight: 6, 32 | }, 33 | checkbox: { 34 | marginRight: 4, 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /website/src/CompareOptionsBox.tsx: -------------------------------------------------------------------------------- 1 | import {css, StyleSheet} from "aphrodite"; 2 | 3 | import CheckBox from "./CheckBox"; 4 | import type {CompareOptions} from "./Constants"; 5 | import OptionsBox from "./OptionsBox"; 6 | 7 | interface CompareOptionsBoxProps { 8 | compareOptions: CompareOptions; 9 | onUpdateCompareOptions: (compareOptions: CompareOptions) => void; 10 | } 11 | 12 | export default function CompareOptionsBox({ 13 | compareOptions, 14 | onUpdateCompareOptions, 15 | }: CompareOptionsBoxProps): JSX.Element { 16 | return ( 17 | 18 |
19 | Compare 20 | { 24 | onUpdateCompareOptions({...compareOptions, compareWithBabel: checked}); 25 | }} 26 | /> 27 | { 31 | onUpdateCompareOptions({...compareOptions, compareWithTypeScript: checked}); 32 | }} 33 | /> 34 |
35 |
36 | ); 37 | } 38 | 39 | const styles = StyleSheet.create({ 40 | optionBox: { 41 | display: "flex", 42 | flexWrap: "wrap", 43 | alignItems: "center", 44 | }, 45 | title: { 46 | fontSize: "1.2em", 47 | padding: 6, 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /website/src/Constants.ts: -------------------------------------------------------------------------------- 1 | import type {Options, Transform} from "sucrase"; 2 | 3 | export const INITIAL_CODE = `\ 4 | // Try typing or pasting some code into the left editor! 5 | import React, { Component } from "react"; 6 | 7 | type Reducer = (u: U, t: T) => U; 8 | 9 | function reduce(arr: Array, reducer: Reducer, base: U): U { 10 | let acc = base; 11 | for (const value of arr) { 12 | acc = reducer(acc, value); 13 | } 14 | return acc; 15 | } 16 | 17 | class App extends Component { 18 | render() { 19 | return Hello, world!; 20 | } 21 | } 22 | 23 | const OtherComponent = React.createClass({ 24 | render() { 25 | return null; 26 | } 27 | }); 28 | 29 | export default App; 30 | 31 | `; 32 | 33 | export const TRANSFORMS: Array = [ 34 | "jsx", 35 | "typescript", 36 | "flow", 37 | "imports", 38 | "react-hot-loader", 39 | "jest", 40 | ]; 41 | 42 | export type HydratedOptions = Omit, "filePath" | "sourceMapOptions">; 43 | 44 | /** 45 | * Default value for each option to show for the website. 46 | * 47 | * This is not required to match the default values from Sucrase itself (e.g. 48 | * it's useful to have a few transforms enabled for the demo), but it's most 49 | * clear to match Sucrase defaults as much as possible. 50 | * 51 | * This object also doubles as a way of list of options and their types for the 52 | * purpose of URL parsing and formatting. 53 | */ 54 | export const DEFAULT_OPTIONS: HydratedOptions = { 55 | transforms: ["jsx", "typescript", "imports"], 56 | disableESTransforms: false, 57 | production: false, 58 | jsxRuntime: "classic", 59 | jsxImportSource: "react", 60 | jsxPragma: "React.createElement", 61 | jsxFragmentPragma: "React.Fragment", 62 | keepUnusedImports: false, 63 | preserveDynamicImport: false, 64 | injectCreateRequireForImportRequire: false, 65 | enableLegacyTypeScriptModuleInterop: false, 66 | enableLegacyBabel5ModuleInterop: false, 67 | }; 68 | 69 | export interface CompareOptions { 70 | compareWithBabel: boolean; 71 | compareWithTypeScript: boolean; 72 | } 73 | 74 | export interface DebugOptions { 75 | showTokens: boolean; 76 | showSourceMap: boolean; 77 | } 78 | 79 | export const DEFAULT_COMPARE_OPTIONS: CompareOptions = { 80 | compareWithBabel: true, 81 | compareWithTypeScript: false, 82 | }; 83 | 84 | export const DEFAULT_DEBUG_OPTIONS: DebugOptions = { 85 | showTokens: false, 86 | showSourceMap: false, 87 | }; 88 | -------------------------------------------------------------------------------- /website/src/DebugOptionsBox.tsx: -------------------------------------------------------------------------------- 1 | import {css, StyleSheet} from "aphrodite"; 2 | import {useState} from "react"; 3 | 4 | import CheckBox from "./CheckBox"; 5 | import type {DebugOptions} from "./Constants"; 6 | import OptionsBox from "./OptionsBox"; 7 | 8 | interface DebugOptionsBoxProps { 9 | debugOptions: DebugOptions; 10 | onUpdateDebugOptions: (debugOptions: DebugOptions) => void; 11 | } 12 | 13 | export default function DebugOptionsBox({ 14 | debugOptions, 15 | onUpdateDebugOptions, 16 | }: DebugOptionsBoxProps): JSX.Element { 17 | const [enabled, setEnabled] = useState(false); 18 | if (!enabled) { 19 | return ( 20 | { 24 | setEnabled(true); 25 | e.preventDefault(); 26 | }} 27 | > 28 | Debug... 29 | 30 | ); 31 | } 32 | return ( 33 | 34 |
35 | Debug 36 | { 40 | onUpdateDebugOptions({...debugOptions, showTokens: checked}); 41 | }} 42 | /> 43 | { 47 | onUpdateDebugOptions({...debugOptions, showSourceMap: checked}); 48 | }} 49 | /> 50 |
51 |
52 | ); 53 | } 54 | 55 | const styles = StyleSheet.create({ 56 | optionBox: { 57 | display: "flex", 58 | flexWrap: "wrap", 59 | alignItems: "center", 60 | }, 61 | link: { 62 | color: "#CCCCCC", 63 | marginLeft: 6, 64 | marginRight: 6, 65 | }, 66 | title: { 67 | fontSize: "1.2em", 68 | padding: 6, 69 | }, 70 | }); 71 | -------------------------------------------------------------------------------- /website/src/Editor.tsx: -------------------------------------------------------------------------------- 1 | import type {editor} from "monaco-editor"; 2 | import {useCallback, useEffect, useRef} from "react"; 3 | import type MonacoEditor from "react-monaco-editor"; 4 | 5 | interface EditorProps { 6 | MonacoEditor: typeof MonacoEditor; 7 | code: string; 8 | onChange?: (code: string) => void; 9 | isReadOnly?: boolean; 10 | isPlaintext?: boolean; 11 | options?: editor.IEditorConstructionOptions; 12 | width: number; 13 | height: number; 14 | onMount: (editor: editor.IStandaloneCodeEditor) => void; 15 | } 16 | 17 | export default function Editor({ 18 | MonacoEditor, 19 | code, 20 | onChange, 21 | isReadOnly, 22 | isPlaintext, 23 | options, 24 | width, 25 | height, 26 | }: EditorProps): JSX.Element { 27 | const editorRef = useRef(null); 28 | 29 | const editorDidMount = useCallback((monacoEditor, monaco) => { 30 | editorRef.current = monacoEditor; 31 | monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ 32 | noSemanticValidation: true, 33 | noSyntaxValidation: true, 34 | noSuggestionDiagnostics: true, 35 | }); 36 | monacoEditor.layout(); 37 | }, []); 38 | 39 | useEffect(() => { 40 | setTimeout(() => { 41 | editorRef.current?.layout(); 42 | }, 0); 43 | }, []); 44 | 45 | return ( 46 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /website/src/FallbackEditor.tsx: -------------------------------------------------------------------------------- 1 | import {css, StyleSheet} from "aphrodite"; 2 | 3 | interface FallbackEditorProps { 4 | width: number; 5 | height: number; 6 | code: string; 7 | onChange?: (code: string) => void; 8 | isReadOnly?: boolean; 9 | } 10 | 11 | /** 12 | * Clone of the default styles in the Monaco editor to get something as close as possible 13 | * while the real editor is loading. Ideally, all text is positioned exactly the same when 14 | * the editor loads, and line numbers, syntax highlighting, etc appear to enhance the 15 | * existing text rather than replacing it. 16 | */ 17 | export default function FallbackEditor({ 18 | width, 19 | height, 20 | code, 21 | onChange, 22 | isReadOnly, 23 | }: FallbackEditorProps): JSX.Element { 24 | return ( 25 |