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 account, exit non-zero if anything is wrongfsp doctor myStore # check one accountfsp doctor --fix # repair, and install so the repairs take effectIt checks three 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.
--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.