Build & Transpile (CI/CD)
Transpile SFCC TypeScript into Rhino-safe JavaScript with the same rules and Babel configuration used by the Intellij SFCC plugin.
SFCC's Rhino engine runs JavaScript - it never executes .ts directly. So your TypeScript is always transpiled, in two places that follow the same rules:
- The plugin compiles your
.tsand uploads the.jsto your active sandbox automatically, as you develop. - Your CI/CD transpiles and deploys a release to shared / staging / production - that pipeline is yours to own.
You can use any TypeScript → JavaScript tool; SFCC only cares that the output is Rhino-safe. This page shows how the plugin does it, the output rules any tool must meet, and the exact Babel config that reproduces the plugin's result.
How the Plugin Transpiles
In the B2C TypeScript Transpiler output channel, the extension does this per cartridge:
- Finds every
.ts, skipping*.d.tsand non-cartridge folders (node_modules,.git,.vscode,.idea,.intellij-sfcc, …). - Transpiles each file with Babel in-process -
@babel/core'stransformAsync,sourceMaps: true- using the fixed config below. - Writes the
.js+ source map to.intellij-sfcc/transpiled/<cartridge>/…, a scratch folder that is never deployed. - Uploads the compiled
.jsto your active connection over WebDAV, so your TypeScript runs on the sandbox. (Source maps stay local so the debugger maps breakpoints back to your.ts.)
Your CI/CD does the same compile, aimed at a release instead of a sandbox - so if you reproduce the plugin's output, what you deploy behaves like what you debugged.
Why Babel - and why you're not required to use it
The plugin uses Babel because it embeds as a library (in-process, per file, with source maps for the debugger), its presets are composable (exactly the down-levels Rhino needs, nothing more), and it keeps the output free of the regenerator-runtime that Rhino can't load. But that's an implementation detail: any tool - tsc, swc, esbuild - works if its output meets the rules below.
The Output Contract
"Rhino-safe" means the transpiled JavaScript satisfies these rules, whatever produced it:
| # | Rule | Why |
|---|---|---|
| 1 | CommonJS - require() / module.exports, no ESM | Rhino has no ES-module loader |
| 2 | ES5-era syntax - down-level arrow functions, classes, spread, for…of, optional chaining, … | Rhino is JavaScript 1.7/1.8, not modern ES2015+ |
| 3 | No regenerator-runtime in the output | Rhino can't load it |
| 4 | Types stripped - all TypeScript syntax erased | the engine only runs JavaScript |
| 5 | No assumed polyfills - down-leveling rewrites syntax, not library built-ins | provide what you use; prefer dw.* APIs |
| 6 | Only .js shipped - no .ts, .d.ts, or build config on the instance | those are build-time artifacts |
SFCC scripting is synchronous. There is no Promise, async/await, setTimeout, or fetch, and the
transpile does not polyfill them (rule #5). Async syntax compiles, but the lowered code references a Promise
that SFCC doesn't provide - so write straight-line code. Full list: What Rhino Does Not
Provide.
The Babel Config
The plugin's exact configuration, as a standalone Babel setup. Copy it verbatim to match what the plugin produces for your sandbox.
{
"presets": [
"@babel/preset-typescript",
[
"@babel/preset-env",
{
"targets": { "ie": "11" },
"modules": "commonjs",
"corejs": 3
}
]
],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
"@babel/plugin-transform-class-properties",
"@babel/plugin-transform-classes",
"babel-plugin-transform-async-to-promises"
]
}{
"devDependencies": {
"@babel/core": "^7.28.5",
"@babel/cli": "^7.28.0",
"@babel/preset-env": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@babel/plugin-proposal-decorators": "^7.28.0",
"@babel/plugin-transform-class-properties": "^7.27.1",
"@babel/plugin-transform-classes": "^7.28.4",
"babel-plugin-transform-async-to-promises": "^0.8.18",
"typescript": "^5.9.3"
}
}Each piece maps to a rule above:
| Preset / plugin | What it does |
|---|---|
@babel/preset-typescript | strips TypeScript types (rule #4) |
@babel/preset-env { ie: 11, commonjs, corejs: 3 } | down-levels syntax to an ES5 baseline and emits CommonJS (rules #1, #2) |
plugin-proposal-decorators (legacy) + transform-class-properties + transform-classes | compile decorators, class fields, and class to prototype form |
babel-plugin-transform-async-to-promises | lowers async syntax without a regenerator runtime (rule #3) |
Two intentional quirks of mirroring the config exactly: with corejs: 3 but useBuiltIns unset, Babel injects no
polyfills (it only declares the version) and prints one harmless build-time warning - "The corejs option only
has an effect when the useBuiltIns option is not false". Both are expected.
Type-check and Build
Babel only strips types - it never reports type errors - so run tsc as a separate, emit-free check:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "node",
"noEmit": true,
"strict": false,
"noImplicitAny": false,
"skipLibCheck": true,
"esModuleInterop": false,
"experimentalDecorators": true,
"types": []
},
"include": ["cartridges/**/cartridge/**/*.ts"],
"exclude": ["**/node_modules/**", "**/cartridge/client/**", "**/cartridge/static/**"]
}target can stay modern (tsc only checks types; Babel does the down-leveling). experimentalDecorators: true is required so tsc reads decorators with the same legacy semantics as Babel - without it, TypeScript 5.x defaults to Stage-3 decorators and rejects a legacy 3-argument decorator. The client/ and static/ trees are front-end assets with their own build, so they're excluded.
{
"scripts": {
"typecheck": "tsc --noEmit -p tsconfig.json",
"build": "babel cartridges --extensions .ts --out-dir build/cartridges --out-file-extension .js --copy-files --ignore \"**/*.d.ts\"",
"build:check": "npm run typecheck && npm run build"
}
}typecheck- fails on type errors without emitting (a good PR gate).build- compiles every.tsto.jsinbuild/cartridges;--copy-filescarries non-TypeScript files (.isml,.properties, hand-written.js, …) across unchanged, and--ignore "**/*.d.ts"drops declarations. The result is a complete, uploadable cartridge set with no.tsleft (rule #6).build:check- type-check, then build.
CI Pipeline
Install → type-check → build, then hand build/cartridges to your existing SFCC upload tool (sfcc-ci, b2c-tools, …):
name: Build & Deploy Cartridges
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Type-check
run: npm run typecheck
- name: Build (transpile TypeScript)
run: npm run build
- name: Deploy code version
env:
SFCC_CLIENT_ID: ${{ secrets.SFCC_CLIENT_ID }}
SFCC_CLIENT_SECRET: ${{ secrets.SFCC_CLIENT_SECRET }}
SFCC_HOSTNAME: ${{ secrets.SFCC_HOSTNAME }}
run: |
# your existing upload/activate step over ./build/cartridges, e.g.:
# npx sfcc-ci code:deploy ./build/cartridges.zip
echo "upload emitted .js cartridges to the instance"Using a Different Transpiler
Not using Babel? Meet the same six rules and validate the output on a sandbox before trusting it:
tsc- set"module": "CommonJS"and"target": "ES5"with emit on (a separatetsconfig.build.json). Its ES5 async lowering uses self-contained__awaiter/__generatorhelpers - no regenerator runtime (rule #3).- swc / esbuild - set CommonJS output and an ES5 target, but confirm they down-level every construct your code uses; some won't fully target ES5.
Async stays off-limits whatever the tool - SFCC has no Promise.
Related Pages
Imports & the Rhino Engine
Why SFCC scripts use CommonJS require() instead of ESM import - and how code is scoped, made visible, and executed on the Rhino engine
Digital Script (.ds)
Develop legacy Digital Script (.ds) with JavaScript-level intelligence, importScript/importPackage/importClass scope analysis, and zero configuration.