Packaging and publishing rules
You ship a plugin as a source zip. You never build the artifact that customers load: a reviewer downloads your zip, runs your build, zips the result, and a second audit decides whether that build may go on the CDN. Two zips, two audits, two different sets of rules — and the second audit runs after you have been told your submission was accepted. This page is the complete rule set for both.
Enforcement levels
| Level | Meaning on this page |
|---|---|
| publish ERROR | Blocks. Source-zip errors block the upload in your browser; built-zip errors block the publish after review. Both are hard failures. |
| publish warning | Shown, never blocks. |
| silent runtime truncation | Accepted, then trimmed with no message. |
| advisory (unvalidated) | Nothing checks it. |
The book-wide legend and the aggregate index of every number and message live in Limits and error index.
Error strings below are quoted verbatim as the tooling emits them. <name>, <path>,
<label>, N and X/Y are runtime substitutions.
The two zips
| Source zip | Built zip | |
|---|---|---|
| Made by | You | The reviewer, from your source |
| Audited by | auditPluginZip (your browser, before any bytes leave) | auditBuildZip (the reviewer’s browser, before any bytes reach the CDN) |
| Contains | manifest.json, package.json, src/, widgets/, media/, config files | Your source plus the whole dist/ tree |
| Must NOT contain | dist/, node_modules/, .git/ | node_modules/ (everything unrecognised is ignored) |
| Size ceiling | 20 MB | 60 MB |
| Uploaded to | A private bucket, presigned PUT, never through a server | The public plugin CDN, one presigned PUT per file |
| Failure means | You fix and re-zip | Your reviewed plugin does not go live; you resubmit with a bumped version |
The source zip
Required root entries
(Both shapes.) Two files must sit at the zip root — or inside a single wrapper folder, see below.
| Entry | Required | Rule | On violation |
|---|---|---|---|
manifest.json | Both — yes | Must parse as a JSON object | publish ERROR: manifest.json is missing from the zip root or is not valid JSON. |
package.json | Both — yes | Must parse as a JSON object | publish ERROR: package.json is missing from the zip root or is not valid JSON. |
package-lock.json | Both — no | Recommended: the reviewer build otherwise resolves fresh | publish warning: No package-lock.json — the reviewer build will resolve dependencies fresh. |
Only those two files are inflated by the audit, and only up to 2 MB each. A manifest.json larger
than 2 MB reads as missing — split a fat single-widget configSchema out, or move to the suite
shape where schemas live in widgets/*.manifest.json.
Wrapper folders and junk
(Both shapes.) If every non-junk entry shares one top-level directory, that directory is treated as the project root and every path rule below is applied relative to it. This is exactly what macOS Finder’s Compress produces, so zipping the project folder is fine.
__MACOSX/ entries and any file named .DS_Store are ignored entirely — they cannot fail an
audit and they are not counted.
One consequence: a zip with two top-level directories has no detected wrapper, so
manifest.json is looked for at the literal zip root and is not found.
Entry rules
(Both shapes.) Every entry in the central directory is checked. All of these are publish ERROR.
| Rule | Scope | On violation |
|---|---|---|
| No absolute paths | Any entry | "<name>": absolute paths are not allowed. |
No .. traversal | Any segment | "<name>": path traversal is not allowed. |
No node_modules | Any depth (path segment match) | "<name>": node_modules must not be included — submit source only. |
No .git | Any depth (path segment match) | "<name>": .git must not be included. |
No dist/ | Root level only, relative to the wrapper folder | "<name>": dist/ must not be included — the reviewer rebuilds it from source. |
| No symlinks | Any entry | "<name>": symlinks are not allowed. |
The dist/ ban is root-scoped: src/vendor/dist/thing.js is accepted, dist/anything is not.
node_modules and .git are banned at every depth.
Backslashes are normalised to / before any of these run, so a Windows-built zip is checked
identically.
Repeated violations of the same rule are de-duplicated, and this entry-level list is capped at
12 lines — a zipped node_modules shows a handful of representative paths, not hundreds.
Size and count limits
(Both shapes.) All publish ERROR.
| Limit | Value | On violation |
|---|---|---|
| Zip size | 20 MB | Zip is <N.N> MB — the limit is 20 MB. |
| Entry count | 2000 | Zip contains too many entries (<N> > 2000). |
| Uncompressed total | 100 MB | Zip expands to <N> MB — the limit is 100 MB. |
| Compression ratio | 100:1 | Zip compression ratio exceeds 100:1. |
Structural failures short-circuit the whole audit:
| Condition | On violation |
|---|---|
| No end-of-central-directory record | Not a zip archive (no end-of-central-directory record). |
| Zip64 archive | Zip64 archives are not supported for plugin submissions. |
| Central directory runs past EOF | Corrupt zip: central directory extends past the end of the file. |
| Malformed central-directory entry | Corrupt zip: bad central-directory entry. |
Zip64 is the one that surprises people: some archivers force the zip64 format regardless of size.
Use the platform archiver or zip -r.
The API re-checks the size independently and refuses anything outside 1 byte..20 MB with
The zip must be between 1 byte and 20 MB.
Manifest fields the source audit reads
The source audit checks the manifest’s shape, not your build. The full field contracts live in Manifest: root fields, Manifest: widget summary and Layout widgets; this table is only what blocks the upload.
| Field | Type | Required | Rule | On violation |
|---|---|---|---|---|
id | string | Both — yes | Non-empty; matches ^[a-z0-9][a-z0-9-]*$. It is the plugin slug. | publish ERROR: manifest.json is missing the required "id" field. / manifest "id" ("<X>") must be lowercase alphanumeric with hyphens — it is the plugin's slug. |
version | string | Both — yes | Non-empty semver (see Version rules) | publish ERROR: manifest.json is missing the required "version" field. / manifest "version" ("<X>") is not valid semver (e.g. 1.2.0). |
name | string | Both — yes | Non-empty | publish ERROR: manifest.json is missing the required "name" field. |
widgets[] | array | Suite — yes | A non-empty array switches the manifest to the suite shape and runs the per-widget summary audit | publish ERROR, see Manifest: widget summary |
widgetType | string | Single-widget — yes | Non-empty | publish ERROR: manifest.json is missing the required "widgetType" field. |
ssr.url | string | Single-widget — yes | Any string | publish ERROR: manifest.json is missing "ssr.url" (the SSR bundle path). |
assets.js | string | Single-widget — yes | Any string | publish ERROR: manifest.json is missing "assets.js" (the client bundle path). |
Which branch runs is decided by exactly one test: widgets is an array and non-empty. An
empty widgets: [] is read as the single-widget shape and will demand widgetType, ssr.url and
assets.js. See The two manifest shapes.
runtime.widgetCore is not checked here. It is stamped by the build, and it is only gated on
the built zip — so a stale stamp in your committed manifest.json cannot fail your submission.
What must be committed
This is the rule that bites after approval. The reviewer runs your build and zips the result; nothing else creates files. Anything your manifest declares that your build does not generate must be in the source zip, or the publish fails — long after you were told the submission was fine.
| Directory | Generated by | Must be in the source zip? |
|---|---|---|
media/ | Nothing. It is authored content: icons, cover, screenshots, README.md, CHANGELOG.md, per-widget docs. | Yes, always. A declared icon, cover, screenshots[] or readme missing from media/ fails the publish. |
widgets/*.manifest.json | Your build:manifest step, if you have one | Commit them unless npm run build regenerates them. They must exist in the built zip. |
dist/ | hr-widget-build | No — banned. The reviewer rebuilds it. |
node_modules/ | npm install | No — banned. |
If media/ is in your .gitignore, or your .svg icons live outside media/, your submission
passes and your publish does not.
A real source zip
# /Users/…/hr-plugins/pdp-suite-1.3.3-source.zip — 50 entries, 82 KB (published suite, v1.3.3)
package.json # required at the root
package-lock.json # optional; absent = warning
manifest.json # required at the root
tsconfig.json
vite.config.ts
scripts/inject-schema.mjs
widgets/pdp-frame.manifest.json # per-widget config schemas — generated, committed
widgets/stay-hero.manifest.json
widgets/stay-facts.manifest.json
widgets/booking-cta.manifest.json
media/README.md # declared as manifest.readme
media/CHANGELOG.md # picked up by convention, never declared
media/pdp-frame.svg # per-widget icons — FLAT media/<file>
media/stay-hero.svg
media/widgets/pdp-frame.md # per-widget READMEs — one nested level allowed
media/widgets/stay-hero.md
src/vite-env.d.ts
src/widgets/<widget>/{index.tsx,widget.tsx,config.ts,ssr-entry.ts,*.css,*.png}
# NOT present: dist/, node_modules/, .git/
media/CHANGELOG.md is fetched by filename convention when it exists. It is declared nowhere and
is never required.
Media reference rules, shape-tagged, all publish ERROR when broken:
| Field | Accepted shape | Max |
|---|---|---|
icon, cover, widgets[].icon | media/<file> (flat, no subdirectory) or an absolute http(s):// URL | 2048 chars |
screenshots[] | media/<file> only — no absolute URL, no subdirectory | no path cap |
readme, widgets[].readme | media/<file>.md or media/<dir>/<file>.md (one nested level) | 255 chars path, 64 KB file |
What the API checks after the upload
The browser audit is UX. Nothing in it is authoritative. After your zip is in the bucket, the metadata leg posts your parsed manifest to the API, which re-runs everything server-side and adds checks the browser cannot make.
| Code | HTTP | Meaning | Message |
|---|---|---|---|
VALIDATION_ERROR | 422 | Malformed submission envelope (bad sha256, size, notes over 5000 chars) | Laravel validation text |
INVALID_MANIFEST | 422 | A manifest field failed the server-side contract | e.g. Manifest missing required field "widgetType"., Manifest "id" must be lowercase alphanumeric with hyphens (it is the plugin slug)., Manifest declares more than 24 widgets. |
INVALID_ZIP_KEY | 422 | The uploaded object is not under your slug’s folder | zip_key must match submissions/<slug>/<file>.zip |
INVALID_VERSION | 422 | Not parseable semver, or not three-part | Manifest "version" is not valid semver: <X> / Manifest "version" must be MAJOR.MINOR.PATCH semver: <X> |
VERSION_NOT_GREATER | 422 | Not strictly greater than what is published | Version <X> must be greater than the published <Y>. |
SLUG_RESERVED | 409 | The slug exists with no owner (a legacy admin-registered row) | The plugin id "<X>" is reserved. Contact an administrator to claim it. |
SLUG_TAKEN | 409 | The slug belongs to another author | The plugin id "<X>" already belongs to another author. — or …was just registered by another author. when you lose a first-submission race |
Server-side manifest limits the browser does not check, all publish ERROR:
| Field | Limit | On violation |
|---|---|---|
id | 255 chars | Manifest "id" exceeds 255 characters. |
name | 255 chars | Manifest "name" exceeds 255 characters. |
version | 64 chars | Manifest "version" exceeds 64 characters. |
description | 4000 chars | Manifest "description" must be a string of at most 4000 characters. |
author.name, author.url | 255 chars each | Manifest "author.<field>" must be a string of at most 255 characters. |
widgets[] | 24 entries | Manifest declares more than 24 widgets. |
What happens to your zip and your submission after this point — review states, resubmission, who acts when — is in Preflight and submit.
Version rules
(Both shapes.) One version number covers the whole plugin, including every widget of a suite.
| Rule | Enforcement | On violation |
|---|---|---|
Semver shape (X.Y.Z, optional -pre and +build) | publish ERROR, browser | manifest "version" ("<X>") is not valid semver (e.g. 1.2.0). |
Exactly three numeric parts — 1.0 and 1 are refused | publish ERROR, API INVALID_VERSION | Manifest "version" must be MAJOR.MINOR.PATCH semver: <X> |
| Strictly greater than the currently published version | publish ERROR, API VERSION_NOT_GREATER | Version <X> must be greater than the published <Y>. |
| Never re-publish an existing version | publish ERROR at publish time | <slug>@<version> already exists on the CDN — published versions are immutable. A changed build needs a resubmission with a bumped version. |
The greater-than check compares against the live current_version (falling back to the cached
manifest’s version). A plugin that has never published has nothing to compare against, so a first
submission may use any valid version.
Not supported yet. Pre-release versions such as
1.0.0-rc.1are accepted by both the browser audit and the API, and they publish — but every immutability optimisation in the platform requires a bareX.Y.Zpath segment. A pre-release publish silently falls back to short-TTL caching everywhere and to proxied instead of content-addressed asset URLs. Do not use them. There is no warning.
The manifest shape is frozen at first publish
(Both shapes.) Once a plugin is live, it can never move between the single-widget shape and the
suite shape. Stored widget keywords (plugin:{slug} versus plugin:{slug}:{widget}) and embed
URLs customers have already copied would stop resolving.
The reviewer’s tooling refuses the build before upload with:
This build changes <slug> between single-widget and multi-widget shapes. Existing placements and
embed URLs would stop resolving — publish the new shape under a new plugin slug.
and the API refuses it at both publish and rollback with MANIFEST_SHAPE_CHANGED:
This manifest changes the plugin between single-widget and multi-widget shapes. Existing widget
placements and embed URLs would stop resolving — publish the new shape under a new plugin slug
instead.
The escape hatch is a new plugin slug, not a new version. Because a suite may contain a single widget, starting new plugins in the suite shape costs nothing and keeps the door open — see The two manifest shapes.
The built zip
You do not produce this zip, but every rule in it is a rule about your build output, and every
failure sends you back to a resubmission with a bumped version. Run npm run build, zip the
project folder without node_modules/, and check the list below before you submit the source.
The publishable surface
Only four kinds of path are read and uploaded. Everything else in the zip — src/,
package.json, tsconfig.json, your lockfile — is ignored and never published. Your source
is not served from the CDN.
# src/lib/plugin-zip.ts — the publishable-path regex, identical in the audit and
# in the server-side presign whitelist.
^(dist\/[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)?
|widgets\/[A-Za-z0-9._-]+\.manifest\.json
|media\/[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)?)$
# …plus the root manifest.json, matched separately.
Read that carefully: exactly one directory level under dist/, widgets/ and media/.
| Path | Published? |
|---|---|
manifest.json | Yes |
dist/acme.iife.js | Yes — flat single-widget layout |
dist/hero/hero.iife.js | Yes — one widget directory deep |
dist/hero/chunks/vendor.js | No. Silently dropped: no error, no warning. |
widgets/hero.manifest.json | Yes |
media/icon.svg, media/widgets/hero.md | Yes |
src/**, package.json, anything else | No |
A build that emits nested chunk directories under dist/{widget}/ produces a plugin whose
bundles 404 at runtime with nothing in the audit to explain it. Keep the SDK’s default output
layout.
Path segments may only use letters, digits, ., _ and -. A file under dist/, widgets/ or
media/ with any other character (a space, a @, a non-ASCII letter) is dropped with a publish
warning:
"<path>" won't be published — file names may only use letters, digits, ".", "_" and "-".
The server re-applies the same whitelist when presigning and rejects anything else outright with
Unexpected file path "<path>".
Size limits
| Limit | Value | Enforcement | On violation |
|---|---|---|---|
| Built zip | 60 MB | publish ERROR | Zip is <N> MB — zip the built plugin WITHOUT node_modules (60 MB limit). |
| Per publishable file | 30 MB | publish ERROR | "<path>" is over the 30 MB per-file limit. |
| Total publishable bytes | 150 MB | publish ERROR | The build expands to <N> MB — over the 150 MB limit. |
| Client bundle | 1.5 MB | publish warning | dist/<name> is <N.NN> MB. |
| Stylesheet | 512 KB | publish warning | dist/<name> is <N.NN> MB. |
| SSR bundle | 1 MB | publish warning | dist/<name> is <N.NN> MB — over the renderer's 1 MB cache threshold (slower cold SSR). |
| README (each) | 64 KB | publish ERROR | <label> <ref> is <N> KB — the limit is 64 KB. |
The 1 MB SSR warning is the one worth acting on: past it, the renderer stops caching your bundle in its fast path and every cold server render pays the load cost.
Publish gates
All publish ERROR. <label> is the plugin for a single-widget build and widget "<slug>" for
each summary of a suite.
| Gate | Shape | On violation |
|---|---|---|
Root manifest.json present and parseable | Both | manifest.json not found at the zip root — zip the plugin folder AFTER building. |
manifest.id matches the submission | Both | manifest id "<X>" does not match the submission's "<Y>". |
manifest.version matches the submission | Both | manifest version "<X>" does not match the submitted "<Y>". |
runtime.widgetCore present and semver | Both | manifest has no "runtime.widgetCore" stamp — this build predates the evergreen runtime. Update @homerunner-next/widget-core to 0.10.0+ and rebuild. |
runtime.widgetCore ≥ 0.10.0 | Both | Built against widget-core <X> — the fleet requires 0.10.0+ (older builds bundle a stale mount that breaks on SSR pages). Update and rebuild. |
dist/manifest.json present with {buildHash, files} | Both | dist/manifest.json (build manifest with {buildHash, files}) is missing — run the widget-core build. |
Every declared asset exists under dist/ | Both | dist/<name> (<label>'s <kind>) is missing from the zip. — <kind> is client bundle, stylesheet or SSR bundle |
| Every declared asset has a hashed twin in the build manifest | Both | The build manifest has no hashed twin for <name>. |
| The hashed twin file itself is in the zip | Both | Hashed twin dist/<hashed> is missing from the zip. |
The client bundle contains the literal HRPlugins | Both | <label>'s client bundle never registers on window.HRPlugins — the widget would never mount. |
| Each declared sub-manifest is present and valid JSON | Suite | widgets/<x>.manifest.json (<label>'s sub-manifest) is missing from the zip or invalid JSON. |
assets.js declared | Single-widget | manifest is missing "assets.js". |
ssr.url declared | Single-widget | manifest is missing "ssr.url". |
icon / cover reference shape | Both | manifest "<field>" must be a "media/<file>" path or an absolute http(s) URL. |
Declared icon / cover file present | Both | Declared <field> <ref> is missing from the zip. |
widgets[].icon reference shape | Suite | widget "<slug>" icon must be a "media/<file>" path or an absolute http(s) URL. |
Declared widgets[].icon file present | Suite | Declared widget "<slug>" icon <ref> is missing from the zip. |
screenshots[] reference shape | Both | manifest screenshot "<X>" must be a "media/<file>" path. |
| Declared screenshot present | Both | Declared screenshot <ref> is missing from the zip. |
| README reference shape | Both | <label> must be a "media/<file>.md" (or "media/<dir>/<file>.md") path. |
| Declared README present | Both | Declared <label> <ref> is missing from the zip. |
| README ≤ 64 KB | Both | <label> <ref> is <N> KB — the limit is 64 KB. |
Three of these deserve a note.
The HRPlugins marker. The audit decodes your client IIFE as UTF-8 and requires the literal
string HRPlugins to appear somewhere in it. Registration is a property write on
window.HRPlugins, a global name no minifier rewrites — so its absence means the bundle does not
register and the widget could never mount. If you tripped this, you did not call
registerPlugin/registerPluginWidget in the IIFE entry, or your bundler dropped it as dead
code. See Runtime, mount and the DOM contract.
Hashed twins. The SDK’s post-build step walks dist/ recursively, writes a sha256-8 twin
next to every file except manifest.json and *.map, and records the mapping in
dist/manifest.json. If a twin is missing, the build was assembled by hand or dist/ was edited
after the build. Rebuild — never patch dist/manifest.json.
Asset path resolution. A declared asset path is reduced to a dist-relative name before
lookup: dist/hero/hero.iife.js → hero/hero.iife.js, and a path with no dist/ segment falls
back to its basename. That name is both the dist/manifest.json key and the lookup key, so
assets.js must name the file exactly as the build emitted it.
// /Users/…/hr-plugins/pdp-suite/dist/manifest.json — a real suite build manifest
{
"buildHash": "77328e0a",
"generatedAt": "2026-09-07T06:25:25.567Z",
"files": {
"booking-cta/booking-cta.css": "booking-cta/booking-cta-a830c68f.css",
"booking-cta/booking-cta.iife.js": "booking-cta/booking-cta.iife-ca74009c.js",
"pdp-frame/pdp-frame-ssr.umd.js": "pdp-frame/pdp-frame-ssr.umd-5872d9b8.js",
"pdp-frame/pdp-frame.css": "pdp-frame/pdp-frame-9992937d.css",
"pdp-frame/pdp-frame.iife.js": "pdp-frame/pdp-frame.iife-be6fddd4.js",
"stay-hero/hero-bg-Cr1v4Zys.png": "stay-hero/hero-bg-Cr1v4Zys-1cc265f5.png",
"stay-hero/stay-hero.iife.js": "stay-hero/stay-hero.iife-f2a3fe21.js"
}
}
Keys and values are both dist-relative and include the widget directory. Bundled images are keyed
too. .map files are deliberately absent from this map — they are still uploaded to the CDN, but
they have no hashed twin and are not reachable through the asset proxy. See
Keywords, assets and URLs.
buildHash is recorded as publish forensics. No serving path reads it; it is not a cache key.
Publish warnings
Never blocking, but each one describes a plugin that will disappoint whoever installs it.
Single-widget — the root manifest never got its schema injected:
manifest has no configSchema — was this zipped after `npm run build`? The dashboard form will be
empty.
Suite — that widget’s sub-manifest is present but carries no schema:
<sub-manifest> has no configSchema — <label>'s dashboard form will be empty.
Both — your config zod does not extend the base, so the standard controls are gone:
<label>'s configSchema doesn't extend the base widgetSchema (no colorScheme/spacing) — standard
theme + spacing controls won't appear. Build config.ts as widgetSchema.extend({...}) and export
zodToManifestSchema(configSchema).
The check behind the last one is exactly this: the emitted JSON Schema has a properties object
that lacks a colorScheme key or a spacing key. See
Config schema and UI schema.
Not supported yet. Built-zip warnings are shown to the reviewer only. You will never see them in your dashboard, and nothing in your own tooling reproduces them. Treat the three rows above as checks to run yourself: build, then confirm
manifest.json(single-widget) or everywidgets/*.manifest.json(suite) contains aconfigSchemawhosepropertiesincludecolorSchemeandspacing.
What lands on the CDN
Key layout
Every published object is stored at {env}/{slug}/{version}/{path}.
# src/lib/r2.ts — cdnPluginKey(), and src/lib/plugin-zip.ts — the cacheControl assignment.
{env}/{slug}/{version}/manifest.json public, max-age=300
{env}/{slug}/{version}/dist/manifest.json public, max-age=300
{env}/{slug}/{version}/dist/<file> public, max-age=31536000, immutable
{env}/{slug}/{version}/dist/{widget}/<file> public, max-age=31536000, immutable
{env}/{slug}/{version}/widgets/{widget}.manifest.json public, max-age=31536000, immutable
{env}/{slug}/{version}/media/<file> public, max-age=31536000, immutable
{env}/{slug}/{version}/media/{dir}/<file>.md public, max-age=31536000, immutable
# {env} is prod (production dashboard), dev (preview deployments) or local (a laptop).
# One bucket serves all three; the prefix is what stops them colliding on an immutable key.
{env} is decided by the deployment doing the publishing, never by a request. Your plugin lives
under prod/. Any example URL without an {env} segment is wrong and will 404.
The cache split
Exactly two objects carry a short TTL: the root manifest.json and dist/manifest.json, both
public, max-age=300. Everything else — every bundle, stylesheet, image, font, source map,
sub-manifest and media/ file — carries public, max-age=31536000, immutable.
That is safe because the version is in the key. A new version is a new prefix; nothing is ever overwritten.
Content types are assigned from the file extension: .js/.mjs →
text/javascript; charset=utf-8, .css → text/css; charset=utf-8, .json/.map →
application/json; charset=utf-8, .md → text/markdown; charset=utf-8, .svg →
image/svg+xml, .woff2 → font/woff2, .png/.jpg/.jpeg/.webp/.gif as expected, and
anything unrecognised → application/octet-stream. Content-Type and Cache-Control are signed into
each presigned upload, so they cannot drift.
A font or image your build emits with an unlisted extension (.woff, .avif, .ttf) publishes
fine but is served as application/octet-stream. Stick to .woff2 for fonts.
Which of these URLs a customer’s page actually loads — direct content-addressed CDN URLs on
server-rendered pages, the /p/ proxy for CSR embeds — is
Keywords, assets and URLs.
Immutability and retry safety
Before a single byte is presigned, the publish HEADs {env}/{slug}/{version}/manifest.json. If it
exists:
<slug>@<version> already exists on the CDN — published versions are immutable. A changed build
needs a resubmission with a bumped version.
If the storage check itself fails, publish fails closed rather than risk overwriting a version the fleet caches forever:
Could not verify that <slug>@<version> is unpublished (storage check failed). Nothing was
uploaded — retry in a moment.
Uploads are then ordered deliberately: everything else first, dist/manifest.json
second-to-last, and the root manifest.json last. The root manifest is both the immutability
sentinel and the object the publish verifies, so a publish that dies mid-upload leaves no sentinel
and the same zip can be retried safely. Between 1 and 200 files may be uploaded per publish, and
the set must include manifest.json.
Practical consequence for you: a version number is spent the moment it publishes. A one-line
CSS fix to 1.2.0 is not a re-publish of 1.2.0; it is a submission of 1.2.1.
Where the rest of the pipeline is documented
- Preflight checklist, the upload flow, review states and resubmission — Preflight and submit
- What publishing does, how an update propagates, and rollback — Publishing, versions and rollback
- Suspension and per-widget kill switches — Install, customers and kill switches
- Which SDK version you need for suites, layouts, fonts and bundled assets — Install and versions
- Every number and message in one index — Limits and error index