Building
Contents
In this chapter we will learn how to set up transpilation of Typescript code - both client- and server-side - to JavaScript.
Create project
We start this tutorial with the most basic setup possible, a vanilla XP project:
enonic create myproject -r starter-vanilla
NPM modules
Types
Install types needed to configure the build system:
npm install --save-dev @types/node
Install types needed to work with Enonic Core API:
npm install --save-dev @enonic-types/core @enonic-types/lib-content @enonic-types/lib-portal @enonic-types/global @enonic-types/lib-context @enonic-types/lib-auth @enonic-types/lib-event @enonic-types/lib-common @enonic-types/lib-repo @enonic-types/lib-websocket @enonic-types/lib-node @enonic-types/lib-i18n @enonic-types/lib-task @enonic-types/lib-io @enonic-types/lib-project @enonic-types/lib-mail @enonic-types/lib-export @enonic-types/lib-scheduler @enonic-types/lib-admin @enonic-types/lib-cluster @enonic-types/lib-value
Tools
Install tools required to set up transpilation and bundling of Typescript code:
npm install --save-dev typescript tsdown glob concurrently
-
typescriptwill enable support of Typescript language by adding syntax for type declarations and annotations -
tsdownis a TypeScript bundler powered by Rolldown (the successor to the now-deprecatedtsup) -
globis used by the build config to discover the source files to bundle -
concurrentlyis a tool to run multiple commands concurrently
Configure
package.json
Add the following build script to the package.json file:
{
"scripts": {
"build": "tsdown"
}
}
| You will typically NOT publish an Enonic XP project to npm, so it’s a good idea to add the following to the package.json file: |
{
"private": true
}
TypeScript configuration
Add the following config files to the project:
{
// This file should only include files that configures the build system.
// It should ignore all actual source files.
// Read more about tsconfig.json at:
// https://www.typescriptlang.org/tsconfig
// Specifies an array of filenames or patterns to include in the program.
// These filenames are resolved relative to the directory containing the
// tsconfig.json file.
"include": [
"**/*.ts"
],
// Specifies an array of filenames or patterns that should be skipped when
// resolving include.
// Important: exclude only changes which files are included as a result of
// the include setting. A file specified by exclude can still become part of
// your codebase due to an import statement in your code, a types inclusion,
// a /// <reference directive, or being specified in the files list.
// It is not a mechanism that prevents a file from being included in the
// codebase - it simply changes what the include setting finds.
"exclude": [
"**/*.d.ts",
"src/**/*.*", // Avoid Enonic XP source files
],
"compilerOptions": {
// Leave baseUrl unset: setting it would let bare module specifiers resolve
// against the project root instead of node_modules, which can shadow real
// packages with same-named local folders.
// "baseUrl": ".",
"lib": [
"es2023" // string.replaceAll
],
// By default all visible ”@types” packages are included in your compilation.
// Packages in node_modules/@types of any enclosing folder are considered visible.
// For example, that means packages within ./node_modules/@types/,
// ../node_modules/@types/, ../../node_modules/@types/, and so on.
// If types is specified, only packages listed will be included in the global scope.
// This feature differs from typeRoots in that it is about specifying only the exact
// types you want included, whereas typeRoots supports saying you want particular folders.
"types": [
"node"
],
// tsdown's types reference `exports`-subpath imports and ship .d.ts using
// private (#) fields; these let the build config type-check cleanly.
"skipLibCheck": true,
"module": "preserve",
"moduleResolution": "bundler"
},
}
{
// This file should only include Enonic XP server-side source files.
// It should exclude any client-side source files.
// Read more about tsconfig.json at:
// https://www.typescriptlang.org/tsconfig
// Specifies an array of filenames or patterns to include in the program.
// These filenames are resolved relative to the directory containing the
// tsconfig.json file.
"include": [
"**/*.ts"
],
// Specifies an array of filenames or patterns that should be skipped when
// resolving include.
// Important: exclude only changes which files are included as a result of
// the include setting. A file specified by exclude can still become part of
// your codebase due to an import statement in your code, a types inclusion,
// a /// <reference directive, or being specified in the files list.
// It is not a mechanism that prevents a file from being included in the
// codebase - it simply changes what the include setting finds.
"exclude": [
"**/*.d.ts",
"assets/**/*.*",
// "static/**/*.*",
],
"compilerOptions": {
// A series of entries which re-map imports to lookup locations relative
// to the baseUrl if set, or to the tsconfig file itself otherwise.
"paths": {
"/lib/xp/*": ["../../../node_modules/@enonic-types/lib-*"],
"/*": ["./*"],
},
"skipLibCheck": true,
// By default all visible ”@types” packages are included in your compilation.
// Packages in node_modules/@types of any enclosing folder are considered visible.
// For example, that means packages within ./node_modules/@types/,
// ../node_modules/@types/, ../../node_modules/@types/, and so on.
// If types is specified, only packages listed will be included in the global scope.
// This feature differs from typeRoots in that it is about specifying only the exact
// types you want included, whereas typeRoots supports saying you want particular folders.
"types": [
// Make the types for the Enonic XP Global objects and functions
// available in the global scope.
// https://developer.enonic.com/docs/xp/stable/framework/globals
// app, exports, log, require, resolve
// https://developer.enonic.com/docs/xp/stable/framework/java-bridge
// __.newBean, __.toNativeObject, __.nullOrValue
"@enonic-types/global"
],
},
}
{
// This file configures TypeScript for client-side assets.
// Read more about tsconfig.json at:
// https://www.typescriptlang.org/tsconfig
// Specifies an array of filenames or patterns to include in the program.
// These filenames are resolved relative to the directory containing the
// tsconfig.json file.
"include": [
"./**/*.ts",
"./**/*.tsx",
],
// Specifies an array of filenames or patterns that should be skipped when
// resolving include.
// Important: exclude only changes which files are included as a result of
// the include setting. A file specified by exclude can still become part of
// your codebase due to an import statement in your code, a types inclusion,
// a /// <reference directive, or being specified in the files list.
// It is not a mechanism that prevents a file from being included in the
// codebase - it simply changes what the include setting finds.
"exclude": [
"./**/*.d.ts",
],
"compilerOptions": {
"lib": [
"DOM", // console, document, window, etc...
],
},
}
tsdown configuration
A single config file defines both build targets (server and client/assets). It discovers every source file with glob and emits one output file per input, keeping the directory tree intact — Enonic XP loads each controller/service/task by its resource path. XP runtime libraries (/lib/xp/* etc.) are marked external so they are never bundled, and a target is skipped entirely when it has no source files.
import {globSync} from 'glob';
import {defineConfig} from 'tsdown';
const SRC = 'src/main/resources';
const SRC_ASSETS = `${SRC}/assets`;
const DST = 'build/resources/main';
const DST_ASSETS = `${DST}/assets`;
const dev = process.env.NODE_ENV === 'development';
const logLevel: 'silent' | 'info' = ['QUIET', 'WARN'].includes(process.env.LOG_LEVEL_FROM_GRADLE || '') ? 'silent' : 'info';
// Enonic XP loads each controller/service/task by its resource path, so every
// source file must become its own output file with the directory tree intact.
// Turn a glob into a tsdown `entry` map ({ "relative/name": "src/path/file.ts" }).
function entries(dir: string, exts: string, ignore: string[] = []): Record<string, string> {
return Object.fromEntries(
globSync(`${dir}/**/*.${exts}`, {posix: true, ignore})
.map(file => [file.slice(dir.length + 1).replace(/\.[^.]+$/, ''), file]),
);
}
const serverEntry = entries(SRC, '{ts,js}', [`${SRC_ASSETS}/**`]);
const assetEntry = entries(SRC_ASSETS, '{tsx,ts,jsx,js}');
// XP runtime libraries are provided by the platform — never bundle them.
const xpExternal = [
'/lib/cache',
'/lib/enonic/static',
/^\/lib\/guillotine/,
'/lib/graphql',
'/lib/graphql-connection',
'/lib/http-client',
'/lib/license',
'/lib/mustache',
'/lib/router',
'/lib/util',
'/lib/vanilla',
'/lib/text-encoding',
'/lib/thymeleaf',
/^\/lib\/xp\//,
];
// Skip a target that has no source files (e.g. a server-only or client-only app).
export default defineConfig([
...(Object.keys(serverEntry).length ? [{
entry: serverEntry,
outDir: DST,
format: 'cjs' as const,
target: 'es2015', // Rolldown/oxc floor; runs on XP 8's GraalJS engine
platform: 'neutral' as const,
clean: false, // outDir also holds Gradle-copied resources + the assets/ subfolder
dts: false, // d.ts files are useless at runtime
minify: false, // minifying server files makes debugging harder
sourcemap: false,
logLevel,
tsconfig: `${SRC}/tsconfig.json`,
inputOptions: {
external: xpExternal,
resolve: {
mainFields: ['module', 'main'],
},
},
outputOptions: {
chunkFileNames: '_chunks/[name]-[hash].js', // avoid chunk-name collisions
},
}] : []),
...(Object.keys(assetEntry).length ? [{
entry: assetEntry,
outDir: DST_ASSETS,
format: 'esm' as const,
target: 'es2015',
platform: 'browser' as const,
clean: false,
dts: false,
minify: !dev,
sourcemap: !dev,
logLevel,
tsconfig: `${SRC_ASSETS}/tsconfig.json`,
}] : []),
]);
Gradle configuration
Node-gradle plugin
In order for Gradle to be able to run npm scripts, you need to use the node-gradle plugin.
Add the following to the build.gradle file:
plugins {
id 'com.github.node-gradle.node' version '7.0.2'
}
node {
// Whether to download and install a specific Node.js version or not
// If false, it will use the globally installed Node.js
// If true, it will download node using above parameters
// Note that npm is bundled with Node.js
download = true
// Version of node to download and install (only used if download is true)
// It will be unpacked in the workDir
version = '20.12.2'
}
npmBuild task
Add the following task to the build.gradle file, which will run the npm build script defined in package.json:
tasks.register('npmBuild', NpmTask) {
args = [
'run',
'--silent',
'build'
]
dependsOn npmInstall
environment = [
'FORCE_COLOR': 'true',
'LOG_LEVEL_FROM_GRADLE': gradle.startParameter.logLevel.toString(),
'NODE_ENV': project.hasProperty('dev') || project.hasProperty('development') ? 'development' : 'production'
]
inputs.dir 'src/main/resources'
outputs.dir 'build/resources/main'
}
jar.dependsOn npmBuild
Clean up jar file
Add the following to the build.gradle file:
tasks.withType(Copy).configureEach {
includeEmptyDirs = false
}
processResources {
exclude '**/.gitkeep'
exclude '**/*.json'
exclude '**/*.ts'
exclude '**/*.tsx'
}
This will make sure that assembled jar file contains only required assets and none of the original source TypeScript files.
Summary
You should now be able to perform basic builds that transpile TypeScript code to JavaScript. In the next chapter we’ll have a look at Type checking.