From fa74e31d770340fe1b503ce3e5174d8835a8508f Mon Sep 17 00:00:00 2001 From: Freddy Montes <751424+fmontes@users.noreply.github.com> Date: Thu, 22 Aug 2024 08:16:00 -0600 Subject: [PATCH] feat(sdk): Build `@dotcms/client` in commonjs and esmodule (#29699) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ⚠️ 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 ``` 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 ** --- core-web/libs/sdk/angular/package.json | 4 +-- core-web/libs/sdk/client/README.md | 16 ++++++++-- core-web/libs/sdk/client/package.json | 3 +- core-web/libs/sdk/client/project.json | 11 ++++--- core-web/libs/sdk/client/src/index.ts | 36 ++++++++++++++++++---- core-web/libs/sdk/client/tsconfig.lib.json | 5 ++- core-web/libs/sdk/experiments/package.json | 4 +-- core-web/libs/sdk/react/package.json | 4 +-- core-web/tools/scripts/publish.mjs | 2 +- examples/angular/package-lock.json | 21 +++++++------ examples/angular/package.json | 12 ++++---- examples/nextjs/package.json | 6 ++-- examples/vuejs/package.json | 2 +- 13 files changed, 84 insertions(+), 42 deletions(-) diff --git a/core-web/libs/sdk/angular/package.json b/core-web/libs/sdk/angular/package.json index 49dd4ea45040..477fcedcdf0d 100644 --- a/core-web/libs/sdk/angular/package.json +++ b/core-web/libs/sdk/angular/package.json @@ -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" }, diff --git a/core-web/libs/sdk/client/README.md b/core-web/libs/sdk/client/README.md index 8d4801e35e8b..d90d233b5111 100644 --- a/core-web/libs/sdk/client/README.md +++ b/core-web/libs/sdk/client/README.md @@ -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', @@ -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/) | \ No newline at end of file diff --git a/core-web/libs/sdk/client/package.json b/core-web/libs/sdk/client/package.json index 9a8c56e50ee7..3c049aa9e858 100644 --- a/core-web/libs/sdk/client/package.json +++ b/core-web/libs/sdk/client/package.json @@ -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", diff --git a/core-web/libs/sdk/client/project.json b/core-web/libs/sdk/client/project.json index 1286a369b1f6..b2a7f198c96b 100644 --- a/core-web/libs/sdk/client/project.json +++ b/core-web/libs/sdk/client/project.json @@ -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": { @@ -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"] }, diff --git a/core-web/libs/sdk/client/src/index.ts b/core-web/libs/sdk/client/src/index.ts index 5c720d05e47d..919b1f1f6981 100644 --- a/core-web/libs/sdk/client/src/index.ts +++ b/core-web/libs/sdk/client/src/index.ts @@ -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 +}; diff --git a/core-web/libs/sdk/client/tsconfig.lib.json b/core-web/libs/sdk/client/tsconfig.lib.json index 163d90724090..2f77580986dc 100644 --- a/core-web/libs/sdk/client/tsconfig.lib.json +++ b/core-web/libs/sdk/client/tsconfig.lib.json @@ -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"] diff --git a/core-web/libs/sdk/experiments/package.json b/core-web/libs/sdk/experiments/package.json index e2429154a078..2eed7e32c417 100644 --- a/core-web/libs/sdk/experiments/package.json +++ b/core-web/libs/sdk/experiments/package.json @@ -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", @@ -25,6 +25,6 @@ "peerDependencies": { "react": ">=18", "react-dom": ">=18", - "@dotcms/client": "0.0.1-alpha.31" + "@dotcms/client": "0.0.1-alpha.32" } } diff --git a/core-web/libs/sdk/react/package.json b/core-web/libs/sdk/react/package.json index 64ce599625a9..598add55b570 100644 --- a/core-web/libs/sdk/react/package.json +++ b/core-web/libs/sdk/react/package.json @@ -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": { diff --git a/core-web/tools/scripts/publish.mjs b/core-web/tools/scripts/publish.mjs index ba40aaa8e9c7..f2eeb6a7d686 100644 --- a/core-web/tools/scripts/publish.mjs +++ b/core-web/tools/scripts/publish.mjs @@ -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`); diff --git a/examples/angular/package-lock.json b/examples/angular/package-lock.json index 8af79e80c618..b7482daf612d 100644 --- a/examples/angular/package-lock.json +++ b/examples/angular/package-lock.json @@ -16,8 +16,8 @@ "@angular/platform-browser": "^17.1.0", "@angular/platform-browser-dynamic": "^17.1.0", "@angular/router": "^17.1.0", - "@dotcms/angular": "0.0.1-alpha.29", - "@dotcms/client": "0.0.1-alpha.29", + "@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", @@ -2396,10 +2396,10 @@ } }, "node_modules/@dotcms/angular": { - "version": "0.0.1-alpha.29", - "resolved": "https://registry.npmjs.org/@dotcms/angular/-/angular-0.0.1-alpha.29.tgz", - "integrity": "sha512-L3LhTOKjRce1r3mxyANEF2m7adzyqnNTdCX/qXTSorw8jLvUbSpXxZyG95NsMH0fi7+iJDmNXNvtQyx0opTD/g==", - + "version": "0.0.1-alpha.32", + "resolved": "https://registry.npmjs.org/@dotcms/angular/-/angular-0.0.1-alpha.32.tgz", + "integrity": "sha512-iyekrLnIDPMIiopjwCHUfTQc1lDOdClRvbKjSAdeSZ/9YzYlTuE4ZlwYVMrxaRwru4NB2VT+8Bz7yb/tOVq1kw==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, @@ -2407,15 +2407,16 @@ "@angular/common": "^17.1.0", "@angular/core": "^17.1.0", "@angular/router": "^17.1.0", - "@dotcms/client": "0.0.1-alpha.29", + "@dotcms/client": "0.0.1-alpha.32", "@tinymce/tinymce-angular": "^8.0.0", "rxjs": "^7.8.0" } }, "node_modules/@dotcms/client": { - "version": "0.0.1-alpha.29", - "resolved": "https://registry.npmjs.org/@dotcms/client/-/client-0.0.1-alpha.29.tgz", - "integrity": "sha512-e+npJyvfpIzPnvmJ28AqEwdbOcb6AXcAXF4Aa/lAGGJTABjjTd/FOHdLFEEeCG5OkGbAi6DzP6lQH75OzYzStw==" + "version": "0.0.1-alpha.32", + "resolved": "file:../../core-web/dist/libs/sdk/client/dotcms-client-0.0.1-alpha.32.tgz", + "integrity": "sha512-CqBpqbBGMW9WI8VEHDRDuD11Ue4ywuJi/OYyZBJ2l/DvsDPlepLvT9OFpJO1ot3dOyQTHgaupRKvaeZTQBVPkg==", + "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.1", diff --git a/examples/angular/package.json b/examples/angular/package.json index 1f03317a969b..9bfc5d64f181 100644 --- a/examples/angular/package.json +++ b/examples/angular/package.json @@ -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", @@ -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", diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 05a29df463f1..d24ace61533d 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -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" diff --git a/examples/vuejs/package.json b/examples/vuejs/package.json index ea548ce66863..8b9b11e0b7d4 100644 --- a/examples/vuejs/package.json +++ b/examples/vuejs/package.json @@ -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" },