Skip to content

NPM version Downloads

@commerce-klaus/vitest-sfcc ​

Cartridge-aware SFCC runtime and dependency mocking for Vitest. It provides a modern alternative to proxyquire for statically analyzable cartridge modules.

TL;DR ​

vitest.config.ts
ts
import { defineConfig } from "vitest/config"
import sfccVitest from "@commerce-klaus/vitest-sfcc"

export default defineConfig({
  plugins: [
    sfccVitest({
      basePath: "./cartridges",
      cartridgePath: ["app_custom", "app_storefront_base"],
    }),
  ],
})
payment.test.ts
ts
import { resetSfccRuntime } from "@commerce-klaus/vitest-sfcc"

const runtime = resetSfccRuntime()
runtime.mock("*/cartridge/scripts/payment/provider", providerMock)

const payment = await import("../cartridge/scripts/payment.js")

Shared resolution model ​

SFCC resolves server-side modules in the context of an ordered cartridge path. Entries on the left have higher precedence, so the order is part of application behavior rather than a filesystem detail. See Cartridges and cartridge paths for the central explanation.

  • */cartridge/... selects the first matching module in cartridge-path order.
  • ~/cartridge/... selects a module from the importing file's own cartridge.
  • <cartridge>/cartridge/... selects a module from one named cartridge.
  • module.superModule selects the next matching implementation after the current cartridge.
  • dw/* refers to platform APIs and is not resolved from the project filesystem.

Prefer ~/ for local cartridge dependencies

Use ~/ when a dependency belongs to the importing file's own cartridge. Reserve */ for cases where cartridge-path precedence should deliberately select the effective implementation.

Commerce Klaus shares these rules across its ESLint, TypeScript, Babel, Vite, and Vitest integrations. See Understanding SFCC module resolution for the complete model, examples, and edge cases.

Commerce Klaus can read these options from one shared commerce-klaus.config.ts or .js file. Package options remain supported and override the central values.

Why this package exists ​

SFCC cartridge code uses CommonJS, cartridge-specific module identifiers, platform modules, globals, hooks, controllers, and job metadata that Vitest cannot execute by itself. This package connects the shared cartridge resolver and framework-independent test runtime to Vite's module graph so tests can load real cartridge modules and replace only their external dependencies.

Choosing the right package ​

Use this package when cartridge code must run in Vitest. It provides the complete test integration: cartridge resolution, CommonJS transformation, SFCC platform-module fallbacks, globals, dependency mocking, hooks, and execution harnesses.

Choose one Vite integration

Use @commerce-klaus/vitest-sfcc when cartridge code must run in Vitest. Use @commerce-klaus/vite-plugin-sfcc-modules when another Vite-based tool needs cartridge-aware module resolution without the test runtime. The Vitest integration already includes the required resolution and transformation, so do not configure both plugins in the same Vitest run.

Features ​

  • Resolves dw/*, */, ~/, cartridge aliases, relative modules, and module.superModule.
  • Infers cartridge order from explicit configuration, environment, jsconfig, or site.xml.
  • Derives focused test cartridge paths from central architecture assignments.
  • Provides focused SFCC runtime modules, globals, SFRA controller execution, hooks, and job steps.
  • Supports specifier mocks and exact resolved-file mocks without proxyquire.
  • Loads CommonJS cartridge modules through the normal Vite and Vitest pipeline.
  • Exposes runtime-only APIs and types through @commerce-klaus/vitest-sfcc/runtime.

Installation ​

bash
pnpm add -D @commerce-klaus/vitest-sfcc vitest
bash
npm install -D @commerce-klaus/vitest-sfcc vitest
bash
yarn add -D @commerce-klaus/vitest-sfcc vitest
bash
vp install -D @commerce-klaus/vitest-sfcc vitest

Runtime APIs are included

Do not install @commerce-klaus/sfcc-test-runtime separately when using the Vitest integration. It is an internal dependency and its public APIs are re-exported through @commerce-klaus/vitest-sfcc/runtime.

Configuration ​

ts
import sfccVitest from "@commerce-klaus/vitest-sfcc"
import { defineConfig } from "vite-plus"

export default defineConfig({
  plugins: [
    sfccVitest({
      basePath: "./cartridges",
      cartridgePath: ["app_custom", "app_storefront_base"],
      runtime: {
        site: { id: "RefArch" },
      },
    }),
  ],
})

The cartridge order can also be read from a site template:

vitest.config.ts
ts
sfccVitest({
  basePath: "./cartridges",
  siteTemplatePath: "./sites/site_template",
  site: "RefArch",
})

This reads sites/<site>/site.xml below siteTemplatePath and uses its custom-cartridges value.

Options ​

OptionTypeRequiredDescription
basePathstringnoCartridge directory override. Defaults to central config, then cartridges.
cartridgePathstring[]noExplicit cartridge order. First match wins.
architecturesCartridgeArchitectureAssignmentsnoArchitecture assignments overriding the central configuration.
cartridgeSelectionCartridgeArchitectureSelectionnoFilter the effective path by architecture roles and shared cartridges.
cwdstringnoWorking directory used to resolve relative paths. Defaults to process.cwd().
siteTemplatePathstringnoSite-template root containing sites/<site>/site.xml.
sitestringnoSite identifier used to read custom-cartridges from site.xml.
solutionConfigPathstringnoPath to cartridges/jsconfig.json for reference-based cartridge order.
envCartridgePathstringnoColon-separated cartridge order, matching SFCC_CARTRIDGE_PATH.
configFilestring | falsenoExplicit central config path, or false to disable discovery.
hookDiscoveryfalse | { cartridges: string[] }noDisable automatic hook discovery or limit it to named cartridges.
runtimeSfccTestRuntimeOptionsnoInitial runtime options, including the current site id and custom preferences.

Commerce Klaus uses the following precedence when determining cartridge order:

  1. An explicit cartridgePath option.
  2. An envCartridgePath option, the Salesforce SFCC_CARTRIDGES environment variable, or the legacy SFCC_CARTRIDGE_PATH environment variable.
  3. References in the configured solution jsconfig.json.
  4. The site's custom-cartridges value in site.xml.
  5. A deterministic alphabetical filesystem fallback.

The first source that produces entries wins. SFCC_CARTRIDGES takes precedence over SFCC_CARTRIDGE_PATH, and both comma- and colon-separated values are accepted. Missing cartridge directories are filtered out, and valid results are represented internally as absolute cartridge-root paths.

For reproducible CI and local behavior, prefer an authoritative project source such as an explicit path, solution references, or site metadata. The alphabetical fallback is convenient for initial setup, but it cannot express intentional override precedence.

The same inference rules are shared by the other Commerce Klaus packages through @commerce-klaus/sfcc-module-resolver.

Focused cartridge paths ​

Architecture assignments in commerce-klaus.config.ts can define the project once. The Cartridge architectures guide defines the shared labels and explains their policy boundaries:

commerce-klaus.config.ts
ts
import { defineConfig } from "@commerce-klaus/config"

export default defineConfig({
  cartridgePath: ["app_headless", "bc_custom_api", "app_storefront_base", "modules"],
  architectures: {
    "storefront-next": ["app_headless", "bc_custom_api"],
    sfra: ["app_storefront_base"],
  },
})

A Vitest project can then select only the relevant role and explicitly retain shared infrastructure:

vitest.config.ts
ts
sfccVitest({
  cartridgeSelection: {
    architectures: ["storefront-next"],
    cartridges: ["modules"],
  },
})

Filtering happens after normal cartridge-path inference and never reorders the result. By default, cartridges without an architecture assignment are excluded. Set includeUnassigned: true when unassigned integration cartridges should remain available.

The selected path controls wildcard and alias resolution, module.superModule, hook discovery, and job step definitions together. An empty selection fails during configuration instead of silently running tests against the wrong module graph. Use separate Vitest projects when suites require different architecture selections.

Runtime API ​

Application tests can import the common helpers from the package root. Tooling integrations and tests that only need runtime APIs or types can use the lightweight subpath:

ts
import {
  getSfccRuntime,
  resetSfccRuntime,
  type SfccModule,
} from "@commerce-klaus/vitest-sfcc/runtime"

Both entry points address the same active runtime. resetSfccRuntime() creates and activates a fresh instance; getSfccRuntime() returns the active instance.

Custom APIs ​

The runtime provides dw/system/RESTResponseMgr and a Custom API harness, so tests do not need to recreate request globals and response spies for every operation:

ts
import { resetSfccRuntime } from "@commerce-klaus/vitest-sfcc"
import { beforeEach, expect, it, vi } from "vitest"

beforeEach(() => {
  vi.resetModules()
})

it("returns one product", async () => {
  const runtime = resetSfccRuntime()
  const imported = await import("../cartridge/rest-apis/products/getProduct.js")
  const api = imported.default ?? imported

  const result = await runtime.customApi(api).invoke("getProduct", {
    customer: {
      authenticated: true,
      registered: true,
      profile: { customerNo: "00001" },
    },
    headers: { authorization: "Bearer test-token" },
    pathParameters: { productId: "sample-product" },
    queryParameters: { locale: "de-DE" },
  })

  expect(result.response).toMatchObject({
    kind: "success",
    statusCode: 200,
    body: { id: "sample-product" },
  })
})

invoke() installs request, response, and customer for the operation. The request supports SCAPI path parameters, query parameters through both httpParameterMap.get() and named properties, request bodies, and headers. Objects passed through request, response, or globals extend the defaults for project-specific behavior.

Rendered success results expose kind, statusCode, body, headers, and expiration. Rendered RFC 9457 errors additionally expose type, title, detail, and custom attributes. createSuccess(), createEmptySuccess(), and the supported createError() overloads validate their HTTP status ranges. Missing operations, operations that render no response, concurrent invocations, and multiple rendered responses fail explicitly.

The harness executes an already imported operation module. Cartridge discovery and CommonJS loading remain the responsibility of vitest-sfcc; the framework-independent runtime does not inspect rest-apis directories or enforce OAS contracts. Metadata-loaded operations can opt into focused contract validation as described below.

Load from project metadata ​

Use loadSfccCustomApi() when a test should follow the effective cartridge path and the checked-in api.json registration instead of repeating an implementation path:

ts
import { loadSfccCustomApi } from "@commerce-klaus/vitest-sfcc"
import { expect, it } from "vitest"

it("returns one product", async () => {
  const api = await loadSfccCustomApi("products", "getProduct")
  const result = await api.invoke({
    pathParameters: { productId: "sample-product" },
    queryParameters: { locale: "de-DE" },
  })

  expect(api.definition.operation).toMatchObject({
    method: "get",
    path: "/products/{productId}",
  })
  expect(result.response.statusCode).toBe(200)
})

The plugin resolves Custom API definitions during configuration using the selected cartridge path. For duplicate API and operation IDs, the first cartridge wins. Loading remains lazy: only the selected implementation module is evaluated. Missing APIs, operations, OAS operation declarations, and implementation scripts produce explicit diagnostics with the available IDs where applicable.

Run sfcc-ts-sync-types from @commerce-klaus/typescript-sfcc to generate the project-specific SfccCustomApis.Operations registry. When it is present, loadSfccCustomApi() completes and validates operation IDs, and invoke() uses the generated request body, path, query, and header parameter types. Projects without generated declarations retain the general string and parameter-record API.

Use runtime.customApi(importedModule) when the implementation module itself is the subject of the test or must be supplied directly. Use loadSfccCustomApi(apiId, operationId) when the metadata registration, cartridge precedence, OAS operation, and implementation should be tested together.

Validate the OAS contract ​

Contract validation is disabled by default so existing tests and direct runtime.customApi() calls remain permissive. Enable it for a metadata-loaded operation when the test should also verify its checked-in OAS contract:

ts
const api = await loadSfccCustomApi("products", "getProduct", {
  validate: true,
})

await api.invoke({
  pathParameters: { productId: "sample-product" },
  queryParameters: { locale: "de-DE" },
})

With validate: true, invoke() validates the request before calling the cartridge module and validates the rendered response afterward. A failure lists every supported contract mismatch with a path such as request.path.productId or response.body.id. Use { validate: { request: true } } or { validate: { response: true } } to enable only one phase.

The validator covers:

  • required path, query, and case-insensitive header parameters and their schemas
  • required JSON request bodies and their schemas
  • exact, 2XX-style, and default response declarations
  • JSON response schemas and responses that declare no body
  • local $ref, objects, arrays, strings, finite numbers, integers, booleans, required, enum, nullable, properties, items, and additionalProperties

Validation is intentionally partial

Authentication and authorization declarations, non-JSON media types, response headers, formats, composition keywords, and further numeric or string constraints are not evaluated. Unknown local references fail explicitly; unsupported keywords are left to full OAS validation tools rather than approximated.

Dependency mocking ​

Vite root-relative cartridge URLs are resolved against the configured Vite root when loading transformed CommonJS modules. Existing absolute filesystem paths remain unchanged; Super Module lookup uses the resolved cartridge file.

The optional SFCC test lint preset checks concurrent runtime use, static SFCC imports alongside runtime setup, runtime mutations in hoisted mock factories, non-absolute exact-file mock paths, and direct access to SFCC globals through globalThis or global. It runs in ESLint and Oxlint without applying cartridge runtime restrictions to test files.

Global checks also cover vi.stubGlobal() and static reflection operations for known SFCC globals. The separate opt-in require-runtime-cleanup rule checks for a runtime reset in afterEach() in the same suite or an ancestor. Reset the existing instance with getSfccRuntime().reset(); replacing it with resetSfccRuntime() does not restore the previous instance's globals. Projects with verified central cleanup can declare { externalCleanup: true }.

The preset also warns about SFCC imports directly inside vi.hoisted(). For projects replacing the active runtime in beforeEach(), the opt-in no-stale-runtime-reference rule reports test and cleanup callbacks using an instance captured earlier at file/suite scope. Fetch it inside the callback instead, or assign the newly created instance to a shared binding in beforeEach().

Test files should load SFCC modules with Vite imports, not native Node.js require(). Use await import() after setting up the runtime. Actual ~/ imports require the test itself to belong to a configured cartridge; a ~/ mock key instead matches the dependency identifier used by cartridge code and is valid from an external test. */ is an SFCC module prefix, not a glob matching every form of mock identifier. The test preset checks native cartridge requires and contextless tilde imports. An additional opt-in extension rule checks file imports and runtime mock IDs, leaving platform modules such as dw/system/Site and server extensionless.

The preset's no-proxyquire rule reports Proxyquire imports and require calls as migration warnings, including chained .noCallThru() loading patterns. Replace dependency maps deliberately; runtime mocks do not automatically reproduce Proxyquire's call-through, global-stub, or cache options.

Register replacements before dynamically importing the module under test:

ts
import { getSfccRuntime } from "@commerce-klaus/vitest-sfcc"
import { beforeEach, expect, it, vi } from "vitest"

beforeEach(() => {
  vi.resetModules()
  getSfccRuntime().reset()
})

it("uses the test payment provider", async () => {
  getSfccRuntime().mock("*/cartridge/scripts/payment/provider", {
    authorize: () => ({ authorized: true }),
  })

  const payment = await import("../cartridge/scripts/payment.js")
  expect(payment.default.authorizePayment().authorized).toBe(true)
})

Mocks can target dw/*, */, ~/, and cartridge-alias module identifiers. Without a cartridge mock, the real file selected by cartridge resolution is loaded.

Static relative imports are mockable as well, covering a common proxyquire pattern:

ts
getSfccRuntime().mock("./helpers/storeOpeningHours", storeOpeningHoursMock)
const storesHook = await import("../cartridge/scripts/hooks/stores.js")

A relative identifier can occur below multiple directories. Use an absolute resolved mock to replace exactly one file:

ts
getSfccRuntime().mockResolved(
  "/workspace/cartridges/app_custom/cartridge/scripts/helpers/storeOpeningHours.js",
  storeOpeningHoursMock,
)

Resolved mocks take precedence over module-identifier mocks. runtime.reset() clears both kinds.

SFCC globals ​

Cartridge code can access controlled SFCC globals without imports:

ts
getSfccRuntime().setGlobals({
  customer: { authenticated: true },
  request: { locale: "de_DE", querystring: {} },
  session: { custom: {} },
})

The runtime exposes focused SfccCustomerGlobal, SfccRequestGlobal, SfccResponseGlobal, and SfccSessionGlobal contracts. Common properties are discoverable while project-specific fields remain supported.

Use the inspection API instead of reading or deleting globalThis manually:

ts
const runtime = getSfccRuntime()

expect(runtime.getGlobal("request")?.locale).toBe("de_DE")
expect(runtime.hasGlobal("session")).toBe(true)
expect(runtime.getGlobals()).toMatchObject({
  customer: { authenticated: true },
})

runtime.unsetGlobals("request")

Project-specific values can provide their expected type at the call site:

ts
const organization = runtime.getGlobal<{ ID: string }>("organization")

Defaults shared by every test in a Vitest project belong in the plugin's runtime options:

vitest.config.ts
ts
sfccVitest({
  runtime: {
    globals: {
      organization: { ID: "org" },
      session: { custom: { channel: "storefront" } },
    },
  },
})

reset() restores these configured defaults after removing per-test changes. getGlobals() only includes values managed by the active SFCC runtime; unrelated Node.js and Vitest globals are intentionally omitted.

The SFCC empty() global is installed automatically and covers nullish values, empty strings and arrays, and SFCC collections through isEmpty(). It can still be replaced through setGlobals() for a specific test.

runtime.reset() removes added globals, restores values that existed before the test, and reinstalls the default empty() and configured project globals. Use it in afterEach as well as test setup to prevent the final test in a worker from leaking context.

SFRA controllers ​

The plugin resolves require("server") to the runtime's SFRA test server. Import a controller after resetting modules, then execute one of its exported routes:

ts
const controller = await import("../cartridge/controllers/Checkout.js")
const response = await getSfccRuntime()
  .controller(controller.default)
  .run("Begin", {
    querystring: { stage: "shipping" },
  })

expect(response.view).toBe("checkout/checkout")
expect(response.viewData).toMatchObject({ currentStage: "shipping" })

The harness supports server.get(), server.post(), ordered middleware with next(), and response state from render(), json(), redirect(), setViewData(), and getViewData(). Calls to setStatusCode(), setContentType(), and print() are exposed through statusCode, contentType, and the ordered printed array.

Redirect metadata from setRedirectStatus(), setHttpHeader(), cacheExpiration(), and log() is available through the corresponding response state. A pending redirect stops subsequent middleware when the route calls next(), matching the SFRA server lifecycle.

Middleware can intentionally stop the chain by omitting next(). Passing an Error to next(error) rejects the Promise returned by run(), and calling next() repeatedly from one middleware produces an explicit diagnostic.

Controller inheritance works through the same module.superModule resolution used by scripts:

js
const server = require("server")

server.extend(module.superModule)
server.prepend("Show", authorizeCustomer)
server.append("Show", addViewData)
server.replace("Submit", submitReplacement)

module.exports = server.exports()

The harness clones inherited routes and preserves the resulting middleware order for assertions and execution.

module.superModule loads the next matching implementation in cartridge-path order. Transitive supermodule chains pass through the same CommonJS transformation.

Job steps ​

CommonJS job modules use the same cartridge transformation and runtime mocks as controllers and hooks:

The built-in dw/system/Status and dw/system/StatusItem support message parameters, details, mutable items, property/getter pairs, and multi-item error aggregation.

The built-in dw/util/ArrayList supports common construction, mutation, sorting, cloning, and iteration patterns used by cartridge scripts.

The built-in dw/util/HashMap supports arbitrary key types, common map operations, bulk copying, cloning, and live collection views.

The built-in dw/util/StringUtils supports numbered message formatting and UTF-8 Base64 encoding and decoding. Locale-sensitive formatting remains mockable application behavior.

The built-in dw/util/Calendar supports deterministic UTC field operations, date arithmetic, and comparisons. Parsing, locale patterns, and time zones remain mockable application behavior.

ts
const jobModule = await import("../cartridge/scripts/jobs/GenerateFeed.js")
const jobStep = getSfccRuntime().jobStep(jobModule, {
  context: { feedNumber: 1 },
})

const result = await jobStep.run("Run", {
  TargetFolder: "IMPEX/src/feeds",
})

expect(result).toBe("OK")
expect(jobStep.jobExecution.context.feedNumber).toBe(2)

This models task-oriented script-module-step functions with the SFCC (parameters, stepExecution) signature. stepExecution.getJobExecution() returns a stable execution object whose context is shared across calls.

Chunk modules are orchestrated with the function names from steptypes.json:

ts
const result = await getSfccRuntime()
  .jobStep(chunkModule)
  .runChunk({
    chunkSize: 1000,
    functions: {
      afterChunk: "afterChunk",
      afterStep: "afterStep",
      beforeStep: "beforeStep",
      getTotalCount: "getTotalCount",
      process: "process",
      read: "read",
      write: "write",
    },
    parameters: { TargetFolder: "IMPEX/src/feeds" },
  })

The lifecycle supports asynchronous callbacks, skipped process results, persistent job context, and configurable function names. The write callback receives an iterable SFCC-like list with size(), get(), toArray(), and isEmpty(). afterStep receives the success state even when another lifecycle function throws.

Load by type ID ​

Use loadSfccJobStep() to execute the effective steptypes.json definition without duplicating its module path, function names, or chunk size in the test:

ts
import { loadSfccJobStep } from "@commerce-klaus/vitest-sfcc"

const jobStep = await loadSfccJobStep("custom.ExportProducts", {
  context: { exportedFiles: [] },
})

const result = await jobStep.run({
  TargetFolder: "IMPEX/src/feeds",
})

expect(jobStep.definition.kind).toBe("chunk-script-module-step")
expect(jobStep.stepExecution.getStepTypeID()).toBe("custom.ExportProducts")
expect(jobStep.jobExecution.context.exportedFiles).toHaveLength(1)

Run sfcc-ts-sync-types from @commerce-klaus/typescript-sfcc to generate the project-specific SfccJobSteps.Definitions registry. When it is present, loadSfccJobStep() completes and validates registered type IDs, and run() uses the parameters, required fields, defaults, and enum values declared by that step. Projects without the generated registry retain the general string and parameter-record API.

The plugin discovers definitions at configuration time using cartridge-path priority and provides them to every Vitest worker. Loading remains lazy, so unrelated job modules and their platform dependencies are not evaluated. Task definitions invoke their configured function; chunk definitions automatically apply their configured lifecycle names and chunk-size. Unknown IDs and registrations whose module cannot be resolved produce the same explicit missing-step diagnostic. The harness exposes the loaded type ID through stepExecution.stepTypeID and getStepTypeID(). Its shared SfccJobContext supports both normal property access and common dw.util.Map operations, including live key, value, and entry collection views with SFCC-style iterators. Iterators can consume their remaining elements into an independent SfccList with parameterless asList(). jobStep.definition also exposes optional description, site-context, organization-context, parallel-execution, and transactional metadata without inventing a job context or enforcing capabilities that depend on one.

Before invoking the module, run() applies declared defaults and @trim, then normalizes Boolean, numeric, string, date/time, and @target-type values. It also validates declared enum-values. This covers SFCC metadata that stores Boolean, numeric, and date defaults as strings. Missing required parameters and invalid converted values reject with a diagnostic containing the type ID and parameter name. Parameters not declared in steptypes.json pass through unchanged for test-specific inputs.

If the definition declares status-codes, a task's direct result or a chunk step's afterStep result is validated when it exposes a string code property or getCode() value, matching dw.system.Status. An undeclared code rejects with the type ID and allowed codes. Definitions without declared status codes remain permissive, as do ordinary data results and undefined.

Task definitions with timeout-in-seconds reject run() when their configured duration elapses. The diagnostic includes the type ID and normalized duration. The harness clears its timeout after either outcome; as with any Promise race, JavaScript already executing in the task cannot be forcibly canceled and may continue its own asynchronous work.

Hook execution ​

dw/system/HookMgr automatically discovers hooks.json through each cartridge's package.json. If multiple cartridges register the same extension point, the first resolvable registration in cartridge-path order is used.

js
const HookMgr = require("dw/system/HookMgr")

if (HookMgr.hasHook("app.payment.authorize")) {
  return HookMgr.callHook("app.payment.authorize", "authorize", paymentId)
}

Hook scripts run through the same cartridge transformer and can use dw/*, */, ~/, aliases, and module.superModule. getSfccRuntime().hookCalls exposes calls for test assertions. Discovered scripts are loaded only when the built-in HookMgr is used. An explicit runtime.mock("dw/system/HookMgr", ...) takes precedence without evaluating unrelated discovered hook implementations.

Use loadSfccHook() when the hook registration itself is part of the test and the implementation path should come from project metadata:

ts
import { loadSfccHook } from "@commerce-klaus/vitest-sfcc"
import { expect, it } from "vitest"

it("authorizes the payment through the effective hook", async () => {
  const hook = await loadSfccHook("app.payment.authorize")
  const result = hook.call("authorize", "payment-1")

  expect(result).toBe("authorized")
  expect(hook.definition.cartridgeRoot).toContain("int_payment")
  expect(hook.definition.hooksJsonPath).toContain("hooks.json")
})

The plugin resolves hooks during configuration, provides the effective registrations to every Vitest worker, and loads only the requested implementation. The returned definition includes the owning cartridge, hooks.json, registered script, and resolved implementation path. Unknown hooks list the available extension points, and calling a function not exported by the selected script fails explicitly.

Generated Salesforce hook declarations include an SfccHooks.Definitions registry. After sfcc-ts-sync-types, known extension points narrow the method name, arguments, and result of call(). Project-specific hooks remain open by default and can augment that registry in a project declaration when a typed contract is useful.

Use dw/system/HookMgr when testing cartridge code that calls the platform API itself. Use loadSfccHook(hookName) when a test should select and invoke the implementation registered by project metadata. Both paths preserve first-cartridge-wins behavior and record invocations in getSfccRuntime().hookCalls.

Automatic discovery can be disabled when a focused test does not need the project's registered hooks:

vitest.config.ts
ts
sfccVitest({
  basePath: "./cartridges",
  cartridgePath: ["int_payment", "app_storefront_base"],
  hookDiscovery: false,
})

Or restrict discovery to selected cartridges while preserving their order in the configured cartridge path:

vitest.config.ts
ts
sfccVitest({
  basePath: "./cartridges",
  cartridgePath: ["int_payment", "app_storefront_base"],
  hookDiscovery: { cartridges: ["int_payment"] },
})

This setting controls both automatic HookMgr registration and metadata-aware loading through loadSfccHook(). The runtime's HookMgr and hooks registered directly with getSfccRuntime().registerHook() remain available. Plugin options are evaluated per Vitest project; use a dedicated project configuration when only a subset of test files needs a different discovery scope.

CommonJS scope ​

The transformer supports static bindings such as:

js
const Transaction = require("dw/system/Transaction")
const provider = require("*/cartridge/scripts/provider")

module.exports = { authorizePayment: authorizePayment }

It also supports object destructuring and direct named exports used by hooks, job steps, and legacy controllers:

js
const { enrichStoreOpeningHours } = require("./helpers/storeOpeningHours")

exports.modifyGETResponse = function (document) {
  enrichStoreOpeningHours(document)
}

module.exports.status = "active"

Static literal requires may appear anywhere in an expression, including inside functions:

js
function getLogger() {
  return require("dw/system/Logger").getLogger("checkout")
}

Local const aliases initialized with a string literal are also resolved:

js
const HOOK_MANAGER = "*/cartridge/scripts/hooks/libHookExtMgr"

function getHookManager() {
  return require(HOOK_MANAGER)
}

Mutable or computed module IDs and mutations of an existing export property fail with an explicit diagnostic. Supporting additional CommonJS patterns is planned as the transformer matures.

Relationship to @commerce-klaus/sfcc-test-runtime ​

@commerce-klaus/vitest-sfcc owns cartridge discovery, Vite transformation, virtual modules, hook and job metadata discovery, and the active test lifecycle. @commerce-klaus/sfcc-test-runtime is the framework-independent core that provides module mocks, globals, platform modules, and execution harnesses.

Most Vitest projects should install only @commerce-klaus/vitest-sfcc. Install the runtime package directly only when building another test-runner integration or using the runtime without Vite and Vitest.

Development ​

This repository uses Vite+ (vp):

bash
vp install
vp check
vp test
vp run build

License ​

MIT

Released under the MIT License.