Skip to content

Storefront Monorepo

The store monorepo is where the customizations for one account’s storefront live.

FastStore does not impose any opinions on tooling around monorepos, as long as the faststore.json file is in the root of the monorepo.

faststore.json

The faststore.json file serves as a guide both for local development, via the @vtex/fsp-cli and for production deployments, via FastStore WebOps.

The root of the faststore.json object contains only a stores key. Inside the stores object, each key should be an account name.

Each account object has at least one of the following keys: checkout, discovery, and sales-app, each mapped to an object. This object, called the module configuration has the following keys:

KeySemanticUsageRequired
paththe path where the given module customization islocal development, production buildtrue
portthe port where the module will be availablelocal developmenttrue
clian argument to override the module CLI being usedlocal development, production buildfalse

Where module CLIs are declared

Each module declares the CLI that builds it, in its own package.json. The root manifest holds only @vtex/fsp-cli, which orchestrates them:

package.json # @vtex/fsp-cli
packages/discovery/package.json # @faststore/cli
packages/checkout/package.json # @vtex/checkout
packages/sales-app/package.json # @vtex/sales-app

This is not only tidiness. Module CLIs bring their own toolchains, and those toolchains disagree with each other: they depend on different majors of @oclif/core, of next, of css-loader. Listing every CLI in the root manifest puts all of them into one dependency tree, where the package manager picks a single version per package name for every module to share. A module can then end up running a dependency it never asked for.

fsp resolves each CLI starting from its module’s directory, so a module gets the version it declared. A CLI missing from the module that uses it fails with an error naming both.

Keeping module dependencies apart

Declaring the CLIs per module is only half of it: most package managers still hoist shared dependencies to the repository root by default, which puts them back into one pile. fsp init writes whichever setting keeps them separated for the package manager your project uses.

It works out which one that is from packageManager, then a volta pin, then whichever lockfile is present — and if the project pins its toolchain with Volta, installs run through volta run so they use the pinned manager rather than whatever the shell has. A store on yarn@1 beside an ambient Yarn 4 would otherwise install with the wrong one.

Package managerWhat init writes
pnpmnothing — it isolates workspaces by default
Yarn 2+nmHoistingLimits: workspaces in .yarnrc.yml
Yarn Classicworkspaces.nohoist listing the module CLIs
npminstall-strategy=nested in .npmrc

If you switch package managers later, replace that setting with the equivalent for the new one. Without it, builds can fail in ways that point at the wrong culprit — most often one module picking up another module’s version of a build tool.

An existing .npmrc or .yarnrc.yml is added to rather than replaced: only the missing settings are appended, and anything else in the file is left as it is. A setting already present with a different value is never rewritten — nodeLinker: pnp or node-linker=hoisted is a deliberate decision about how the project resolves modules, so it is reported and left for you to resolve.

That check is why pnpm is not simply skipped. It writes no settings of its own, but it can be told to hoist, and it reads that from both .npmrc (node-linker, shamefully-hoist) and pnpm-workspace.yaml (nodeLinker, shamefullyHoist) — so both are checked. Anything else in pnpm-workspace.yaml, packages globs included, is left alone.

Keeping the setup correct

fsp dev and fsp build check this layout before they run anything, and fsp create applies it to each module it adds. What they do about a problem depends on where they are running:

fsp dev and fsp build only ever report. They never rewrite your manifests: a build that edits the repository underneath you is a surprise wherever it happens, and repairing is what fsp doctor --fix is for. What changes is how loudly they complain — on your machine a warning, on a build server (anywhere CI is set to something other than false or 0) a failure, because a repository committed in this state produces builds whose modules resolve each other’s dependencies.

fsp create is the exception: it has just scaffolded the module it is repairing, and module CLIs scaffold a manifest that does not name themselves.

fsp doctor

fsp doctor is the one command that repairs.

Terminal window
fsp doctor # check every account, exit non-zero if anything is wrong
fsp doctor myStore # check one account
fsp doctor --fix # repair, and install so the repairs take effect

It checks three things for every module in faststore.json:

  • the module’s directory is covered by the workspace globs — workspaces in the root manifest, or packages in pnpm-workspace.yaml. A module outside them is not a workspace at all, so its dependencies are never installed and no hoisting setting can rescue it.
  • the module declares the CLI that builds it.
  • the package manager is told to keep each workspace’s dependencies to itself.

--fix adds the module to the globs, moves the CLI into the module’s manifest, writes the missing isolation settings, and then installs — rewriting a manifest moves nothing that is already in node_modules, so without the install the next command would still fail for the reason just repaired. The changes land in your working tree to review and commit like any other edit.

Not every problem has a fix — a module with no package.json, or an isolation setting that was deliberately set to defeat isolation, is reported instead of rewritten. --fix still exits non-zero when one of those survives the repair, so a CI step chaining on it does not read a partial repair as success.

Only Yarn Classic’s isolation setting names the CLIs individually, so it is the only one that changes as modules come and go; the others are single settings that already cover whatever you add.

How modules are run

fsp runs each module’s CLI as a separate process rather than loading it into its own. Modules therefore do not share a module registry, and a crash or a memory-hungry build in one does not affect the others. A module that exits with a non-zero status fails the fsp command that ran it.