@shopbb/helium 0.1.0 → 0.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/client.d.ts +48 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +64 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/components/AddToCartButton.d.ts +41 -0
  6. package/dist/components/AddToCartButton.d.ts.map +1 -0
  7. package/dist/components/AddToCartButton.js +51 -0
  8. package/dist/components/AddToCartButton.js.map +1 -0
  9. package/dist/components/CartLineQuantityAdjustButton.d.ts +40 -0
  10. package/dist/components/CartLineQuantityAdjustButton.d.ts.map +1 -0
  11. package/dist/components/CartLineQuantityAdjustButton.js +58 -0
  12. package/dist/components/CartLineQuantityAdjustButton.js.map +1 -0
  13. package/dist/components/Image.d.ts +39 -0
  14. package/dist/components/Image.d.ts.map +1 -0
  15. package/dist/components/Image.js +28 -0
  16. package/dist/components/Image.js.map +1 -0
  17. package/dist/components/Money.d.ts +41 -0
  18. package/dist/components/Money.d.ts.map +1 -0
  19. package/dist/components/Money.js +48 -0
  20. package/dist/components/Money.js.map +1 -0
  21. package/dist/components/ProductPrice.d.ts +28 -0
  22. package/dist/components/ProductPrice.d.ts.map +1 -0
  23. package/dist/components/ProductPrice.js +10 -0
  24. package/dist/components/ProductPrice.js.map +1 -0
  25. package/dist/components/VariantSelector.d.ts +80 -0
  26. package/dist/components/VariantSelector.d.ts.map +1 -0
  27. package/dist/components/VariantSelector.js +87 -0
  28. package/dist/components/VariantSelector.js.map +1 -0
  29. package/dist/components/index.d.ts +24 -0
  30. package/dist/components/index.d.ts.map +1 -0
  31. package/dist/components/index.js +18 -0
  32. package/dist/components/index.js.map +1 -0
  33. package/dist/react.d.ts +98 -0
  34. package/dist/react.d.ts.map +1 -0
  35. package/dist/react.js +168 -0
  36. package/dist/react.js.map +1 -0
  37. package/package.json +33 -3
  38. package/src/client.tsx +103 -0
  39. package/src/components/AddToCartButton.tsx +90 -0
  40. package/src/components/CartLineQuantityAdjustButton.tsx +119 -0
  41. package/src/components/Image.tsx +93 -0
  42. package/src/components/Money.tsx +106 -0
  43. package/src/components/ProductPrice.tsx +61 -0
  44. package/src/components/VariantSelector.tsx +148 -0
  45. package/src/components/index.ts +33 -0
  46. package/src/react.tsx +296 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VariantSelector.d.ts","sourceRoot":"","sources":["../../src/components/VariantSelector.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,cAAc;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;CACpC;AAED,UAAU,WAAW;IACnB,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACpD,QAAQ,EAAE;QAAE,KAAK,EAAE,cAAc,EAAE,CAAA;KAAE,CAAC;CACvC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,aAAa,CAAC;IACtB,mCAAmC;IACnC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,gDAAgD;IAChD,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,WAAW,CAAC;IACrB,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,sBAAsB;IACtB,QAAQ,EAAE,CAAC,WAAW,EAAE,0BAA0B,KAAK,KAAK,CAAC,SAAS,CAAC;CACxE;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CAsE1D"}
@@ -0,0 +1,87 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * <VariantSelector> — 商品 variant 选择器
4
+ *
5
+ * 对齐 @shopify/hydrogen <VariantSelector>:
6
+ * - 自动解析 variants 的 selectedOptions 维度(颜色 / 尺寸 等)
7
+ * - 渲染每个维度的选项按钮
8
+ * - 不可购买组合 disabled
9
+ * - URL 同步(可选)
10
+ * - 默认选第一个 availableForSale
11
+ *
12
+ * 设计:组件用 render prop,把每个 option 的选项交给商家渲染。
13
+ *
14
+ * 用法:
15
+ * <VariantSelector
16
+ * product={product}
17
+ * value={selectedVariantId}
18
+ * onChange={setSelectedVariantId}
19
+ * >
20
+ * {({ option, value, onSelect }) => (
21
+ * <div>
22
+ * <h4>{option.name}</h4>
23
+ * {option.values.map((v) => (
24
+ * <button
25
+ * key={v.value}
26
+ * disabled={!v.available}
27
+ * onClick={() => onSelect(v.value)}
28
+ * aria-pressed={v.value === value}
29
+ * >
30
+ * {v.value}
31
+ * </button>
32
+ * ))}
33
+ * </div>
34
+ * )}
35
+ * </VariantSelector>
36
+ */
37
+ import * as React from 'react';
38
+ export function VariantSelector(props) {
39
+ const { product, value, onChange, children } = props;
40
+ const variants = product.variants.nodes;
41
+ const currentVariant = variants.find((v) => v.id === value) || variants[0];
42
+ // 推断 options(如果 product.options 没传,从 variants.selectedOptions 提取)
43
+ const options = React.useMemo(() => {
44
+ if (product.options && product.options.length > 0) {
45
+ return product.options.map((opt) => ({
46
+ name: opt.name,
47
+ values: opt.values.map((v) => ({
48
+ value: v,
49
+ available: variants.some((variant) => variant.availableForSale &&
50
+ variant.selectedOptions?.some((so) => so.name === opt.name && so.value === v)),
51
+ })),
52
+ }));
53
+ }
54
+ // 从 variants.selectedOptions 反推
55
+ const dimMap = new Map();
56
+ for (const v of variants) {
57
+ for (const so of v.selectedOptions ?? []) {
58
+ if (!dimMap.has(so.name))
59
+ dimMap.set(so.name, new Set());
60
+ dimMap.get(so.name).add(so.value);
61
+ }
62
+ }
63
+ return Array.from(dimMap.entries()).map(([name, values]) => ({
64
+ name,
65
+ values: Array.from(values).map((v) => ({
66
+ value: v,
67
+ available: variants.some((variant) => variant.availableForSale &&
68
+ variant.selectedOptions?.some((so) => so.name === name && so.value === v)),
69
+ })),
70
+ }));
71
+ }, [product, variants]);
72
+ // 切换某 option 的 value:找一个匹配的 variant
73
+ const handleSelect = (optionName, optionValue) => {
74
+ const currentSelected = new Map((currentVariant?.selectedOptions ?? []).map((so) => [so.name, so.value]));
75
+ currentSelected.set(optionName, optionValue);
76
+ // 找最匹配 + available 的 variant
77
+ const target = variants.find((v) => Array.from(currentSelected.entries()).every(([n, val]) => v.selectedOptions?.some((so) => so.name === n && so.value === val)));
78
+ if (target)
79
+ onChange?.(target.id);
80
+ };
81
+ return (_jsx(_Fragment, { children: options.map((option) => (_jsx(React.Fragment, { children: children({
82
+ option,
83
+ value: currentVariant?.selectedOptions?.find((so) => so.name === option.name)?.value,
84
+ onSelect: (val) => handleSelect(option.name, val),
85
+ }) }, option.name))) }));
86
+ }
87
+ //# sourceMappingURL=VariantSelector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VariantSelector.js","sourceRoot":"","sources":["../../src/components/VariantSelector.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @shopbb/helium/components — 商家用 React 组件
3
+ *
4
+ * 用法:
5
+ * import { Money, Image, ProductPrice, AddToCartButton } from '@shopbb/helium/components';
6
+ *
7
+ * 设计原则:
8
+ * - 无样式:组件只管行为 + 语义化 DOM,样式商家自己写
9
+ * - data-* 钩子:方便选择器
10
+ * - 对齐 Shopify Hydrogen 同名组件的 API,迁移成本低
11
+ */
12
+ export { Money } from './Money';
13
+ export type { MoneyProps, MoneyData, MoneyMeasurement } from './Money';
14
+ export { Image } from './Image';
15
+ export type { ImageProps, ImageData } from './Image';
16
+ export { ProductPrice } from './ProductPrice';
17
+ export type { ProductPriceProps } from './ProductPrice';
18
+ export { AddToCartButton } from './AddToCartButton';
19
+ export type { AddToCartButtonProps } from './AddToCartButton';
20
+ export { CartLineQuantityAdjustButton } from './CartLineQuantityAdjustButton';
21
+ export type { CartLineQuantityAdjustButtonProps } from './CartLineQuantityAdjustButton';
22
+ export { VariantSelector } from './VariantSelector';
23
+ export type { VariantSelectorProps, VariantSelectorRenderProps, VariantOption, } from './VariantSelector';
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEvE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAErD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAE9D,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAC9E,YAAY,EAAE,iCAAiC,EAAE,MAAM,gCAAgC,CAAC;AAExF,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EACV,oBAAoB,EACpB,0BAA0B,EAC1B,aAAa,GACd,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @shopbb/helium/components — 商家用 React 组件
3
+ *
4
+ * 用法:
5
+ * import { Money, Image, ProductPrice, AddToCartButton } from '@shopbb/helium/components';
6
+ *
7
+ * 设计原则:
8
+ * - 无样式:组件只管行为 + 语义化 DOM,样式商家自己写
9
+ * - data-* 钩子:方便选择器
10
+ * - 对齐 Shopify Hydrogen 同名组件的 API,迁移成本低
11
+ */
12
+ export { Money } from './Money';
13
+ export { Image } from './Image';
14
+ export { ProductPrice } from './ProductPrice';
15
+ export { AddToCartButton } from './AddToCartButton';
16
+ export { CartLineQuantityAdjustButton } from './CartLineQuantityAdjustButton';
17
+ export { VariantSelector } from './VariantSelector';
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGhC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGhC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG9C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGpD,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAG9E,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @shopbb/helium/react — React SSR renderer for Helium storefronts
3
+ *
4
+ * 用法(server worker entry):
5
+ *
6
+ * import { createReactRenderer, HeliumContextProvider } from '@shopbb/helium/react';
7
+ * import { App } from './app';
8
+ *
9
+ * export default {
10
+ * fetch: createReactRenderer({
11
+ * clientBundle: '/assets/client.js',
12
+ * title: '我的店铺',
13
+ * app: ({ ctx, url }) => (
14
+ * <HeliumContextProvider value={ctx}>
15
+ * <App url={url} />
16
+ * </HeliumContextProvider>
17
+ * ),
18
+ * }),
19
+ * };
20
+ *
21
+ * 行为:
22
+ * - 每个请求 createHeliumContext()
23
+ * - 拿 React 树调 react-dom/server.edge renderToReadableStream
24
+ * - 包一层 <!doctype html><html>...<body><div id="root">[SSR]</div><script src="clientBundle"></script>
25
+ * - 把 helium context 序列化嵌入 window.__HELIUM__ 给客户端 hydrate
26
+ *
27
+ * 客户端配套:见 ./client.ts
28
+ */
29
+ import * as React from 'react';
30
+ import type { HeliumContext } from './types';
31
+ export declare function HeliumContextProvider({ value, children, }: {
32
+ value: HeliumContext;
33
+ children: React.ReactNode;
34
+ }): React.FunctionComponentElement<React.ProviderProps<HeliumContext | null>>;
35
+ /**
36
+ * Server-side hook: 拿当前请求的 HeliumContext。
37
+ * 只能在 React 组件树内调用。
38
+ */
39
+ export declare function useHeliumContext(): HeliumContext;
40
+ export interface ReactRendererOptions {
41
+ /**
42
+ * 返回 React 元素的工厂函数。
43
+ * 每个请求会调用一次。
44
+ * 内部应该用 <HeliumContextProvider value={ctx}> 包裹应用根。
45
+ */
46
+ app: (input: {
47
+ ctx: HeliumContext;
48
+ url: URL;
49
+ request: Request;
50
+ }) => React.ReactElement | Promise<React.ReactElement>;
51
+ /**
52
+ * 浏览器加载的客户端 JS bundle URL,
53
+ * 例如 '/assets/client.js'。Vite 构建后通常会带 hash。
54
+ *
55
+ * 不传则只 SSR、不 hydrate(适合纯 SEO 场景)。
56
+ */
57
+ clientBundle?: string;
58
+ /**
59
+ * 注入到 <head> 的 CSS bundle URL。
60
+ */
61
+ cssBundle?: string;
62
+ /**
63
+ * <title>
64
+ */
65
+ title?: string;
66
+ /**
67
+ * <head> 里额外的 HTML(meta、link、script)。
68
+ */
69
+ headHtml?: string;
70
+ /**
71
+ * Storefront 配置(覆盖默认 header 解析行为)。
72
+ * 如不传,会自动从 Gateway 注入的 header 读:
73
+ * X-Public-Storefront-Token / X-Private-Storefront-Token / X-Store-Id
74
+ */
75
+ storefront?: {
76
+ apiUrl?: string;
77
+ publicAccessToken?: string;
78
+ privateAccessToken?: string;
79
+ storeId?: string;
80
+ };
81
+ /**
82
+ * Cart 配置(默认走 cookie)。
83
+ */
84
+ cart?: {
85
+ maxAgeSeconds?: number;
86
+ };
87
+ /**
88
+ * 是否启用 streaming SSR(默认 false,等全部渲染完)。
89
+ * 启用后首字节更快,但要求所有数据加载都用 Suspense。
90
+ */
91
+ streaming?: boolean;
92
+ /**
93
+ * 处理错误的兜底(异常时返回的响应)。
94
+ */
95
+ onError?: (err: unknown, request: Request) => Response | Promise<Response>;
96
+ }
97
+ export declare function createReactRenderer(opts: ReactRendererOptions): (request: Request, env: any, executionContext: ExecutionContext) => Promise<Response>;
98
+ //# sourceMappingURL=react.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAO/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAQ7C,wBAAgB,qBAAqB,CAAC,EACpC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,aAAa,CAAC;IACrB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,6EAMA;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,CAQhD;AAMD,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,GAAG,EAAE,CAAC,KAAK,EAAE;QACX,GAAG,EAAE,aAAa,CAAC;QACnB,GAAG,EAAE,GAAG,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;KAClB,KAAK,KAAK,CAAC,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAEvD;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;OAIG;IACH,UAAU,CAAC,EAAE;QACX,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IAEF;;OAEG;IACH,IAAI,CAAC,EAAE;QACL,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IAEF;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC5E;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,oBAAoB,IAE1D,SAAS,OAAO,EAChB,KAAK,GAAG,EACR,kBAAkB,gBAAgB,KACjC,OAAO,CAAC,QAAQ,CAAC,CAkHrB"}
package/dist/react.js ADDED
@@ -0,0 +1,168 @@
1
+ /**
2
+ * @shopbb/helium/react — React SSR renderer for Helium storefronts
3
+ *
4
+ * 用法(server worker entry):
5
+ *
6
+ * import { createReactRenderer, HeliumContextProvider } from '@shopbb/helium/react';
7
+ * import { App } from './app';
8
+ *
9
+ * export default {
10
+ * fetch: createReactRenderer({
11
+ * clientBundle: '/assets/client.js',
12
+ * title: '我的店铺',
13
+ * app: ({ ctx, url }) => (
14
+ * <HeliumContextProvider value={ctx}>
15
+ * <App url={url} />
16
+ * </HeliumContextProvider>
17
+ * ),
18
+ * }),
19
+ * };
20
+ *
21
+ * 行为:
22
+ * - 每个请求 createHeliumContext()
23
+ * - 拿 React 树调 react-dom/server.edge renderToReadableStream
24
+ * - 包一层 <!doctype html><html>...<body><div id="root">[SSR]</div><script src="clientBundle"></script>
25
+ * - 把 helium context 序列化嵌入 window.__HELIUM__ 给客户端 hydrate
26
+ *
27
+ * 客户端配套:见 ./client.ts
28
+ */
29
+ import * as React from 'react';
30
+ import { renderToReadableStream } from 'react-dom/server';
31
+ import { createHeliumContext, cartGetIdDefault, cartSetIdDefault, } from './index';
32
+ // ============================================================
33
+ // React Context for HeliumContext
34
+ // ============================================================
35
+ const HeliumReactContext = React.createContext(null);
36
+ export function HeliumContextProvider({ value, children, }) {
37
+ return React.createElement(HeliumReactContext.Provider, { value }, children);
38
+ }
39
+ /**
40
+ * Server-side hook: 拿当前请求的 HeliumContext。
41
+ * 只能在 React 组件树内调用。
42
+ */
43
+ export function useHeliumContext() {
44
+ const ctx = React.useContext(HeliumReactContext);
45
+ if (!ctx) {
46
+ throw new Error('useHeliumContext() must be called within <HeliumContextProvider>');
47
+ }
48
+ return ctx;
49
+ }
50
+ export function createReactRenderer(opts) {
51
+ return async function fetch(request, env, executionContext) {
52
+ try {
53
+ // 1. 装配 HeliumContext
54
+ const storefrontApiUrl = opts.storefront?.apiUrl ||
55
+ env?.PUBLIC_STOREFRONT_API_URL ||
56
+ 'https://api.oxygen-demo.cloudc.top/api/2026-04/graphql.json';
57
+ const publicToken = opts.storefront?.publicAccessToken ||
58
+ request.headers.get('X-Public-Storefront-Token') ||
59
+ '';
60
+ const privateToken = opts.storefront?.privateAccessToken ||
61
+ request.headers.get('X-Private-Storefront-Token') ||
62
+ undefined;
63
+ const storeId = opts.storefront?.storeId ||
64
+ request.headers.get('X-Store-Id') ||
65
+ '';
66
+ const ctx = createHeliumContext({
67
+ request,
68
+ env,
69
+ executionContext,
70
+ storefront: {
71
+ apiUrl: storefrontApiUrl,
72
+ publicAccessToken: publicToken,
73
+ privateAccessToken: privateToken,
74
+ storeId,
75
+ cache: typeof caches !== 'undefined'
76
+ ? await caches.open('helium-storefront').catch(() => undefined)
77
+ : undefined,
78
+ },
79
+ cart: {
80
+ getId: cartGetIdDefault(request.headers),
81
+ setId: cartSetIdDefault({
82
+ maxage: opts.cart?.maxAgeSeconds ?? 60 * 60 * 24 * 365,
83
+ }),
84
+ },
85
+ });
86
+ // 2. 拿 React 元素
87
+ const url = new URL(request.url);
88
+ const element = await opts.app({ ctx, url, request });
89
+ // 3. SSR Boot data:把店铺标识 + cart-id 提前传给客户端
90
+ const boot = {
91
+ storefront: {
92
+ apiUrl: storefrontApiUrl,
93
+ // 仅 public token 暴露到客户端
94
+ publicAccessToken: publicToken,
95
+ storeId,
96
+ },
97
+ url: request.url,
98
+ // 不传 token 等敏感字段
99
+ };
100
+ const bootScript = `<script>window.__HELIUM__=${JSON.stringify(boot).replace(/</g, '\\u003c')}</script>`;
101
+ // 4. Render React 树
102
+ const wrappedApp = React.createElement(HeliumReactContext.Provider, { value: ctx }, element);
103
+ const stream = await renderToReadableStream(wrappedApp, {
104
+ onError(err) {
105
+ console.error('[helium SSR error]', err);
106
+ },
107
+ });
108
+ // 等所有数据加载完成(除非启用 streaming)
109
+ if (!opts.streaming) {
110
+ await stream.allReady;
111
+ }
112
+ // 5. 拼接最终响应
113
+ const head = `<!doctype html>
114
+ <html lang="zh-CN">
115
+ <head>
116
+ <meta charset="UTF-8" />
117
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
118
+ ${opts.title ? `<title>${escapeHtml(opts.title)}</title>` : ''}
119
+ ${opts.cssBundle ? `<link rel="stylesheet" href="${opts.cssBundle}" />` : ''}
120
+ ${opts.headHtml ?? ''}
121
+ </head>
122
+ <body>
123
+ <div id="root">`;
124
+ const tail = `</div>${bootScript}${opts.clientBundle
125
+ ? `<script type="module" src="${opts.clientBundle}"></script>`
126
+ : ''}</body></html>`;
127
+ // 合并 stream
128
+ const body = composeStream(head, stream, tail);
129
+ const headers = new Headers(ctx.responseHeaders);
130
+ headers.set('Content-Type', 'text/html; charset=utf-8');
131
+ headers.set('X-Powered-By', '@shopbb/helium');
132
+ return new Response(body, { headers });
133
+ }
134
+ catch (err) {
135
+ if (opts.onError)
136
+ return opts.onError(err, request);
137
+ console.error('[helium] unhandled', err);
138
+ return new Response('Internal Error', { status: 500 });
139
+ }
140
+ };
141
+ }
142
+ // ============================================================
143
+ // helpers
144
+ // ============================================================
145
+ function escapeHtml(s) {
146
+ return s.replace(/[<>&"']/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#39;' }[c]));
147
+ }
148
+ /**
149
+ * 把 head + react stream + tail 拼成一个 ReadableStream<Uint8Array>
150
+ */
151
+ function composeStream(head, body, tail) {
152
+ const encoder = new TextEncoder();
153
+ return new ReadableStream({
154
+ async start(controller) {
155
+ controller.enqueue(encoder.encode(head));
156
+ const reader = body.getReader();
157
+ while (true) {
158
+ const { value, done } = await reader.read();
159
+ if (done)
160
+ break;
161
+ controller.enqueue(value);
162
+ }
163
+ controller.enqueue(encoder.encode(tail));
164
+ controller.close();
165
+ },
166
+ });
167
+ }
168
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shopbb/helium",
3
- "version": "0.1.0",
4
- "description": "shopbb storefront framework — GraphQL client, cart handler, and cache helpers for Cloudflare Workers",
3
+ "version": "0.3.0-alpha.1",
4
+ "description": "shopbb storefront framework — components, React SSR, GraphQL client, cart handler, cache for Cloudflare Workers",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": false,
@@ -12,6 +12,18 @@
12
12
  ".": {
13
13
  "types": "./dist/index.d.ts",
14
14
  "import": "./dist/index.js"
15
+ },
16
+ "./react": {
17
+ "types": "./dist/react.d.ts",
18
+ "import": "./dist/react.js"
19
+ },
20
+ "./client": {
21
+ "types": "./dist/client.d.ts",
22
+ "import": "./dist/client.js"
23
+ },
24
+ "./components": {
25
+ "types": "./dist/components/index.d.ts",
26
+ "import": "./dist/components/index.js"
15
27
  }
16
28
  },
17
29
  "files": [
@@ -30,11 +42,29 @@
30
42
  "ecommerce",
31
43
  "cloudflare",
32
44
  "workers",
33
- "graphql"
45
+ "graphql",
46
+ "react",
47
+ "ssr"
34
48
  ],
35
49
  "repository": "https://github.com/shopbb/shopbb",
50
+ "peerDependencies": {
51
+ "react": ">=18",
52
+ "react-dom": ">=18"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "react": {
56
+ "optional": true
57
+ },
58
+ "react-dom": {
59
+ "optional": true
60
+ }
61
+ },
36
62
  "devDependencies": {
37
63
  "@cloudflare/workers-types": "^4.20240117.0",
64
+ "@types/react": "^18",
65
+ "@types/react-dom": "^18",
66
+ "react": "^18.3.0",
67
+ "react-dom": "^18.3.0",
38
68
  "typescript": "^5.3.3"
39
69
  },
40
70
  "engines": {
package/src/client.tsx ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @shopbb/helium/client — Browser-side hydration helpers
3
+ *
4
+ * 商家的客户端 entry:
5
+ *
6
+ * import { hydrate } from '@shopbb/helium/client';
7
+ * import { App } from './app';
8
+ *
9
+ * hydrate(<App />);
10
+ *
11
+ * 行为:
12
+ * - 从 window.__HELIUM__ 拿 SSR 时塞进去的 boot data
13
+ * - 包一层 BrowserHeliumContextProvider(含 storefront.query helper,纯客户端 fetch)
14
+ * - 调 hydrateRoot
15
+ */
16
+
17
+ import * as React from 'react';
18
+ import { hydrateRoot } from 'react-dom/client';
19
+
20
+ // ============================================================
21
+ // Browser-side context shape
22
+ // 与 server 端 HeliumContext 不完全一致:cart 操作要通过 form action 或专门客户端 mutation
23
+ // 这里先暴露最常用的 storefront.query
24
+ // ============================================================
25
+
26
+ interface ClientStorefrontClient {
27
+ query<T = any>(graphql: string, options?: { variables?: any }): Promise<T>;
28
+ mutate<T = any>(graphql: string, options?: { variables?: any }): Promise<T>;
29
+ }
30
+
31
+ interface ClientHeliumContext {
32
+ storefront: ClientStorefrontClient;
33
+ url: string;
34
+ }
35
+
36
+ declare global {
37
+ interface Window {
38
+ __HELIUM__?: {
39
+ storefront: { apiUrl: string; publicAccessToken: string; storeId: string };
40
+ url: string;
41
+ };
42
+ }
43
+ }
44
+
45
+ const ClientHeliumReactContext = React.createContext<ClientHeliumContext | null>(null);
46
+
47
+ export function useHeliumClient(): ClientHeliumContext {
48
+ const ctx = React.useContext(ClientHeliumReactContext);
49
+ if (!ctx) {
50
+ throw new Error(
51
+ 'useHeliumClient() must be called within HeliumClientProvider (use hydrate())',
52
+ );
53
+ }
54
+ return ctx;
55
+ }
56
+
57
+ function makeClientStorefront(boot: {
58
+ apiUrl: string;
59
+ publicAccessToken: string;
60
+ storeId: string;
61
+ }): ClientStorefrontClient {
62
+ async function call<T>(graphql: string, variables?: any): Promise<T> {
63
+ const res = await fetch(boot.apiUrl, {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ 'X-Storefront-Access-Token': boot.publicAccessToken,
68
+ },
69
+ body: JSON.stringify({ query: graphql, variables }),
70
+ });
71
+ const json = (await res.json()) as { data?: T; errors?: Array<{ message: string }> };
72
+ if (json.errors) {
73
+ throw new Error(json.errors[0]?.message || 'GraphQL error');
74
+ }
75
+ return json.data as T;
76
+ }
77
+ return {
78
+ query: (q, opts) => call(q, opts?.variables),
79
+ mutate: (q, opts) => call(q, opts?.variables),
80
+ };
81
+ }
82
+
83
+ /**
84
+ * 客户端 hydrate 入口。
85
+ * 通常调用一次即可。
86
+ */
87
+ export function hydrate(app: React.ReactElement) {
88
+ const boot = window.__HELIUM__;
89
+ if (!boot) {
90
+ console.warn('[helium] window.__HELIUM__ missing, hydrate skipped');
91
+ return;
92
+ }
93
+ const ctx: ClientHeliumContext = {
94
+ storefront: makeClientStorefront(boot.storefront),
95
+ url: boot.url,
96
+ };
97
+ const wrapped = React.createElement(
98
+ ClientHeliumReactContext.Provider,
99
+ { value: ctx },
100
+ app,
101
+ );
102
+ hydrateRoot(document.getElementById('root')!, wrapped);
103
+ }