Setting the file. One moment.
Generate Auto Patterns · Wix App · wix/skills · Skills Docs
ContentsBack to the top of the page scripts/generate-auto-patterns.js
scripts/ generate-auto-patterns.js
JavaScript · 510 lines · 15 KB
16 *
17 * <SKILL_ROOT> is the absolute path to the wix-app skill bundle (the folder containing SKILL.md).
18 * The script is not installed in the user's app repo — invoke it by absolute path from the project directory.
19 *
20 * Input JSON shape:
21 * {
22 * "collection": {
23 * "idSuffix": "additional-fees",
24 * "fields": [{ "key": "feeTitle", "displayName": "Fee Title", "type": "TEXT" }, ...]
25 * },
26 * "schema": {
27 * "content": { "collectionRouteId": "...", ... (20 string fields) },
28 * "layout": { "main": [...], "sidebar": [...] },
29 * "columns": [{ "id": "feeTitle", "displayName": "Title" }],
30 * "gridItem": null | { "titleFieldId": "...", ... }
31 * },
32 * "relevantCollectionId": "my-namespace/additional-fees",
33 * "extensionName": "Additional Fees Manager"
34 * }
35 *
36 * Output:
37 * Writes patterns.json and <folder>.tsx to the specified output directory.
38 * Prints JSON result to stdout: { "files": ["patterns.json", "<folder>.tsx"] }
39 *
40 * Exit codes:
41 * 0 - Success
42 * 1 - Invalid arguments or missing required fields
43 * 2 - File system error
44 */
45
46 import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs' ;
47 import { basename, join, resolve } from 'path' ;
48
49 // --- Argument parsing ---
50
51 const args = process.argv. slice ( 2 );
52
53 function getArg ( name ) {
54 const idx = args. indexOf ( `--${ name }` );
55 if (idx === - 1 || idx + 1 >= args. length ) return null ;
56 return args[idx + 1 ];
57 }
58
59 if (args. includes ( '--help' ) || args. includes ( '-h' )) {
60 console. log ( `Auto Patterns Generator
61
62 Usage:
63 node <SKILL_ROOT>/scripts/generate-auto-patterns.js --input <path> --output <dir>
64
65 <SKILL_ROOT> is the absolute path to the wix-app skill bundle (the folder containing SKILL.md).
66
67 Options:
68 --input Path to input JSON file (required)
69 --output Target directory for generated files (required)
70 --help Show this help message
71
72 Input JSON shape:
73 {
74 "collection": {
75 "idSuffix": "string",
76 "fields": [{ "key": "string", "displayName": "string", "type": "string" }]
77 },
78 "schema": {
79 "content": { "collectionRouteId": "string", ... },
80 "layout": { "main": [...], "sidebar": [...] },
81 "columns": [{ "id": "string", "displayName": "string" }],
82 "gridItem": null | { "titleFieldId": "string", ... }
83 },
84 "relevantCollectionId": "string",
85 "extensionName": "string"
86 }
87
88 Output:
89 Writes patterns.json and <folder>.tsx to the output directory.
90 Prints JSON to stdout: { "files": ["patterns.json", "<folder>.tsx"] }` );
91 process. exit ( 0 );
92 }
93
94 const inputPath = getArg ( 'input' );
95 const outputDir = getArg ( 'output' );
96
97 if ( ! inputPath) {
98 console. error ( 'Error: --input is required. Use --help for usage.' );
99 process. exit ( 1 );
100 }
101 if ( ! outputDir) {
102 console. error ( 'Error: --output is required. Use --help for usage.' );
103 process. exit ( 1 );
104 }
105
106 // --- Read and validate input ---
107
108 let input;
109 try {
110 const raw = readFileSync ( resolve (inputPath), 'utf-8' );
111 input = JSON . parse (raw);
112 } catch (err) {
113 console. error ( `Error: Failed to read input file: ${ err . message }` );
114 process. exit ( 1 );
115 }
116
117 const { collection , schema , relevantCollectionId } = input;
118
119 if ( ! collection || ! collection.idSuffix || ! Array. isArray (collection.fields)) {
120 console. error (
121 'Error: Input must include "collection" with "idSuffix" and "fields" array.' ,
122 );
123 process. exit ( 1 );
124 }
125 if (
126 ! schema ||
127 ! schema.content ||
128 ! schema.layout ||
129 ! Array. isArray (schema.columns)
130 ) {
131 console. error (
132 'Error: Input must include "schema" with "content", "layout", and "columns".' ,
133 );
134 process. exit ( 1 );
135 }
136
137 // --- Generator logic (mirrors AutoPatternsGenerator.ts) ---
138
139 function generatePatternsConfig ( collection , schema ) {
140 const collectionRouteId = schema.content.collectionRouteId;
141 const singularEntityName = schema.content.singularEntityName;
142
143 // Build field map
144 const fieldMap = new Map ();
145 for ( const field of collection.fields) {
146 if (field.key && field.displayName) {
147 fieldMap. set (field.key, field);
148 }
149 }
150
151 const sortableFieldTypes = [ 'TEXT' , 'DATE' , 'NUMBER' , 'BOOLEAN' , 'URL' ];
152
153 // Generate columns
154 const columns = schema.columns
155 . map (( columnConfig ) => {
156 const field = fieldMap. get (columnConfig.id);
157 if ( ! field || ! field.key || ! field.type) return null ;
158
159 let width = '200px' ;
160 if ([ 'BOOLEAN' , 'IMAGE' , 'NUMBER' ]. includes (field.type)) width = '100px' ;
161 if (field.type === 'URL' ) width = '300px' ;
162
163 return {
164 id: field.key,
165 name: columnConfig.displayName || field.displayName || 'Field' ,
166 width,
167 sortable: sortableFieldTypes. includes (field.type || '' ),
168 };
169 })
170 . filter (Boolean);
171
172 // Generate filters
173 const filterableFieldTypes = [ 'DATE' , 'NUMBER' , 'BOOLEAN' ];
174 const filters = columns
175 . map (( column ) => {
176 const field = fieldMap. get (column.id);
177 if (
178 ! field ||
179 ! field.key ||
180 ! filterableFieldTypes. includes (field.type || '' )
181 )
182 return null ;
183
184 const baseFilter = {
185 id: `${ field . key }-filter` ,
186 fieldId: field.key,
187 displayName: field.displayName || '' ,
188 tagLabel: field.displayName || '' ,
189 };
190
191 if (field.type === 'DATE' ) {
192 return {
193 ... baseFilter,
194 dateConfig: {
195 mode: 'COMBINE' ,
196 presets: [
197 'TODAY' ,
198 'SEVEN_DAYS' ,
199 'MONTH' ,
200 'NEXT_SEVEN_DAYS' ,
201 'NEXT_THIRTY_DAYS' ,
202 ],
203 includeTime: false ,
204 },
205 };
206 }
207 if (field.type === 'NUMBER' ) {
208 return {
209 ... baseFilter,
210 numberConfig: { allowedDecimals: true },
211 };
212 }
213 if (field.type === 'BOOLEAN' ) {
214 return baseFilter;
215 }
216 return null ;
217 })
218 . filter (Boolean);
219
220 // Generate layout (Table + optional Grid)
221 const layout = [
222 {
223 type: 'Table' ,
224 table: {
225 columns,
226 customColumns: { enabled: true },
227 },
228 },
229 ];
230
231 if (schema.gridItem) {
232 layout. push ({
233 type: 'Grid' ,
234 grid: {
235 item: {
236 titleFieldId: schema.gridItem.titleFieldId,
237 ... (schema.gridItem.subtitleFieldId
238 ? { subtitleFieldId: schema.gridItem.subtitleFieldId }
239 : {}),
240 ... (schema.gridItem.imageFieldId
241 ? { imageFieldId: schema.gridItem.imageFieldId }
242 : {}),
243 cardContentMode: schema.gridItem.subtitleFieldId ? 'full' : 'title' ,
244 },
245 },
246 });
247 }
248
249 // Generate entity page layout
250 function generateEntityPageLayout ( layoutData ) {
251 if ( ! layoutData) return { main: [] };
252
253 const main = layoutData.main. map (( section ) => ({
254 type: 'card' ,
255 card: {
256 title: { text: section.title },
257 subtitle: { text: section.subtitle },
258 children: section.fields. map (( fieldKey ) => ({
259 type: 'field' ,
260 field: { span: 12 , fieldId: fieldKey },
261 })),
262 },
263 }));
264
265 const sidebar = layoutData.sidebar. map (( section ) => ({
266 type: 'card' ,
267 card: {
268 title: { text: section.title },
269 subtitle: { text: section.subtitle },
270 children: section.fields. map (( fieldKey ) => ({
271 type: 'field' ,
272 field: { span: 12 , fieldId: fieldKey },
273 })),
274 },
275 }));
276
277 return {
278 main,
279 ... (sidebar. length > 0 ? { sidebar } : {}),
280 };
281 }
282
283 // Use relevantCollectionId for the collection reference in the config
284 const collectionId = relevantCollectionId || collection.idSuffix;
285
286 return {
287 pages: [
288 {
289 id: `${ collectionRouteId }-collection` ,
290 type: 'collectionPage' ,
291 appMainPage: true ,
292 collectionPage: {
293 route: { path: '/' },
294 title: {
295 text: schema.content.pageTitle || '' ,
296 hideTotal: false ,
297 },
298 subtitle: {
299 text: schema.content.pageSubtitle || '' ,
300 },
301 actions: {
302 primaryActions: {
303 type: 'action' ,
304 action: {
305 item: {
306 id: `create-${ collectionRouteId }` ,
307 type: 'create' ,
308 label: schema.content.actionButtonLabel,
309 collection: {
310 collectionId,
311 entityTypeSource: 'cms' ,
312 },
313 create: {
314 mode: 'page' ,
315 page: { id: `${ collectionRouteId }-entity` },
316 },
317 },
318 },
319 },
320 },
321 components: [
322 {
323 type: 'collection' ,
324 layout,
325 entityPageId: `${ collectionRouteId }-entity` ,
326 collection: {
327 collectionId,
328 entityTypeSource: 'cms' ,
329 },
330 toolbarTitle: {
331 title: schema.content.toolbarTitle || '' ,
332 subtitle: {
333 text: schema.content.toolbarSubtitle || '' ,
334 },
335 showTotal: true ,
336 },
337 filters: { items: filters },
338 emptyState: {
339 title: schema.content.emptyStateTitle,
340 subtitle: schema.content.emptyStateSubtitle,
341 addNewCta: {
342 id: `create-${ collectionRouteId }` ,
343 text: schema.content.emptyStateButtonText,
344 },
345 },
346 actionCell: {
347 primaryAction: {
348 item: {
349 id: `edit-${ collectionRouteId }` ,
350 type: 'update' ,
351 update: {
352 mode: 'page' ,
353 page: { id: `${ collectionRouteId }-entity` },
354 },
355 },
356 },
357 secondaryActions: {
358 items: [
359 {
360 id: `delete-${ collectionRouteId }` ,
361 type: 'delete' ,
362 label: 'Delete' ,
363 delete: {
364 mode: 'modal' ,
365 modal: {
366 title: {
367 text: schema.content.deleteModalTitle || '' ,
368 },
369 description: {
370 text: schema.content.deleteModalDescription || '' ,
371 },
372 feedback: {
373 successToast: {
374 text: schema.content.deleteSuccessToast || '' ,
375 },
376 errorToast: {
377 text: schema.content.deleteErrorToast || '' ,
378 },
379 },
380 },
381 },
382 },
383 ],
384 },
385 },
386 bulkActionToolbar: {
387 primaryActions: [
388 {
389 type: 'action' ,
390 action: {
391 item: {
392 id: `bulk-delete-${ collectionRouteId }` ,
393 type: 'bulkDelete' ,
394 bulkDelete: {
395 mode: 'modal' ,
396 modal: {
397 title: {
398 text: schema.content.bulkDeleteModalTitle || '' ,
399 },
400 description: {
401 text:
402 schema.content.bulkDeleteModalDescription || '' ,
403 },
404 feedback: {
405 successToast: {
406 text:
407 schema.content.bulkDeleteSuccessToast || '' ,
408 },
409 errorToast: {
410 text: schema.content.bulkDeleteErrorToast || '' ,
411 },
412 },
413 },
414 },
415 },
416 },
417 },
418 ],
419 },
420 },
421 ],
422 },
423 },
424 {
425 id: `${ collectionRouteId }-entity` ,
426 type: 'entityPage' ,
427 entityPage: {
428 route: {
429 path: `/${ singularEntityName }/:entityId` ,
430 params: { id: 'entityId' },
431 },
432 title: { text: schema.content.entityPageTitle || '' },
433 subtitle: { text: schema.content.entityPageSubtitle },
434 parentPageId: `${ collectionRouteId }-collection` ,
435 layout: generateEntityPageLayout (schema.layout),
436 collectionId,
437 entityTypeSource: 'cms' ,
438 },
439 },
440 ],
441 };
442 }
443
444 function generatePageTsx () {
445 return `import React from 'react';
446 import { WixDesignSystemProvider } from '@wix/design-system';
447 import '@wix/design-system/styles.global.css';
448 import { WixPatternsProvider } from '@wix/patterns/provider';
449 import { PatternsWizardOverridesProvider, AutoPatternsApp, type AppConfig } from '@wix/auto-patterns';
450 import { withDashboard } from '@wix/patterns';
451 import config from './patterns.json';
452
453 const Index: React.FC = () => {
454 return (
455 <WixDesignSystemProvider features={{ newColorsBranding: true }}>
456 <WixPatternsProvider>
457 <PatternsWizardOverridesProvider value={{}}>
458 <AutoPatternsApp configuration={config as AppConfig} />
459 </PatternsWizardOverridesProvider>
460 </WixPatternsProvider>
461 </WixDesignSystemProvider>
462 );
463 };
464
465 export default withDashboard(Index);
466 ` ;
467 }
468
469 // --- Generate and write output ---
470
471 const resolvedOutput = resolve (outputDir);
472
473 try {
474 if ( ! existsSync (resolvedOutput)) {
475 mkdirSync (resolvedOutput, { recursive: true });
476 }
477 } catch (err) {
478 console. error ( `Error: Failed to create output directory: ${ err . message }` );
479 process. exit ( 2 );
480 }
481
482 const patternsConfig = generatePatternsConfig (collection, schema);
483 const pageTsx = generatePageTsx ();
484
485 // The Wix CLI scaffolds the page component as `<folder>.tsx` and registers THAT
486 // file in the generated `<folder>.extension.ts`. Write the auto-patterns wrapper
487 // to the same filename so it overwrites the CLI stub and is wired up with no
488 // manual edit to the builder file. (Writing `page.tsx` would leave the wrapper
489 // unregistered next to the empty stub the CLI registered.)
490 const componentFileName = `${ basename ( resolvedOutput ) }.tsx` ;
491
492 try {
493 writeFileSync (
494 join (resolvedOutput, 'patterns.json' ),
495 JSON . stringify (patternsConfig, null , 2 ),
496 );
497 writeFileSync ( join (resolvedOutput, componentFileName), pageTsx);
498 } catch (err) {
499 console. error ( `Error: Failed to write output files: ${ err . message }` );
500 process. exit ( 2 );
501 }
502
503 // Print structured result to stdout
504 console. log (
505 JSON . stringify ({
506 success: true ,
507 files: [ 'patterns.json' , componentFileName],
508 outputDir: resolvedOutput,
509 }),
510 );