Skip to content

NPM version Downloads

@commerce-klaus/sfcc-module-resolver ​

Shared SFCC cartridge path and module resolution utilities.

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.

  • */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.

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.

At the API level, this package also centralizes cartridge-order detection, hooks.json registration lookups, and steptypes.json job-step discovery.

The project-wide configuration contract is owned by @commerce-klaus/config. This package consumes the resolved settings but does not expose the configuration loader.

Why this package? ​

Before centralization, resolution logic was spread across multiple packages, which made edge-case drift likely. With this package, all consumers use the same rules and fallback stack.

Typical consumers in this monorepo:

  • @commerce-klaus/typescript-sfcc
  • @commerce-klaus/eslint-config-sfcc
  • @commerce-klaus/babel-plugin-sfcc-modules
  • @commerce-klaus/vite-plugin-sfcc-modules
  • @commerce-klaus/b2c-plugin

Installation ​

Inside this workspace:

package.json
json
{
  "dependencies": {
    "@commerce-klaus/sfcc-module-resolver": "workspace:*"
  }
}

Quick start ​

resolver.ts
ts
import path from "node:path"
import {
  createModuleResolver,
  inferCartridgeOrder,
} from "@commerce-klaus/sfcc-module-resolver/resolution"

const cwd = process.cwd()
const cartridgeRoots = inferCartridgeOrder({
  cartridgesDir: "cartridges",
  cwd,
})

const resolveSfccModule = createModuleResolver(cartridgeRoots)

const importer = path.resolve("cartridges/app_custom/cartridge/controllers/Home.js")
const resolved = resolveSfccModule("*/cartridge/scripts/util", importer)

Cartridge order (priority) ​

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.

API overview ​

The public API is grouped by SFCC domain:

Entry pointResponsibility
@commerce-klaus/sfcc-module-resolverResolution API, equivalent to /resolution
@commerce-klaus/sfcc-module-resolver/resolutionCartridge order, module resolution, and superModule
@commerce-klaus/sfcc-module-resolver/hooksHook registration discovery and contracts
@commerce-klaus/sfcc-module-resolver/job-stepsJob step definitions and script contracts
@commerce-klaus/sfcc-module-resolver/custom-apisCustom API and OAS discovery
@commerce-klaus/sfcc-module-resolver/projectProject validation and dependency graphs

Import from the domain entry point that owns the required behavior. The package root intentionally exposes only the resolution API.

Constants ​

  • SUPPORTED_RUNTIME_EXTENSIONS: readonly ["js", "ds", "json"]
  • SUPER_MODULE_TOKEN: "__sfcc_superModule__"
  • DEFAULT_SITE_TEMPLATE_PATH: "sites/site_template"

Cartridge order and paths ​

  • ModuleResolutionOptions
    • Shared cartridge-resolution configuration used by the Vite and Vitest adapters.
  • ResolveCartridgeRootsOptions
    • Extends ModuleResolutionOptions with the resolver-only containingFile option.
  • resolveCartridgesDir(cartridgesDir, cwd): string
  • resolveCartridgeRoots(options): string[]
  • findCartridgesDir(startDirectory): string | undefined
  • readSolutionReferences(solutionConfigPath): string[]
  • resolveSiteTemplatePath(siteTemplatePath, cwd, fallbackPath?): string | undefined
  • getSiteTemplateCartridgePath(siteTemplatePath, site, cwd): string[]
  • inferCartridgeOrder(options): string[]

Module resolution ​

  • createModuleResolver(cartridgeRoots)
    • Returns resolveSfccModule(moduleName, containingFile): string | undefined
    • Supports server, server/*, ~/, */, and cartridge aliases (app_x/cartridge/...)
  • explainModuleResolution(moduleName, containingFile, cartridgeRoots): ModuleResolutionTrace
    • Uses the same lookup path as createModuleResolver
    • Reports the resolution kind, containing cartridge, each attempted file path, and the selected match
    • Accepts module.superModule to trace lower-precedence fallback lookup
  • resolveCandidateFile(basePath, moduleName): string | undefined
  • findContainingCartridgeRoot(filePath, cartridgeRoots): string | undefined

SuperModule ​

  • resolveSuperModuleFilePath(filePath, cartridgeRoots): string | undefined
  • resolveSuperModuleSpecifier(filePath, cartridgeRoots): string | undefined
    • Returns a cartridge specifier, for example app_storefront_base/cartridge/controllers/Page
  • transformSuperModuleSource(sourceCode, filePath, cartridgeRoots): string
  • injectTopLevelStatement(sourceCode, statement): string

Hook registrations ​

  • getHookRegistrationsFromDocument(document): HookRegistration[] | undefined
    • Validates a parsed hooks.json document and returns its { name, script } entries
  • resolveHookRegistrations(cartridgeRoots): ResolvedHookRegistration[]
    • Discovers resolvable hook scripts in cartridge-path order and keeps the first registration for each extension point
  • resolveHookScriptPath(hooksDirectory, script): string | undefined
    • Resolves a registration's script field to an existing file, trying .js, .cjs, .mjs, and .ds
  • getHookRegistrationsForScriptFile(filePath): HookRegistration[]
    • Returns all Salesforce and project-specific hook registrations that resolve to a script file
  • getRequiredHookExportName(hookName): string | undefined
    • Infers the required export name for Salesforce dw.* hooks only (the last segment of the extension point)
  • getRequiredHookExportsForScriptFile(filePath): RequiredHookExport[]
    • Given a script file, returns every { hookName, exportName } it must statically export according to its cartridge's hooks.json

Job step definitions ​

  • parseStepTypeDefinitionsFromDocument(document): StepTypeDocumentParseResult
    • Returns valid definitions alongside structured diagnostics for invalid document sections and individual entries
    • Includes a stable JSON path and message for each diagnostic so CLI, lint, and editor integrations can share the same validation behavior
  • resolveStepTypeDefinitions(cartridgeRoots): ResolvedStepTypeDefinition[]
    • Reads steptypes.json from each cartridge root
    • Resolves module paths with the standard SFCC runtime extensions and index-module fallback
    • Keeps the first resolvable definition for each type ID in cartridge-path order

Capability fields remain optional so consumers can distinguish an omitted declaration from an explicit false. The resolver exposes this metadata without inferring or enforcing a job context.

Project validation ​

  • validateProject({ cartridgesDir, cartridgeRoots }): ProjectValidationResult
    • Validates cartridge package.json hook declarations and hooks.json
    • Resolves hook scripts and job step modules
    • Validates Custom API entries, OAS schemas, operation IDs, and implementation scripts
    • Reports hook and job step registrations hidden by cartridge precedence as warnings

The result contains ok, errors, warnings, and a deterministic diagnostics array. Each diagnostic provides a stable code, severity, absolute source file, and human-readable message. Validation is additive: the existing discovery APIs continue to skip malformed or unresolved entries. Invalid job step entries are reported individually while valid sibling entries continue through module resolution and precedence validation.

Project graph ​

  • createProjectGraph({ cartridgesDir, cwd?, cartridgePath?, module? }): ProjectGraph
    • Adds cartridge nodes and precedes edges in effective path order
    • Discovers JavaScript and Demandware Script files that use module.superModule
    • Adds resolved hooks, job steps, Custom APIs, implementation modules, and schemas
    • Accepts an optional */cartridge/... module filter for focused graphs

The graph contains deterministic nodes and typed edges. Node kinds are cartridge, module, hook, job-step, custom-api, and schema; edge kinds are precedes, overrides, super-module, implements, and uses-schema.

Utilities ​

  • stripExtension(filePath): string
  • toPosixPath(filePath): string

Examples ​

1) Resolve */ and ~/ ​

ts
const resolveSfccModule = createModuleResolver(cartridgeRoots)

resolveSfccModule("*/cartridge/scripts/foo", importer)
resolveSfccModule("~/cartridge/scripts/local", importer)

2) Rewrite module.superModule in CommonJS ​

ts
const nextSource = transformSuperModuleSource(source, filePath, cartridgeRoots)

When a fallback exists, module.superModule is replaced by SUPER_MODULE_TOKEN and a matching require(...) line is injected at the top of the file. When no fallback exists, module.superModule is rewritten to undefined.

3) Read site.xml ​

ts
const order = getSiteTemplateCartridgePath(
  "/workspace/sites/site_template",
  "RefArch",
  process.cwd(),
)

4) Find hook registrations and required exports for a script file ​

hooks.ts
ts
import {
  getHookRegistrationsForScriptFile,
  getRequiredHookExportsForScriptFile,
} from "@commerce-klaus/sfcc-module-resolver/hooks"

const registrations = getHookRegistrationsForScriptFile(scriptPath)
// [{ name: "dw.ocapi.shop.basket.afterPOST", script: "./hooks/basket" }]

const requiredExports = getRequiredHookExportsForScriptFile(
  scriptPath,
)
// [{ hookName: "dw.ocapi.shop.basket.afterPOST", exportName: "afterPOST" }]

5) Discover job step definitions ​

ts
import { resolveStepTypeDefinitions } from "@commerce-klaus/sfcc-module-resolver/job-steps"

const definitions = resolveStepTypeDefinitions(cartridgeRoots)
const exportStep = definitions.find((definition) => definition.typeId === "custom.ExportProducts")

if (exportStep?.kind === "chunk-script-module-step") {
  console.log(exportStep.modulePath)
  console.log(exportStep.chunkSize)
  console.log(exportStep.functions.read)
}

Malformed documents and definitions whose modules cannot be resolved are skipped during filesystem discovery. Duplicate type IDs use the same first-cartridge-wins priority as module and hook resolution.

Each definition exposes normalized parameters and statusCodes arrays. Empty parameter containers and missing status declarations become empty arrays. Boolean metadata flags accept both JSON booleans and SFCC's string forms ("true" and "false"); default values remain lossless so consumers can apply runtime-specific conversion.

6) Validate project contracts ​

ts
import { validateProject } from "@commerce-klaus/sfcc-module-resolver/project"

const validation = validateProject({
  cartridgesDir: path.resolve("cartridges"),
  cartridgeRoots,
})

for (const diagnostic of validation.diagnostics) {
  console.log(diagnostic.code, diagnostic.file, diagnostic.message)
}

7) Build a project graph ​

ts
import { createProjectGraph } from "@commerce-klaus/sfcc-module-resolver/project"

const graph = createProjectGraph({
  cartridgesDir: path.resolve("cartridges"),
  cartridgePath: ["app_custom", "app_storefront_base"],
})

for (const edge of graph.edges) {
  console.log(edge.kind, edge.from, edge.to)
}

Design decisions ​

  • A central resolver core with consumer-specific adapters kept in each package.
  • Filesystem-based resolution is intentionally Node-only.
  • Return formats are deterministic: absolute file paths for resolver hooks, cartridge-based specifiers for super module references.

Development ​

bash
vp test
vp check
vp pack

If consumer tests in other packages need this resolver and exports point to dist/*, build this package first:

bash
cd packages/sfcc-module-resolver
vp pack

Released under the MIT License.