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 .ts and uploads the .js to 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:

  1. Finds every .ts, skipping *.d.ts and non-cartridge folders (node_modules, .git, .vscode, .idea, .intellij-sfcc, …).
  2. Transpiles each file with Babel in-process - @babel/core's transformAsync, sourceMaps: true - using the fixed config below.
  3. Writes the .js + source map to .intellij-sfcc/transpiled/<cartridge>/…, a scratch folder that is never deployed.
  4. Uploads the compiled .js to 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:

#RuleWhy
1CommonJS - require() / module.exports, no ESMRhino has no ES-module loader
2ES5-era syntax - down-level arrow functions, classes, spread, for…of, optional chaining, …Rhino is JavaScript 1.7/1.8, not modern ES2015+
3No regenerator-runtime in the outputRhino can't load it
4Types stripped - all TypeScript syntax erasedthe engine only runs JavaScript
5No assumed polyfills - down-leveling rewrites syntax, not library built-insprovide what you use; prefer dw.* APIs
6Only .js shipped - no .ts, .d.ts, or build config on the instancethose 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.

babel.config.json
{
    "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"
    ]
}
package.json (devDependencies)
{
    "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 / pluginWhat it does
@babel/preset-typescriptstrips 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-classescompile decorators, class fields, and class to prototype form
babel-plugin-transform-async-to-promiseslowers 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:

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

package.json (scripts)
{
    "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 .ts to .js in build/cartridges; --copy-files carries 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 .ts left (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, …):

.github/workflows/deploy.yml
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 separate tsconfig.build.json). Its ES5 async lowering uses self-contained __awaiter / __generator helpers - 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.

On this page