@etohq/admin-vite-plugin 1.0.0

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/dist/index.mjs ADDED
@@ -0,0 +1,1866 @@
1
+ // src/plugin.ts
2
+ import path2 from "path";
3
+
4
+ // src/custom-fields/generate-custom-field-displays.ts
5
+ import {
6
+ isValidCustomFieldDisplayPath,
7
+ isValidCustomFieldDisplayZone
8
+ } from "@etohq/admin-shared";
9
+ import fs from "fs/promises";
10
+
11
+ // src/babel.ts
12
+ import { parse } from "@babel/parser";
13
+ import _traverse from "@babel/traverse";
14
+ import {
15
+ isArrayExpression,
16
+ isCallExpression,
17
+ isFunctionDeclaration,
18
+ isIdentifier,
19
+ isJSXElement,
20
+ isJSXFragment,
21
+ isMemberExpression,
22
+ isObjectExpression,
23
+ isObjectProperty,
24
+ isStringLiteral,
25
+ isTemplateLiteral,
26
+ isVariableDeclaration,
27
+ isVariableDeclarator
28
+ } from "@babel/types";
29
+ var traverse;
30
+ if (typeof _traverse === "function") {
31
+ traverse = _traverse;
32
+ } else {
33
+ traverse = _traverse.default;
34
+ }
35
+
36
+ // src/logger.ts
37
+ import colors from "picocolors";
38
+ function getTimestamp() {
39
+ const now = /* @__PURE__ */ new Date();
40
+ return now.toLocaleTimeString("en-US", { hour12: true });
41
+ }
42
+ function getPrefix(type) {
43
+ const timestamp = colors.dim(getTimestamp());
44
+ const typeColor = type === "warn" ? colors.yellow : type === "info" ? colors.green : colors.red;
45
+ const prefix = typeColor("[@etohq/admin-vite-plugin]");
46
+ return `${timestamp} ${prefix}`;
47
+ }
48
+ function getFile(options) {
49
+ if (!options.file) {
50
+ return "";
51
+ }
52
+ const value = Array.isArray(options.file) ? options.file.map((f) => f).join(", ") : options.file;
53
+ return colors.dim(`${value}`);
54
+ }
55
+ function formatError(error) {
56
+ if (error instanceof Error) {
57
+ return colors.red(`${error.name}: ${error.message}
58
+ ${error.stack}`);
59
+ } else if (typeof error === "object") {
60
+ return colors.red(JSON.stringify(error, null, 2));
61
+ } else {
62
+ return colors.red(String(error));
63
+ }
64
+ }
65
+ var logger = {
66
+ warn(msg, options = {}) {
67
+ console.warn(`${getPrefix("warn")} ${msg} ${getFile(options)}`);
68
+ },
69
+ info(msg, options = {}) {
70
+ console.info(`${getPrefix("info")} ${msg} ${getFile(options)}`);
71
+ },
72
+ error(msg, options = {}) {
73
+ console.error(`${getPrefix("error")} ${msg} ${getFile(options)}`);
74
+ if (options.error) {
75
+ console.error(formatError(options.error));
76
+ }
77
+ }
78
+ };
79
+
80
+ // src/utils.ts
81
+ import { fdir } from "fdir";
82
+ import MagicString from "magic-string";
83
+ import crypto from "crypto";
84
+ import path from "path";
85
+ function normalizePath(file) {
86
+ return path.normalize(file).replace(/\\/g, "/");
87
+ }
88
+ function getParserOptions(file) {
89
+ const options = {
90
+ sourceType: "module",
91
+ plugins: ["jsx"]
92
+ };
93
+ if (file.endsWith(".tsx")) {
94
+ options.plugins?.push("typescript");
95
+ }
96
+ return options;
97
+ }
98
+ function generateModule(code) {
99
+ const magicString = new MagicString(code);
100
+ return {
101
+ code: magicString.toString(),
102
+ map: magicString.generateMap({ hires: true })
103
+ };
104
+ }
105
+ var VALID_FILE_EXTENSIONS = [".tsx", ".jsx"];
106
+ async function crawl(dir, file, depth) {
107
+ const dirDepth = dir.split(path.sep).length;
108
+ const crawler = new fdir().withBasePath().exclude((dirName) => dirName.startsWith("_")).filter((path3) => {
109
+ return VALID_FILE_EXTENSIONS.some((ext) => path3.endsWith(ext));
110
+ });
111
+ if (file) {
112
+ crawler.filter((path3) => {
113
+ return VALID_FILE_EXTENSIONS.some((ext) => path3.endsWith(file + ext));
114
+ });
115
+ }
116
+ if (depth) {
117
+ crawler.filter((file2) => {
118
+ const pathDepth = file2.split(path.sep).length - 1;
119
+ if (depth.max && pathDepth > dirDepth + depth.max) {
120
+ return false;
121
+ }
122
+ if (pathDepth < dirDepth + depth.min) {
123
+ return false;
124
+ }
125
+ return true;
126
+ });
127
+ }
128
+ return crawler.crawl(dir).withPromise();
129
+ }
130
+ function getConfigObjectProperties(path3) {
131
+ const declaration = path3.node.declaration;
132
+ if (isVariableDeclaration(declaration)) {
133
+ const configDeclaration = declaration.declarations.find(
134
+ (d) => isVariableDeclarator(d) && isIdentifier(d.id, { name: "config" })
135
+ );
136
+ if (configDeclaration && isCallExpression(configDeclaration.init) && configDeclaration.init.arguments.length > 0 && isObjectExpression(configDeclaration.init.arguments[0])) {
137
+ return configDeclaration.init.arguments[0].properties;
138
+ }
139
+ }
140
+ return null;
141
+ }
142
+ async function hasDefaultExport(ast) {
143
+ let hasDefaultExport2 = false;
144
+ traverse(ast, {
145
+ ExportDefaultDeclaration() {
146
+ hasDefaultExport2 = true;
147
+ }
148
+ });
149
+ return hasDefaultExport2;
150
+ }
151
+ function generateHash(content) {
152
+ return crypto.createHash("md5").update(content).digest("hex");
153
+ }
154
+ function isFileInAdminSubdirectory(file, subdirectory) {
155
+ const normalizedPath = normalizePath(file);
156
+ return normalizedPath.includes(`/src/admin/${subdirectory}/`);
157
+ }
158
+
159
+ // src/custom-fields/helpers.ts
160
+ import {
161
+ isValidCustomFieldModel
162
+ } from "@etohq/admin-shared";
163
+ function getModel(path3, file) {
164
+ const configArgument = getConfigArgument(path3);
165
+ if (!configArgument) {
166
+ return null;
167
+ }
168
+ const modelProperty = configArgument.properties.find(
169
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "model" })
170
+ );
171
+ if (!modelProperty) {
172
+ return null;
173
+ }
174
+ if (isTemplateLiteral(modelProperty.value)) {
175
+ logger.warn(
176
+ `'model' property cannot be a template literal (e.g. \`product\`).`,
177
+ { file }
178
+ );
179
+ return null;
180
+ }
181
+ if (!isStringLiteral(modelProperty.value)) {
182
+ logger.warn(
183
+ `'model' is invalid. The 'model' property must be a string literal, e.g. 'product' or 'customer'.`,
184
+ { file }
185
+ );
186
+ return null;
187
+ }
188
+ const model = modelProperty.value.value.trim();
189
+ if (!isValidCustomFieldModel(model)) {
190
+ logger.warn(
191
+ `'model' is invalid, received: ${model}. The 'model' property must be set to a valid model, e.g. 'product' or 'customer'.`,
192
+ { file }
193
+ );
194
+ return null;
195
+ }
196
+ return model;
197
+ }
198
+ function getConfigArgument(path3) {
199
+ if (!isCallExpression(path3.node.declaration)) {
200
+ return null;
201
+ }
202
+ if (!isIdentifier(path3.node.declaration.callee, {
203
+ name: "unstable_defineCustomFieldsConfig"
204
+ })) {
205
+ return null;
206
+ }
207
+ const configArgument = path3.node.declaration.arguments[0];
208
+ if (!isObjectExpression(configArgument)) {
209
+ return null;
210
+ }
211
+ return configArgument;
212
+ }
213
+ function validateLink(path3, file) {
214
+ const configArgument = getConfigArgument(path3);
215
+ if (!configArgument) {
216
+ return false;
217
+ }
218
+ const linkProperty = configArgument.properties.find(
219
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "link" })
220
+ );
221
+ if (!linkProperty) {
222
+ logger.warn(`'link' property is missing.`, { file });
223
+ return false;
224
+ }
225
+ return true;
226
+ }
227
+
228
+ // src/custom-fields/generate-custom-field-displays.ts
229
+ async function generateCustomFieldDisplays(sources) {
230
+ const files = await getFilesFromSources(sources);
231
+ const results = await getCustomFieldDisplayResults(files);
232
+ const imports = results.map((result) => result.import).flat();
233
+ const code = generateDisplayCode(results);
234
+ return {
235
+ imports,
236
+ code
237
+ };
238
+ }
239
+ async function getFilesFromSources(sources) {
240
+ const files = (await Promise.all(
241
+ Array.from(sources).map(
242
+ async (source) => crawl(`${source}/custom-fields`)
243
+ )
244
+ )).flat();
245
+ return files;
246
+ }
247
+ function generateDisplayCode(results) {
248
+ const groupedByModel = /* @__PURE__ */ new Map();
249
+ results.forEach((result) => {
250
+ const model = result.model;
251
+ if (!groupedByModel.has(model)) {
252
+ groupedByModel.set(model, []);
253
+ }
254
+ groupedByModel.get(model).push(result);
255
+ });
256
+ const segments = [];
257
+ groupedByModel.forEach((results2, model) => {
258
+ const displays = results2.map((result) => formatDisplays(result.displays)).filter((display) => display !== "").join(",\n");
259
+ segments.push(`
260
+ ${model}: [
261
+ ${displays}
262
+ ],
263
+ `);
264
+ });
265
+ return `
266
+ displays: {
267
+ ${segments.join("\n")}
268
+ }
269
+ `;
270
+ }
271
+ function formatDisplays(displays) {
272
+ if (!displays || displays.length === 0) {
273
+ return "";
274
+ }
275
+ return displays.map(
276
+ (display) => `
277
+ {
278
+ zone: "${display.zone}",
279
+ Component: ${display.Component},
280
+ }
281
+ `
282
+ ).join(",\n");
283
+ }
284
+ async function getCustomFieldDisplayResults(files) {
285
+ return (await Promise.all(
286
+ files.map(async (file, index) => parseDisplayFile(file, index))
287
+ )).filter(Boolean);
288
+ }
289
+ async function parseDisplayFile(file, index) {
290
+ const content = await fs.readFile(file, "utf8");
291
+ let ast;
292
+ try {
293
+ ast = parse(content, getParserOptions(file));
294
+ } catch (e) {
295
+ logger.error(`An error occurred while parsing the file`, { file, error: e });
296
+ return null;
297
+ }
298
+ const import_ = generateImport(file, index);
299
+ let displays = null;
300
+ let model = null;
301
+ let hasLink = false;
302
+ try {
303
+ traverse(ast, {
304
+ ExportDefaultDeclaration(path3) {
305
+ const _model = getModel(path3, file);
306
+ if (!_model) {
307
+ return;
308
+ }
309
+ model = _model;
310
+ displays = getDisplays(path3, model, index, file);
311
+ hasLink = validateLink(path3, file);
312
+ }
313
+ });
314
+ } catch (err) {
315
+ logger.error(`An error occurred while traversing the file.`, {
316
+ file,
317
+ error: err
318
+ });
319
+ return null;
320
+ }
321
+ if (!model) {
322
+ logger.warn(`'model' property is missing.`, { file });
323
+ return null;
324
+ }
325
+ if (!hasLink) {
326
+ logger.warn(`'link' property is missing.`, { file });
327
+ return null;
328
+ }
329
+ return {
330
+ import: import_,
331
+ model,
332
+ displays
333
+ };
334
+ }
335
+ function getDisplays(path3, model, index, file) {
336
+ const configArgument = getConfigArgument(path3);
337
+ if (!configArgument) {
338
+ return null;
339
+ }
340
+ const displayProperty = configArgument.properties.find(
341
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "displays" })
342
+ );
343
+ if (!displayProperty) {
344
+ return null;
345
+ }
346
+ if (!isArrayExpression(displayProperty.value)) {
347
+ logger.warn(
348
+ `'displays' is not an array. The 'displays' property must be an array of objects.`,
349
+ { file }
350
+ );
351
+ return null;
352
+ }
353
+ const displays = [];
354
+ displayProperty.value.elements.forEach((element, j) => {
355
+ if (!isObjectExpression(element)) {
356
+ return;
357
+ }
358
+ const zoneProperty = element.properties.find(
359
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "zone" })
360
+ );
361
+ if (!zoneProperty) {
362
+ logger.warn(
363
+ `'zone' property is missing at the ${j} index of the 'displays' property.`,
364
+ { file }
365
+ );
366
+ return;
367
+ }
368
+ if (!isStringLiteral(zoneProperty.value)) {
369
+ logger.warn(
370
+ `'zone' property at index ${j} in the 'displays' property is not a string literal. 'zone' must be a string literal, e.g. 'general' or 'attributes'.`,
371
+ { file }
372
+ );
373
+ return;
374
+ }
375
+ const zone = zoneProperty.value.value;
376
+ const fullPath = getDisplayEntryPath(model, zone);
377
+ if (!isValidCustomFieldDisplayZone(zone) || !isValidCustomFieldDisplayPath(fullPath)) {
378
+ logger.warn(
379
+ `'zone' is invalid at index ${j} in the 'displays' property. Received: ${zone}.`,
380
+ { file }
381
+ );
382
+ return;
383
+ }
384
+ const componentProperty = element.properties.find(
385
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "component" })
386
+ );
387
+ if (!componentProperty) {
388
+ logger.warn(
389
+ `'component' property is missing at index ${j} in the 'displays' property.`,
390
+ { file }
391
+ );
392
+ return;
393
+ }
394
+ displays.push({
395
+ zone,
396
+ Component: getDisplayComponent(index, j)
397
+ });
398
+ });
399
+ return displays.length > 0 ? displays : null;
400
+ }
401
+ function getDisplayEntryPath(model, zone) {
402
+ return `${model}.${zone}.$display`;
403
+ }
404
+ function getDisplayComponent(fileIndex, displayEntryIndex) {
405
+ const import_ = generateCustomFieldConfigName(fileIndex);
406
+ return `${import_}.displays[${displayEntryIndex}].component`;
407
+ }
408
+ function generateCustomFieldConfigName(index) {
409
+ return `CustomFieldConfig${index}`;
410
+ }
411
+ function generateImport(file, index) {
412
+ const path3 = normalizePath(file);
413
+ return `import ${generateCustomFieldConfigName(index)} from "${path3}"`;
414
+ }
415
+
416
+ // src/custom-fields/generate-custom-field-forms.ts
417
+ import {
418
+ isValidCustomFieldFormConfigPath,
419
+ isValidCustomFieldFormFieldPath,
420
+ isValidCustomFieldFormTab,
421
+ isValidCustomFieldFormZone
422
+ } from "@etohq/admin-shared";
423
+ import fs2 from "fs/promises";
424
+ import { outdent } from "outdent";
425
+ async function generateCustomFieldForms(sources) {
426
+ const files = await getFilesFromSources2(sources);
427
+ const results = await getCustomFieldResults(files);
428
+ const imports = results.map((result) => result.import).flat();
429
+ const code = generateCode(results);
430
+ return {
431
+ imports,
432
+ code
433
+ };
434
+ }
435
+ async function getFilesFromSources2(sources) {
436
+ const files = (await Promise.all(
437
+ Array.from(sources).map(
438
+ async (source) => crawl(`${source}/custom-fields`)
439
+ )
440
+ )).flat();
441
+ return files;
442
+ }
443
+ function generateCode(results) {
444
+ const groupedByModel = /* @__PURE__ */ new Map();
445
+ results.forEach((result) => {
446
+ const model = result.model;
447
+ if (!groupedByModel.has(model)) {
448
+ groupedByModel.set(model, []);
449
+ }
450
+ groupedByModel.get(model).push(result);
451
+ });
452
+ const segments = [];
453
+ groupedByModel.forEach((results2, model) => {
454
+ const configs = results2.map((result) => formatConfig(result.configs)).filter((config) => config !== "").join(",\n");
455
+ const forms = results2.map((result) => formatForms(result.forms)).filter((form) => form !== "").join(",\n");
456
+ segments.push(outdent`
457
+ ${model}: {
458
+ configs: [
459
+ ${configs}
460
+ ],
461
+ forms: [
462
+ ${forms}
463
+ ],
464
+ }
465
+ `);
466
+ });
467
+ return outdent`
468
+ customFields: {
469
+ ${segments.join("\n")}
470
+ }
471
+ `;
472
+ }
473
+ function formatConfig(configs) {
474
+ if (!configs || configs.length === 0) {
475
+ return "";
476
+ }
477
+ return outdent`
478
+ ${configs.map(
479
+ (config) => outdent`
480
+ {
481
+ zone: "${config.zone}",
482
+ fields: {
483
+ ${config.fields.map(
484
+ (field) => `${field.name}: {
485
+ defaultValue: ${field.defaultValue},
486
+ validation: ${field.validation},
487
+ }`
488
+ ).join(",\n")}
489
+ },
490
+ }
491
+ `
492
+ ).join(",\n")}
493
+ `;
494
+ }
495
+ function formatForms(forms) {
496
+ if (!forms || forms.length === 0) {
497
+ return "";
498
+ }
499
+ return forms.map(
500
+ (form) => outdent`
501
+ {
502
+ zone: "${form.zone}",
503
+ tab: ${form.tab === void 0 ? void 0 : `"${form.tab}"`},
504
+ fields: {
505
+ ${form.fields.map(
506
+ (field) => `${field.name}: {
507
+ validation: ${field.validation},
508
+ Component: ${field.Component},
509
+ label: ${field.label},
510
+ description: ${field.description},
511
+ placeholder: ${field.placeholder},
512
+ }`
513
+ ).join(",\n")}
514
+ },
515
+ }
516
+ `
517
+ ).join(",\n");
518
+ }
519
+ async function getCustomFieldResults(files) {
520
+ return (await Promise.all(files.map(async (file, index) => parseFile(file, index)))).filter(Boolean);
521
+ }
522
+ async function parseFile(file, index) {
523
+ const content = await fs2.readFile(file, "utf8");
524
+ let ast;
525
+ try {
526
+ ast = parse(content, getParserOptions(file));
527
+ } catch (e) {
528
+ logger.error(`An error occurred while parsing the file`, { file, error: e });
529
+ return null;
530
+ }
531
+ const import_ = generateImport2(file, index);
532
+ let configs = [];
533
+ let forms = [];
534
+ let model = null;
535
+ let hasLink = false;
536
+ try {
537
+ traverse(ast, {
538
+ ExportDefaultDeclaration(path3) {
539
+ const _model = getModel(path3, file);
540
+ if (!_model) {
541
+ return;
542
+ }
543
+ model = _model;
544
+ hasLink = validateLink(path3, file);
545
+ configs = getConfigs(path3, model, index, file);
546
+ forms = getForms(path3, model, index, file);
547
+ }
548
+ });
549
+ } catch (err) {
550
+ logger.error(`An error occurred while traversing the file.`, {
551
+ file,
552
+ error: err
553
+ });
554
+ return null;
555
+ }
556
+ if (!model) {
557
+ logger.warn(`'model' property is missing.`, { file });
558
+ return null;
559
+ }
560
+ if (!hasLink) {
561
+ logger.warn(`'link' property is missing.`, { file });
562
+ return null;
563
+ }
564
+ return {
565
+ import: import_,
566
+ model,
567
+ configs,
568
+ forms
569
+ };
570
+ }
571
+ function generateCustomFieldConfigName2(index) {
572
+ return `CustomFieldConfig${index}`;
573
+ }
574
+ function generateImport2(file, index) {
575
+ const path3 = normalizePath(file);
576
+ return `import ${generateCustomFieldConfigName2(index)} from "${path3}"`;
577
+ }
578
+ function getForms(path3, model, index, file) {
579
+ const formArray = getFormsArgument(path3, file);
580
+ if (!formArray) {
581
+ return null;
582
+ }
583
+ const forms = [];
584
+ formArray.elements.forEach((element, j) => {
585
+ if (!isObjectExpression(element)) {
586
+ return;
587
+ }
588
+ const zoneProperty = element.properties.find(
589
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "zone" })
590
+ );
591
+ if (!zoneProperty) {
592
+ logger.warn(
593
+ `'zone' property is missing from the ${j} index of the 'forms' property. The 'zone' property is required to load a custom field form.`,
594
+ { file }
595
+ );
596
+ return;
597
+ }
598
+ if (!isStringLiteral(zoneProperty.value)) {
599
+ logger.warn(
600
+ `'zone' property at the ${j} index of the 'forms' property is not a string literal. The 'zone' property must be a string literal, e.g. 'general' or 'attributes'.`,
601
+ { file }
602
+ );
603
+ return;
604
+ }
605
+ const tabProperty = element.properties.find(
606
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "tab" })
607
+ );
608
+ let tab;
609
+ if (tabProperty) {
610
+ if (!isStringLiteral(tabProperty.value)) {
611
+ logger.warn(
612
+ `'tab' property at the ${j} index of the 'forms' property is not a string literal. The 'tab' property must be a string literal, e.g. 'general' or 'attributes'.`,
613
+ { file }
614
+ );
615
+ return;
616
+ }
617
+ tab = tabProperty.value.value;
618
+ }
619
+ if (tab && !isValidCustomFieldFormTab(tab)) {
620
+ logger.warn(
621
+ `'tab' property at the ${j} index of the 'forms' property is not a valid custom field form tab for the '${model}' model. Received: ${tab}.`,
622
+ { file }
623
+ );
624
+ return;
625
+ }
626
+ const zone = zoneProperty.value.value;
627
+ const fullPath = getFormEntryFieldPath(model, zone, tab);
628
+ if (!isValidCustomFieldFormZone(zone) || !isValidCustomFieldFormFieldPath(fullPath)) {
629
+ logger.warn(
630
+ `'zone' and 'tab' properties at the ${j} index of the 'forms' property are not a valid for the '${model}' model. Received: { zone: ${zone}, tab: ${tab} }.`,
631
+ { file }
632
+ );
633
+ return;
634
+ }
635
+ const fieldsObject = element.properties.find(
636
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "fields" })
637
+ );
638
+ if (!fieldsObject) {
639
+ logger.warn(
640
+ `The 'fields' property is missing at the ${j} index of the 'forms' property. The 'fields' property is required to load a custom field form.`,
641
+ { file }
642
+ );
643
+ return;
644
+ }
645
+ const fields = [];
646
+ if (!isObjectExpression(fieldsObject.value)) {
647
+ logger.warn(
648
+ `The 'fields' property at the ${j} index of the 'forms' property is malformed. The 'fields' property must be an object.`,
649
+ { file }
650
+ );
651
+ return;
652
+ }
653
+ fieldsObject.value.properties.forEach((field) => {
654
+ if (!isObjectProperty(field) || !isIdentifier(field.key)) {
655
+ return;
656
+ }
657
+ const name = field.key.name;
658
+ if (!isObjectExpression(field.value) && !(isCallExpression(field.value) && isMemberExpression(field.value.callee) && isIdentifier(field.value.callee.object) && isIdentifier(field.value.callee.property) && field.value.callee.object.name === "form" && field.value.callee.property.name === "define" && field.value.arguments.length === 1 && isObjectExpression(field.value.arguments[0]))) {
659
+ logger.warn(
660
+ `'${name}' property in the 'fields' property at the ${j} index of the 'forms' property in ${file} is malformed. The property must be an object or a call to form.define().`,
661
+ { file }
662
+ );
663
+ return;
664
+ }
665
+ const fieldObject = isObjectExpression(field.value) ? field.value : field.value.arguments[0];
666
+ const labelProperty = fieldObject.properties.find(
667
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "label" })
668
+ );
669
+ const descriptionProperty = fieldObject.properties.find(
670
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "description" })
671
+ );
672
+ const componentProperty = fieldObject.properties.find(
673
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "component" })
674
+ );
675
+ const validationProperty = fieldObject.properties.find(
676
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "validation" })
677
+ );
678
+ const placeholderProperty = fieldObject.properties.find(
679
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "placeholder" })
680
+ );
681
+ const label = getFormFieldSectionValue(
682
+ !!labelProperty,
683
+ index,
684
+ j,
685
+ name,
686
+ "label"
687
+ );
688
+ const description = getFormFieldSectionValue(
689
+ !!descriptionProperty,
690
+ index,
691
+ j,
692
+ name,
693
+ "description"
694
+ );
695
+ const placeholder = getFormFieldSectionValue(
696
+ !!placeholderProperty,
697
+ index,
698
+ j,
699
+ name,
700
+ "placeholder"
701
+ );
702
+ const component = getFormFieldSectionValue(
703
+ !!componentProperty,
704
+ index,
705
+ j,
706
+ name,
707
+ "component"
708
+ );
709
+ const validation = getFormFieldSectionValue(
710
+ !!validationProperty,
711
+ index,
712
+ j,
713
+ name,
714
+ "validation"
715
+ );
716
+ fields.push({
717
+ name,
718
+ label,
719
+ description,
720
+ Component: component,
721
+ validation,
722
+ placeholder
723
+ });
724
+ });
725
+ forms.push({
726
+ zone,
727
+ tab,
728
+ fields
729
+ });
730
+ });
731
+ return forms.length > 0 ? forms : null;
732
+ }
733
+ function getFormFieldSectionValue(exists, fileIndex, formIndex, fieldKey, value) {
734
+ if (!exists) {
735
+ return "undefined";
736
+ }
737
+ const import_ = generateCustomFieldConfigName2(fileIndex);
738
+ return `${import_}.forms[${formIndex}].fields.${fieldKey}.${value}`;
739
+ }
740
+ function getFormEntryFieldPath(model, zone, tab) {
741
+ return `${model}.${zone}.${tab ? `${tab}.` : ""}$field`;
742
+ }
743
+ function getConfigs(path3, model, index, file) {
744
+ const formArray = getFormsArgument(path3, file);
745
+ if (!formArray) {
746
+ logger.warn(`'forms' property is missing.`, { file });
747
+ return null;
748
+ }
749
+ const configs = [];
750
+ formArray.elements.forEach((element, j) => {
751
+ if (!isObjectExpression(element)) {
752
+ logger.warn(
753
+ `'forms' property at the ${j} index is malformed. The 'forms' property must be an object.`,
754
+ { file }
755
+ );
756
+ return;
757
+ }
758
+ const zoneProperty = element.properties.find(
759
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "zone" })
760
+ );
761
+ if (!zoneProperty) {
762
+ logger.warn(
763
+ `'zone' property is missing from the ${j} index of the 'forms' property.`,
764
+ { file }
765
+ );
766
+ return;
767
+ }
768
+ if (isTemplateLiteral(zoneProperty.value)) {
769
+ logger.warn(
770
+ `'zone' property at the ${j} index of the 'forms' property cannot be a template literal (e.g. \`general\`).`,
771
+ { file }
772
+ );
773
+ return;
774
+ }
775
+ if (!isStringLiteral(zoneProperty.value)) {
776
+ logger.warn(
777
+ `'zone' property at the ${j} index of the 'forms' property is not a string literal (e.g. 'general' or 'attributes').`,
778
+ { file }
779
+ );
780
+ return;
781
+ }
782
+ const zone = zoneProperty.value.value;
783
+ const fullPath = getFormEntryConfigPath(model, zone);
784
+ if (!isValidCustomFieldFormZone(zone) || !isValidCustomFieldFormConfigPath(fullPath)) {
785
+ logger.warn(
786
+ `'zone' property at the ${j} index of the 'forms' property is not a valid custom field form zone for the '${model}' model. Received: ${zone}.`
787
+ );
788
+ return;
789
+ }
790
+ const fieldsObject = element.properties.find(
791
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "fields" })
792
+ );
793
+ if (!fieldsObject) {
794
+ logger.warn(
795
+ `'fields' property is missing from the ${j} entry in the 'forms' property in ${file}.`,
796
+ { file }
797
+ );
798
+ return;
799
+ }
800
+ const fields = [];
801
+ if (!isObjectExpression(fieldsObject.value)) {
802
+ logger.warn(
803
+ `'fields' property at the ${j} index of the 'forms' property is malformed. The 'fields' property must be an object.`,
804
+ { file }
805
+ );
806
+ return;
807
+ }
808
+ fieldsObject.value.properties.forEach((field) => {
809
+ if (!isObjectProperty(field) || !isIdentifier(field.key)) {
810
+ return;
811
+ }
812
+ const name = field.key.name;
813
+ if (!isObjectExpression(field.value) && !(isCallExpression(field.value) && isMemberExpression(field.value.callee) && isIdentifier(field.value.callee.object) && isIdentifier(field.value.callee.property) && field.value.callee.object.name === "form" && field.value.callee.property.name === "define" && field.value.arguments.length === 1 && isObjectExpression(field.value.arguments[0]))) {
814
+ logger.warn(
815
+ `'${name}' property in the 'fields' property at the ${j} index of the 'forms' property in ${file} is malformed. The property must be an object or a call to form.define().`,
816
+ { file }
817
+ );
818
+ return;
819
+ }
820
+ const fieldObject = isObjectExpression(field.value) ? field.value : field.value.arguments[0];
821
+ const defaultValueProperty = fieldObject.properties.find(
822
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "defaultValue" })
823
+ );
824
+ if (!defaultValueProperty) {
825
+ logger.warn(
826
+ `'defaultValue' property is missing at the ${j} index of the 'forms' property in ${file}.`,
827
+ { file }
828
+ );
829
+ return;
830
+ }
831
+ const validationProperty = fieldObject.properties.find(
832
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "validation" })
833
+ );
834
+ if (!validationProperty) {
835
+ logger.warn(
836
+ `'validation' property is missing at the ${j} index of the 'forms' property in ${file}.`,
837
+ { file }
838
+ );
839
+ return;
840
+ }
841
+ const defaultValue = getFormFieldValue(index, j, name, "defaultValue");
842
+ const validation = getFormFieldValue(index, j, name, "validation");
843
+ fields.push({
844
+ name,
845
+ defaultValue,
846
+ validation
847
+ });
848
+ });
849
+ configs.push({
850
+ zone,
851
+ fields
852
+ });
853
+ });
854
+ return configs.length > 0 ? configs : null;
855
+ }
856
+ function getFormFieldValue(fileIndex, formIndex, fieldKey, value) {
857
+ const import_ = generateCustomFieldConfigName2(fileIndex);
858
+ return `${import_}.forms[${formIndex}].fields.${fieldKey}.${value}`;
859
+ }
860
+ function getFormEntryConfigPath(model, zone) {
861
+ return `${model}.${zone}.$config`;
862
+ }
863
+ function getFormsArgument(path3, file) {
864
+ const configArgument = getConfigArgument(path3);
865
+ if (!configArgument) {
866
+ return null;
867
+ }
868
+ const formProperty = configArgument.properties.find(
869
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "forms" })
870
+ );
871
+ if (!formProperty) {
872
+ return null;
873
+ }
874
+ if (!isArrayExpression(formProperty.value)) {
875
+ logger.warn(
876
+ `The 'forms' property is malformed. The 'forms' property must be an array of objects.`,
877
+ { file }
878
+ );
879
+ return null;
880
+ }
881
+ return formProperty.value;
882
+ }
883
+
884
+ // src/custom-fields/generate-custom-field-hashes.ts
885
+ import fs3 from "fs/promises";
886
+ async function generateCustomFieldHashes(sources) {
887
+ const files = await getFilesFromSources3(sources);
888
+ const contents = await Promise.all(files.map(getCustomFieldContents));
889
+ const linkContents = contents.map((c) => c.link).filter(Boolean);
890
+ const formContents = contents.map((c) => c.form).filter(Boolean);
891
+ const displayContents = contents.map((c) => c.display).filter(Boolean);
892
+ const totalLinkContent = linkContents.join("");
893
+ const totalFormContent = formContents.join("");
894
+ const totalDisplayContent = displayContents.join("");
895
+ return {
896
+ linkHash: generateHash(totalLinkContent),
897
+ formHash: generateHash(totalFormContent),
898
+ displayHash: generateHash(totalDisplayContent)
899
+ };
900
+ }
901
+ async function getFilesFromSources3(sources) {
902
+ return (await Promise.all(
903
+ Array.from(sources).map(
904
+ async (source) => crawl(`${source}/custom-fields`)
905
+ )
906
+ )).flat();
907
+ }
908
+ async function getCustomFieldContents(file) {
909
+ const code = await fs3.readFile(file, "utf-8");
910
+ let ast = null;
911
+ try {
912
+ ast = parse(code, getParserOptions(file));
913
+ } catch (e) {
914
+ logger.error(`An error occurred while parsing the file.`, {
915
+ file,
916
+ error: e
917
+ });
918
+ return { link: null, form: null, display: null };
919
+ }
920
+ let linkContent = null;
921
+ let formContent = null;
922
+ let displayContent = null;
923
+ try {
924
+ traverse(ast, {
925
+ ExportDefaultDeclaration(path3) {
926
+ const configArgument = getConfigArgument(path3);
927
+ if (!configArgument) {
928
+ return;
929
+ }
930
+ configArgument.properties.forEach((prop) => {
931
+ if (!isObjectProperty(prop) || !prop.key || !isIdentifier(prop.key)) {
932
+ return;
933
+ }
934
+ switch (prop.key.name) {
935
+ case "link":
936
+ linkContent = code.slice(prop.start, prop.end);
937
+ break;
938
+ case "forms":
939
+ formContent = code.slice(prop.start, prop.end);
940
+ break;
941
+ case "display":
942
+ displayContent = code.slice(prop.start, prop.end);
943
+ break;
944
+ }
945
+ });
946
+ }
947
+ });
948
+ } catch (e) {
949
+ logger.error(
950
+ `An error occurred while processing ${file}. See the below error for more details:
951
+ ${e}`,
952
+ { file, error: e }
953
+ );
954
+ return { link: null, form: null, display: null };
955
+ }
956
+ return { link: linkContent, form: formContent, display: displayContent };
957
+ }
958
+
959
+ // src/custom-fields/generate-custom-field-links.ts
960
+ import fs4 from "fs/promises";
961
+ async function generateCustomFieldLinks(sources) {
962
+ const files = await getFilesFromSources4(sources);
963
+ const results = await getCustomFieldLinkResults(files);
964
+ const imports = results.map((result) => result.import);
965
+ const code = generateCode2(results);
966
+ return {
967
+ imports,
968
+ code
969
+ };
970
+ }
971
+ async function getFilesFromSources4(sources) {
972
+ const files = (await Promise.all(
973
+ Array.from(sources).map(
974
+ async (source) => crawl(`${source}/custom-fields`)
975
+ )
976
+ )).flat();
977
+ return files;
978
+ }
979
+ function generateCode2(results) {
980
+ const groupedByModel = /* @__PURE__ */ new Map();
981
+ results.forEach((result) => {
982
+ const model = result.model;
983
+ if (!groupedByModel.has(model)) {
984
+ groupedByModel.set(model, []);
985
+ }
986
+ groupedByModel.get(model).push(result);
987
+ });
988
+ const segments = [];
989
+ groupedByModel.forEach((results2, model) => {
990
+ const links = results2.map((result) => result.link).join(",\n");
991
+ segments.push(`
992
+ ${model}: [
993
+ ${links}
994
+ ],
995
+ `);
996
+ });
997
+ return `
998
+ links: {
999
+ ${segments.join("\n")}
1000
+ }
1001
+ `;
1002
+ }
1003
+ async function getCustomFieldLinkResults(files) {
1004
+ return (await Promise.all(files.map(async (file, index) => parseFile2(file, index)))).filter(Boolean);
1005
+ }
1006
+ async function parseFile2(file, index) {
1007
+ const content = await fs4.readFile(file, "utf8");
1008
+ let ast;
1009
+ try {
1010
+ ast = parse(content, getParserOptions(file));
1011
+ } catch (e) {
1012
+ logger.error(`An error occurred while parsing the file`, { file, error: e });
1013
+ return null;
1014
+ }
1015
+ const import_ = generateImport3(file, index);
1016
+ let link = null;
1017
+ let model = null;
1018
+ try {
1019
+ traverse(ast, {
1020
+ ExportDefaultDeclaration(path3) {
1021
+ const _model = getModel(path3, file);
1022
+ if (!_model) {
1023
+ return;
1024
+ }
1025
+ model = _model;
1026
+ link = getLink(path3, index, file);
1027
+ }
1028
+ });
1029
+ } catch (err) {
1030
+ logger.error(`An error occurred while traversing the file.`, {
1031
+ file,
1032
+ error: err
1033
+ });
1034
+ return null;
1035
+ }
1036
+ if (!link || !model) {
1037
+ return null;
1038
+ }
1039
+ return {
1040
+ import: import_,
1041
+ model,
1042
+ link
1043
+ };
1044
+ }
1045
+ function generateCustomFieldConfigName3(index) {
1046
+ return `CustomFieldConfig${index}`;
1047
+ }
1048
+ function generateImport3(file, index) {
1049
+ const path3 = normalizePath(file);
1050
+ return `import ${generateCustomFieldConfigName3(index)} from "${path3}"`;
1051
+ }
1052
+ function getLink(path3, index, file) {
1053
+ const configArgument = getConfigArgument(path3);
1054
+ if (!configArgument) {
1055
+ return null;
1056
+ }
1057
+ const linkProperty = configArgument.properties.find(
1058
+ (p) => isObjectProperty(p) && isIdentifier(p.key, { name: "link" })
1059
+ );
1060
+ if (!linkProperty) {
1061
+ logger.warn(`'link' is missing.`, { file });
1062
+ return null;
1063
+ }
1064
+ const import_ = generateCustomFieldConfigName3(index);
1065
+ return `${import_}.link`;
1066
+ }
1067
+
1068
+ // src/routes/generate-menu-items.ts
1069
+ import fs5 from "fs/promises";
1070
+ import { outdent as outdent2 } from "outdent";
1071
+
1072
+ // src/routes/helpers.ts
1073
+ function getRoute(file) {
1074
+ const importPath = normalizePath(file);
1075
+ return importPath.replace(/.*\/admin\/(routes)/, "").replace(/\[([^\]]+)\]/g, ":$1").replace(/\/page\.(tsx|jsx)/, "");
1076
+ }
1077
+
1078
+ // src/routes/generate-menu-items.ts
1079
+ import { NESTED_ROUTE_POSITIONS } from "@etohq/admin-shared";
1080
+ async function generateMenuItems(sources) {
1081
+ const files = await getFilesFromSources5(sources);
1082
+ const results = await getMenuItemResults(files);
1083
+ const imports = results.map((result) => result.import);
1084
+ const code = generateCode3(results);
1085
+ return { imports, code };
1086
+ }
1087
+ function generateCode3(results) {
1088
+ return outdent2`
1089
+ menuItems: [
1090
+ ${results.map((result) => formatMenuItem(result.menuItem)).join(",\n")}
1091
+ ]
1092
+ }
1093
+ `;
1094
+ }
1095
+ function formatMenuItem(route) {
1096
+ const { label, icon, path: path3, nested } = route;
1097
+ return `{
1098
+ label: ${label},
1099
+ icon: ${icon || "undefined"},
1100
+ path: "${path3}",
1101
+ nested: ${nested ? `"${nested}"` : "undefined"}
1102
+ }`;
1103
+ }
1104
+ async function getFilesFromSources5(sources) {
1105
+ const files = (await Promise.all(
1106
+ Array.from(sources).map(
1107
+ async (source) => crawl(`${source}/routes`, "page", { min: 1 })
1108
+ )
1109
+ )).flat();
1110
+ return files;
1111
+ }
1112
+ async function getMenuItemResults(files) {
1113
+ const results = await Promise.all(files.map(parseFile3));
1114
+ return results.filter((item) => item !== null);
1115
+ }
1116
+ async function parseFile3(file, index) {
1117
+ const config = await getRouteConfig(file);
1118
+ if (!config) {
1119
+ return null;
1120
+ }
1121
+ if (!config.label) {
1122
+ logger.warn(`Config is missing a label.`, {
1123
+ file
1124
+ });
1125
+ }
1126
+ const import_ = generateImport4(file, index);
1127
+ const menuItem = generateMenuItem(config, file, index);
1128
+ return {
1129
+ import: import_,
1130
+ menuItem
1131
+ };
1132
+ }
1133
+ function generateImport4(file, index) {
1134
+ const path3 = normalizePath(file);
1135
+ return `import { config as ${generateRouteConfigName(index)} } from "${path3}"`;
1136
+ }
1137
+ function generateMenuItem(config, file, index) {
1138
+ const configName = generateRouteConfigName(index);
1139
+ return {
1140
+ label: `${configName}.label`,
1141
+ icon: config.icon ? `${configName}.icon` : void 0,
1142
+ path: getRoute(file),
1143
+ nested: config.nested
1144
+ };
1145
+ }
1146
+ async function getRouteConfig(file) {
1147
+ const code = await fs5.readFile(file, "utf-8");
1148
+ let ast = null;
1149
+ try {
1150
+ ast = parse(code, getParserOptions(file));
1151
+ } catch (e) {
1152
+ logger.error(`An error occurred while parsing the file.`, {
1153
+ file,
1154
+ error: e
1155
+ });
1156
+ return null;
1157
+ }
1158
+ let config = null;
1159
+ try {
1160
+ traverse(ast, {
1161
+ ExportNamedDeclaration(path3) {
1162
+ const properties = getConfigObjectProperties(path3);
1163
+ if (!properties) {
1164
+ return;
1165
+ }
1166
+ const hasProperty = (name) => properties.some(
1167
+ (prop) => isObjectProperty(prop) && isIdentifier(prop.key, { name })
1168
+ );
1169
+ const hasLabel = hasProperty("label");
1170
+ if (!hasLabel) {
1171
+ return;
1172
+ }
1173
+ const nested = properties.find(
1174
+ (prop) => isObjectProperty(prop) && isIdentifier(prop.key, { name: "nested" })
1175
+ );
1176
+ const nestedValue = nested ? nested.value.value : void 0;
1177
+ if (nestedValue && !NESTED_ROUTE_POSITIONS.includes(nestedValue)) {
1178
+ logger.error(
1179
+ `Invalid nested route position: "${nestedValue}". Allowed values are: ${NESTED_ROUTE_POSITIONS.join(
1180
+ ", "
1181
+ )}`,
1182
+ {
1183
+ file
1184
+ }
1185
+ );
1186
+ return;
1187
+ }
1188
+ config = {
1189
+ label: hasLabel,
1190
+ icon: hasProperty("icon"),
1191
+ nested: nestedValue
1192
+ };
1193
+ }
1194
+ });
1195
+ } catch (e) {
1196
+ logger.error(`An error occurred while traversing the file.`, {
1197
+ file,
1198
+ error: e
1199
+ });
1200
+ }
1201
+ return config;
1202
+ }
1203
+ function generateRouteConfigName(index) {
1204
+ return `RouteConfig${index}`;
1205
+ }
1206
+
1207
+ // src/routes/generate-route-hashes.ts
1208
+ import fs6 from "fs/promises";
1209
+ async function generateRouteHashes(sources) {
1210
+ const files = await getFilesFromSources6(sources);
1211
+ const contents = await Promise.all(files.map(getRouteContents));
1212
+ const defaultExportContents = contents.map((c) => c.defaultExport).filter(Boolean);
1213
+ const configContents = contents.map((c) => c.config).filter(Boolean);
1214
+ const totalDefaultExportContent = defaultExportContents.join("");
1215
+ const totalConfigContent = configContents.join("");
1216
+ return {
1217
+ defaultExportHash: generateHash(totalDefaultExportContent),
1218
+ configHash: generateHash(totalConfigContent)
1219
+ };
1220
+ }
1221
+ async function getFilesFromSources6(sources) {
1222
+ return (await Promise.all(
1223
+ Array.from(sources).map(
1224
+ async (source) => crawl(`${source}/routes`, "page", { min: 1 })
1225
+ )
1226
+ )).flat();
1227
+ }
1228
+ async function getRouteContents(file) {
1229
+ const code = await fs6.readFile(file, "utf-8");
1230
+ let ast = null;
1231
+ try {
1232
+ ast = parse(code, getParserOptions(file));
1233
+ } catch (e) {
1234
+ logger.error(`An error occurred while parsing the file.`, {
1235
+ file,
1236
+ error: e
1237
+ });
1238
+ return { defaultExport: null, config: null };
1239
+ }
1240
+ let defaultExportContent = null;
1241
+ let configContent = null;
1242
+ try {
1243
+ traverse(ast, {
1244
+ ExportDefaultDeclaration(path3) {
1245
+ defaultExportContent = code.slice(path3.node.start, path3.node.end);
1246
+ },
1247
+ ExportNamedDeclaration(path3) {
1248
+ const properties = getConfigObjectProperties(path3);
1249
+ if (properties) {
1250
+ configContent = code.slice(path3.node.start, path3.node.end);
1251
+ }
1252
+ }
1253
+ });
1254
+ } catch (e) {
1255
+ logger.error(
1256
+ `An error occurred while processing ${file}. See the below error for more details:
1257
+ ${e}`,
1258
+ { file, error: e }
1259
+ );
1260
+ return { defaultExport: null, config: null };
1261
+ }
1262
+ return { defaultExport: defaultExportContent, config: configContent };
1263
+ }
1264
+
1265
+ // src/routes/generate-routes.ts
1266
+ import fs7 from "fs/promises";
1267
+ import { outdent as outdent3 } from "outdent";
1268
+ async function generateRoutes(sources) {
1269
+ const files = await getFilesFromSources7(sources);
1270
+ const results = await getRouteResults(files);
1271
+ const imports = results.map((result) => result.imports).flat();
1272
+ const code = generateCode4(results);
1273
+ return {
1274
+ imports,
1275
+ code
1276
+ };
1277
+ }
1278
+ function generateCode4(results) {
1279
+ return outdent3`
1280
+ routes: [
1281
+ ${results.map((result) => formatRoute(result.route)).join(",\n")}
1282
+ ]
1283
+ }
1284
+ `;
1285
+ }
1286
+ function formatRoute(route) {
1287
+ return `{
1288
+ Component: ${route.Component},
1289
+ path: "${route.path}",
1290
+ }`;
1291
+ }
1292
+ async function getFilesFromSources7(sources) {
1293
+ const files = (await Promise.all(
1294
+ Array.from(sources).map(
1295
+ async (source) => crawl(`${source}/routes`, "page", { min: 1 })
1296
+ )
1297
+ )).flat();
1298
+ return files;
1299
+ }
1300
+ async function getRouteResults(files) {
1301
+ const results = (await Promise.all(files.map(parseFile4))).filter(
1302
+ (result) => result !== null
1303
+ );
1304
+ return results;
1305
+ }
1306
+ async function parseFile4(file, index) {
1307
+ if (!await isValidRouteFile(file)) {
1308
+ return null;
1309
+ }
1310
+ const routePath = getRoute(file);
1311
+ const imports = generateImports(file, index);
1312
+ const route = generateRoute(routePath, index);
1313
+ return {
1314
+ imports,
1315
+ route
1316
+ };
1317
+ }
1318
+ async function isValidRouteFile(file) {
1319
+ const code = await fs7.readFile(file, "utf-8");
1320
+ let ast = null;
1321
+ try {
1322
+ ast = parse(code, getParserOptions(file));
1323
+ } catch (e) {
1324
+ logger.error("An error occurred while parsing the file.", {
1325
+ file,
1326
+ error: e
1327
+ });
1328
+ return false;
1329
+ }
1330
+ try {
1331
+ return await hasDefaultExport(ast);
1332
+ } catch (e) {
1333
+ logger.error(
1334
+ `An error occurred while checking for a default export in ${file}. The file will be ignored. See the below error for more details:
1335
+ ${e}`
1336
+ );
1337
+ return false;
1338
+ }
1339
+ }
1340
+ function generateImports(file, index) {
1341
+ const imports = [];
1342
+ const route = generateRouteComponentName(index);
1343
+ const importPath = normalizePath(file);
1344
+ imports.push(`import ${route} from "${importPath}"`);
1345
+ return imports;
1346
+ }
1347
+ function generateRoute(route, index) {
1348
+ return {
1349
+ Component: generateRouteComponentName(index),
1350
+ path: route
1351
+ };
1352
+ }
1353
+ function generateRouteComponentName(index) {
1354
+ return `RouteComponent${index}`;
1355
+ }
1356
+
1357
+ // src/virtual-modules/generate-virtual-display-module.ts
1358
+ import { outdent as outdent4 } from "outdent";
1359
+ async function generateVirtualDisplayModule(sources) {
1360
+ const displays = await generateCustomFieldDisplays(sources);
1361
+ const code = outdent4`
1362
+ ${displays.imports.join("\n")}
1363
+
1364
+ export default {
1365
+ ${displays.code}
1366
+ }
1367
+ `;
1368
+ return generateModule(code);
1369
+ }
1370
+
1371
+ // src/virtual-modules/generate-virtual-form-module.ts
1372
+ import outdent6 from "outdent";
1373
+
1374
+ // src/widgets/generate-widget-hash.ts
1375
+ import fs8 from "fs/promises";
1376
+
1377
+ // src/widgets/helpers.ts
1378
+ async function getWidgetFilesFromSources(sources) {
1379
+ return (await Promise.all(
1380
+ Array.from(sources).map(async (source) => crawl(`${source}/widgets`))
1381
+ )).flat();
1382
+ }
1383
+
1384
+ // src/widgets/generate-widget-hash.ts
1385
+ async function generateWidgetHash(sources) {
1386
+ const files = await getWidgetFilesFromSources(sources);
1387
+ const contents = await Promise.all(files.map(getWidgetContents));
1388
+ const totalContent = contents.flatMap(({ config, defaultExport }) => [config, defaultExport]).filter(Boolean).join("");
1389
+ return generateHash(totalContent);
1390
+ }
1391
+ async function getWidgetContents(file) {
1392
+ const code = await fs8.readFile(file, "utf-8");
1393
+ let ast;
1394
+ try {
1395
+ ast = parse(code, getParserOptions(file));
1396
+ } catch (e) {
1397
+ logger.error(
1398
+ `An error occurred while parsing the file. Due to the error we cannot validate whether the widget has changed. If your changes aren't correctly reflected try restarting the dev server.`,
1399
+ {
1400
+ file,
1401
+ error: e
1402
+ }
1403
+ );
1404
+ return { config: null, defaultExport: null };
1405
+ }
1406
+ let configContent = null;
1407
+ let defaultExportContent = null;
1408
+ traverse(ast, {
1409
+ ExportNamedDeclaration(path3) {
1410
+ const properties = getConfigObjectProperties(path3);
1411
+ if (properties) {
1412
+ configContent = code.slice(path3.node.start, path3.node.end);
1413
+ }
1414
+ },
1415
+ ExportDefaultDeclaration(path3) {
1416
+ defaultExportContent = code.slice(path3.node.start, path3.node.end);
1417
+ }
1418
+ });
1419
+ return { config: configContent, defaultExport: defaultExportContent };
1420
+ }
1421
+
1422
+ // src/widgets/generate-widgets.ts
1423
+ import { isValidInjectionZone } from "@etohq/admin-shared";
1424
+ import fs9 from "fs/promises";
1425
+ import outdent5 from "outdent";
1426
+ async function generateWidgets(sources) {
1427
+ const files = await getWidgetFilesFromSources(sources);
1428
+ const results = await getWidgetResults(files);
1429
+ const imports = results.map((r) => r.import);
1430
+ const code = generateCode5(results);
1431
+ return {
1432
+ imports,
1433
+ code
1434
+ };
1435
+ }
1436
+ async function getWidgetResults(files) {
1437
+ return (await Promise.all(files.map(parseFile5))).filter(
1438
+ (r) => r !== null
1439
+ );
1440
+ }
1441
+ function generateCode5(results) {
1442
+ return outdent5`
1443
+ widgets: [
1444
+ ${results.map((r) => formatWidget(r.widget)).join(",\n")}
1445
+ ]
1446
+ `;
1447
+ }
1448
+ function formatWidget(widget) {
1449
+ return outdent5`
1450
+ {
1451
+ Component: ${widget.Component},
1452
+ zone: [${widget.zone.map((z) => `"${z}"`).join(", ")}]
1453
+ }
1454
+ `;
1455
+ }
1456
+ async function parseFile5(file, index) {
1457
+ const code = await fs9.readFile(file, "utf-8");
1458
+ let ast;
1459
+ try {
1460
+ ast = parse(code, getParserOptions(file));
1461
+ } catch (e) {
1462
+ logger.error(`An error occurred while parsing the file.`, {
1463
+ file,
1464
+ error: e
1465
+ });
1466
+ return null;
1467
+ }
1468
+ let fileHasDefaultExport = false;
1469
+ try {
1470
+ fileHasDefaultExport = await hasDefaultExport(ast);
1471
+ } catch (e) {
1472
+ logger.error(`An error occurred while checking for a default export.`, {
1473
+ file,
1474
+ error: e
1475
+ });
1476
+ return null;
1477
+ }
1478
+ if (!fileHasDefaultExport) {
1479
+ return null;
1480
+ }
1481
+ let zone;
1482
+ try {
1483
+ zone = await getWidgetZone(ast, file);
1484
+ } catch (e) {
1485
+ logger.error(`An error occurred while traversing the file.`, {
1486
+ file,
1487
+ error: e
1488
+ });
1489
+ return null;
1490
+ }
1491
+ if (!zone) {
1492
+ logger.warn(`'zone' property is missing from the widget config.`, { file });
1493
+ return null;
1494
+ }
1495
+ const import_ = generateImport5(file, index);
1496
+ const widget = generateWidget(zone, index);
1497
+ return {
1498
+ widget,
1499
+ import: import_
1500
+ };
1501
+ }
1502
+ function generateWidgetComponentName(index) {
1503
+ return `WidgetComponent${index}`;
1504
+ }
1505
+ function generateWidgetConfigName(index) {
1506
+ return `WidgetConfig${index}`;
1507
+ }
1508
+ function generateImport5(file, index) {
1509
+ const path3 = normalizePath(file);
1510
+ return `import ${generateWidgetComponentName(
1511
+ index
1512
+ )}, { config as ${generateWidgetConfigName(index)} } from "${path3}"`;
1513
+ }
1514
+ function generateWidget(zone, index) {
1515
+ return {
1516
+ Component: generateWidgetComponentName(index),
1517
+ zone
1518
+ };
1519
+ }
1520
+ async function getWidgetZone(ast, file) {
1521
+ const zones = [];
1522
+ traverse(ast, {
1523
+ ExportNamedDeclaration(path3) {
1524
+ const properties = getConfigObjectProperties(path3);
1525
+ if (!properties) {
1526
+ return;
1527
+ }
1528
+ const zoneProperty = properties.find(
1529
+ (p) => p.type === "ObjectProperty" && p.key.type === "Identifier" && p.key.name === "zone"
1530
+ );
1531
+ if (!zoneProperty) {
1532
+ logger.warn(`'zone' property is missing from the widget config.`, {
1533
+ file
1534
+ });
1535
+ return;
1536
+ }
1537
+ if (isTemplateLiteral(zoneProperty.value)) {
1538
+ logger.warn(
1539
+ `'zone' property cannot be a template literal (e.g. \`product.details.after\`).`,
1540
+ { file }
1541
+ );
1542
+ return;
1543
+ }
1544
+ if (isStringLiteral(zoneProperty.value)) {
1545
+ zones.push(zoneProperty.value.value);
1546
+ } else if (isArrayExpression(zoneProperty.value)) {
1547
+ const values = [];
1548
+ for (const element of zoneProperty.value.elements) {
1549
+ if (isStringLiteral(element)) {
1550
+ values.push(element.value);
1551
+ }
1552
+ }
1553
+ zones.push(...values);
1554
+ }
1555
+ }
1556
+ });
1557
+ const validatedZones = zones.filter(isValidInjectionZone);
1558
+ return validatedZones.length > 0 ? validatedZones : null;
1559
+ }
1560
+
1561
+ // src/virtual-modules/generate-virtual-form-module.ts
1562
+ async function generateVirtualFormModule(sources) {
1563
+ const menuItems = await generateMenuItems(sources);
1564
+ const widgets = await generateWidgets(sources);
1565
+ const customFields = await generateCustomFieldForms(sources);
1566
+ const imports = [
1567
+ ...menuItems.imports,
1568
+ ...widgets.imports,
1569
+ ...customFields.imports
1570
+ ];
1571
+ const code = outdent6`
1572
+ ${imports.join("\n")}
1573
+
1574
+ export default {
1575
+ ${menuItems.code},
1576
+ ${widgets.code},
1577
+ ${customFields.code},
1578
+ }
1579
+ `;
1580
+ return generateModule(code);
1581
+ }
1582
+
1583
+ // src/virtual-modules/generate-virtual-link-module.ts
1584
+ import { outdent as outdent7 } from "outdent";
1585
+ async function generateVirtualLinkModule(sources) {
1586
+ const links = await generateCustomFieldLinks(sources);
1587
+ const code = outdent7`
1588
+ ${links.imports.join("\n")}
1589
+
1590
+ export default {
1591
+ ${links.code}
1592
+ }
1593
+ `;
1594
+ return generateModule(code);
1595
+ }
1596
+
1597
+ // src/virtual-modules/generate-virtual-menu-item-module.ts
1598
+ import outdent8 from "outdent";
1599
+ async function generateVirtualMenuItemModule(sources) {
1600
+ const menuItems = await generateMenuItems(sources);
1601
+ const code = outdent8`
1602
+ ${menuItems.imports.join("\n")}
1603
+
1604
+ export default {
1605
+ ${menuItems.code},
1606
+ }
1607
+ `;
1608
+ return generateModule(code);
1609
+ }
1610
+
1611
+ // src/virtual-modules/generate-virtual-route-module.ts
1612
+ import { outdent as outdent9 } from "outdent";
1613
+ async function generateVirtualRouteModule(sources) {
1614
+ const routes = await generateRoutes(sources);
1615
+ const imports = [...routes.imports];
1616
+ const code = outdent9`
1617
+ ${imports.join("\n")}
1618
+
1619
+ export default {
1620
+ ${routes.code}
1621
+ }
1622
+ `;
1623
+ return generateModule(code);
1624
+ }
1625
+
1626
+ // src/virtual-modules/generate-virtual-widget-module.ts
1627
+ import outdent10 from "outdent";
1628
+ async function generateVirtualWidgetModule(sources) {
1629
+ const widgets = await generateWidgets(sources);
1630
+ const imports = [...widgets.imports];
1631
+ const code = outdent10`
1632
+ ${imports.join("\n")}
1633
+
1634
+ export default {
1635
+ ${widgets.code},
1636
+ }
1637
+ `;
1638
+ return generateModule(code);
1639
+ }
1640
+
1641
+ // src/vmod.ts
1642
+ import {
1643
+ DISPLAY_VIRTUAL_MODULE,
1644
+ FORM_VIRTUAL_MODULE,
1645
+ LINK_VIRTUAL_MODULE,
1646
+ MENU_ITEM_VIRTUAL_MODULE,
1647
+ ROUTE_VIRTUAL_MODULE,
1648
+ WIDGET_VIRTUAL_MODULE
1649
+ } from "@etohq/admin-shared";
1650
+ var RESOLVED_LINK_VIRTUAL_MODULE = `\0${LINK_VIRTUAL_MODULE}`;
1651
+ var RESOLVED_FORM_VIRTUAL_MODULE = `\0${FORM_VIRTUAL_MODULE}`;
1652
+ var RESOLVED_DISPLAY_VIRTUAL_MODULE = `\0${DISPLAY_VIRTUAL_MODULE}`;
1653
+ var RESOLVED_ROUTE_VIRTUAL_MODULE = `\0${ROUTE_VIRTUAL_MODULE}`;
1654
+ var RESOLVED_MENU_ITEM_VIRTUAL_MODULE = `\0${MENU_ITEM_VIRTUAL_MODULE}`;
1655
+ var RESOLVED_WIDGET_VIRTUAL_MODULE = `\0${WIDGET_VIRTUAL_MODULE}`;
1656
+ var VIRTUAL_MODULES = [
1657
+ LINK_VIRTUAL_MODULE,
1658
+ FORM_VIRTUAL_MODULE,
1659
+ DISPLAY_VIRTUAL_MODULE,
1660
+ ROUTE_VIRTUAL_MODULE,
1661
+ MENU_ITEM_VIRTUAL_MODULE,
1662
+ WIDGET_VIRTUAL_MODULE
1663
+ ];
1664
+ var RESOLVED_VIRTUAL_MODULES = [
1665
+ RESOLVED_LINK_VIRTUAL_MODULE,
1666
+ RESOLVED_FORM_VIRTUAL_MODULE,
1667
+ RESOLVED_DISPLAY_VIRTUAL_MODULE,
1668
+ RESOLVED_ROUTE_VIRTUAL_MODULE,
1669
+ RESOLVED_MENU_ITEM_VIRTUAL_MODULE,
1670
+ RESOLVED_WIDGET_VIRTUAL_MODULE
1671
+ ];
1672
+ function resolveVirtualId(id) {
1673
+ return `\0${id}`;
1674
+ }
1675
+ function isVirtualModuleId(id) {
1676
+ return VIRTUAL_MODULES.includes(id);
1677
+ }
1678
+ function isResolvedVirtualModuleId(id) {
1679
+ return RESOLVED_VIRTUAL_MODULES.includes(
1680
+ id
1681
+ );
1682
+ }
1683
+ var resolvedVirtualModuleIds = {
1684
+ link: RESOLVED_LINK_VIRTUAL_MODULE,
1685
+ form: RESOLVED_FORM_VIRTUAL_MODULE,
1686
+ display: RESOLVED_DISPLAY_VIRTUAL_MODULE,
1687
+ route: RESOLVED_ROUTE_VIRTUAL_MODULE,
1688
+ menuItem: RESOLVED_MENU_ITEM_VIRTUAL_MODULE,
1689
+ widget: RESOLVED_WIDGET_VIRTUAL_MODULE
1690
+ };
1691
+ var virtualModuleIds = {
1692
+ link: LINK_VIRTUAL_MODULE,
1693
+ form: FORM_VIRTUAL_MODULE,
1694
+ display: DISPLAY_VIRTUAL_MODULE,
1695
+ route: ROUTE_VIRTUAL_MODULE,
1696
+ menuItem: MENU_ITEM_VIRTUAL_MODULE,
1697
+ widget: WIDGET_VIRTUAL_MODULE
1698
+ };
1699
+ var vmod = {
1700
+ resolved: resolvedVirtualModuleIds,
1701
+ virtual: virtualModuleIds
1702
+ };
1703
+
1704
+ // src/plugin.ts
1705
+ var etoVitePlugin = (options) => {
1706
+ const hashMap = /* @__PURE__ */ new Map();
1707
+ const _sources = new Set(options?.sources ?? []);
1708
+ let watcher;
1709
+ function isFileInSources(file) {
1710
+ for (const source of _sources) {
1711
+ if (file.startsWith(path2.resolve(source))) {
1712
+ return true;
1713
+ }
1714
+ }
1715
+ return false;
1716
+ }
1717
+ async function loadVirtualModule(config) {
1718
+ const hash = await config.hashGenerator(_sources);
1719
+ hashMap.set(config.hashKey, hash);
1720
+ return config.moduleGenerator(_sources);
1721
+ }
1722
+ async function handleFileChange(server, config) {
1723
+ const hashes = await config.hashGenerator(_sources);
1724
+ for (const module of config.modules) {
1725
+ const newHash = hashes[module.hashKey];
1726
+ if (newHash !== hashMap.get(module.virtualModule)) {
1727
+ const moduleToReload = server.moduleGraph.getModuleById(
1728
+ module.resolvedModule
1729
+ );
1730
+ if (moduleToReload) {
1731
+ await server.reloadModule(moduleToReload);
1732
+ }
1733
+ hashMap.set(module.virtualModule, newHash);
1734
+ }
1735
+ }
1736
+ }
1737
+ return {
1738
+ name: "@etohq/admin-vite-plugin",
1739
+ enforce: "pre",
1740
+ configureServer(server) {
1741
+ watcher = server.watcher;
1742
+ watcher?.add(Array.from(_sources));
1743
+ watcher?.on("all", async (_event, file) => {
1744
+ if (!isFileInSources(file)) {
1745
+ return;
1746
+ }
1747
+ for (const config of watcherConfigs) {
1748
+ if (isFileInAdminSubdirectory(file, config.subdirectory)) {
1749
+ await handleFileChange(server, config);
1750
+ }
1751
+ }
1752
+ });
1753
+ },
1754
+ resolveId(id) {
1755
+ if (!isVirtualModuleId(id)) {
1756
+ return null;
1757
+ }
1758
+ return resolveVirtualId(id);
1759
+ },
1760
+ async load(id) {
1761
+ if (!isResolvedVirtualModuleId(id)) {
1762
+ return null;
1763
+ }
1764
+ const config = loadConfigs[id];
1765
+ if (!config) {
1766
+ return null;
1767
+ }
1768
+ return loadVirtualModule(config);
1769
+ },
1770
+ async closeBundle() {
1771
+ if (watcher) {
1772
+ await watcher.close();
1773
+ }
1774
+ }
1775
+ };
1776
+ };
1777
+ var loadConfigs = {
1778
+ [vmod.resolved.widget]: {
1779
+ hashGenerator: async (sources) => generateWidgetHash(sources),
1780
+ moduleGenerator: async (sources) => generateVirtualWidgetModule(sources),
1781
+ hashKey: vmod.virtual.widget
1782
+ },
1783
+ [vmod.resolved.link]: {
1784
+ hashGenerator: async (sources) => (await generateCustomFieldHashes(sources)).linkHash,
1785
+ moduleGenerator: async (sources) => generateVirtualLinkModule(sources),
1786
+ hashKey: vmod.virtual.link
1787
+ },
1788
+ [vmod.resolved.form]: {
1789
+ hashGenerator: async (sources) => (await generateCustomFieldHashes(sources)).formHash,
1790
+ moduleGenerator: async (sources) => generateVirtualFormModule(sources),
1791
+ hashKey: vmod.virtual.form
1792
+ },
1793
+ [vmod.resolved.display]: {
1794
+ hashGenerator: async (sources) => (await generateCustomFieldHashes(sources)).displayHash,
1795
+ moduleGenerator: async (sources) => generateVirtualDisplayModule(sources),
1796
+ hashKey: vmod.virtual.display
1797
+ },
1798
+ [vmod.resolved.route]: {
1799
+ hashGenerator: async (sources) => (await generateRouteHashes(sources)).defaultExportHash,
1800
+ moduleGenerator: async (sources) => generateVirtualRouteModule(sources),
1801
+ hashKey: vmod.virtual.route
1802
+ },
1803
+ [vmod.resolved.menuItem]: {
1804
+ hashGenerator: async (sources) => (await generateRouteHashes(sources)).configHash,
1805
+ moduleGenerator: async (sources) => generateVirtualMenuItemModule(sources),
1806
+ hashKey: vmod.virtual.menuItem
1807
+ }
1808
+ };
1809
+ var watcherConfigs = [
1810
+ {
1811
+ subdirectory: "routes",
1812
+ hashGenerator: async (sources) => generateRouteHashes(sources),
1813
+ modules: [
1814
+ {
1815
+ virtualModule: vmod.virtual.route,
1816
+ resolvedModule: vmod.resolved.route,
1817
+ hashKey: "defaultExportHash"
1818
+ },
1819
+ {
1820
+ virtualModule: vmod.virtual.menuItem,
1821
+ resolvedModule: vmod.resolved.menuItem,
1822
+ hashKey: "configHash"
1823
+ }
1824
+ ]
1825
+ },
1826
+ {
1827
+ subdirectory: "widgets",
1828
+ hashGenerator: async (sources) => ({
1829
+ widgetConfigHash: await generateWidgetHash(sources)
1830
+ }),
1831
+ modules: [
1832
+ {
1833
+ virtualModule: vmod.virtual.widget,
1834
+ resolvedModule: vmod.resolved.widget,
1835
+ hashKey: "widgetConfigHash"
1836
+ }
1837
+ ]
1838
+ },
1839
+ {
1840
+ subdirectory: "custom-fields",
1841
+ hashGenerator: async (sources) => generateCustomFieldHashes(sources),
1842
+ modules: [
1843
+ {
1844
+ virtualModule: vmod.virtual.link,
1845
+ resolvedModule: vmod.resolved.link,
1846
+ hashKey: "linkHash"
1847
+ },
1848
+ {
1849
+ virtualModule: vmod.virtual.form,
1850
+ resolvedModule: vmod.resolved.form,
1851
+ hashKey: "formHash"
1852
+ },
1853
+ {
1854
+ virtualModule: vmod.virtual.display,
1855
+ resolvedModule: vmod.resolved.display,
1856
+ hashKey: "displayHash"
1857
+ }
1858
+ ]
1859
+ }
1860
+ ];
1861
+
1862
+ // src/index.ts
1863
+ var src_default = etoVitePlugin;
1864
+ export {
1865
+ src_default as default
1866
+ };