Skip to content

chore(typescript): change moduleResolution #1605

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
May 15, 2025
Merged

Conversation

alexandre-abrioux
Copy link
Member

@alexandre-abrioux alexandre-abrioux commented Apr 14, 2025

In this PR, I'm proposing to change TypeScript's module and moduleResolution to the latest recommended versions (see the section below for TypeScript recommendations). This would allow us to use ESM features in CommonJS, and forbid us to use non-ESM compatible imports, which should ease the transition to ESM later on. We would also gain:

  • Support for import ... from "esm": allows to import ESM modules synchronously in CommonJS, without the need to use await import()
  • Support for the new package.json's exports property, which allows importing part of a module with import {} from module/part. e.g. dotenv/config. This change is required for chore(WIP): migrate to @noble/curves and eciesjs #1236
  • Support for code splitting in consumer code: dynamic imports await import('module') will be kept as-is during transpilation to CommonJS, instead of being converted to yield Promise.resolve().then(() => require('module'));, which was preventing code splitting with consuming bundlers.

TypeScript tsconfig.json Documentation

"module": "commonjs" is deprecated

https://www.typescriptlang.org/docs/handbook/modules/reference.html#commonjs

commonjs

You probably shouldn’t use this. Use node16, node18, or nodenext to emit CommonJS modules

"moduleResolution": "node" is deprecated

https://www.typescriptlang.org/docs/handbook/modules/reference.html#node10-formerly-known-as-node

node10 (formerly known as node)

--moduleResolution node ... should no longer be used.

Issue to address before merge

Summary by CodeRabbit

  • New Features

    • Improved validation format support by integrating ajv-formats for enhanced data validation.
    • Jest test execution now adapts to available system resources for better performance during testing.
  • Bug Fixes

    • Updated test assertions to match new validation error message formats.
  • Chores

    • Upgraded multiple dependencies across packages, including TypeScript, tslib, dotenv, ajv, and ts-jest, for improved compatibility and security.
    • Updated TypeScript configuration for modern module resolution and interoperability.
    • Refined type imports and type aliasing to reduce reliance on internal module paths.
    • Clarified type-only exports across various modules to improve type safety and code clarity.

Copy link
Contributor

coderabbitai bot commented Apr 14, 2025

Walkthrough

This update applies a broad set of dependency version bumps across multiple package.json files throughout the repository, notably upgrading TypeScript, tslib, ts-jest, and several other libraries. The TypeScript compiler configuration (tsconfig.json) is updated to use the "nodenext" module system and enables "esModuleInterop" and "isolatedModules". The CircleCI configuration is adjusted to set a JEST_MAX_WORKERS environment variable for concurrent Jest test runs. Jest's configuration is made responsive to this environment variable. Several internal type imports are refactored to avoid reliance on internal or legacy paths by deriving types from GraphQLClient prototypes. The Ajv JSON schema validator integration is updated to use ajv-formats, affecting both code and test expectations. Additionally, multiple export statements are changed to type-only exports for clarity and correctness.

Changes

File(s) Change Summary
.circleci/config.yml, jest.config.js CircleCI test job sets JEST_MAX_WORKERS to '50%'; Jest config conditionally uses maxWorkers from env var.
tsconfig.json Changed TypeScript module system and resolution to "nodenext"; enabled esModuleInterop and isolatedModules.
package.json, packages/*/package.json Updated TypeScript, tslib, ts-jest, and other dependencies/devDependencies to newer versions.
packages/data-format/package.json, packages/data-format/src/index.ts, packages/data-format/test/test.ts Upgraded ajv, added ajv-formats, updated import style, and adjusted test error assertions to match new Ajv error messages.
packages/payment-detection/src/thegraph/client.ts, packages/payment-detection/src/thegraph/superfluid.ts, packages/thegraph-data-access/src/subgraph-client.ts, packages/thegraph-data-access/src/types.ts Replaced direct imports of RequestConfig from internal graphql-request paths with local type aliases derived from GraphQLClient prototype. Changed some imports to use import type.
packages/data-access/src/index.ts, packages/payment-detection/src/index.ts, packages/payment-processor/src/payment/index.ts, packages/payment-processor/src/payment/swap-any-to-erc20.ts, packages/request-client.js/src/index.ts, packages/types/src/data-access-types.ts, packages/types/src/extensions/pn-any-fee-reference-based-types.ts, packages/types/src/extensions/pn-any-reference-based-types.ts, packages/types/src/extensions/pn-any-stream-reference-based-types.ts, packages/types/src/extensions/pn-any-to-any-conversion-types.ts Changed multiple exports to type-only exports; added explicit type exports for some interfaces; reordered imports and exports for clarity.
* (all other files) No changes to exported/public entities; only dependency version updates or minor internal refactors.

Sequence Diagram(s)

sequenceDiagram
    participant CircleCI
    participant Jest
    participant Lerna

    CircleCI->>Jest: Set JEST_MAX_WORKERS='50%'
    Lerna->>Jest: Run 2 Jest commands concurrently
    Jest->>Jest: Use maxWorkers from env if set
Loading
sequenceDiagram
    participant Validator
    participant Ajv
    participant ajv-formats

    Validator->>Ajv: Create Ajv instance
    Validator->>ajv-formats: addFormats(Ajv)
    Validator->>Ajv: Add meta schema, address schema
    Validator->>Ajv: Validate data
Loading

Possibly related PRs

Tip

⚡️ Faster reviews with caching
  • CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.

Enjoy the performance boost—your workflow just got faster.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between 7960f48 and cd24ea8.

📒 Files selected for processing (1)
  • packages/payment-detection/src/thegraph/client.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/payment-detection/src/thegraph/client.ts

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@@ -52,7 +52,7 @@
"npm-package-json-lint": "5.1.0",
"prettier": "2.8.8",
"prettier-plugin-solidity": "1.0.0-beta.19",
"typescript": "5.1.3"
"typescript": "5.8.3"
Copy link
Member Author

@alexandre-abrioux alexandre-abrioux Apr 14, 2025

Choose a reason for hiding this comment

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

Updated Typescript to support loading ES modules in CommonJS synchronously (without await import()):
https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require

This is needed to support the latest version of graphql-request used in the payment-detection package.

Copy link
Member

Choose a reason for hiding this comment

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

I'm guessing we satisfy the final bullet point because we use .js extension and not "type": "commonjs"

Per the link:

require() only supports loading ECMAScript modules that meet the following requirements:

  • The module is fully synchronous (contains no top-level await); and
  • One of these conditions are met:
    • The file has a .mjs extension.
    • The file has a .js extension, and the closest package.json contains "type": "module"
    • The file has a .js extension, the closest package.json does not contain "type": "commonjs", and the module contains ES module syntax.

If the ES Module being loaded meets the requirements, require() can load it and return the module namespace object. In this case it is similar to dynamic import() but is run synchronously and returns the name space object directly.

"@graphql-codegen/typescript-graphql-request": "6.0.1",
"@graphql-codegen/typescript-graphql-request": "6.2.0",
Copy link
Member Author

Choose a reason for hiding this comment

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

Fixes the generated GraphQL code that previously contained imports not compatible with latest module resolution.

import { getSdk as getNearSdk } from './generated/graphql-near';
import { RequestConfig } from 'graphql-request/src/types';
Copy link
Member Author

@alexandre-abrioux alexandre-abrioux Apr 14, 2025

Choose a reason for hiding this comment

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

This import is not compatible with ESM. We can't do such type of imports anymore, as graphql-request/src/types is not an exported module.

"dotenv": "8.2.0",
"dotenv": "16.5.0",
Copy link
Member Author

@alexandre-abrioux alexandre-abrioux Apr 14, 2025

Choose a reason for hiding this comment

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

There was an issue with how dotenv exported their types in older versions, resulting in error

TS7016: Could not find a declaration file for module dotenv.

@nkoreli
Copy link
Contributor

nkoreli commented Apr 15, 2025

@alexandre-abrioux:
we updated hinkal/common to 0.2.12 and were able to built the project without the errors.

@alexandre-abrioux
Copy link
Member Author

Thanks a lot @nkoreli 🙏

import * as AJV from 'ajv';
import { Ajv } from 'ajv';
Copy link
Member Author

@alexandre-abrioux alexandre-abrioux Apr 17, 2025

Choose a reason for hiding this comment

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

With the old version (v6) of Avj there was a TypeScript issue, but only during tests. I fixed it by updating Avj to v8 and handling the breaking changes. This was before I found out that ts-jest needs isolatedModules=true to support module=nodenext.

Later, I tested again with Ajv v6, this time with isolatedModules=true, and it also works. But I figured I would keep this change since I went through the trouble of updating it.

"module": "commonjs",
"module": "nodenext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
Copy link
Member Author

@alexandre-abrioux alexandre-abrioux Apr 17, 2025

Choose a reason for hiding this comment

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

esModuleInterop=true is now the default with module=nodenext: https://www.typescriptlang.org/tsconfig/#esModuleInterop ; but we still specify it so that other tools (IDE, eslint, ts-jest) are all aligned with it.

For instance, there was an issue with ts-jest showing warnings when this field was not set, which was also fixed by updating the ts-jest package later on: kulshekhar/ts-jest#4266 ; but I kept esModuleInterop=true for clarity.

Comment on lines +122 to +125
environment:
# Lerna starts 2 Jest commands at the same time (see above --concurrency=2),
# so we use 50% of our CPU cores on each
JEST_MAX_WORKERS: '50%'
Copy link
Member Author

@alexandre-abrioux alexandre-abrioux Apr 17, 2025

Choose a reason for hiding this comment

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

This change is not mandatory.

While fiddling with our TS config, I had some issues with the CI, especially before I found out that we had to activate isolatedModules=true for ts-jest to work with module=nodenext (see explanation in the tsconfig.json file). At this point, ts-jest was consuming a considerable amount of RAM during tests. The job highlighted above (test-unit) constantly consumed 100% of the RAM and failed with timeouts or out-of-memory errors.

While digging, I found that configuring Jest to only use 4 workers (50% of CPU cores) instead of 8 (default 100% CPU cores) completely stabilized the tests, even with this buggy ts-jest config consuming a lot of memory.

After enabling isolatedModules=true, I could have reverted that change, because ts-jest was back to consuming a normal amount of memory. However this change seems to improve test time quite a bit:

In the end, even if this change is not mandatory, I have kept it as it made our pipeline faster.

Side note that confirms this: Adobe dev team advises in their blog using a maximum of 50% CPU cores for Jest workers, especially on small machines like CI runners, so as to leave some room for Jest to spawn NodeJS processes.

Here, I have only modified the configuration for this specific test-unit job, which was causing me issues, but the new JEST_MAX_WORKERS environment variable I introduced could be extended later on to other jobs if needed, to reduce potential flakiness or increase CI speed.

@alexandre-abrioux alexandre-abrioux marked this pull request as draft April 17, 2025 21:59
@alexandre-abrioux alexandre-abrioux marked this pull request as ready for review April 17, 2025 22:47
@alexandre-abrioux alexandre-abrioux requested review from MantisClone, aimensahnoun and yomarion and removed request for kevindavee, leoslr and yomarion April 17, 2025 22:48
@MantisClone MantisClone requested a review from bassgeta May 2, 2025 11:14
Copy link
Member

@MantisClone MantisClone left a comment

Choose a reason for hiding this comment

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

Directionally, I like this change, but I"m confused on some details:

Questions:

  1. When you say "code splitting" is this the same as tree-shaking? Nevermind ✅ According to Google AI:

    Code splitting is a technique used in web development to reduce the initial load time of a website by breaking down large JavaScript bundles into smaller, independent chunks. These chunks can be loaded on demand, meaning only the code needed for a specific feature or page is loaded at a particular time. This approach can significantly improve performance and user experience, especially for large or complex applications.

  2. Does this cause our build to output ES Modules or CommonJS modules?
  3. Why doesn't this change require you to append .js onto every import (like node16 did in feat!: output ES modules instead of CommonJS #1205)?
  4. What is import ... from "esm" and why do we want it?
  5. How did you handle the smart-contracts? As far as I know,

    smart-contracts must remain a CommonJS module because Hardhat uses require() statements. smart-contracts now uses import() to dynamically import ES modules. Reference: Interoperability with CommonJS

  6. Are the RN packages still usable in CommonJS projects?
  7. Why don't you set type: 'module' for each module?
  8. Does this PR bring us closer to the tree-shaking desired by As a Builder, I want package tree-shaking, so I can minimize my build size, compile time, test time, and page load time. #1148?

Context:

@alexandre-abrioux
Copy link
Member Author

alexandre-abrioux commented May 14, 2025

Thanks for the review @MantisClone 🙏

Context

Sorry about the confusion, I should have added more context.

ℹ️ This PR is not about migrating to ESM, our emitted code remains CommonJS.

We are migrating to a more "modern" version of CommonJS supported only by Node.js >= 12. This makes us adopt stricter import statements that will facilitate the migration to ESM later-on. For instance, we can't import random files in a library anymore (this won't work: import x from "mod/dist/not-exported.js"). We can only import objects explicitly exported by the library. This is how it behaves in ESM, but we stay in CommonJS. We also gain the option of importing predefined parts of a module to optimize bundle size, like importing only dotenv/config instead of the whole dotenv module. This is a modern functionality of the package.json specification, and we can already use it in CommonJS.

How the code is transpiled

TS Transpiled Comment
import x from "mod"; const x = require("mod") mod can be CommonJS or ESM (this is new)
const x = await import("mod"); const x = yield import("mod") This is new, before it would transpile to:
Promise.resolve().then(() => require('mod'));
Thanks to this, build tools can split
code more efficiently for frontend apps

Answers

  1. CommonJS
  2. This will be required to migrate to ESM, yes
  3. More and more packages export only ESM code (e.g. @ecies/ciphers, needed for chore(WIP): migrate to @noble/curves and eciesjs #1236). Today, our only option to load an ESM module is to load it asynchronously with const x = await("esm"). With this change, we can import an ESM module the same way as any other module, as long as the module in question does not contain any top-level await. We do not use that feature in this PR, but it will be made possible.
  4. Nothing specific as we stayed in CommonJS
  5. Yes
  6. Same as 3, the type: 'module' property is how we indicate that our code is built for ESM. We should do this to migrate to ESM later-on.
  7. I don't think it will change much regarding tree shaking. Migrating to ESM probably will 👍

I hope this answered your questions 🙂

Copy link
Member

@MantisClone MantisClone left a comment

Choose a reason for hiding this comment

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

Thanks for explaining! Approved 👍

@alexandre-abrioux alexandre-abrioux merged commit d999342 into master May 15, 2025
10 checks passed
@alexandre-abrioux alexandre-abrioux deleted the moduleresolution branch May 15, 2025 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants