Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 70 additions & 14 deletions platforms/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,24 @@ Once the first stable `4.0.0` ships, the standard `pnpm add @shopify/checkout-ki

## Basic Usage

Import the package once anywhere in your application. The import has a side
effect — it registers `<shopify-checkout>` with `customElements`:
Import the component once anywhere in your application. This entry registers
only `<shopify-checkout>` with `customElements`:

```ts
import '@shopify/checkout-kit';
import '@shopify/checkout-kit/shopify-checkout';
```

Components use separate entry points within the same npm package. Import only
the components you need. The package root (`@shopify/checkout-kit`) exports the
classes, events, and types without registering anything; see
[manual registration](#manual-registration-and-custom-tag-names).

> [!IMPORTANT]
> `import '@shopify/checkout-kit'` no longer registers `<shopify-checkout>`.
> Import `@shopify/checkout-kit/shopify-checkout` instead, or call
> `ShopifyCheckout.register()`. Without either, `<shopify-checkout>` elements
> stay unregistered and checkout won't open.

Then render the element anywhere in your HTML and call `open()` to present
checkout:

Expand All @@ -106,7 +117,7 @@ checkout:
<button id="buy-now">Buy now</button>

<script type="module">
import '@shopify/checkout-kit';
import '@shopify/checkout-kit/shopify-checkout';

const checkout = document.getElementById('checkout');
document.getElementById('buy-now').addEventListener('click', () => {
Expand All @@ -127,7 +138,7 @@ below for details on how to obtain a checkout URL.
If you'd rather not declare the element in HTML, create one from JavaScript:

```ts
import '@shopify/checkout-kit';
import '@shopify/checkout-kit/shopify-checkout';
import type {ShopifyCheckout} from '@shopify/checkout-kit';

const checkout = document.createElement('shopify-checkout') as ShopifyCheckout;
Expand All @@ -144,18 +155,47 @@ checkout.open();
checkout.close();
```

The `ShopifyCheckout` class is also exported directly when you need the
constructor. The package has a single entry point, so this named import also
registers `<shopify-checkout>` with `customElements`:
## Manual registration and custom tag names

The package root exports the `ShopifyCheckout` class without registering it.
Call `ShopifyCheckout.register()` to register `<shopify-checkout>` yourself, for
example to control when registration happens:

```ts
import {ShopifyCheckout} from '@shopify/checkout-kit';

ShopifyCheckout.register();

const checkout = new ShopifyCheckout();
checkout.src = 'https://your-store.myshopify.com/checkouts/cn/abc123';
document.body.append(checkout);
```

Pass a tag name to register the element under a different name. `register()`
returns the registered class: each custom name gets its own subclass of
`ShopifyCheckout`, because the browser accepts each constructor only once.

```ts
import {ShopifyCheckout} from '@shopify/checkout-kit';

const AcmeCheckout = ShopifyCheckout.register('acme-checkout');

const checkout = document.querySelector('acme-checkout') as InstanceType<typeof AcmeCheckout>;
checkout.open();
```

- Calling `register()` again for a name this class already registered returns
the existing class, so it's safe to call more than once.
- It throws if another element already uses the name, including a separate copy
of Checkout Kit. The component entry instead leaves an existing
`<shopify-checkout>` in place, so loading Checkout Kit twice doesn't fail.
- Importing `@shopify/checkout-kit/shopify-checkout` registers
`<shopify-checkout>`. It also works alongside custom names: the page then has
both tags.
- TypeScript only knows the default tag: `document.createElement('shopify-checkout')`
returns a `ShopifyCheckout`. Custom tags return a plain `HTMLElement`, so use
the class `register()` returns or a cast.

## Usage with other frameworks

### React
Expand All @@ -168,7 +208,7 @@ subscribing to Checkout Kit events.

```tsx
import {useEffect, useRef} from 'react';
import '@shopify/checkout-kit';
import '@shopify/checkout-kit/shopify-checkout';
import type {ShopifyCheckout} from '@shopify/checkout-kit';

export function BuyNowButton({checkoutUrl}: {checkoutUrl: string}) {
Expand Down Expand Up @@ -239,11 +279,10 @@ declare module 'react' {
> the `react` module.

> [!NOTE]
> The `import '@shopify/checkout-kit'` side effect registers the element with
> `customElements` and touches browser-only globals, so it must run on the
> client. In server-rendered frameworks (Next.js, Remix), keep the import and
> the component in a client component — e.g. add `'use client'` to the top of
> the file.
> Both `import '@shopify/checkout-kit/shopify-checkout'` and the package root
> touch browser-only globals when they load, so they must run on the client. In
> server-rendered frameworks (Next.js, Remix), keep the import and the component
> in a client component — e.g. add `'use client'` to the top of the file.

## Usage with the Shopify Storefront API

Expand Down Expand Up @@ -614,3 +653,20 @@ conventions, and one-time setup notes.
## License

Shopify's Checkout Kit is provided under an [MIT License](LICENSE).

## Source organization

Component implementations live in `src/components/<component-name>/`, with an
explicitly named implementation file such as
`src/components/shopify-checkout/shopify-checkout.ts`. Keep component-specific
styles, events, types, and tests alongside it; `register.ts` registers its custom
element. Import files directly rather than adding component barrel files.

New components such as `accelerated-checkouts`, `universal-checkout`, and
`shop-wallet` should follow the same layout when added. Shared models and helpers
stay outside component directories. `src/index.ts` is the public npm entry
for `@shopify/checkout-kit`: it exports classes, events, and types, and must not
register elements. Expose each component through its own package subpath
pointing to `register.ts`, which registers the element through the class's static
`register()`. Add `@tagname` to the class so the custom elements manifest maps the
tag to it, and list the component entry in `sideEffects`.
44 changes: 44 additions & 0 deletions platforms/web/custom-elements-manifest.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,53 @@
// Storybook consume to provide HTML attribute autocompletion and docs.
//
// Docs: https://custom-elements-manifest.open-wc.org/analyzer/getting-started/

// Each component's `register.ts` defines its element through the class's static
// `register()` rather than a literal `customElements.define()` call, which the
// analyzer can't follow. Re-create the definition export from the component's
// `@tagname`, so tools still map the tag to its class.
function componentDefinitions() {
return {
name: 'checkout-kit:component-definitions',
packageLinkPhase({customElementsManifest}) {
const {modules} = customElementsManifest;
for (const module of modules) {
const component = module.path.match(/^src\/components\/([^/]+)\/register\.ts$/)?.[1];
if (!component) continue;
const path = `src/components/${component}/${component}.ts`;
const element = modules
.find((candidate) => candidate.path === path)
?.declarations?.find((declaration) => declaration.customElement && declaration.tagName);
if (!element) {
throw new Error(`${module.path}: expected a custom element class with @tagname in ${path}`);
}
module.exports = [
...(module.exports ?? []).filter(({kind}) => kind !== 'custom-element-definition'),
{
kind: 'custom-element-definition',
name: element.tagName,
declaration: {name: element.name, module: `/${path.replace(/\.ts$/, '')}`},
},
];
}
// TypeScript `this` parameters are type annotations, not arguments.
for (const module of modules) {
for (const declaration of module.declarations ?? []) {
for (const member of declaration.members ?? []) {
if (member.parameters) {
member.parameters = member.parameters.filter(({name}) => name !== 'this');
}
}
}
}
},
};
}

export default {
globs: ['src/**/*.ts'],
exclude: ['src/**/*.test.ts'],
outdir: 'dist',
packagejson: true,
plugins: [componentDefinitions()],
};
9 changes: 6 additions & 3 deletions platforms/web/dependency-cruiser.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@ export default {
{
name: "all-runtime-source-is-reachable-from-public-entrypoint",
comment:
"Production source must be reachable from src/index.ts so it is either shipped or deleted.",
"Production source must be reachable from a package entry (src/index.ts or a component's register.ts) so it is either shipped or deleted.",
severity: "error",
from: { path: "^src/index\\.ts$" },
from: { path: "^src/(index|components/[^/]+/register)\\.ts$" },
to: {
reachable: false,
pathNot: [
"(^|/)node_modules/",
"^package\\.json$",
"^src/checkout\\.types\\.ts$",
// The entries themselves; a module doesn't count as reachable from itself.
"^src/(index|components/[^/]+/register)\\.ts$",
// Type-only modules are erased at compile time, so nothing reaches them at runtime.
"\\.types\\.ts$",
"\\.d\\.ts$",
"\\.test\\.ts$",
"\\.test-helpers\\.ts$",
Expand Down
25 changes: 15 additions & 10 deletions platforms/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,17 @@
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./shopify-checkout": {
"types": "./dist/shopify-checkout.d.ts",
"import": "./dist/shopify-checkout.js",
"default": "./dist/shopify-checkout.js"
},
"./custom-elements.json": "./dist/custom-elements.json",
"./package.json": "./package.json"
},
"sideEffects": [
"./src/index.ts",
"./src/checkout-web-component.ts",
"./dist/index.js"
"./src/components/shopify-checkout/register.ts",
"./dist/shopify-checkout.js"
],
"files": [
"LICENSE",
Expand All @@ -57,22 +61,23 @@
"dev": "vite build --watch",
"test": "vitest run --coverage",
"test:watch": "vitest",
"lint": "pnpm run typecheck && pnpm run sample:typecheck && pnpm run lint:compat && pnpm run lint:dependencies && pnpm run lint:js && pnpm run format:check",
"lint": "pnpm run typecheck && pnpm run sample:typecheck && pnpm run scripts:typecheck && pnpm run lint:compat && pnpm run lint:dependencies && pnpm run lint:js && pnpm run format:check",
"lint:compat": "eslint --config eslint.compat.config.mjs src",
"lint:dependencies": "depcruise --config dependency-cruiser.config.mjs src",
"lint:js": "oxlint --report-unused-disable-directives --max-warnings 0 src sample && pnpm --dir test/e2e lint",
"lint:js:fix": "oxlint --fix src sample && pnpm --dir test/e2e lint:fix",
"format": "oxfmt src sample dependency-cruiser.config.mjs '!sample/dist/**' && pnpm --dir test/e2e format",
"format:check": "oxfmt --check src sample dependency-cruiser.config.mjs '!sample/dist/**' && pnpm --dir test/e2e format:check",
"lint:js": "oxlint --report-unused-disable-directives --max-warnings 0 src sample scripts && pnpm --dir test/e2e lint",
"lint:js:fix": "oxlint --fix src sample scripts && pnpm --dir test/e2e lint:fix",
"format": "oxfmt src sample scripts dependency-cruiser.config.mjs '!sample/dist/**' && pnpm --dir test/e2e format",
"format:check": "oxfmt --check src sample scripts dependency-cruiser.config.mjs '!sample/dist/**' && pnpm --dir test/e2e format:check",
"typecheck": "tsc --noEmit",
"sample": "vite --config sample/vite.config.ts",
"sample:build": "vite build --config sample/vite.config.ts",
"sample:preview": "vite preview --config sample/vite.config.ts",
"sample:typecheck": "tsc --noEmit -p sample/tsconfig.json",
"verify": "publint",
"verify": "publint && vitest run --config vitest.package.config.ts",
"snapshot": "./scripts/create_snapshot",
"compare-snapshot": "./scripts/compare_snapshot",
"prepack": "pnpm run build"
"prepack": "pnpm run build",
"scripts:typecheck": "tsc --noEmit -p scripts/tsconfig.json"
},
"devDependencies": {
"@custom-elements-manifest/analyzer": "^0.10.4",
Expand Down
6 changes: 5 additions & 1 deletion platforms/web/package.snapshot.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
[
"dist/chunks/shopify-checkout.js",
"dist/chunks/shopify-checkout.js.map",
"dist/custom-elements.json",
"dist/index.d.ts",
"dist/index.js",
"dist/index.js.map",
"dist/shopify-checkout.d.ts",
"dist/shopify-checkout.js",
"dist/shopify-checkout.js.map",
"LICENSE",
"package.json",
"README.md"
Expand Down
2 changes: 1 addition & 1 deletion platforms/web/sample/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Web Component Playground

A development harness for the `<shopify-checkout>` web component. It imports the same entry as published consumers (`@shopify/checkout-kit`, aliased to `../src/index.ts` in dev), registers the custom element, and logs Checkout Kit lifecycle events.
A development harness for the `<shopify-checkout>` web component. It imports the same entry as published consumers (`@shopify/checkout-kit/shopify-checkout`, aliased to `../src/components/shopify-checkout/register.ts` in dev), registers the custom element, and logs Checkout Kit lifecycle events.

## Run locally

Expand Down
2 changes: 1 addition & 1 deletion platforms/web/sample/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import "@shopify/checkout-kit";
import "@shopify/checkout-kit/shopify-checkout";
import type { ShopifyCheckout } from "@shopify/checkout-kit";

import { normalizeQuantity, normalizeStorefrontDomain, upsertCartLine } from "./cart";
Expand Down
3 changes: 2 additions & 1 deletion platforms/web/sample/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"noEmit": true,
"types": ["vite/client"],
"paths": {
"@shopify/checkout-kit": ["../src/index.ts"]
"@shopify/checkout-kit": ["../src/index.ts"],
"@shopify/checkout-kit/shopify-checkout": ["../src/components/shopify-checkout/register.ts"]
}
},
"include": ["./**/*.ts", "./**/*.d.ts"]
Expand Down
11 changes: 7 additions & 4 deletions platforms/web/sample/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ export default defineConfig({
// Treat `sample/` as the project root so vite serves `index.html` from here.
root: here,
resolve: {
alias: {
// Same entry consumers use from npm (`import '@shopify/checkout-kit'`).
"@shopify/checkout-kit": resolve(here, "../src/index.ts"),
},
alias: [
{
find: "@shopify/checkout-kit/shopify-checkout",
replacement: resolve(here, "../src/components/shopify-checkout/register.ts"),
},
{ find: /^@shopify\/checkout-kit$/, replacement: resolve(here, "../src/index.ts") },
],
},
build: {
outDir: resolve(here, "dist"),
Expand Down
Loading
Loading