Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(sdk): Build @dotcms/client in commonjs and esmodule #29699

Merged
merged 10 commits into from
Aug 22, 2024

Conversation

fmontes
Copy link
Member

@fmontes fmontes commented Aug 21, 2024

⚠️ If you are not familiar with the JavaScript Module Ecosystem, scroll down to the FAQs section

What happen?

TLDR: The library was not working with some frameworks, example: Astro, Nodejs or Nextra (and probably others) because they wanted commonjs and didn't have that.

What did I do?

I transform our library into a “dual-package” library, that supports both CommonJS and ES Modules.

Is 2024 why do we need this?

Under certain conditions, Astro, Nextra, Nodejs and probaly others require CommonJS.

What conditions?

Imagine your library package.json looks like this:

{
  "name": "@dotcms/test-123",
  "version": "0.0.1",
  "dependencies": {},
  "main": "./index.cjs.js",
  "module": "./index.esm.js",
  "types": "./index.esm.d.ts"
}

For this, Astro will look for the main field and use index.cjs.js as the entry point in the server.

---
import { test123 } from '@dotcms/test-123';
console.log(test123());
---

But in the browser, Astro will look for the module field and use index.esm.js as the entry point.

<script>
	import { test123 } from '@dotcms/test-123';
	console.log(test123());
</script>

Now we add "type": "module" to the package.json:

{
  "name": "@dotcms/test-123",
  "version": "0.0.1",
  "type": "module",
  "dependencies": {},
  "main": "./index.cjs.js",
  "module": "./index.esm.js",
  "types": "./index.esm.d.ts"
}

Astro fails to build:

17:04:27 [ERROR] exports is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension and '/Users/fmontes/Developer/@deleteme/test-astro/node_modules/@dotcms/test-123/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.

And this is because both files are .js and Astro (well node resolution strategy) will try to load the .js file as an ES module but the main is the commonjs one.

So it doesn't matter if we have "module": "./index.esm.js" it will go to the "main": "./index.cjs.js".

If instead the package.json looked like this:

{
  "name": "@dotcms/test-123",
  "version": "0.0.1",
  "type": "module",
  "main": "./index.cjs",
  "module": "./index.mjs",
  "types": "./index.d.ts"
}

It would work because the file extensions are .cjs and .mjs. You will see more of this in the Node module resolution strategy.

Another apporach to make it works is if we use exports because we can specify the entry point for import and require:

{
  "name": "@dotcms/test-123",
  "version": "0.0.1",
  "main": "./index.cjs.js",
  "module": "./index.esm.js",
  "type": "module",
  "exports": {
    ".": {
      "types": "./index.esm.d.ts",
      "import": "./index.esm.js",
      "require": "./index.cjs.js"
    }
  }
}

This will work too because we are specifying the entry point for import (esmodule) and require (commonjs).

Node module resolution strategy

Strategy that determines how Node.js finds and loads modules.

First node look for modules in the following order:

  1. Core modules
  2. Local modules (starting with './' or '../')
  3. node_modules directories (starting from the current directory and moving up)

File extensions:

  • .js, .cjs.js, .mjs.js node think it can be CommonJS or ES module, only care the real extension.
  • .cjs node think it is CommonJS
  • .mjs node think it is ES module

type property in the package.json:

  • "type": "module": Node think all .js files are ES modules.
  • If no type is provided node thinks all .js files are CommonJS.

"exports" in the package.json:

  • Defines entry points for the package.
  • Can specify different entry points for different environments (e.g., browser vs. Node.js)
  • Takes precedence over "main" and "module" fields.

Dual package are @#$#$% hard

My proposal is to provide a dual package that works with all the frameworks. In order to do this I leverage the exports property.

So @dotcms/client package.json used to look like this:

{
    "name": "@dotcms/client",
    "version": "0.0.1-alpha.31",
    "type": "module",
    "module": "./src/index.js",
    "main": "./src/index.js",
    "types": "./src/index.d.ts"
  }

And now it looks like this:

{
  "name": "@dotcms/client",
  "version": "0.0.1-alpha.32",
  "exports": {
    "./package.json": "./package.json",
    ".": {
      "module": "./index.esm.js",
      "types": "./index.esm.d.ts",
      "import": "./index.cjs.mjs",
      "default": "./index.cjs.js"
    }
  },
  "module": "./index.esm.js",
  "main": "./index.cjs.js",
  "types": "./index.esm.d.ts"
}

And this is pretty much it, hope you find it useful.

FAQs: Understanding the JavaScript Ecosystem and Module Systems

1. What is a JavaScript Module?

A JavaScript module is a reusable piece of code that you can import into other JavaScript files. Modules help keep code organized and maintainable by breaking it into smaller, manageable parts. There are two main module formats in JavaScript:

  • CommonJS (CJS): Traditionally used in Node.js. Modules are loaded synchronously using require.
  • ES Modules (ESM): A newer standard that allows you to use import and export syntax. This is increasingly used in modern JavaScript environments, especially in browsers.

2. What is Astro?

Astro is a modern web framework designed for building fast, content-focused websites. It allows developers to use components from different frameworks (like React, Vue, etc.) while focusing on delivering optimized, static sites. Astro automatically determines the most efficient way to render pages, which sometimes requires CommonJS modules.

3. Why do we need CommonJS (CJS) in 2024?

Even though ES Modules are the future of JavaScript, some frameworks (like Astro, Node.js, and others) still require CommonJS under certain conditions, especially on the server-side. This is because Node.js has long supported CommonJS, and many tools and libraries still rely on it.

4. What is a "dual-package" library?

A dual-package library is a package that supports both CommonJS (CJS) and ES Modules (ESM). This allows the same library to work across different environments, such as older Node.js versions (which may need CJS) and modern JavaScript environments (which prefer ESM).

5. Why do we need a "dual-package" library?

To ensure compatibility across various frameworks and environments. Some tools expect CJS modules, while others expect ESM modules. A dual-package library can satisfy both requirements, preventing compatibility issues during builds or runtime.

6. What is the package.json file?

The package.json file is a configuration file in JavaScript projects that defines the project metadata (e.g., name, version, dependencies) and how the package is structured. It also specifies the entry points (main, module, exports) that tell Node.js or the browser which files to load.

7. What is the exports field in package.json?

The exports field is a more modern way to define entry points in a package. It allows specifying different entry points for different environments (e.g., browser vs. Node.js, or ES Module vs. CommonJS). This field takes precedence over main and module and helps avoid issues when resolving modules.

8. What is Node.js?

Node.js is a runtime environment that allows you to run JavaScript on the server. It's widely used for building backend services, tools, and even desktop applications. Node.js primarily used CommonJS in the past, but it now supports ES Modules as well.

9. What is the problem this pull request solves?

The pull request addresses an issue where the library wasn't compatible with some frameworks (like Astro) because it didn't support CommonJS modules. By transforming the library into a dual-package that supports both CommonJS and ES Modules, it ensures compatibility across a wider range of environments.

10. What is Rollup?

Rollup is a JavaScript module bundler that compiles small pieces of code into something larger and more complex, like a library or application. It's particularly good at bundling ES Modules and is used to generate both CommonJS and ES Module versions of a library.

11. What changes were made to the build system?

The build system was updated to use Rollup, which allows us to generate both CommonJS and ES Module versions of the library. Additionally, the package.json was updated with the exports field to clearly define entry points for different environments.

12. Why did Astro fail to build before?

Astro failed to build because it was trying to load a CommonJS file (index.cjs.js) as an ES Module due to the way Node.js resolves modules. By adding specific file extensions and using the exports field in package.json, we can avoid this issue and ensure Astro loads the correct module format.


Proposed Changes

  • Update the build system to rollup
  • Add exports to package.json

Checklist

  • Tests
  • Translations
  • [ x] Security Implications Contemplated (add notes if applicable)

Additional Info

** any additional useful context or info **

Screenshots

Original Updated
** original screenshot ** ** updated screenshot **

@fmontes fmontes requested review from a team, nicobytes, zJaaal and valentinogiardino and removed request for a team August 22, 2024 00:00
Copy link

Copy link
Contributor

@nicobytes nicobytes left a comment

Choose a reason for hiding this comment

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

Wow, great research on this, and yes, library publishing is a complex topic!

@fmontes fmontes added this pull request to the merge queue Aug 22, 2024
Merged via the queue into master with commit fa74e31 Aug 22, 2024
17 checks passed
@fmontes fmontes deleted the fix-client-library-build-2 branch August 22, 2024 15:32
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.

SDK: Unable to use @dotcms/client library in nextra due to import error
4 participants