Hello all,
I’d like to propose a standardization of our use of node in maven projects.
There is many benefit to it:
- easier and more uniform definition of new front-end modules
- easier definition of relationships between front-end modules (e.g., typescript type checking)
- access to node specific code minification tools (e.g., Vite)
- easier definition of front-end tests (e.g., Vitest)
This proposal requires some understanding of how maven and npm are working. I’ll try to be as didactic as possible but let me know if something is not clear.
This proposal suggests a lot of new technologies. They are based on our my experience selecting technologies for Cristal. I’ll try to motivate them quickly, but can’t fully develop to keep this proposal at an acceptable size.
We discussed recommending typescript as the default for front-end implementation. While this is part of the examples provided here, this proposal is not about that. Though, once this proposal implemented, it will be much easier to use typescript.
The same is true for other aspects such as testing, or migrating to Vue 3 (which uses Vite by default as its build tool).
Once this proposal is accepted, I’ll work on migrating xwiki-platform-livedata-webjar
and xwiki-platform-notifications-webjar
, as they are the two modules we have involving node in their build process.
I’ve created two draft pull request as a base for the following explanations:
- PNPM Experiment by manuelleduc · Pull Request #2880 · xwiki/xwiki-platform · GitHub introduction of the new
webjar-node
packaging type - PNPM Experiment by manuelleduc · Pull Request #772 · xwiki/xwiki-commons · GitHub demonstration of the use of
webjar-node
in two new toy modulesxwiki-platform-lib-a
andxwiki-platform-lib-b
:xwiki-platform-lib-b
depends onxwiki-platform-lib-a
- introduction of
xwiki-platform-tool-node-viteconfig
andxwiki-platform-tool-node-tsconfig
respectively for the shared configuration of Vite and typescript
Note: Testing is not present in the PoC, but is quite easy to integrate following the same principles.
Involved technologies
- pnpm
- typescript
- vite
Design principles
- Each module is independent of the others. In particular, the generation of the JavaScript from Typescript should not involve the inlining of code from imported modules
- Modules can be type checked against the code they reuse from other modules
- Defining a new
webjar-node
should involve as little code declaration as possible
Packaging choices
The history of dependency management in JavaScript lead to the introduction of a LOT of formats.
The most recent is ES modules (esm), but this is not supported by requirejs, which is the library we currently use for modules resolution.
Therefore, I suggest to use amd which is the format best supported by requirejs. Note that this is easy to switch to move to esm later simply by changing some vite configurations.
npm vs pnpm
The default dependency manager for node is npm. But, I propose to use pnpm because:
- installation time and disk usage as much lower (local
node_modules
are built using symbolic links) - built in notion of “monorepo” (see pnpm-workspace.yaml)
- linking modules using npm would require relative paths, which is really not convenient
- the list of resolved dependencies is grouped in a single pnpm-lock.yaml at the root of the project instead of scattered in
package-lock.json
files on all modules (pnpm lock file is also easier to diff)
Vite
While other bundlers exist (e.g., esbuild, rollup), Vite has several advantages:
- large community with a lot of plugins
- default bundler for Vue, which will greatly simplify the migration to Vue 3
- default bundler for Cristal
Module structure
├── pom.xml (see https://github.com/xwiki/xwiki-platform/pull/2880/files#diff-b817a3603e73d5e2abe089978a6024629e37ebfe6743e230093be7774dad37fb)
├── src
│ └── main
│ └── node // root of the node module
│ ├── dist // produced by "vite build", later copied on the webjar
│ │ ├── main.amd.js // actual code
│ │ ├── main.amd.js.map // source map between the typescript code and the compiled javascript
│ │ ├── main.d.ts // exported types
│ │ └── main.d.ts.map // source map of the types
│ ├── lib // root of the source code
│ │ └── main.ts // import and reuse a simple demo function from lib-a
│ ├── node_modules // resolved dependencies, created when calling "pnpm install"
│ │ ├── typescript -> ../../../../../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript
│ │ ├── vite -> ../../../../../../node_modules/.pnpm/vite@5.1.1/node_modules/vite
│ │ ├── xwiki-platform-lib-a -> ../../../../../xwiki-platform-lib-a/src/main/node
│ │ ├── xwiki-platform-tool-node-tsconfig -> ../../../../../../xwiki-platform-tools/xwiki-platform-tool-node/xwiki-platform-tool-node-tsconfig/src/main/node
│ │ └── xwiki-platform-tool-node-viteconfig -> ../../../../../../xwiki-platform-tools/xwiki-platform-tool-node/xwiki-platform-tool-node-viteconfig/src/main/node
│ ├── package.json // node project dependencies
│ ├── tsconfig.json // type script configuration
│ └── vite.config.js // vite configuration
└── target
├── checkstyle-result.xml
├── classes
│ └── META-INF
│ └── resources
│ └── webjars
│ └── xwiki-platform-lib-b
│ └── 16.1.0-SNAPSHOT // copied from src/main/node/dist
│ ├── main.amd.js
│ ├── main.amd.js.map
│ ├── main.d.ts
│ └── main.d.ts.map
├── maven-archiver
│ └── pom.properties
├── maven-shared-archive-resources
│ └── META-INF
│ ├── LICENSE
│ └── NOTICE
├── site
│ └── license-plugin-report.xml
├── test-classes
│ └── META-INF
│ ├── LICENSE
│ └── NOTICE
└── xwiki-platform-lib-b-16.1.0-SNAPSHOT.jar // final jar
Important details:
- module
lib-b
depends onlib-a
. This dependency is declared at two places: pom.xml to make sure that the EM install lib-a wheneverlib-b
is installed. In package.json for the bundler; the typeckecker (and also IDE support and link resolutions). - I’ve made the choice to keep the
dist
andnode_modules
outsidetarget
for several reasons:- copying the content of
src/main/node
to target causes trouble because of the duplicated module declaration - this is following the usual structure of node project, making it easier to have a good IDE support
- copying the content of
Tools
xwiki-platform-tool-node-tsconfig
Provide the default configuration to be inherited by all webjar-node modules.
{
"compilerOptions": {
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
"declarationMap": true, /* Create sourcemaps for d.ts files. */
"forceConsistentCasingInFileNames": true,
"strict": true,
}
}
xwiki-platform-tool-node-viteconfig
Provide the default vite configuration to be inherited by all webjar-node modules.
import {resolve} from 'path'
import {defineConfig} from 'vite'
import dts from "vite-plugin-dts"
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
export default defineConfig({
build: {
sourcemap: true, // Generate the source maps
lib: {
entry: resolve(process.cwd(), 'lib/main.ts'), // main.ts is the default entry point
fileName: 'main',
formats: ['amd'] // Default export format.
},
},
plugins: [
dts(), // Generate typescript types
peerDepsExternal() // All peer dependencies are excluded from the bundle.
]
})
Build steps
- download node and pnpm
- call “pnmp install”
- execute “tsc --noEmit” to type check the module (vite will later do the compilation of typescript to javascript, without typechecking)
- call “vite build” to bundle the project (involving the compilation of typescript here, but could also involve the transformation of esm based javascript modules to amd)
- call “vitest”
- copy the content of “src/main/node/dist” to the webjar
- package and install the maven module
Conclusion
With this setup, setting up a node based webjar will be much easier and will allow us to more easily:
- test our front-end code without having to deploy it in a browser
- import and reuse code from other modules
WDYT?