znode-plugin 0.0.0-stage → 0.1.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.
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "znode-plugin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Build, check, pack and deploy Znode runtime plugins and themes: the SDK behind every create-znode-plugin project.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "bin": {
8
+ "znode": "lib/znode-package.mjs"
9
+ },
10
+ "files": [
11
+ "lib",
12
+ "types",
13
+ "tsconfig.json",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "keywords": [
20
+ "znode",
21
+ "runtime-plugin",
22
+ "theme",
23
+ "nextjs"
24
+ ],
25
+ "dependencies": {
26
+ "adm-zip": "0.5.16",
27
+ "postcss": "8.4.38",
28
+ "rollup": "4.59.0",
29
+ "typescript": "5.9.3"
30
+ },
31
+ "publishConfig": { "registry": "https://registry.npmjs.org/", "access": "public" }
32
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ // The preset a plugin project's tsconfig extends (through tsconfig.store.json): JSX as Next.js compiles it, modern
3
+ // modules, no emit (the build compiles file by file, `znode build`). `znode link-store <storeRepo>` adds the store's
4
+ // own tsconfig.base.json, so every @znode alias resolves to the store's real source and types.
5
+ "compilerOptions": {
6
+ "target": "es2022",
7
+ "module": "esnext",
8
+ "moduleResolution": "bundler",
9
+ "lib": ["es2022", "dom", "dom.iterable"],
10
+ "jsx": "react-jsx",
11
+ "allowSyntheticDefaultImports": true,
12
+ "esModuleInterop": true,
13
+ "resolveJsonModule": true,
14
+ "noEmit": true,
15
+ "skipLibCheck": true,
16
+ "types": []
17
+ }
18
+ }
@@ -0,0 +1,227 @@
1
+ // Znode runtime extension API, version 1: what a theme, plugin or widget package builds against.
2
+ // A package imports nothing from the webstore; everything comes in through these hooks and the browser host.
3
+
4
+ // ---- server part (IRuntimePlugin, the default export of the server bundle) ------------------------------------
5
+
6
+ export type IRuntimeData = Record<string, unknown>;
7
+
8
+ export interface IRuntimeHookContext {
9
+ store: { code: string };
10
+ // this package's settings for the store (registry)
11
+ settings: Record<string, unknown>;
12
+ cookies: { get: (name: string) => string | undefined; delete: (name: string) => void };
13
+ }
14
+
15
+ export interface IRuntimeCartLine {
16
+ id: string;
17
+ sku: string;
18
+ quantity: number;
19
+ totalPrice: number;
20
+ personalization: { code: string; value: string }[];
21
+ }
22
+
23
+ export interface ICartHookContext {
24
+ store: { code: string };
25
+ cart: { cartNumber: string; lines: IRuntimeCartLine[] };
26
+ settings: Record<string, unknown>;
27
+ actions: {
28
+ addItem: (item: { sku: string; parentSku?: string; quantity: number; personalization?: { code: string; value: string }[] }) => Promise<boolean>;
29
+ removeLine: (lineId: string) => Promise<boolean>;
30
+ };
31
+ }
32
+
33
+ // A plugin's own server endpoints: /api/extensions/plugins/<plugin>/<route> (GET, POST, PUT, DELETE), only for a
34
+ // store the plugin is enabled for. The browser part calls them; the context brings the store, settings, the shopper's
35
+ // cart and user, and Znode's API with the store's headers.
36
+ export interface IPluginRouteRequest {
37
+ method: string;
38
+ query: Record<string, string>;
39
+ // JSON parsed, a form as its fields, other types (XML, CSV) as text; null when empty
40
+ body: unknown;
41
+ // the body exactly as sent, for a signature check
42
+ rawBody: string;
43
+ // lower-cased, without the shopper's cookies
44
+ headers: Record<string, string>;
45
+ // files of a multipart/form-data request: only with the "uploads" permission, up to 5 MB; the text fields are in body
46
+ files?: { field: string; name: string; type: string; size: number; data: string /* base64 */ }[];
47
+ }
48
+
49
+ export interface IPluginRouteContext {
50
+ // attribute(code): one store attribute's value ("" when missing); attributes: all of them by lower-cased code
51
+ store: { code: string; portalId: number; localeId: number; attribute: (code: string) => string; attributes: Record<string, string> };
52
+ // all of this package's settings, with those bound to Znode attributes (settingsSchema "attribute") already taken
53
+ // from the store, and from the buyer's account and user where the field allows those levels
54
+ settings: Record<string, unknown>;
55
+ cartNumber: string;
56
+ // the signed-in buyer, with the B2B fields Znode keeps on the session (0 / "" when not set)
57
+ user: {
58
+ userId: number;
59
+ userName: string;
60
+ accountId: number;
61
+ accountCode: string;
62
+ roleName: string;
63
+ permissionCode: string;
64
+ profileId: number;
65
+ budgetAmount: number;
66
+ } | null;
67
+ // path relative to the API root: "v1/WebStoreLocator/List?filter=..." or "commerceapi/v1/carts/..."
68
+ znode: (path: string, init?: { method?: string; body?: unknown }) => Promise<{ status: number; data: unknown }>;
69
+ // when this route takes over a store route (znode-package.json "overrides"): that store route as the browser called
70
+ // it (method, shopper's cookies; pass a body to send another). Call it and change its answer, or don't and replace it.
71
+ original: (init?: { method?: string; body?: unknown }) => Promise<{ status: number; data: unknown }>;
72
+ // for routes other systems call (ERP, payment, punchout): the HMAC of rawBody with the secret, in constant time
73
+ verifySignature: (rawBody: string, signature: string | null | undefined, secret: string, options?: { algorithm?: string; encoding?: "hex" | "base64"; prefix?: string }) => boolean;
74
+ }
75
+
76
+ // Next.js in a package: import it as a Next.js app does, in server and browser parts alike:
77
+ // import Image from "next/image"; import Link from "next/link"; import { useRouter, redirect } from "next/navigation";
78
+ // import { createElement as h, Fragment } from "react"; (the JSX factory, in a server part)
79
+ // import { Montserrat } from "next/font/google"; const montserrat = Montserrat({ subsets: ["latin"], weight: ["700"] });
80
+ // The package build (sdk/next-imports.mjs) points the first four at the store's own modules at runtime, so the store's
81
+ // images config, router and React apply; next/font/google is built into the package (files downloaded from Google,
82
+ // className / style.fontFamily / variable as in Next.js; options must be a literal object). Other next/* modules stop
83
+ // the build. Server-only functions (redirect, notFound) work in server parts, hooks (useRouter) in browser parts.
84
+
85
+ // What a server slot gets besides its props: the store's server React (createElement and Fragment: a server component
86
+ // has no hooks; bind your JSX factory to it), the store, and all of this package's settings (never sent to the browser).
87
+ export interface IServerSlotContext {
88
+ React: { createElement: (type: unknown, props?: unknown, ...children: unknown[]) => unknown; Fragment: unknown };
89
+ store: { code: string };
90
+ settings: Record<string, unknown>;
91
+ }
92
+
93
+ export interface IRuntimePlugin {
94
+ name: string;
95
+ apiVersion: number;
96
+ // a route answers JSON (any value), or a Response for anything else (a CSV or PDF download, a redirect). Limits: 64 KB
97
+ // body, 15 s, 120 calls a minute per shopper address (the store can change them)
98
+ routes?: Record<string, (request: IPluginRouteRequest, context: IPluginRouteContext) => Promise<unknown>>;
99
+ // server components by slot name: rendered on the server only, in the page's HTML; may be async and return async
100
+ // components (fetch data, call an API); no hooks, window or document. props.children is what is there so far: keep
101
+ // it, add to it or replace it. Not in a sandboxed package.
102
+ // layout.root every page, header to footer props: storeCode, theme; children = the store's page layout
103
+ // (header, page, footer); the body is a flex column: keep wrappers flex: 1 so the footer stays down
104
+ // login.layout the login route's layout props: storeCode; children = the login page (with its loading
105
+ // state); stays mounted while the page inside loads
106
+ // login.page the login page's content props: storeCode, isLoginRequired; children = the login form
107
+ // (with the login.form slot inside it)
108
+ serverSlots?: Record<string, (props: any, context: IServerSlotContext) => unknown>;
109
+ // whole pages at new URLs, by path: { "/apex-offers": { render, metadata } }. A server component the store renders in
110
+ // its layout, at that URL (the browser keeps it); list the same paths in znode-package.json "serverPages". Lowercase
111
+ // paths, not under /api, /login or another store route. Not in a sandboxed package.
112
+ serverPages?: Record<string, IServerPage>;
113
+ hooks: {
114
+ // the cart was read (cart page, checkout): bring it in line with the plugin's rules through the actions
115
+ onCartChanged?: (context: ICartHookContext) => Promise<void>;
116
+ // an add-to-cart request before it goes to Znode: return it changed
117
+ beforeAddToCart?: (request: IRuntimeData, context: IRuntimeHookContext) => Promise<IRuntimeData | void>;
118
+ afterAddToCart?: (response: IRuntimeData, context: IRuntimeHookContext) => Promise<void>;
119
+ // the cart's lines before the cart page or checkout shows them: return them changed
120
+ mapCartItems?: (cartItems: IRuntimeData, context: IRuntimeHookContext) => Promise<IRuntimeData | void>;
121
+ };
122
+ }
123
+
124
+ export interface IServerPage {
125
+ // the page: its path and the query string's values; may be async (fetch data); redirect() / notFound() work
126
+ render: (props: { path: string; searchParams: Record<string, string> }, context: IServerSlotContext) => unknown;
127
+ // the page's <title> and description
128
+ metadata?: { title?: string; description?: string };
129
+ }
130
+
131
+ // ---- browser part (default export of the browser bundle: setup(host) => IBrowserPlugin) ----------------------
132
+
133
+ // What the checkout slots hand a plugin: the store's own calls for saving a shipping address, so a plugin follows the
134
+ // store's checkout instead of calling its endpoints itself.
135
+ export interface ICheckoutApi {
136
+ shippingOptionId: number | null;
137
+ refreshCartSummary: () => void;
138
+ getCartNumber: () => Promise<string>;
139
+ saveAddress: (address: Record<string, unknown>, isFormChange: boolean, addressType: string) => Promise<{ addressModel?: Record<string, unknown>; status?: boolean; error?: string } | null>;
140
+ createGuestAccount: (address: Record<string, unknown>, email: string, baseUrl: string) => Promise<{ userId?: number } | null>;
141
+ createGuestAddressIds: (request: { address?: unknown; shippingAddress?: unknown; isBothBillingShipping: boolean }) => Promise<{ shippingAddressId?: number; billingAddressId?: number; billingAddress?: unknown; hasError?: boolean } | null>;
142
+ updateAddressIds: (shippingAddressId: number, billingAddressId: number, shippingOptionId: number | null) => Promise<boolean>;
143
+ getGuestUser: (cartNumber: string) => { guestUserId?: number; shippingAddressId?: number; billingAddressId?: number; billingAddress?: unknown } | null;
144
+ saveGuestUser: (cartNumber: string, details: { guestUserId: number; shippingAddressId: number; billingAddressId: number; billingAddress?: unknown }) => void;
145
+ }
146
+
147
+ export interface IReact {
148
+ createElement: (type: unknown, props?: unknown, ...children: unknown[]) => unknown;
149
+ Fragment: unknown;
150
+ useState: <T>(initial: T | (() => T)) => [T, (next: T | ((previous: T) => T)) => void];
151
+ useEffect: (effect: () => void | (() => void), deps?: unknown[]) => void;
152
+ useRef: <T>(initial: T) => { current: T };
153
+ useMemo: <T>(factory: () => T, deps: unknown[]) => T;
154
+ cloneElement: (element: unknown, props?: unknown, ...children: unknown[]) => unknown;
155
+ isValidElement: (value: unknown) => boolean;
156
+ useSyncExternalStore: <T>(subscribe: (listener: () => void) => () => void, getSnapshot: () => T, getServerSnapshot?: () => T) => T;
157
+ }
158
+
159
+ export interface IReactDOM {
160
+ createPortal: (children: unknown, container: Element) => unknown;
161
+ }
162
+
163
+ export interface IBrowserHost {
164
+ apiVersion: number;
165
+ // the store's React and react-dom: one React, so hooks work
166
+ React: IReact;
167
+ ReactDOM: IReactDOM;
168
+ store: { code: string };
169
+ theme: string;
170
+ // this package's browser-safe settings (settingsSchema keys marked "browser": true)
171
+ settings: Record<string, unknown>;
172
+ // the store's own state, as hooks (call them inside components)
173
+ stores: { useCartCount: () => number; useUser: () => { userId: number; userName: string } | null };
174
+ }
175
+
176
+ // Slots: a component gets the slot's props plus children (what the store renders there) and decides whether to keep it.
177
+ // layout.head every page's <head> props: store, theme (<title>, <meta>, <link>, <script async>)
178
+ // layout.header every page's header props: the Header widget's props
179
+ // layout.footer the end of every page
180
+ // header.search the header's search box props: SearchBox's own (dataTestSelector, isMobile,
181
+ // isTypeaheadSearch, isHydratedSearch, ...); children = the store's search box (desktop
182
+ // and mobile render it once each)
183
+ // login.form the login page's form props: storeCode, isLoginRequired; children = the store's
184
+ // Login (rendered on the server: keep the first render free of window, document and Date.now())
185
+ // product.image product page image props: product, isQuickView
186
+ // product.addToCart product page Add to Cart props: product, isQuickView
187
+ // product.layout the whole product details area props: product, configurableProducts (the page's variants:
188
+ // render options from them, not from a fetch, so they are in the server HTML),
189
+ // part(name) => ref (parts: product.image, product.variants, product.addToCart moved into
190
+ // your layout, logic intact)
191
+ // product.card a product card props: product, selectedMode
192
+ // productList.layout a category / search listing props: productData, isFromSearch
193
+ // addToCart.notification the added-to-cart popup props: data
194
+ // checkout.address the checkout's address section props: its props (updateShippingOptions, userDetails,
195
+ // addressData, ...) and checkout: the store's own address calls (ICheckoutApi)
196
+ // checkout.shippingOptions the shipping-method list props: its props (shippingOptions, ...) and checkout
197
+ // Transforms: addToCart.notificationData, productList.data
198
+ // Widgets: page-builder widget types by name: a component, or an IEditorWidget (shown in the page builder's widget
199
+ // list with its settings). A type the store already has (ProductsCarousel) keeps the store's fields and adds these.
200
+ export interface IEditorWidget {
201
+ render: (props: any) => unknown;
202
+ label: string;
203
+ // the page builder's widget-list group (default "Store widgets")
204
+ category?: string;
205
+ // Puck 0.17 field definitions: text, textarea, number, select, radio, array, object, custom
206
+ fields: Record<string, Record<string, unknown>>;
207
+ // the props a newly placed widget starts with
208
+ defaultProps?: Record<string, unknown>;
209
+ }
210
+
211
+ export interface IBrowserPlugin {
212
+ slots?: Record<string, (props: any) => unknown>;
213
+ transforms?: Record<string, (value: any) => unknown>;
214
+ widgets?: Record<string, ((props: any) => unknown) | IEditorWidget>;
215
+ }
216
+
217
+ declare global {
218
+ namespace JSX {
219
+ type Element = any;
220
+ interface IntrinsicAttributes {
221
+ key?: string | number;
222
+ }
223
+ interface IntrinsicElements {
224
+ [name: string]: any;
225
+ }
226
+ }
227
+ }