Skip to content

Commit

Permalink
feat(sdk): Build @dotcms/client in commonjs and esmodule (#29699)
Browse files Browse the repository at this point in the history
## ⚠️ 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:

```json
{
  "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.

```html
<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:

```json
{
  "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.](#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`:

```json
{
  "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:

```json
{
    "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:
```json
{
  "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
- [x] Tests
- [x] Translations
- [ x] Security Implications Contemplated (add notes if applicable)

### Additional Info
** any additional useful context or info **

### Screenshots
Original             |  Updated
:-------------------------:|:-------------------------:
** original screenshot **  |  ** updated screenshot **
  • Loading branch information
fmontes authored Aug 22, 2024
1 parent 46b0a3d commit fa74e31
Show file tree
Hide file tree
Showing 13 changed files with 84 additions and 42 deletions.
4 changes: 2 additions & 2 deletions core-web/libs/sdk/angular/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"name": "@dotcms/angular",
"version": "0.0.1-alpha.31",
"version": "0.0.1-alpha.32",
"peerDependencies": {
"@angular/common": "^17.1.0",
"@angular/core": "^17.1.0",
"@angular/router": "^17.1.0",
"@dotcms/client": "0.0.1-alpha.31",
"@dotcms/client": "0.0.1-alpha.32",
"@tinymce/tinymce-angular": "^8.0.0",
"rxjs": "^7.8.0"
},
Expand Down
16 changes: 14 additions & 2 deletions core-web/libs/sdk/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,23 @@ yarn add @dotcms/client

## Usage

First, initialize the client with your DotCMS instance details.
`@dotcms/client` supports both ES modules and CommonJS. You can import it using either syntax:

### ES Modules

```javascript
import { dotcmsClient } from '@dotcms/client';
```

### CommonJS

```javascript
const { dotcmsClient } = require('@dotcms/client');
```

First, initialize the client with your DotCMS instance details.

```javascript
const client = dotcmsClient.init({
dotcmsUrl: 'https://your-dotcms-instance.com',
authToken: 'your-auth-token',
Expand Down Expand Up @@ -107,4 +119,4 @@ Always refer to the official [DotCMS documentation](https://www.dotcms.com/docs/
| Videos | [Helpful Videos](http://dotcms.com/videos/) |
| Forums/Listserv | [via Google Groups](https://groups.google.com/forum/#!forum/dotCMS) |
| Twitter | @dotCMS |
| Main Site | [dotCMS.com](https://dotcms.com/) |
| Main Site | [dotCMS.com](https://dotcms.com/) |
3 changes: 1 addition & 2 deletions core-web/libs/sdk/client/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"name": "@dotcms/client",
"version": "0.0.1-alpha.31",
"type": "module",
"version": "0.0.1-alpha.32",
"description": "Official JavaScript library for interacting with DotCMS REST APIs.",
"repository": {
"type": "git",
Expand Down
11 changes: 7 additions & 4 deletions core-web/libs/sdk/client/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
"projectType": "library",
"targets": {
"build": {
"executor": "@nrwl/js:tsc",
"executor": "@nx/rollup:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"format": ["esm", "cjs"],
"compiler": "tsc",
"generateExportsField": true,
"assets": [{ "input": ".", "output": ".", "glob": "*.md" }],
"outputPath": "dist/libs/sdk/client",
"main": "libs/sdk/client/src/index.ts",
"tsConfig": "libs/sdk/client/tsconfig.lib.json",
"assets": ["libs/sdk/client/*.md"]
"tsConfig": "libs/sdk/client/tsconfig.lib.json"
}
},
"build:js": {
Expand All @@ -34,7 +37,7 @@
"publish": {
"executor": "nx:run-commands",
"options": {
"command": "node tools/scripts/publish.mjs sdk-js-client {args.ver} {args.tag}"
"command": "node tools/scripts/publish.mjs sdk-client {args.ver} {args.tag}"
},
"dependsOn": ["build"]
},
Expand Down
36 changes: 30 additions & 6 deletions core-web/libs/sdk/client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
export * from './lib/client/sdk-js-client';
export * from './lib/editor/sdk-editor';
export * from './lib/editor/models/editor.model';
export * from './lib/editor/models/client.model';
export * from './lib/query-builder/sdk-query-builder';
export * from './lib/utils';
import { ClientConfig, DotCmsClient } from './lib/client/sdk-js-client';
import { CUSTOMER_ACTIONS, postMessageToEditor } from './lib/editor/models/client.model';
import {
CustomClientParams,
DotCMSPageEditorConfig,
EditorConfig
} from './lib/editor/models/editor.model';
import {
destroyEditor,
initEditor,
isInsideEditor,
updateNavigation
} from './lib/editor/sdk-editor';
import { getPageRequestParams, graphqlToPageEntity } from './lib/utils';

export {
graphqlToPageEntity,
getPageRequestParams,
isInsideEditor,
DotCmsClient,
DotCMSPageEditorConfig,
CUSTOMER_ACTIONS,
CustomClientParams,
postMessageToEditor,
EditorConfig,
initEditor,
updateNavigation,
destroyEditor,
ClientConfig
};
5 changes: 4 additions & 1 deletion core-web/libs/sdk/client/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
"types": [""],
"target": "es2020",
"module": "es2020",
"moduleResolution": "node"
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
Expand Down
4 changes: 2 additions & 2 deletions core-web/libs/sdk/experiments/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dotcms/experiments",
"version": "0.0.1-alpha.31",
"version": "0.0.1-alpha.32",
"description": "Official JavaScript library to use Experiments with DotCMS.",
"repository": {
"type": "git",
Expand All @@ -25,6 +25,6 @@
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"@dotcms/client": "0.0.1-alpha.31"
"@dotcms/client": "0.0.1-alpha.32"
}
}
4 changes: 2 additions & 2 deletions core-web/libs/sdk/react/package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "@dotcms/react",
"version": "0.0.1-alpha.31",
"version": "0.0.1-alpha.32",
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
"@dotcms/client": "0.0.1-alpha.31"
"@dotcms/client": "0.0.1-alpha.32"
},
"description": "Official React Components library to render a dotCMS page.",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion core-web/tools/scripts/publish.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,4 @@ try {
}

// Execute "npm publish" to publish
execSync(`npm publish --access public --tag ${tag}`);
execSync(`npm publish --access public --tag ${tag} --dry-run`);
21 changes: 11 additions & 10 deletions examples/angular/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions examples/angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
},
"private": true,
"dependencies": {
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2",
"@angular/animations": "^17.1.0",
"@angular/common": "^17.1.0",
"@angular/compiler": "^17.1.0",
Expand All @@ -21,9 +18,12 @@
"@angular/platform-browser": "^17.1.0",
"@angular/platform-browser-dynamic": "^17.1.0",
"@angular/router": "^17.1.0",
"@dotcms/client": "0.0.1-alpha.31",
"@dotcms/angular": "0.0.1-alpha.31",
"@tinymce/tinymce-angular": "^8.0.0"
"@dotcms/angular": "0.0.1-alpha.32",
"@dotcms/client": "0.0.1-alpha.32",
"@tinymce/tinymce-angular": "^8.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.0.7",
Expand Down
6 changes: 3 additions & 3 deletions examples/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
"lint": "next lint"
},
"dependencies": {
"@dotcms/client": "0.0.1-alpha.31",
"@dotcms/react": "0.0.1-alpha.31",
"@dotcms/experiments": "0.0.1-alpha.31",
"@dotcms/client": "0.0.1-alpha.32",
"@dotcms/react": "0.0.1-alpha.32",
"@dotcms/experiments": "0.0.1-alpha.32",
"next": "14.1.1",
"react": "^18",
"react-dom": "^18"
Expand Down
2 changes: 1 addition & 1 deletion examples/vuejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"format": "prettier --write src/"
},
"dependencies": {
"@dotcms/client": "^0.0.1-alpha.31",
"@dotcms/client": "^0.0.1-alpha.32",
"vue": "^3.4.15",
"vue-router": "^4.2.5"
},
Expand Down

0 comments on commit fa74e31

Please sign in to comment.