Setting the file. One moment.
Seed Storefront Plan · Wix Headless Templates · wix/skills · Skills Docs
ContentsBack to the top of the page
Number 16.825
Position 825 of 828
Type JavaScript
Size 9 KB
Lines 201 storefront/seed/ seed-storefront-plan.mjs
JavaScript · 201 lines · 9 KB
"&"
,
"<"
:
"<"
,
">"
:
">"
,
"""
:
'"'
,
"'"
:
"'"
,
" "
:
" "
};
13
14 export function decodeEntities ( s ) {
15 return s. replace ( /&(?:amp | lt | gt | quot | #39 | nbsp);/ g , ( m ) => HTML_ENTITIES [m] ?? m);
16 }
17
18 export function mkTextNodes ( html ) {
19 const nodes = [];
20 let bold = 0 , italic = 0 , last = 0 , m;
21 const tag = /<( \/ ? )(strong | b | em | i) \s * \/ ? >/ gi ;
22 const push = ( raw ) => {
23 const text = decodeEntities (raw. replace ( /< [ ^ >] * >/ g , "" ));
24 if ( ! text) return ;
25 const decorations = [];
26 if (bold > 0 ) decorations. push ({ type: "BOLD" });
27 if (italic > 0 ) decorations. push ({ type: "ITALIC" });
28 nodes. push ({ type: "TEXT" , textData: { text, decorations } });
29 };
30 while ((m = tag. exec (html)) !== null ) {
31 push (html. slice (last, m.index));
32 const step = m[ 1 ] ? - 1 : 1 ;
33 if ( / ^ (strong | b) $ / i . test (m[ 2 ])) bold = Math. max ( 0 , bold + step);
34 else italic = Math. max ( 0 , italic + step);
35 last = tag.lastIndex;
36 }
37 push (html. slice (last));
38 return nodes. length ? nodes : [{ type: "TEXT" , textData: { text: "" , decorations: [] } }];
39 }
40
41 export function mkDesc ( text , i ) {
42 const blocks = String (text ?? "" ). split ( /< \/ p \s * > | <br \s * \/ ? >/ i ). map (( b ) => b. trim ()). filter (Boolean);
43 return {
44 nodes: (blocks. length ? blocks : [ "" ]). map (( block , n ) => ({
45 type: "PARAGRAPH" , id: `desc-${ i }-${ n }` ,
46 nodes: mkTextNodes (block),
47 paragraphData: { textStyle: { textAlignment: "AUTO" } },
48 })),
49 metadata: { version: 1 , id: `desc-meta-${ i }` },
50 };
51 }
52
53 // ---- options / variants -------------------------------------------------------------------------
54
55 export function buildOptions ( options = []) {
56 return options. map (( o ) => {
57 const color = o.type === "color" ;
58 return {
59 name: o.name,
60 optionRenderType: color ? "SWATCH_CHOICES" : "TEXT_CHOICES" ,
61 choicesSettings: {
62 choices: o.choices. map (( c ) =>
63 color
64 ? { choiceType: "ONE_COLOR" , name: c.name, colorCode: c.colorCode }
65 : { choiceType: "CHOICE_TEXT" , name: typeof c === "string" ? c : c.name }),
66 },
67 };
68 });
69 }
70
71 // Modifiers collect buyer input WITHOUT creating variants (gift wrap, engraving) — defined inline
72 // like options, each becomes a customization. `type: "text"` → FREE_TEXT with the merchant's
73 // character limits; anything else → TEXT_CHOICES. `mandatory` defaults to TRUE, which is how the
74 // storefront reads an omitted flag.
75 export function buildModifiers ( modifiers = []) {
76 return modifiers. map (( m ) => {
77 const text = m.type === "text" ;
78 return {
79 name: m.name,
80 mandatory: m.mandatory !== false ,
81 ... (text
82 ? {
83 modifierRenderType: "FREE_TEXT" ,
84 freeTextSettings: {
85 title: m.title ?? m.name,
86 ... (m.maxChars ? { maxCharCount: m.maxChars } : {}),
87 ... (m.minChars ? { minCharCount: m.minChars } : {}),
88 },
89 }
90 : {
91 modifierRenderType: "TEXT_CHOICES" ,
92 choicesSettings: {
93 choices: (m.choices ?? []). map (( c ) => ({
94 choiceType: "CHOICE_TEXT" ,
95 name: typeof c === "string" ? c : c.name,
96 ... ( typeof c === "object" && c.addedPrice != null ? { addedPrice: String (c.addedPrice) } : {}),
97 })),
98 },
99 }),
100 };
101 });
102 }
103
104 // Info sections (materials, shipping, care) — inline definitions; the same title on two products
105 // shares one section (uniqueName is derived from the title).
106 export function buildInfoSections ( sections = [], i ) {
107 return sections. map (( s , n ) => ({
108 uniqueName: String (s.title). toLowerCase (). replace ( / [ ^ a-z0-9] + / g , "-" ). replace ( / ^ - | - $ / g , "" ). slice ( 0 , 100 ) || `section-${ n }` ,
109 title: s.title,
110 description: mkDesc (s.description, `info-${ i }-${ n }` ),
111 }));
112 }
113
114 // Pre-order needs counted stock: `preorderInfo` on the inventory item, enabled with the merchant's
115 // message and the number of units buyers may pre-order once stock hits zero.
116 export function preorderInfo ( preorder ) {
117 if ( ! preorder) return { enabled: false };
118 return {
119 enabled: true ,
120 ... (preorder.message ? { message: preorder.message } : {}),
121 ... (Number. isInteger (preorder.limit) ? { limit: preorder.limit } : {}),
122 };
123 }
124
125 // Full Cartesian product, each variant priced/stocked from the product; visible:true baked in.
126 // `variantPrices` prices a variant by one of its choice names ("Large": 32) — the first choice
127 // with a price wins; the product's compareAtPrice is kept only when it stays above that price.
128 export function expandVariants ( options = [], { price , compareAtPrice , quantity , inStock , preorder , variantPrices = {} }, digitalFileId ) {
129 const priceOf = ( amount ) => ({
130 actualPrice: { amount: String (amount) },
131 ... (compareAtPrice && Number (compareAtPrice) > Number (amount) ? { compareAtPrice: { amount: String (compareAtPrice) } } : {}),
132 });
133 const base = {
134 price: priceOf (price),
135 visible: true ,
136 ... (digitalFileId
137 ? { digitalProperties: { digitalFile: { id: digitalFileId } }, inventoryItem: { inStock: true } }
138 // inStock:true == untracked stock — always buyable, no count. Otherwise track a quantity.
139 : { physicalProperties: {}, inventoryItem: inStock === true
140 ? { inStock: true }
141 : { quantity: quantity ?? 0 , preorderInfo: preorderInfo (preorder) } }),
142 };
143 if ( ! options. length ) return [base];
144 let combos = [[]];
145 for ( const o of options) {
146 const rt = o.type === "color" ? "SWATCH_CHOICES" : "TEXT_CHOICES" ;
147 const names = o.choices. map (( c ) => ( typeof c === "string" ? c : c.name));
148 combos = combos. flatMap (( combo ) =>
149 names. map (( choiceName ) => [ ... combo, { optionChoiceNames: { optionName: o.name, choiceName, renderType: rt } }]));
150 }
151 return combos. map (( choices ) => {
152 const override = choices. map (( c ) => variantPrices[c.optionChoiceNames.choiceName]). find (( v ) => v != null );
153 return { ... base, choices, ... (override != null ? { price: priceOf (override) } : {}) };
154 });
155 }
156
157 // Reject plans the API would reject halfway through, while nothing has been created yet —
158 // a mid-batch 400 leaves a half-seeded store that the agent then has to reason about.
159 export function validateProducts ( products ) {
160 const problems = [];
161 const colorByName = new Map ();
162 products. forEach (( p , i ) => {
163 const where = p.name ? `"${ p . name }"` : `product #${ i + 1 }` ;
164 if ( ! p.name) problems. push ( `${ where }: name is required` );
165 if (p.quantity != null && ( ! Number. isInteger (p.quantity) || p.quantity < 0 )) {
166 problems. push ( `${ where }: quantity must be a non-negative integer (got ${ p . quantity }) — omit it and set inStock:true for untracked stock` );
167 }
168 if (p.preorder && (p.inStock === true || p.digitalFilePath || p.digitalFileUrl)) {
169 problems. push ( `${ where }: preorder needs counted stock (a quantity) on a physical product` );
170 }
171 if (p.preorder?.limit != null && ( ! Number. isInteger (p.preorder.limit) || p.preorder.limit < 1 )) {
172 problems. push ( `${ where }: preorder.limit must be a positive integer` );
173 }
174 for ( const m of p.modifiers ?? []) {
175 if ( ! m?.name) problems. push ( `${ where }: every modifier needs a name` );
176 else if (m.type !== "text" && ! (m.choices?. length > 0 )) problems. push ( `${ where }: modifier "${ m . name }" needs choices (or type: "text")` );
177 }
178 for ( const s of p.infoSections ?? []) if ( ! s?.title) problems. push ( `${ where }: every info section needs a title` );
179 const choiceNames = new Set ((p.options ?? []). flatMap (( o ) => (o.choices ?? []). map (( c ) => ( typeof c === "string" ? c : c?.name))));
180 for ( const [ choice , amount ] of Object. entries (p.variantPrices ?? {})) {
181 if ( ! choiceNames. has (choice)) problems. push ( `${ where }: variantPrices names "${ choice }", which is not a choice of its options` );
182 if ( ! ( Number (amount) >= 0 )) problems. push ( `${ where }: variantPrices["${ choice }"] must be a non-negative number` );
183 }
184 for ( const opt of p.options ?? []) {
185 const seen = new Set ();
186 for ( const c of opt.choices ?? []) {
187 const key = typeof c === "string" ? c : c?.name;
188 if (seen. has (key)) problems. push ( `${ where }: option "${ opt . name }" repeats the choice "${ key }"` );
189 seen. add (key);
190 // Wix keys a color choice by name, so the same name with two codes collides.
191 const code = typeof c === "object" ? c?.colorCode : undefined ;
192 if (code) {
193 const prev = colorByName. get (key);
194 if (prev && prev !== code) problems. push ( `color "${ key }" is ${ prev } on one product and ${ code } on another — pick one` );
195 colorByName. set (key, code);
196 }
197 }
198 }
199 });
200 if (problems. length ) throw new Error ( `invalid seed plan: \n - ${ problems . join ( " \n - " ) }` );
201 }