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/README.md +33 -2
- package/lib/css-dedupe.mjs +62 -0
- package/lib/next-imports.mjs +298 -0
- package/lib/templates.mjs +316 -0
- package/lib/znode-package.mjs +680 -0
- package/package.json +30 -4
- package/tsconfig.json +18 -0
- package/types/extension-api.d.ts +227 -0
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "znode-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|