Skip to content

feat: Script API IntelliSense via TypeScript definitions#451

Merged
clavery merged 15 commits into
mainfrom
feature/typescript-defs
May 26, 2026
Merged

feat: Script API IntelliSense via TypeScript definitions#451
clavery merged 15 commits into
mainfrom
feature/typescript-defs

Conversation

@clavery
Copy link
Copy Markdown
Collaborator

@clavery clavery commented May 22, 2026

Summary

Adds Script API IntelliSense for cartridge JavaScript across both VS Code (via the B2C DX extension) and any LSP-based editor (Neovim, Helix, Zed, Sublime, plain VS Code, WebStorm/IntelliJ Ultimate).

  • New workspace package @salesforce/b2c-script-types containing modular TypeScript definitions for the B2C Commerce Script API (dw/*, version 26.7) plus a TypeScript Server plugin.
  • The B2C DX VS Code extension auto-loads the plugin via contributes.typescriptServerPlugins and pushes the resolved cartridge list in (powered by a new shared CartridgeService hoisted from the cartridge tree provider). Resolution is scoped to detected cartridges so non-cartridge JS/TS files in the same workspace are unaffected. No files are written into the user's repo.
  • New CLI commands for non-extension users:
    • b2c setup ide tsserver-plugin — prints the absolute path to the bundled TS Server plugin and types directory (also --json). LSP clients wire the printed path into init_options.plugins[].location. Full feature parity with the VS Code extension, including cartridge-relative require resolution.
    • b2c setup ide vscode-types — vendors ./.b2c-script-types/types/ and writes a dw/*-only jsconfig.json for plain tsconfig-based setups.
  • The plugin matches SFCC runtime semantics for cartridge-style requires:
    • ~/cartridge/... resolves only within the cartridge that owns the calling file.
    • */cartridge/... walks the cartridge path with owner-first override priority.
    • <cartridgeName>/cartridge/... resolves against the named cartridge.
    • Cartridge order follows the resolved cartridges from dw.json / SFCC_CARTRIDGES; the cartridge tree view mirrors it.
  • LSP-mode auto-discovery: when no host pushes a cartridge list (plain Neovim/Helix/Zed), the plugin walks the project root for .project markers and honors dw.json's cartridges field for ordering — no separate vendoring step needed.
  • IDE Integration guide gains a top-level Script API IntelliSense section covering all setup paths.

Bundle layout note

The plugin ships at dist/script-types/node_modules/@salesforce/b2c-script-types/ so TypeScript's plugin probe (<location>/node_modules/<pluginName>) can load it. b2c-script-types remains private: true and is not added as a dep of any published package — production npm distribution is unaffected.

VSIX packaging note

vsce --no-dependencies hard-codes a node_modules/** ignore that .vscodeignore negations can't override. The extension's package script runs scripts/inject-script-types.mjs after vsce, which appends the staged plugin tree to the produced zip. Verified the final VSIX contains 540 script-types files including plugin/index.js.

Test plan

  • pnpm run lint:agent, pnpm run typecheck:agent, pnpm run -r format:check, pnpm run build all pass
  • pnpm --filter @salesforce/b2c-cli run test:agent (1232 passing)
  • b2c setup ide vscode-types --project-directory /tmp/test --force produces a working jsconfig.json and .b2c-script-types/types/ tree
  • b2c setup ide tsserver-plugin --json returns valid pluginPath/typesPath; verified that ts.resolveJSModule('@salesforce/b2c-script-types', '<pluginPath>/node_modules', ts.sys) resolves and require() returns the plugin's init function
  • pnpm --filter b2c-vs-extension run package produces a VSIX containing extension/node_modules/@salesforce/b2c-script-types/plugin/index.js and the full types/ tree
  • Install the VSIX in a debug host with a cartridge workspace; confirm require('dw/catalog/shows module completions,~/cartridge/...and*/cartridge/...` resolve, and no files appear in the workspace
  • Confirm a non-cartridge .js/.ts file in the same workspace does NOT see dw/* completions (plugin scoping works)
  • Toggle b2c-dx.features.scriptTypes to false and reload; confirm IntelliSense for dw/* drops while the rest of the language service continues working
  • Wire the printed plugin path into Neovim's ts_ls init_options.plugins[] and confirm cartridge JS gets dw/* IntelliSense plus ~//*/ resolution against an SFRA-style project

Adds @salesforce/b2c-script-types workspace package containing modular
TypeScript definitions for the B2C Commerce Script API (dw/*, version
26.7) plus a TypeScript Language Service plugin.

The B2C DX VS Code extension registers the plugin via
contributes.typescriptServerPlugins; it scopes resolution to detected
cartridge directories (using the existing cartridge enumeration, hoisted
into a shared CartridgeService) so non-cartridge JavaScript and
TypeScript files in the same workspace are unaffected. No files are
written into the user's repo.

For other IDEs (plain VS Code, WebStorm, Neovim), adds
`b2c setup ide vscode-types` which vendors the same bundle into
.b2c-script-types/ and writes a jsconfig.json.

VSIX packaging: vsce --no-dependencies hard-codes a node_modules ignore
that .vscodeignore can't override, so the extension's package script now
runs an inject-script-types.mjs pass that adds the staged plugin tree
into the produced zip.
@github-actions github-actions Bot added the needs-3pl-review PR introduces net-new third-party dependencies and needs discussion label May 22, 2026
@clavery clavery added the 3pl-approved Maintainer approved net-new third-party dependency additions label May 22, 2026
@github-actions github-actions Bot removed the needs-3pl-review PR introduces net-new third-party dependencies and needs discussion label May 22, 2026
clavery added 14 commits May 21, 2026 20:30
tsserver canonicalizes containingFile paths to forward slashes on every
platform, but findCartridges() returns backslash-separated paths on
Windows. The plugin's startsWith check was therefore always false on
Windows, so dw/* IntelliSense silently did nothing.

Normalize both the cartridgeRoots and the incoming containingFile to
forward slashes, and fold case when ts.sys.useCaseSensitiveFileNames is
false (Windows + default macOS) so different drive-letter casing or
upper/lowercase paths still match.
…n Windows

path.join produces backslashes on Windows, but tsserver keys its internal
file map on forward-slash paths. Returning a backslash resolvedFileName
from the plugin can cause subtle duplicate-file or cannot-find-module
issues in cartridge files until the TS server restarts.
The TS Server plugin now resolves SFCC cartridge-style imports across the
workspace's cartridge path, not just dw/* modules:
  ~/cartridge/scripts/foo   -> any cartridge, owner-first override semantics
  */cartridge/scripts/foo   -> equivalent to ~/
  bar/cartridge/scripts/foo -> only the cartridge named "bar"

Cartridge order is sourced from the resolved configuration's `cartridges`
field (dw.json / SFCC_CARTRIDGES / .env), matching the runtime cartridge
path. When that's not set, cartridges fall back to discovery order with
known base cartridges (app_storefront_base, modules) sorted last.

The same ordering now drives the B2C-DX cartridges tree view, so the
sidebar and IntelliSense agree on priority.
Adds a CLI-reference entry for `b2c setup ide vscode-types` covering
flags, examples, and outputs, and expands the IDE integration guide's
LSP section with concrete recommendations for Neovim (ts_ls via
nvim-lspconfig), Helix, Zed, and Sublime LSP.

Also clarifies that the generated jsconfig.json belongs at the repo
root because its `paths` mappings are repo-root-relative.
…tics

Address review feedback and extend the Script API IntelliSense to LSP-based
editors (Neovim, Helix, Zed, Sublime, etc.) without the VS Code extension:

- Add `b2c setup ide tsserver-plugin` (also `--json`) which prints the absolute
  paths to the bundled TS Server plugin and types directory. Editors wire
  `pluginPath` into `init_options.plugins[].location` and TypeScript probe-loads
  `@salesforce/b2c-script-types` from `<location>/node_modules/...`.
- Bundle the plugin under the probe-shaped layout
  `dist/script-types/node_modules/@salesforce/b2c-script-types/` so TS's plugin
  loader finds it. Update vscode-types' types-path resolution to match.
- Auto-discovery: when no host pushes a cartridge list (i.e. plain LSP usage),
  the plugin walks the project root for `.project` markers and honors
  `dw.json`'s `cartridges` field for ordering. `applyConfig` no longer wipes
  the discovered list when subsequent updates omit the `cartridges` field.
  Adds an `autoDiscover` config flag (default on) for hosts that want to opt out.
- Restrict `~/cartridge/...` resolution to the cartridge that owns the calling
  file (matches SFCC runtime). `*/cartridge/...` keeps the cartridge-path walk
  with owner-first override priority.
- Drop the multi-wildcard cartridge entries from the generated jsconfig.json
  and the `jsconfig.template.json` — TypeScript only allows one `*` per pattern,
  so `~/cartridge/*` and `*/cartridge/scripts/*` produced TS5061/TS5062 and
  never functioned. The vendored `jsconfig.json` now configures `dw/*` only;
  cartridge-relative IntelliSense for plain IDEs requires the LSP plugin path.
- Update IDE Integration guide with the LSP plugin setup, document the new CLI
  command in cli/setup.md, refresh the changeset summary.
…rdering

Both the VS Code cartridge tree and the LSP plugin's auto-discovery used a
Set of "known base cartridges" with a single rank, which let modules sort
before app_storefront_base depending on filesystem discovery order. SFRA's
runtime cartridge path ends in `...:app_storefront_base:modules` so modules
should be strictly last.

Replace the set with a priority map (app_storefront_base=1, modules=2);
non-base cartridges keep their discovery-order rank (0).
Set CartridgeItem.command to 'revealInExplorer'. Activating a cartridge in
the B2C-DX cartridges tree (single or double click depending on the user's
workbench.list.openMode) now expands the file tree to the cartridge
directory in the sidebar Explorer view.
…rtridge

The SFRA `modules` cartridge exposes its entire tree to script-side requires
without the `cartridge/scripts/` prefix used by other cartridges, so calls
like `require('server')` -> `<modules>/server[.js|/index.js]` and
`require('server/middleware')` -> `<modules>/server/middleware.js`. The
plugin previously didn't recognize these and fell through to the original
resolver, leaving them unresolved.

Add a fallback resolver that fires after `dw/*` and cartridge-style
resolution when the require is a bare name (no relative or absolute path,
no cartridge prefix) and a cartridge literally named `modules` is in the
list. Tries the standard candidate extensions plus a package.json `main`
fallback for directories without an index.js.

The SFRA modules cartridge does not ship its own .d.ts files; properly typed
declarations would replace this discovery once available.
…handler

Tree-view activation gestures are user-global (workbench.list.openMode), so
making cartridge clicks reveal-only-on-double-click isn't possible per item.
Switch to a context menu item instead: right-click a cartridge in the
B2C-DX → Cartridges view → Reveal in Explorer.

Single-clicking a cartridge now just selects it; double-clicking does
nothing (the previous behavior).
…out jsconfig

types/global.d.ts declares SFCC's ambient identifiers (session, request,
response, customer, empty(...), and the `dw` namespace alias) inside a
`declare global { ... }` block. That only takes effect when the file is part
of the TS program. The vendored `jsconfig.json` includes it explicitly, but
the VS Code extension (and any other plugin host without a jsconfig) didn't
load it — so cartridge JS opened in those modes had `dw/*` IntelliSense via
the plugin's module resolver but no globals.

Wrap `host.getScriptFileNames` to append the bundled global.d.ts when the
project contains at least one cartridge file, deduping by normalized path
so projects that already include it via jsconfig are unaffected.
Add ambient declarations for the SFRA `modules` cartridge (server, route,
request, response, middleware, render, querystring, forms) so cartridge code
that does `require('server').middleware.https` etc. type-checks under
`checkJs: true`. TypeScript can't infer the dynamic property assignments in
modules/server.js (`var x = require(...); x.foo = ...; module.exports = x`),
so without ambient declarations the augmented Server type is invisible.

The plugin now injects types/sfra/server.d.ts alongside the existing
global.d.ts injection when a cartridge literally named `modules` is in the
project. resolveModulesCartridge skips bare names covered by the ambient
(`server`, `server/middleware`, etc.) so TS uses the typed declarations
instead of the inferred .js types. Other bare names (e.g.
`server/EventEmitter`) still fall through to filesystem resolution.

Targets vanilla SFRA — projects that have customized the `modules` cartridge
may see drift between the declarations and the actual implementation.
…ules cartridge source

The bundled types/sfra/server.d.ts gives cartridge JS the typed shape it needs
under checkJs:true, but go-to-definition on `require('server')`,
`server.middleware`, etc. landed inside the .d.ts instead of the actual SFRA
implementation in dependencies/modules/server*.js.

Wrap the language service so getDefinitionAtPosition,
getDefinitionAndBoundSpan, getTypeDefinitionAtPosition, and
getImplementationAtPosition rewrite results that fall inside server.d.ts.
Each `declare module 'X' { ... }` block in the d.ts is mapped to a byte
range; a result whose textSpan starts inside one of those ranges is
redirected to <modulesCart>/<X>.js (or .../X/index.js).

Member-level go-to-def (e.g. clicking on a method within a Server interface
member declaration) lands at the start of the matching JS file rather than
the specific function declaration — better than the d.ts; refining this
further would require scanning the JS for the matching name.
@clavery clavery marked this pull request as ready for review May 26, 2026 13:46
@clavery clavery requested a review from wei-liu-sf as a code owner May 26, 2026 13:46
@clavery clavery merged commit 665b2a1 into main May 26, 2026
7 checks passed
@clavery clavery deleted the feature/typescript-defs branch May 26, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3pl-approved Maintainer approved net-new third-party dependency additions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant