Skip to content

Add Checkout Kit CDN module loader - #918

Open
kiftio wants to merge 8 commits into
mainfrom
dk/cdn-module-loader
Open

kiftio wants to merge 8 commits into
mainfrom
dk/cdn-module-loader

Conversation

@kiftio

@kiftio kiftio commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

What changes are you making?

Publish Checkout Kit Web to Shopify's CDN alongside npm.

Loader. A major-versioned ES module at https://cdn.shopify.com/checkout-kit/v<major>/web-components.js registers components on demand:

<script type="module">
  import {loadComponents} from 'https://cdn.shopify.com/checkout-kit/v4/web-components.js';
  await loadComponents(['shopify-checkout']);
</script>

The URL is evergreen within a major: compatible updates ship automatically; a breaking change needs a new major path. Implementation chunks are content-hashed deployment details, not public URLs. The loader also exports version and supportedComponents. A maintainer-only v<major>/unstable/ channel exists for exercising prereleases through the real CDN; it is not a supported entrypoint and is documented as such.

Release workflow (web-publish.yml, runs on web/X.Y.Z releases as today):

  • Uploads content-hashed chunks before the loader that references them.
  • The stable URL updates only for a non-prerelease version, not flagged as a GitHub prerelease, published to npm latest; everything else goes to the unstable channel for its major.
  • A token-only pre-flight confirms the deploy identity holds the bucket permissions the upload needs, before anything irreversible happens and without leaving credentials on disk for package code. Full authentication happens only after npm publication; npm publish runs with --ignore-scripts so the built-and-verified dist/ is what ships.
  • Re-running a release (or a failed manual run) redeploys the CDN assets for an already-published version, which doubles as rollback. A fresh manual dispatch of an already-published version is refused, since main may no longer match the published tarball.
  • The CDN applies one cache policy to every /checkout-kit/ path, so the workflow sets no per-object Cache-Control; docs describe the ~30 minute propagation window and a bad-deploy runbook.

npm package. import '@shopify/checkout-kit' still registers <shopify-checkout>. Generated chunks are marked side-effectful; a built-package test bundles as a consumer and checks registration. Registration is skipped if the element is already defined, so loading Checkout Kit twice (npm bundle plus CDN loader) does not throw. package.json exports are unchanged, so the new dist/ layout is not consumer-visible.

Housekeeping. The packed-files snapshot normalises content hashes so it does not churn on every source change; the new tests use vitest like the rest of the package (the built-package check has its own config and runs from pnpm verify); root *.ts config files are now covered by the oxfmt globs; dependency-cruiser treats src/cdn-loader.ts as a second public entrypoint.

Details: platforms/web/CDN-PUBLISHING.md, platforms/web/RELEASING.md.

Known limitation. There is no supported path yet for patching an older major after a new major ships (npm has one latest; manual dispatch runs from main). Tracked as a follow-up before 5.0.0.

Open question — entrypoint filename. The loader is published as /checkout-kit/v4/web-components.js, mirroring Storefront Web Components (/storefront/web-components.js, per-component files at /storefront/web-components/<name>.js) and leaving /checkout-kit/v4/web-components/shopify-checkout.js as the natural slot for a per-component drop-in later. The earlier draft used /checkout-kit/v4/checkout-kit.js (payments-SDK style, e.g. stripe.js), which repeats the namespace. One caveat with the Storefront parallel: ours is a loader you call, not a side-effect bundle, and the docs say so. This is a one-way door after first publish — please weigh in if you prefer the brand-named file.

How to test

cd platforms/web
pnpm lint && pnpm test
pnpm build && pnpm verify && pnpm compare-snapshot
actionlint .github/workflows/web-publish.yml   # from the repo root

pnpm verify bundles a consumer with import "@shopify/checkout-kit" and checks <shopify-checkout> registers, and loads the built CDN loader from isolated v4/ and v4/unstable/ directory layouts via native ESM.

Once merged, a workflow_dispatch dry run from main exercises the pre-flight against the real deploy identity without publishing anything. The first real deployment will be a prerelease to the unstable channel; the stable URL becomes available with 4.0.0.


Before you merge

Important

  • I've added tests to support my implementation
  • I have read and agree with the Contribution Guidelines
  • I have read and agree with the Code of Conduct
  • I've updated the relevant platform README (platforms/web/README.md)

@kiftio
kiftio requested a review from a team as a code owner October 6, 2026 13:13
@github-actions github-actions Bot added the #gsd:50662 Rebase Checkout Kit on UCP label Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Web — Coverage Report

Lines Statements Branches Functions
Coverage: 97%
95.41% (458/480) 85.92% (238/277) 97.39% (112/115)

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Bundle Size Budgets

Budget Size Limits Result
Web JavaScript (uncompressed) 35.25 KiB (+1.32 KiB) 35 KiB soft / 50 KiB hard ✅ Accepted by @kiftio: Adds a second distribution entrypoint (CDN loader web-components.js, 969 B) and an npm re-export shim (276 B); the shared component chunk grew 104 B for the registration guard and chunk (comment)
  • web: accepted 1 exceeded metric(s).

Bundle and package size

Web bundle sizes cover shipped runtime JavaScript. Package sizes cover the full published archive, including any source maps, declarations, and documentation it contains.

Platform Measurement Compression Base Head Delta
Web JavaScript bundle Uncompressed 33.9 KiB 35.2 KiB +1.3 KiB
Web JavaScript bundle gzip 10.5 KiB 11.3 KiB +834 B
Web npm package (.tgz) gzip 91.0 KiB 95.4 KiB +4.4 KiB
Web package files (uncompressed)

These are uncompressed file sizes; they do not sum to the compressed package size above.

File Base Head Delta
dist/assets/shopify-checkout-CMRYy2le.js.map — 251.6 KiB +251.6 KiB
dist/index.js.map 251.0 KiB — -251.0 KiB
dist/custom-elements.json 48.8 KiB 51.4 KiB +2.6 KiB
dist/index.d.ts 48.0 KiB 48.0 KiB 0 B
dist/assets/shopify-checkout-CMRYy2le.js — 34.0 KiB +34.0 KiB
dist/index.js 33.9 KiB 276 B -33.7 KiB
README.md 20.8 KiB 21.5 KiB +679 B
dist/web-components.js.map — 5.9 KiB +5.9 KiB
package.json 3.6 KiB 3.8 KiB +202 B
dist/web-components.d.ts — 1.3 KiB +1.3 KiB
LICENSE 1.1 KiB 1.1 KiB 0 B
dist/web-components.js — 969 B +969 B
How sizes are measured

Measured from the PR base SHA and PR head SHA. Web bundle rows sum shipped .js, .mjs, and .cjs files under dist/, excluding source maps and declarations. The gzip bundle size sums files compressed individually with gzip -n -9. npm package sizes are gzip-compressed .tgz archives; Android AAR sizes are ZIP archives. Package sizes are not final app binary sizes.

@kiftio
kiftio force-pushed the dk/cdn-module-loader branch from 2b35d32 to 3a429e4 Compare October 6, 2026 16:30
kiftio added 5 commits October 7, 2026 09:51
Publish Checkout Kit Web to the CDN alongside npm. A major-versioned ES
module loader at /checkout-kit/v<major>/checkout-kit.js registers
components on demand via loadComponent("embedded-checkout"), with a
maintainer-only /v<major>/unstable/ channel for exercising prereleases.

Release workflow:
- Content-hashed implementation chunks upload before the loader that
  references them. The stable URL updates only for a non-prerelease
  version, not flagged as a GitHub prerelease, published to npm `latest`;
  everything else goes to the unstable channel for its major.
- A token-only pre-flight confirms the deploy identity holds the bucket
  permissions the upload needs before anything irreversible happens,
  without leaving credentials on disk for package code. Full
  authentication happens only after npm publication, and `npm publish`
  runs with --ignore-scripts so the verified dist/ is what ships.
- Re-running a release (or a failed manual run) redeploys the CDN assets
  for an already-published version, which doubles as rollback. A fresh
  manual dispatch of an already-published version is refused, since
  main may no longer match the published tarball.
- The CDN route applies one cache policy to every /checkout-kit/ path,
  so the workflow sets no per-object Cache-Control and the docs describe
  the real ~30 minute propagation window and a bad-deploy runbook.

Loader and npm package:
- `import '@shopify/checkout-kit'` still registers <shopify-checkout>;
  generated chunks are marked side-effectful and a built-package test
  bundles as a consumer and checks registration. Registration is skipped
  if the element is already defined, so loading twice does not throw.
- loadComponent rejects unknown and inherited names; the loader exports
  `version` so testers can confirm which build a page received.
- The packed-files snapshot normalises content hashes so it does not
  fail on every component change.

Tests use vitest throughout; the built-package verification has its own
config and runs from `pnpm verify` after a build. Root config files are
now covered by the oxfmt globs.

Known limitation: no supported path yet for patching an older major after
a new major ships; tracked separately.
dist/index.js now imports a content-hashed chunk from dist/assets/, which
the previous two-URL allow-list returned 404 for, so the custom element
never registered and every test timed out at whenDefined.
Browsers record a failed dynamic import for that URL for the rest of the
page, so one transient network error would leave loadComponent() unable to
register the component until reload. The loader now retries (3 attempts,
backoff) through a cache-busting query string on the chunk URL, which the
browser treats as a fresh module, and dedupes concurrent callers onto one
in-flight load.

To make that work the component is a single self-contained chunk: the npm
entry imports it through the CDN component module, so index.js and the
loader share one embedded-checkout-<hash>.js instead of a stub that depends
on a second chunk whose failure would still be cached. A small build plugin
injects the hashed chunk URL into the loader once the chunk graph is known.
Browsers remember a failed import per URL for the life of the page, so a
sequence that failed outright during an outage left every retry URL
poisoned for later loadComponent() calls. Each sequence now gets its own
token, and components that loaded successfully are remembered so they are
not re-fetched under a new URL.
@kiftio
kiftio force-pushed the dk/cdn-module-loader branch from 3a429e4 to 26f4b7b Compare October 7, 2026 08:51
kiftio added 3 commits October 7, 2026 10:34
loadComponent("shopify-checkout") registers <shopify-checkout>: the string
you pass is the tag you get, which also matches the existing Checkout Kit
naming. The chunk follows (shopify-checkout-<hash>.js).
/checkout-kit/v4/web-components.js replaces the repeated namespace in
/checkout-kit/v4/checkout-kit.js and mirrors Storefront Web Components
(/storefront/web-components.js), leaving v<major>/web-components/<tag>.js
as the natural slot for per-component drop-ins. Unlike Storefront's bundle
it is still a loader: importing it registers nothing until loadComponent().
The loader API is part of the evergreen CDN URL contract, so pick the
shape that scales now rather than adding a plural later. loadComponents
takes an array, validates every name before fetching anything, loads in
parallel sharing in-flight work, and rejects if any component fails.
@kiftio

kiftio commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

/accept-size web Adds a second distribution entrypoint (CDN loader web-components.js, 969 B) and an npm re-export shim (276 B); the shared component chunk grew 104 B for the registration guard and chunk
boundary. No consumer downloads both entrypoints: npm consumers see +380 B, CDN consumers had nothing before.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

#gsd:50662 Rebase Checkout Kit on UCP

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant