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:
| Key | Semantic | Usage | Required |
|---|---|---|---|
| path | the path where the given module customization is | local development, production build | true |
| port | the port where the module will be available | local development | true |
| cli | an argument to override the module CLI being used | local development, production build | false |
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-clipackages/discovery/package.json # @faststore/clipackages/checkout/package.json # @vtex/checkoutpackages/sales-app/package.json # @vtex/sales-appThis 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 manager | What init writes |
|---|---|
| pnpm | nothing — it isolates workspaces by default |
| Yarn 2+ | nmHoistingLimits: workspaces in .yarnrc.yml |
| Yarn Classic | workspaces.nohoist listing the module CLIs |
| npm | install-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.
fsp doctor # check every accountfsp doctor myStore # check one accountfsp doctor --fix # repair, and install so the repairs take effectIt checks four things for every module in faststore.json:
- the module’s directory is covered by the workspace globs —
workspacesin the root manifest, orpackagesinpnpm-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.
- on a machine with Volta installed, the module has its own Volta pin for whichever package manager the project declares — see Volta and generated build directories.
--fix adds the module to the globs, moves the CLI into the module’s manifest, writes the missing isolation settings, pins Volta where it can, 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, an isolation setting that was deliberately set to defeat isolation, or a Volta pin --fix can’t determine on its own, is reported instead of rewritten.
Most problems also fail the command outright: fsp doctor exits non-zero if any of them are still unrepaired, and so does --fix when one of those survives the repair, so a CI step chaining on either does not read a partial repair as success. The Volta pin check is the exception — it only warns, never fails, even when it can’t be repaired automatically. Its absence has always been the normal state everywhere Volta isn’t installed, so it can’t be allowed to fail a build the moment an image happens to gain it.
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.
Volta and generated build directories
Some module CLIs (@faststore/cli among them) generate a temporary build directory and run a package manager inside it. Volta resolves the nearest package.json’s own volta field before anything on PATH — so a module with no pin of its own hands that generated directory whatever Volta’s machine-wide default happens to be, not the version packageManager declares. If the two disagree, the generated directory — never declared as a workspace member of its own — can end up built with a package manager version that refuses to touch it.
fsp doctor only checks for this on a machine with Volta installed, and only when the project declares an exact packageManager version and the module has no pin at all for it — a pin that merely differs from packageManager (pinning an older patch for one module, say) is left alone as a deliberate choice.
When it can repair, it pins the manager alongside a Node version, in the module’s own manifest — never the root’s, since some deploy pipelines read the root manifest’s volta.node for an unrelated purpose. The Node version comes from whichever of these already exists in the repo, in order: an existing partial pin, the root manifest’s own pin, then .nvmrc or .tool-versions. If none of those name one, --fix reports the problem instead of guessing — committing whichever Node happened to be running the repair would just move the same kind of accidental, machine-dependent state somewhere else.
A Yarn Berry project gets reported without a fix: Berry refuses a non-workspace directory regardless of which version resolves there, so pinning one doesn’t change the outcome.
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.