@webflow/cosmic-deploy-contract
v1.0.2
Published
Versioned schema for the webflow.json#cloud block consumed by cosmic-deployer. Shared between cosmic-builder (webflow/infrastructure) and the Webflow CLI.
Downloads
153
Maintainers
Keywords
Readme
@webflow/cosmic-deploy-contract
Versioned schema for the webflow.json#cloud block consumed by cosmic-deployer.
Single source of truth shared by both producers (cosmic-builder, Webflow CLI) and
the consumer (cosmic-deployer).
History: codified in response to inc-1158 under CLD-1628. Before this package, the contract was implicit — producers and consumers each declared their own optional types, the deployer applied default fallbacks for missing fields, and a build/deploy refactor silently broke Next.js deploys from the CLI without anyone noticing until customer reports.
Installation
Published to the public npm registry under the @webflow scope.
npm install @webflow/cosmic-deploy-contractNo auth, no .npmrc, no token. The contract is intentionally public — it
documents the handshake between producers and the deployer, and there's
nothing in it that benefits from being kept private.
Each release publishes with npm provenance,
so consumers can verify the tarball came from this repo's publish.yml
workflow run via the package's npm page.
The contract
webflow.json MUST contain a cloud object with the following fields:
| Field | Type | Required | Meaning |
| ------------------- | -------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------- |
| contract_version | '1' | yes | Pins this file to v1 of the deploy contract. Producers always emit this. |
| framework | 'astro' \| 'nextjs' \| 'vite' | yes | Framework that produced the build. |
| framework_version | semver string (e.g. "14.2.3") | no | User's installed framework version. Omit if unresolvable; when present, must be valid semver. |
| entrypoint_path | non-empty string | yes | Path to the bundled worker entrypoint, relative to the output root (e.g. ./index.js). |
| deployment_type | 'spa' \| 'ssg' \| 'ssr' \| 'ssr-spa-combo' | yes | Selects the deploy-time wrangler template. |
| skip_no_bundle | boolean | yes | When true, wrangler bundles the entry with its deps. When false, deploy uses --no-bundle. |
Under strict mode (target end state), the deployer applies no default fallbacks and any missing or malformed field is a hard contract-violation error. See Transition mode for the current rollout state.
Extra fields are allowed
Producers may add additional cloud.* fields the deployer doesn't read
(build-phase hints like assets_path_prefix, assets_source_skip_mount_path,
future producer metadata, etc.). The contract validates only what the deployer
consumes; everything else passes through.
Top-level fields outside cloud (user config, framework hints, etc.) are also
tolerated.
Producers
Both producers MUST emit a complete cloud block:
- cosmic-builder —
webflow/infrastructure→pulumi/projects/cosmic-builder/images/builder/helpers.ts#createWebflowJson - Webflow CLI —
webflow/webflow-cli→ itsFrameworkBuilderhierarchy
Producers SHOULD self-check by calling validateForDeploy() against their own
output before shipping it.
Consumer
- cosmic-deployer —
webflow/infrastructure→pulumi/projects/cosmic-builder/images/builder/cosmic.ts#cosmicDeploy
The deployer calls validateForDeploy(JSON.parse(webflowJson)) immediately
after loading the file and before invoking any wrangler step.
Transition mode (current)
Right now cosmic-deployer runs validation in observability mode: violations
are logged as warnings but the deploy proceeds via the legacy fallback path
(detectFrameworkFromBuiltOutput + || 'unknown' reads). This is the
temporary window while the Webflow CLI catches up. When warning telemetry shows
~zero failures, the deployer flips to strict and contract violations become
hard ContractErrors.
Note that the producer self-check in cosmic-builder (the validateForDeploy
call inside createWebflowJson) is loud now — we control that code, so a
producer bug fails the build immediately rather than slipping through.
Versioning
The contract version is in-band — cloud.contract_version lives in the file
itself, not just in this package's version. That's deliberate: producers
(especially the CLI) ship and update independently of the deployer, so the file
is the only stable handshake point.
Bump the major ('1' → '2') on any breaking change. Add the new version to
SUPPORTED_VERSIONS and either dual-validate or migrate as appropriate.
Non-breaking additions (new optional fields, additional enum variants) can stay
on the current major.
Cutting a release
- Bump
versioninpackage.json(semver). - Commit, open PR, merge to
main. - From
main, tag the commit:git tag vX.Y.Z && git push origin vX.Y.Z. - The
Publish to GitHub Packagesworkflow fires on the tag, verifies the tag matchespackage.json(fails the run on mismatch), builds, tests, and publishes. - Bump consumer
package.jsonversions to pick up the new release.
The tag-matches-package.json guard is what keeps us honest: if someone tags
v1.2.0 against a commit where package.json still says 1.1.0, the publish
step bails before mutating the registry.
Usage
import { validateForDeploy, ContractError } from '@webflow/cosmic-deploy-contract';
const raw = JSON.parse(fs.readFileSync('webflow.json', 'utf8'));
try {
const contract = validateForDeploy(raw);
// contract.framework, contract.entrypoint_path, etc. are all typed and present.
} catch (e) {
if (e instanceof ContractError) {
// e.message has the full field-by-field violation, e.issues has structured zod issues.
}
throw e;
}Development
npm install
npm test # runs jest against src/**/*.test.ts
npm run build # compiles src/ → dist/
npm run typecheck