@strapi2front/generators 0.2.0 → 0.3.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.js CHANGED
@@ -1,8 +1,8 @@
1
- import path7 from 'path';
1
+ import path9 from 'path';
2
2
  import * as prettier from 'prettier';
3
3
  import fs from 'fs/promises';
4
4
 
5
- // src/types/generator.ts
5
+ // src/output/typescript/types.ts
6
6
  async function formatCode(code) {
7
7
  try {
8
8
  return await prettier.format(code, {
@@ -38,7 +38,7 @@ async function ensureDir(dirPath) {
38
38
  }
39
39
  }
40
40
  async function writeFile(filePath, content) {
41
- const dir = path7.dirname(filePath);
41
+ const dir = path9.dirname(filePath);
42
42
  await ensureDir(dir);
43
43
  await fs.writeFile(filePath, content, "utf-8");
44
44
  }
@@ -65,7 +65,7 @@ async function deleteFile(filePath) {
65
65
  async function listFiles(dirPath, extension) {
66
66
  try {
67
67
  const entries = await fs.readdir(dirPath, { withFileTypes: true });
68
- let files = entries.filter((entry) => entry.isFile()).map((entry) => path7.join(dirPath, entry.name));
68
+ let files = entries.filter((entry) => entry.isFile()).map((entry) => path9.join(dirPath, entry.name));
69
69
  if (extension) {
70
70
  files = files.filter((file) => file.endsWith(extension));
71
71
  }
@@ -96,33 +96,179 @@ function pluralize(word) {
96
96
  return word + "s";
97
97
  }
98
98
 
99
- // src/types/generator.ts
100
- async function generateTypes(schema, options) {
99
+ // src/shared/type-mapper.ts
100
+ function mapAttributeToType(attr, _components) {
101
+ switch (attr.type) {
102
+ case "string":
103
+ case "text":
104
+ case "richtext":
105
+ case "email":
106
+ case "password":
107
+ case "uid":
108
+ return { type: "string", needsImport: false };
109
+ case "blocks":
110
+ return {
111
+ type: "BlocksContent",
112
+ needsImport: true,
113
+ import: { name: "BlocksContent", from: "utils" }
114
+ };
115
+ case "integer":
116
+ case "biginteger":
117
+ case "float":
118
+ case "decimal":
119
+ return { type: "number", needsImport: false };
120
+ case "boolean":
121
+ return { type: "boolean", needsImport: false };
122
+ case "date":
123
+ case "time":
124
+ case "datetime":
125
+ case "timestamp":
126
+ return { type: "string", needsImport: false };
127
+ case "json":
128
+ return { type: "unknown", needsImport: false };
129
+ case "enumeration":
130
+ if ("enum" in attr && attr.enum) {
131
+ return {
132
+ type: attr.enum.map((v) => `'${v}'`).join(" | "),
133
+ needsImport: false
134
+ };
135
+ }
136
+ return { type: "string", needsImport: false };
137
+ case "media":
138
+ if ("multiple" in attr && attr.multiple) {
139
+ return {
140
+ type: "StrapiMedia[]",
141
+ needsImport: true,
142
+ import: { name: "StrapiMedia", from: "utils" }
143
+ };
144
+ }
145
+ return {
146
+ type: "StrapiMedia | null",
147
+ needsImport: true,
148
+ import: { name: "StrapiMedia", from: "utils" }
149
+ };
150
+ case "relation":
151
+ if ("target" in attr && attr.target) {
152
+ const targetName = attr.target.split(".").pop() || "unknown";
153
+ const typeName = toPascalCase(targetName);
154
+ const isMany = attr.relation === "oneToMany" || attr.relation === "manyToMany";
155
+ return {
156
+ type: isMany ? `${typeName}[]` : `${typeName} | null`,
157
+ needsImport: true,
158
+ import: { name: typeName, from: targetName, isRelation: true }
159
+ };
160
+ }
161
+ return { type: "unknown", needsImport: false };
162
+ case "component":
163
+ if ("component" in attr && attr.component) {
164
+ const componentName = attr.component.split(".").pop() || "unknown";
165
+ const typeName = toPascalCase(componentName);
166
+ if ("repeatable" in attr && attr.repeatable) {
167
+ return {
168
+ type: `${typeName}[]`,
169
+ needsImport: true,
170
+ import: { name: typeName, from: componentName, isComponent: true }
171
+ };
172
+ }
173
+ return {
174
+ type: `${typeName} | null`,
175
+ needsImport: true,
176
+ import: { name: typeName, from: componentName, isComponent: true }
177
+ };
178
+ }
179
+ return { type: "unknown", needsImport: false };
180
+ case "dynamiczone":
181
+ if ("components" in attr && attr.components) {
182
+ const types = attr.components.map((c) => {
183
+ const name = c.split(".").pop() || "unknown";
184
+ return toPascalCase(name);
185
+ });
186
+ return {
187
+ type: `(${types.join(" | ")})[]`,
188
+ needsImport: true
189
+ // Dynamic zones need multiple imports, handled separately
190
+ };
191
+ }
192
+ return { type: "unknown[]", needsImport: false };
193
+ default:
194
+ return { type: "unknown", needsImport: false };
195
+ }
196
+ }
197
+ function getAttributeComment(attr) {
198
+ const parts = [];
199
+ if (attr.required) {
200
+ parts.push("Required");
201
+ }
202
+ if ("minLength" in attr && attr.minLength !== void 0) {
203
+ parts.push(`Min length: ${attr.minLength}`);
204
+ }
205
+ if ("maxLength" in attr && attr.maxLength !== void 0) {
206
+ parts.push(`Max length: ${attr.maxLength}`);
207
+ }
208
+ if ("min" in attr && attr.min !== void 0) {
209
+ parts.push(`Min: ${attr.min}`);
210
+ }
211
+ if ("max" in attr && attr.max !== void 0) {
212
+ parts.push(`Max: ${attr.max}`);
213
+ }
214
+ return parts.length > 0 ? parts.join(", ") : null;
215
+ }
216
+ function extractDependencies(attributes, selfName) {
217
+ const relations = /* @__PURE__ */ new Set();
218
+ const components = /* @__PURE__ */ new Set();
219
+ for (const attr of Object.values(attributes)) {
220
+ if (attr.type === "relation" && "target" in attr && attr.target) {
221
+ const targetName = attr.target.split(".").pop() || "";
222
+ const typeName = toPascalCase(targetName);
223
+ if (typeName !== selfName && targetName) {
224
+ relations.add(targetName);
225
+ }
226
+ }
227
+ if (attr.type === "component" && "component" in attr && attr.component) {
228
+ const componentName = attr.component.split(".").pop() || "";
229
+ if (componentName) {
230
+ components.add(componentName);
231
+ }
232
+ }
233
+ if (attr.type === "dynamiczone" && "components" in attr && attr.components) {
234
+ for (const comp of attr.components) {
235
+ const componentName = comp.split(".").pop() || "";
236
+ if (componentName) {
237
+ components.add(componentName);
238
+ }
239
+ }
240
+ }
241
+ }
242
+ return { relations, components };
243
+ }
244
+
245
+ // src/output/typescript/types.ts
246
+ async function generateTypeScriptTypes(schema, options) {
101
247
  const { outputDir } = options;
102
248
  const generatedFiles = [];
103
- await ensureDir(path7.join(outputDir, "collections"));
104
- await ensureDir(path7.join(outputDir, "components"));
105
- const utilsPath = path7.join(outputDir, "utils.ts");
249
+ await ensureDir(path9.join(outputDir, "collections"));
250
+ await ensureDir(path9.join(outputDir, "components"));
251
+ const utilsPath = path9.join(outputDir, "utils.ts");
106
252
  const strapiVersion = options.strapiVersion ?? "v5";
107
253
  await writeFile(utilsPath, await formatCode(generateUtilityTypes(options.blocksRendererInstalled ?? false, strapiVersion)));
108
254
  generatedFiles.push(utilsPath);
109
255
  for (const collection of schema.collections) {
110
256
  const fileName = `${toKebabCase(collection.singularName)}.ts`;
111
- const filePath = path7.join(outputDir, "collections", fileName);
257
+ const filePath = path9.join(outputDir, "collections", fileName);
112
258
  const content = generateCollectionType(collection, schema.components);
113
259
  await writeFile(filePath, await formatCode(content));
114
260
  generatedFiles.push(filePath);
115
261
  }
116
262
  for (const single of schema.singles) {
117
263
  const fileName = `${toKebabCase(single.singularName)}.ts`;
118
- const filePath = path7.join(outputDir, "collections", fileName);
264
+ const filePath = path9.join(outputDir, "collections", fileName);
119
265
  const content = generateSingleType(single, schema.components);
120
266
  await writeFile(filePath, await formatCode(content));
121
267
  generatedFiles.push(filePath);
122
268
  }
123
269
  for (const component of schema.components) {
124
270
  const fileName = `${toKebabCase(component.name)}.ts`;
125
- const filePath = path7.join(outputDir, "components", fileName);
271
+ const filePath = path9.join(outputDir, "components", fileName);
126
272
  const content = generateComponentType(component);
127
273
  await writeFile(filePath, await formatCode(content));
128
274
  generatedFiles.push(filePath);
@@ -306,34 +452,6 @@ ${rawResponseTypes}
306
452
  ${blocksContentType}
307
453
  `;
308
454
  }
309
- function extractDependencies(attributes, selfName) {
310
- const relations = /* @__PURE__ */ new Set();
311
- const components = /* @__PURE__ */ new Set();
312
- for (const attr of Object.values(attributes)) {
313
- if (attr.type === "relation" && "target" in attr && attr.target) {
314
- const targetName = attr.target.split(".").pop() || "";
315
- const typeName = toPascalCase(targetName);
316
- if (typeName !== selfName && targetName) {
317
- relations.add(targetName);
318
- }
319
- }
320
- if (attr.type === "component" && "component" in attr && attr.component) {
321
- const componentName = attr.component.split(".").pop() || "";
322
- if (componentName) {
323
- components.add(componentName);
324
- }
325
- }
326
- if (attr.type === "dynamiczone" && "components" in attr && attr.components) {
327
- for (const comp of attr.components) {
328
- const componentName = comp.split(".").pop() || "";
329
- if (componentName) {
330
- components.add(componentName);
331
- }
332
- }
333
- }
334
- }
335
- return { relations, components };
336
- }
337
455
  function generateDependencyImports(relations, components) {
338
456
  const imports = [];
339
457
  for (const relation of relations) {
@@ -461,195 +579,369 @@ ${attributes}
461
579
  function generateAttributes(attributes, components) {
462
580
  const lines = [];
463
581
  for (const [name, attr] of Object.entries(attributes)) {
464
- const tsType = attributeToTsType(attr);
582
+ const mappedType = mapAttributeToType(attr);
465
583
  const optional = attr.required ? "" : "?";
466
584
  const comment = getAttributeComment(attr);
467
585
  if (comment) {
468
586
  lines.push(` /** ${comment} */`);
469
587
  }
470
- lines.push(` ${name}${optional}: ${tsType};`);
588
+ lines.push(` ${name}${optional}: ${mappedType.type};`);
471
589
  }
472
590
  return lines.join("\n");
473
591
  }
474
- function attributeToTsType(attr, _components) {
475
- switch (attr.type) {
476
- case "string":
477
- case "text":
478
- case "richtext":
479
- case "email":
480
- case "password":
481
- case "uid":
482
- return "string";
483
- case "blocks":
484
- return "BlocksContent";
485
- case "integer":
486
- case "biginteger":
487
- case "float":
488
- case "decimal":
489
- return "number";
490
- case "boolean":
491
- return "boolean";
492
- case "date":
493
- case "time":
494
- case "datetime":
495
- case "timestamp":
496
- return "string";
497
- case "json":
498
- return "unknown";
499
- case "enumeration":
500
- if ("enum" in attr && attr.enum) {
501
- return attr.enum.map((v) => `'${v}'`).join(" | ");
502
- }
503
- return "string";
504
- case "media":
505
- if ("multiple" in attr && attr.multiple) {
506
- return "StrapiMedia[]";
507
- }
508
- return "StrapiMedia | null";
509
- case "relation":
510
- if ("target" in attr && attr.target) {
511
- const targetName = toPascalCase(attr.target.split(".").pop() || "unknown");
512
- const isMany = attr.relation === "oneToMany" || attr.relation === "manyToMany";
513
- return isMany ? `${targetName}[]` : `${targetName} | null`;
514
- }
515
- return "unknown";
516
- case "component":
517
- if ("component" in attr && attr.component) {
518
- const componentName = toPascalCase(attr.component.split(".").pop() || "unknown");
519
- if ("repeatable" in attr && attr.repeatable) {
520
- return `${componentName}[]`;
521
- }
522
- return `${componentName} | null`;
523
- }
524
- return "unknown";
525
- case "dynamiczone":
526
- if ("components" in attr && attr.components) {
527
- const types = attr.components.map((c) => toPascalCase(c.split(".").pop() || "unknown"));
528
- return `(${types.join(" | ")})[]`;
529
- }
530
- return "unknown[]";
531
- default:
532
- return "unknown";
533
- }
534
- }
535
- function getAttributeComment(attr) {
536
- const parts = [];
537
- if (attr.required) {
538
- parts.push("Required");
539
- }
540
- if ("minLength" in attr && attr.minLength !== void 0) {
541
- parts.push(`Min length: ${attr.minLength}`);
542
- }
543
- if ("maxLength" in attr && attr.maxLength !== void 0) {
544
- parts.push(`Max length: ${attr.maxLength}`);
545
- }
546
- if ("min" in attr && attr.min !== void 0) {
547
- parts.push(`Min: ${attr.min}`);
548
- }
549
- if ("max" in attr && attr.max !== void 0) {
550
- parts.push(`Max: ${attr.max}`);
551
- }
552
- return parts.length > 0 ? parts.join(", ") : null;
553
- }
554
- async function generateServices(schema, options) {
555
- const { outputDir, typesImportPath, strapiVersion = "v5" } = options;
592
+ async function generateJSDocTypes(schema, options) {
593
+ const { outputDir } = options;
556
594
  const generatedFiles = [];
557
- await ensureDir(outputDir);
595
+ await ensureDir(path9.join(outputDir, "collections"));
596
+ await ensureDir(path9.join(outputDir, "components"));
597
+ const utilsPath = path9.join(outputDir, "utils.js");
598
+ const strapiVersion = options.strapiVersion ?? "v5";
599
+ await writeFile(utilsPath, generateUtilityTypes2(strapiVersion));
600
+ generatedFiles.push(utilsPath);
558
601
  for (const collection of schema.collections) {
559
- const fileName = `${toKebabCase(collection.singularName)}.service.ts`;
560
- const filePath = path7.join(outputDir, fileName);
561
- const content = generateCollectionService(collection, typesImportPath, strapiVersion);
562
- await writeFile(filePath, await formatCode(content));
602
+ const fileName = `${toKebabCase(collection.singularName)}.js`;
603
+ const filePath = path9.join(outputDir, "collections", fileName);
604
+ const content = generateCollectionType2(collection, schema.components, strapiVersion);
605
+ await writeFile(filePath, content);
563
606
  generatedFiles.push(filePath);
564
607
  }
565
608
  for (const single of schema.singles) {
566
- const fileName = `${toKebabCase(single.singularName)}.service.ts`;
567
- const filePath = path7.join(outputDir, fileName);
568
- const content = generateSingleService(single, typesImportPath, strapiVersion);
569
- await writeFile(filePath, await formatCode(content));
609
+ const fileName = `${toKebabCase(single.singularName)}.js`;
610
+ const filePath = path9.join(outputDir, "collections", fileName);
611
+ const content = generateSingleType2(single, schema.components, strapiVersion);
612
+ await writeFile(filePath, content);
613
+ generatedFiles.push(filePath);
614
+ }
615
+ for (const component of schema.components) {
616
+ const fileName = `${toKebabCase(component.name)}.js`;
617
+ const filePath = path9.join(outputDir, "components", fileName);
618
+ const content = generateComponentType2(component, strapiVersion);
619
+ await writeFile(filePath, content);
570
620
  generatedFiles.push(filePath);
571
621
  }
572
622
  return generatedFiles;
573
623
  }
574
- function generateCollectionService(collection, typesImportPath, strapiVersion) {
575
- const typeName = toPascalCase(collection.singularName);
576
- const serviceName = toCamelCase(collection.singularName) + "Service";
577
- const fileName = toKebabCase(collection.singularName);
578
- const endpoint = collection.pluralName;
624
+ function generateUtilityTypes2(strapiVersion) {
579
625
  const isV4 = strapiVersion === "v4";
580
- const idParam = isV4 ? "id: number" : "documentId: string";
581
- const idName = isV4 ? "id" : "documentId";
582
- const omitFields = isV4 ? "'id' | 'createdAt' | 'updatedAt' | 'publishedAt'" : "'id' | 'documentId' | 'createdAt' | 'updatedAt' | 'publishedAt'";
583
- const hasSlug = "slug" in collection.attributes;
584
- const { localized, draftAndPublish } = collection;
585
- const imports = [
586
- `import { collection } from '../client';`,
587
- `import type { ${typeName}, ${typeName}Filters } from '${typesImportPath}/collections/${fileName}';`,
588
- `import type { StrapiPagination } from '${typesImportPath}/utils';`
589
- ];
590
- if (localized) {
591
- imports.push(`import type { Locale } from '../locales';`);
592
- }
593
- const findManyOptionsFields = [
594
- ` filters?: ${typeName}Filters;`,
595
- ` pagination?: {`,
596
- ` /** Page number (1-indexed) - use with pageSize */`,
597
- ` page?: number;`,
598
- ` /** Number of items per page (default: 25) - use with page */`,
599
- ` pageSize?: number;`,
600
- ` /** Offset to start from (0-indexed) - use with limit */`,
601
- ` start?: number;`,
602
- ` /** Maximum number of items to return - use with start */`,
603
- ` limit?: number;`,
604
- ` };`,
605
- ` sort?: string | string[];`,
606
- ` populate?: string | string[] | Record<string, unknown>;`
607
- ];
608
- if (localized) {
609
- findManyOptionsFields.push(` locale?: Locale;`);
610
- }
611
- if (draftAndPublish) {
612
- findManyOptionsFields.push(` status?: 'draft' | 'published';`);
626
+ const baseEntityFields = isV4 ? ` * @property {number} id
627
+ * @property {string} createdAt
628
+ * @property {string} updatedAt
629
+ * @property {string|null} publishedAt` : ` * @property {number} id
630
+ * @property {string} documentId
631
+ * @property {string} createdAt
632
+ * @property {string} updatedAt
633
+ * @property {string|null} publishedAt`;
634
+ const mediaFields = isV4 ? ` * @property {number} id` : ` * @property {number} id
635
+ * @property {string} documentId`;
636
+ return `/**
637
+ * Strapi utility types (JSDoc)
638
+ * Generated by strapi2front
639
+ * Strapi version: ${strapiVersion}
640
+ *
641
+ * These types provide IntelliSense support in JavaScript projects.
642
+ */
643
+
644
+ /**
645
+ * Strapi media format
646
+ * @typedef {Object} StrapiMediaFormat
647
+ * @property {string} name
648
+ * @property {string} hash
649
+ * @property {string} ext
650
+ * @property {string} mime
651
+ * @property {number} width
652
+ * @property {number} height
653
+ * @property {number} size
654
+ * @property {string} url
655
+ */
656
+
657
+ /**
658
+ * Strapi media object
659
+ * @typedef {Object} StrapiMedia
660
+ ${mediaFields}
661
+ * @property {string} name
662
+ * @property {string|null} alternativeText
663
+ * @property {string|null} caption
664
+ * @property {number} width
665
+ * @property {number} height
666
+ * @property {{thumbnail?: StrapiMediaFormat, small?: StrapiMediaFormat, medium?: StrapiMediaFormat, large?: StrapiMediaFormat}|null} formats
667
+ * @property {string} hash
668
+ * @property {string} ext
669
+ * @property {string} mime
670
+ * @property {number} size
671
+ * @property {string} url
672
+ * @property {string|null} previewUrl
673
+ * @property {string} provider
674
+ * @property {string} createdAt
675
+ * @property {string} updatedAt
676
+ */
677
+
678
+ /**
679
+ * Strapi pagination
680
+ * @typedef {Object} StrapiPagination
681
+ * @property {number} page
682
+ * @property {number} pageSize
683
+ * @property {number} pageCount
684
+ * @property {number} total
685
+ */
686
+
687
+ /**
688
+ * Strapi response wrapper
689
+ * @template T
690
+ * @typedef {Object} StrapiResponse
691
+ * @property {T} data
692
+ * @property {{pagination?: StrapiPagination}} meta
693
+ */
694
+
695
+ /**
696
+ * Strapi list response
697
+ * @template T
698
+ * @typedef {Object} StrapiListResponse
699
+ * @property {T[]} data
700
+ * @property {{pagination: StrapiPagination}} meta
701
+ */
702
+
703
+ /**
704
+ * Base entity fields
705
+ * @typedef {Object} StrapiBaseEntity
706
+ ${baseEntityFields}
707
+ */
708
+
709
+ /**
710
+ * Blocks content type (rich text)
711
+ * @typedef {unknown[]} BlocksContent
712
+ */
713
+
714
+ // Export empty object to make this a module
715
+ export {};
716
+ `;
717
+ }
718
+ function generateCollectionType2(collection, components, strapiVersion) {
719
+ const typeName = toPascalCase(collection.singularName);
720
+ const isV4 = strapiVersion === "v4";
721
+ const { relations, components: componentDeps } = extractDependencies(collection.attributes, typeName);
722
+ const imports = generateJSDocImports(relations, componentDeps, "collection");
723
+ const properties = generateJSDocProperties(collection.attributes);
724
+ const baseFields = isV4 ? ` * @property {number} id
725
+ * @property {string} createdAt
726
+ * @property {string} updatedAt
727
+ * @property {string|null} publishedAt` : ` * @property {number} id
728
+ * @property {string} documentId
729
+ * @property {string} createdAt
730
+ * @property {string} updatedAt
731
+ * @property {string|null} publishedAt`;
732
+ return `/**
733
+ * ${collection.displayName}
734
+ * ${collection.description || ""}
735
+ * Generated by strapi2front
736
+ * Strapi version: ${strapiVersion}
737
+ */
738
+
739
+ ${imports}
740
+
741
+ /**
742
+ * ${collection.displayName}
743
+ * @typedef {Object} ${typeName}
744
+ ${baseFields}
745
+ ${properties}
746
+ */
747
+
748
+ /**
749
+ * ${typeName} filters for querying
750
+ * @typedef {Object} ${typeName}Filters
751
+ * @property {number|{$eq?: number, $ne?: number, $in?: number[], $notIn?: number[]}} [id]
752
+ * @property {string|{$eq?: string, $ne?: string}} [documentId]
753
+ * @property {string|{$eq?: string, $gt?: string, $gte?: string, $lt?: string, $lte?: string}} [createdAt]
754
+ * @property {string|{$eq?: string, $gt?: string, $gte?: string, $lt?: string, $lte?: string}} [updatedAt]
755
+ * @property {string|null|{$eq?: string, $ne?: string, $null?: boolean}} [publishedAt]
756
+ * @property {${typeName}Filters[]} [$and]
757
+ * @property {${typeName}Filters[]} [$or]
758
+ * @property {${typeName}Filters} [$not]
759
+ */
760
+
761
+ // Export empty object to make this a module
762
+ export {};
763
+ `;
764
+ }
765
+ function generateSingleType2(single, components, strapiVersion) {
766
+ const typeName = toPascalCase(single.singularName);
767
+ const isV4 = strapiVersion === "v4";
768
+ const { relations, components: componentDeps } = extractDependencies(single.attributes, typeName);
769
+ const imports = generateJSDocImports(relations, componentDeps, "collection");
770
+ const properties = generateJSDocProperties(single.attributes);
771
+ const baseFields = isV4 ? ` * @property {number} id
772
+ * @property {string} createdAt
773
+ * @property {string} updatedAt` : ` * @property {number} id
774
+ * @property {string} documentId
775
+ * @property {string} createdAt
776
+ * @property {string} updatedAt`;
777
+ return `/**
778
+ * ${single.displayName}
779
+ * ${single.description || ""}
780
+ * Generated by strapi2front
781
+ * Strapi version: ${strapiVersion}
782
+ */
783
+
784
+ ${imports}
785
+
786
+ /**
787
+ * ${single.displayName}
788
+ * @typedef {Object} ${typeName}
789
+ ${baseFields}
790
+ ${properties}
791
+ */
792
+
793
+ // Export empty object to make this a module
794
+ export {};
795
+ `;
796
+ }
797
+ function generateComponentType2(component, strapiVersion) {
798
+ const typeName = toPascalCase(component.name);
799
+ const { relations, components: componentDeps } = extractDependencies(component.attributes, typeName);
800
+ const imports = generateJSDocImports(relations, componentDeps, "component");
801
+ const properties = generateJSDocProperties(component.attributes);
802
+ return `/**
803
+ * ${component.displayName} component
804
+ * Category: ${component.category}
805
+ * ${component.description || ""}
806
+ * Generated by strapi2front
807
+ * Strapi version: ${strapiVersion}
808
+ */
809
+
810
+ ${imports}
811
+
812
+ /**
813
+ * ${component.displayName}
814
+ * @typedef {Object} ${typeName}
815
+ * @property {number} id
816
+ ${properties}
817
+ */
818
+
819
+ // Export empty object to make this a module
820
+ export {};
821
+ `;
822
+ }
823
+ function generateJSDocImports(relations, components, context) {
824
+ const imports = [];
825
+ imports.push(`/** @typedef {import('../utils').StrapiMedia} StrapiMedia */`);
826
+ imports.push(`/** @typedef {import('../utils').BlocksContent} BlocksContent */`);
827
+ for (const relation of relations) {
828
+ const typeName = toPascalCase(relation);
829
+ const fileName = toKebabCase(relation);
830
+ if (context === "component") {
831
+ imports.push(`/** @typedef {import('../collections/${fileName}').${typeName}} ${typeName} */`);
832
+ } else {
833
+ imports.push(`/** @typedef {import('./${fileName}').${typeName}} ${typeName} */`);
834
+ }
613
835
  }
614
- const findOneOptionsFields = [
615
- ` populate?: string | string[] | Record<string, unknown>;`
616
- ];
617
- if (localized) {
618
- findOneOptionsFields.push(` locale?: Locale;`);
836
+ for (const component of components) {
837
+ const typeName = toPascalCase(component);
838
+ const fileName = toKebabCase(component);
839
+ if (context === "component") {
840
+ imports.push(`/** @typedef {import('./${fileName}').${typeName}} ${typeName} */`);
841
+ } else {
842
+ imports.push(`/** @typedef {import('../components/${fileName}').${typeName}} ${typeName} */`);
843
+ }
619
844
  }
620
- if (draftAndPublish) {
621
- findOneOptionsFields.push(` status?: 'draft' | 'published';`);
845
+ return imports.join("\n");
846
+ }
847
+ function generateJSDocProperties(attributes, components) {
848
+ const lines = [];
849
+ for (const [name, attr] of Object.entries(attributes)) {
850
+ const mappedType = mapAttributeToType(attr);
851
+ const jsDocType = convertToJSDocType(mappedType.type);
852
+ const comment = getAttributeComment(attr);
853
+ const commentPart = comment ? ` - ${comment}` : "";
854
+ if (attr.required) {
855
+ lines.push(` * @property {${jsDocType}} ${name}${commentPart}`);
856
+ } else {
857
+ lines.push(` * @property {${jsDocType}} [${name}]${commentPart}`);
858
+ }
622
859
  }
623
- const findParams = [
624
- ` filters: options.filters,`,
625
- ` pagination: options.pagination,`,
626
- ` sort: options.sort,`,
627
- ` populate: options.populate,`
628
- ];
629
- if (localized) {
630
- findParams.push(` locale: options.locale,`);
860
+ return lines.join("\n");
861
+ }
862
+ function convertToJSDocType(tsType) {
863
+ if (tsType.includes(" | null")) {
864
+ const baseType = tsType.replace(" | null", "");
865
+ return `${baseType}|null`;
631
866
  }
632
- if (draftAndPublish) {
633
- findParams.push(` status: options.status,`);
867
+ if (tsType.endsWith("[]")) {
868
+ const baseType = tsType.slice(0, -2);
869
+ if (baseType.startsWith("(") && baseType.endsWith(")")) {
870
+ return `Array<${baseType.slice(1, -1).replace(/ \| /g, "|")}>`;
871
+ }
872
+ return `${baseType}[]`;
634
873
  }
635
- const findOneParams = [
636
- ` populate: options.populate,`
637
- ];
638
- if (localized) {
639
- findOneParams.push(` locale: options.locale,`);
874
+ if (tsType.includes("'") && tsType.includes(" | ")) {
875
+ return "string";
640
876
  }
641
- if (draftAndPublish) {
642
- findOneParams.push(` status: options.status,`);
877
+ return tsType;
878
+ }
879
+
880
+ // src/types/generator.ts
881
+ async function generateTypes(schema, options) {
882
+ const outputFormat = options.outputFormat ?? "typescript";
883
+ if (outputFormat === "jsdoc") {
884
+ return generateJSDocTypes(schema, {
885
+ outputDir: options.outputDir,
886
+ blocksRendererInstalled: options.blocksRendererInstalled,
887
+ strapiVersion: options.strapiVersion
888
+ });
643
889
  }
644
- const findBySlugParams = [
645
- ` populate: options.populate,`
646
- ];
647
- if (localized) {
648
- findBySlugParams.push(` locale: options.locale,`);
890
+ return generateTypeScriptTypes(schema, {
891
+ outputDir: options.outputDir,
892
+ blocksRendererInstalled: options.blocksRendererInstalled,
893
+ strapiVersion: options.strapiVersion
894
+ });
895
+ }
896
+ async function generateJSDocServices(schema, options) {
897
+ const { outputDir, typesImportPath, strapiVersion = "v5" } = options;
898
+ const generatedFiles = [];
899
+ await ensureDir(outputDir);
900
+ for (const collection of schema.collections) {
901
+ const fileName = `${toKebabCase(collection.singularName)}.service.js`;
902
+ const filePath = path9.join(outputDir, fileName);
903
+ const content = generateCollectionService(collection, typesImportPath, strapiVersion);
904
+ await writeFile(filePath, content);
905
+ generatedFiles.push(filePath);
649
906
  }
650
- if (draftAndPublish) {
651
- findBySlugParams.push(` status: options.status,`);
907
+ for (const single of schema.singles) {
908
+ const fileName = `${toKebabCase(single.singularName)}.service.js`;
909
+ const filePath = path9.join(outputDir, fileName);
910
+ const content = generateSingleService(single, typesImportPath, strapiVersion);
911
+ await writeFile(filePath, content);
912
+ generatedFiles.push(filePath);
652
913
  }
914
+ return generatedFiles;
915
+ }
916
+ function generateCollectionService(collection, typesImportPath, strapiVersion) {
917
+ const typeName = toPascalCase(collection.singularName);
918
+ const serviceName = toCamelCase(collection.singularName) + "Service";
919
+ const fileName = toKebabCase(collection.singularName);
920
+ const endpoint = collection.pluralName;
921
+ const isV4 = strapiVersion === "v4";
922
+ const idParam = isV4 ? "id" : "documentId";
923
+ const idType = isV4 ? "number" : "string";
924
+ const hasSlug = "slug" in collection.attributes;
925
+ const { localized, draftAndPublish } = collection;
926
+ const localeOption = localized ? "\n * @property {string} [locale] - Locale code" : "";
927
+ const statusOption = draftAndPublish ? "\n * @property {'draft'|'published'} [status] - Publication status" : "";
928
+ const findBySlugMethod = hasSlug ? `
929
+ /**
930
+ * Find one ${collection.singularName} by slug
931
+ * @param {string} slug - The slug to search for
932
+ * @param {FindOneOptions} [options={}] - Query options
933
+ * @returns {Promise<${typeName}|null>}
934
+ */
935
+ async findBySlug(slug, options = {}) {
936
+ const { data } = await this.findMany({
937
+ filters: { slug: { $eq: slug } },
938
+ pagination: { pageSize: 1 },
939
+ populate: options.populate,${localized ? "\n locale: options.locale," : ""}${draftAndPublish ? "\n status: options.status," : ""}
940
+ });
941
+
942
+ return data[0] || null;
943
+ },
944
+ ` : "";
653
945
  return `/**
654
946
  * ${collection.displayName} Service
655
947
  * ${collection.description || ""}
@@ -657,26 +949,41 @@ function generateCollectionService(collection, typesImportPath, strapiVersion) {
657
949
  * Strapi version: ${strapiVersion}
658
950
  */
659
951
 
660
- ${imports.join("\n")}
952
+ import { collection } from '../client.js';
661
953
 
662
- export interface FindManyOptions {
663
- ${findManyOptionsFields.join("\n")}
664
- }
954
+ /** @typedef {import('${typesImportPath}/collections/${fileName}').${typeName}} ${typeName} */
955
+ /** @typedef {import('${typesImportPath}/collections/${fileName}').${typeName}Filters} ${typeName}Filters */
956
+ /** @typedef {import('${typesImportPath}/utils').StrapiPagination} StrapiPagination */
957
+ ${localized ? `/** @typedef {import('../locales').Locale} Locale */` : ""}
665
958
 
666
- export interface FindOneOptions {
667
- ${findOneOptionsFields.join("\n")}
668
- }
959
+ /**
960
+ * @typedef {Object} FindManyOptions
961
+ * @property {${typeName}Filters} [filters] - Filter conditions
962
+ * @property {{page?: number, pageSize?: number, start?: number, limit?: number}} [pagination] - Pagination options
963
+ * @property {string|string[]} [sort] - Sort order
964
+ * @property {string|string[]|Object} [populate] - Relations to populate${localeOption}${statusOption}
965
+ */
669
966
 
670
- // Create typed collection helper
671
- const ${toCamelCase(collection.singularName)}Collection = collection<${typeName}>('${endpoint}');
967
+ /**
968
+ * @typedef {Object} FindOneOptions
969
+ * @property {string|string[]|Object} [populate] - Relations to populate${localeOption}${statusOption}
970
+ */
971
+
972
+ /** @type {ReturnType<typeof collection<${typeName}>>} */
973
+ const ${toCamelCase(collection.singularName)}Collection = collection('${endpoint}');
672
974
 
673
975
  export const ${serviceName} = {
674
976
  /**
675
977
  * Find multiple ${collection.pluralName}
978
+ * @param {FindManyOptions} [options={}] - Query options
979
+ * @returns {Promise<{data: ${typeName}[], pagination: StrapiPagination}>}
676
980
  */
677
- async findMany(options: FindManyOptions = {}): Promise<{ data: ${typeName}[]; pagination: StrapiPagination }> {
981
+ async findMany(options = {}) {
678
982
  const response = await ${toCamelCase(collection.singularName)}Collection.find({
679
- ${findParams.join("\n")}
983
+ filters: options.filters,
984
+ pagination: options.pagination,
985
+ sort: options.sort,
986
+ populate: options.populate,${localized ? "\n locale: options.locale," : ""}${draftAndPublish ? "\n status: options.status," : ""}
680
987
  });
681
988
 
682
989
  return {
@@ -687,9 +994,12 @@ ${findParams.join("\n")}
687
994
 
688
995
  /**
689
996
  * Find all ${collection.pluralName} (handles pagination automatically)
997
+ * @param {Omit<FindManyOptions, 'pagination'>} [options={}] - Query options
998
+ * @returns {Promise<${typeName}[]>}
690
999
  */
691
- async findAll(options: Omit<FindManyOptions, 'pagination'> = {}): Promise<${typeName}[]> {
692
- const allItems: ${typeName}[] = [];
1000
+ async findAll(options = {}) {
1001
+ /** @type {${typeName}[]} */
1002
+ const allItems = [];
693
1003
  let page = 1;
694
1004
  let hasMore = true;
695
1005
 
@@ -708,64 +1018,62 @@ ${findParams.join("\n")}
708
1018
  },
709
1019
 
710
1020
  /**
711
- * Find one ${collection.singularName} by ${idName}
1021
+ * Find one ${collection.singularName} by ${idParam}
1022
+ * @param {${idType}} ${idParam} - The ${idParam} to find
1023
+ * @param {FindOneOptions} [options={}] - Query options
1024
+ * @returns {Promise<${typeName}|null>}
712
1025
  */
713
- async findOne(${idParam}, options: FindOneOptions = {}): Promise<${typeName} | null> {
1026
+ async findOne(${idParam}, options = {}) {
714
1027
  try {
715
- const response = await ${toCamelCase(collection.singularName)}Collection.findOne(${idName}, {
716
- ${findOneParams.join("\n")}
1028
+ const response = await ${toCamelCase(collection.singularName)}Collection.findOne(${idParam}, {
1029
+ populate: options.populate,${localized ? "\n locale: options.locale," : ""}${draftAndPublish ? "\n status: options.status," : ""}
717
1030
  });
718
1031
 
719
1032
  return response.data;
720
1033
  } catch (error) {
721
- // Return null if not found
722
1034
  if (error instanceof Error && error.message.includes('404')) {
723
1035
  return null;
724
1036
  }
725
1037
  throw error;
726
1038
  }
727
1039
  },
728
- ${hasSlug ? `
729
- /**
730
- * Find one ${collection.singularName} by slug
731
- */
732
- async findBySlug(slug: string, options: FindOneOptions = {}): Promise<${typeName} | null> {
733
- const { data } = await this.findMany({
734
- filters: { slug: { $eq: slug } } as ${typeName}Filters,
735
- pagination: { pageSize: 1 },
736
- ${findBySlugParams.join("\n")}
737
- });
738
-
739
- return data[0] || null;
740
- },
741
- ` : ""}
1040
+ ${findBySlugMethod}
742
1041
  /**
743
1042
  * Create a new ${collection.singularName}
1043
+ * @param {Partial<${typeName}>} data - The data to create
1044
+ * @returns {Promise<${typeName}>}
744
1045
  */
745
- async create(data: Partial<Omit<${typeName}, ${omitFields}>>): Promise<${typeName}> {
1046
+ async create(data) {
746
1047
  const response = await ${toCamelCase(collection.singularName)}Collection.create({ data });
747
1048
  return response.data;
748
1049
  },
749
1050
 
750
1051
  /**
751
1052
  * Update a ${collection.singularName}
1053
+ * @param {${idType}} ${idParam} - The ${idParam} to update
1054
+ * @param {Partial<${typeName}>} data - The data to update
1055
+ * @returns {Promise<${typeName}>}
752
1056
  */
753
- async update(${idParam}, data: Partial<Omit<${typeName}, ${omitFields}>>): Promise<${typeName}> {
754
- const response = await ${toCamelCase(collection.singularName)}Collection.update(${idName}, { data });
1057
+ async update(${idParam}, data) {
1058
+ const response = await ${toCamelCase(collection.singularName)}Collection.update(${idParam}, { data });
755
1059
  return response.data;
756
1060
  },
757
1061
 
758
1062
  /**
759
1063
  * Delete a ${collection.singularName}
1064
+ * @param {${idType}} ${idParam} - The ${idParam} to delete
1065
+ * @returns {Promise<void>}
760
1066
  */
761
- async delete(${idParam}): Promise<void> {
762
- await ${toCamelCase(collection.singularName)}Collection.delete(${idName});
1067
+ async delete(${idParam}) {
1068
+ await ${toCamelCase(collection.singularName)}Collection.delete(${idParam});
763
1069
  },
764
1070
 
765
1071
  /**
766
1072
  * Count ${collection.pluralName}
1073
+ * @param {${typeName}Filters} [filters] - Filter conditions
1074
+ * @returns {Promise<number>}
767
1075
  */
768
- async count(filters?: ${typeName}Filters): Promise<number> {
1076
+ async count(filters) {
769
1077
  const { pagination } = await this.findMany({
770
1078
  filters,
771
1079
  pagination: { pageSize: 1 },
@@ -781,34 +1089,9 @@ function generateSingleService(single, typesImportPath, strapiVersion) {
781
1089
  const serviceName = toCamelCase(single.singularName) + "Service";
782
1090
  const fileName = toKebabCase(single.singularName);
783
1091
  const endpoint = single.singularName;
784
- const isV4 = strapiVersion === "v4";
785
- const omitFields = isV4 ? "'id' | 'createdAt' | 'updatedAt'" : "'id' | 'documentId' | 'createdAt' | 'updatedAt'";
786
1092
  const { localized, draftAndPublish } = single;
787
- const imports = [
788
- `import { single } from '../client';`,
789
- `import type { ${typeName} } from '${typesImportPath}/collections/${fileName}';`
790
- ];
791
- if (localized) {
792
- imports.push(`import type { Locale } from '../locales';`);
793
- }
794
- const findOptionsFields = [
795
- ` populate?: string | string[] | Record<string, unknown>;`
796
- ];
797
- if (localized) {
798
- findOptionsFields.push(` locale?: Locale;`);
799
- }
800
- if (draftAndPublish) {
801
- findOptionsFields.push(` status?: 'draft' | 'published';`);
802
- }
803
- const findParams = [
804
- ` populate: options.populate,`
805
- ];
806
- if (localized) {
807
- findParams.push(` locale: options.locale,`);
808
- }
809
- if (draftAndPublish) {
810
- findParams.push(` status: options.status,`);
811
- }
1093
+ const localeOption = localized ? "\n * @property {string} [locale] - Locale code" : "";
1094
+ const statusOption = draftAndPublish ? "\n * @property {'draft'|'published'} [status] - Publication status" : "";
812
1095
  return `/**
813
1096
  * ${single.displayName} Service (Single Type)
814
1097
  * ${single.description || ""}
@@ -816,23 +1099,29 @@ function generateSingleService(single, typesImportPath, strapiVersion) {
816
1099
  * Strapi version: ${strapiVersion}
817
1100
  */
818
1101
 
819
- ${imports.join("\n")}
1102
+ import { single } from '../client.js';
820
1103
 
821
- export interface FindOptions {
822
- ${findOptionsFields.join("\n")}
823
- }
1104
+ /** @typedef {import('${typesImportPath}/collections/${fileName}').${typeName}} ${typeName} */
1105
+ ${localized ? `/** @typedef {import('../locales').Locale} Locale */` : ""}
824
1106
 
825
- // Create typed single helper
826
- const ${toCamelCase(single.singularName)}Single = single<${typeName}>('${endpoint}');
1107
+ /**
1108
+ * @typedef {Object} FindOptions
1109
+ * @property {string|string[]|Object} [populate] - Relations to populate${localeOption}${statusOption}
1110
+ */
1111
+
1112
+ /** @type {ReturnType<typeof single<${typeName}>>} */
1113
+ const ${toCamelCase(single.singularName)}Single = single('${endpoint}');
827
1114
 
828
1115
  export const ${serviceName} = {
829
1116
  /**
830
1117
  * Get ${single.displayName}
1118
+ * @param {FindOptions} [options={}] - Query options
1119
+ * @returns {Promise<${typeName}|null>}
831
1120
  */
832
- async find(options: FindOptions = {}): Promise<${typeName} | null> {
1121
+ async find(options = {}) {
833
1122
  try {
834
1123
  const response = await ${toCamelCase(single.singularName)}Single.find({
835
- ${findParams.join("\n")}
1124
+ populate: options.populate,${localized ? "\n locale: options.locale," : ""}${draftAndPublish ? "\n status: options.status," : ""}
836
1125
  });
837
1126
 
838
1127
  return response.data;
@@ -846,249 +1135,388 @@ ${findParams.join("\n")}
846
1135
 
847
1136
  /**
848
1137
  * Update ${single.displayName}
1138
+ * @param {Partial<${typeName}>} data - The data to update
1139
+ * @returns {Promise<${typeName}>}
849
1140
  */
850
- async update(data: Partial<Omit<${typeName}, ${omitFields}>>): Promise<${typeName}> {
1141
+ async update(data) {
851
1142
  const response = await ${toCamelCase(single.singularName)}Single.update({ data });
852
1143
  return response.data;
853
1144
  },
854
1145
 
855
1146
  /**
856
1147
  * Delete ${single.displayName}
1148
+ * @returns {Promise<void>}
857
1149
  */
858
- async delete(): Promise<void> {
1150
+ async delete() {
859
1151
  await ${toCamelCase(single.singularName)}Single.delete();
860
1152
  },
861
1153
  };
862
1154
  `;
863
1155
  }
864
- async function generateActions(schema, options) {
865
- const { outputDir, servicesImportPath, strapiVersion = "v5" } = options;
1156
+
1157
+ // src/services/generator.ts
1158
+ async function generateServices(schema, options) {
1159
+ const outputFormat = options.outputFormat ?? "typescript";
1160
+ if (outputFormat === "jsdoc") {
1161
+ return generateJSDocServices(schema, {
1162
+ outputDir: options.outputDir,
1163
+ typesImportPath: options.typesImportPath,
1164
+ strapiVersion: options.strapiVersion
1165
+ });
1166
+ }
1167
+ return generateTypeScriptServices(schema, options);
1168
+ }
1169
+ async function generateTypeScriptServices(schema, options) {
1170
+ const { outputDir, typesImportPath, strapiVersion = "v5" } = options;
866
1171
  const generatedFiles = [];
867
1172
  await ensureDir(outputDir);
868
1173
  for (const collection of schema.collections) {
869
- const fileName = `${toKebabCase(collection.singularName)}.ts`;
870
- const filePath = path7.join(outputDir, fileName);
871
- const content = generateCollectionActions(collection, servicesImportPath, strapiVersion);
1174
+ const fileName = `${toKebabCase(collection.singularName)}.service.ts`;
1175
+ const filePath = path9.join(outputDir, fileName);
1176
+ const content = generateCollectionService2(collection, typesImportPath, strapiVersion);
872
1177
  await writeFile(filePath, await formatCode(content));
873
1178
  generatedFiles.push(filePath);
874
1179
  }
875
1180
  for (const single of schema.singles) {
876
- const fileName = `${toKebabCase(single.singularName)}.ts`;
877
- const filePath = path7.join(outputDir, fileName);
878
- const content = generateSingleActions(single, servicesImportPath);
1181
+ const fileName = `${toKebabCase(single.singularName)}.service.ts`;
1182
+ const filePath = path9.join(outputDir, fileName);
1183
+ const content = generateSingleService2(single, typesImportPath, strapiVersion);
879
1184
  await writeFile(filePath, await formatCode(content));
880
1185
  generatedFiles.push(filePath);
881
1186
  }
882
1187
  return generatedFiles;
883
1188
  }
884
- function generateCollectionActions(collection, servicesImportPath, strapiVersion) {
1189
+ function generateCollectionService2(collection, typesImportPath, strapiVersion) {
1190
+ const typeName = toPascalCase(collection.singularName);
885
1191
  const serviceName = toCamelCase(collection.singularName) + "Service";
886
- const actionsName = toCamelCase(collection.singularName);
887
1192
  const fileName = toKebabCase(collection.singularName);
1193
+ const endpoint = collection.pluralName;
888
1194
  const isV4 = strapiVersion === "v4";
889
- const idInputSchema = isV4 ? "z.number().int().positive()" : "z.string().min(1)";
890
- const idParamName = isV4 ? "id" : "documentId";
891
- const idComment = isV4 ? "id" : "documentId";
1195
+ const idParam = isV4 ? "id: number" : "documentId: string";
1196
+ const idName = isV4 ? "id" : "documentId";
1197
+ const omitFields = isV4 ? "'id' | 'createdAt' | 'updatedAt' | 'publishedAt'" : "'id' | 'documentId' | 'createdAt' | 'updatedAt' | 'publishedAt'";
892
1198
  const hasSlug = "slug" in collection.attributes;
1199
+ const { localized, draftAndPublish } = collection;
1200
+ const imports = [
1201
+ `import { collection } from '../client';`,
1202
+ `import type { ${typeName}, ${typeName}Filters } from '${typesImportPath}/collections/${fileName}';`,
1203
+ `import type { StrapiPagination } from '${typesImportPath}/utils';`
1204
+ ];
1205
+ if (localized) {
1206
+ imports.push(`import type { Locale } from '../locales';`);
1207
+ }
1208
+ const findManyOptionsFields = [
1209
+ ` filters?: ${typeName}Filters;`,
1210
+ ` pagination?: {`,
1211
+ ` /** Page number (1-indexed) - use with pageSize */`,
1212
+ ` page?: number;`,
1213
+ ` /** Number of items per page (default: 25) - use with page */`,
1214
+ ` pageSize?: number;`,
1215
+ ` /** Offset to start from (0-indexed) - use with limit */`,
1216
+ ` start?: number;`,
1217
+ ` /** Maximum number of items to return - use with start */`,
1218
+ ` limit?: number;`,
1219
+ ` };`,
1220
+ ` sort?: string | string[];`,
1221
+ ` populate?: string | string[] | Record<string, unknown>;`
1222
+ ];
1223
+ if (localized) {
1224
+ findManyOptionsFields.push(` locale?: Locale;`);
1225
+ }
1226
+ if (draftAndPublish) {
1227
+ findManyOptionsFields.push(` status?: 'draft' | 'published';`);
1228
+ }
1229
+ const findOneOptionsFields = [
1230
+ ` populate?: string | string[] | Record<string, unknown>;`
1231
+ ];
1232
+ if (localized) {
1233
+ findOneOptionsFields.push(` locale?: Locale;`);
1234
+ }
1235
+ if (draftAndPublish) {
1236
+ findOneOptionsFields.push(` status?: 'draft' | 'published';`);
1237
+ }
1238
+ const findParams = [
1239
+ ` filters: options.filters,`,
1240
+ ` pagination: options.pagination,`,
1241
+ ` sort: options.sort,`,
1242
+ ` populate: options.populate,`
1243
+ ];
1244
+ if (localized) {
1245
+ findParams.push(` locale: options.locale,`);
1246
+ }
1247
+ if (draftAndPublish) {
1248
+ findParams.push(` status: options.status,`);
1249
+ }
1250
+ const findOneParams = [
1251
+ ` populate: options.populate,`
1252
+ ];
1253
+ if (localized) {
1254
+ findOneParams.push(` locale: options.locale,`);
1255
+ }
1256
+ if (draftAndPublish) {
1257
+ findOneParams.push(` status: options.status,`);
1258
+ }
1259
+ const findBySlugParams = [
1260
+ ` populate: options.populate,`
1261
+ ];
1262
+ if (localized) {
1263
+ findBySlugParams.push(` locale: options.locale,`);
1264
+ }
1265
+ if (draftAndPublish) {
1266
+ findBySlugParams.push(` status: options.status,`);
1267
+ }
893
1268
  return `/**
894
- * ${collection.displayName} Actions
1269
+ * ${collection.displayName} Service
895
1270
  * ${collection.description || ""}
896
1271
  * Generated by strapi2front
897
1272
  * Strapi version: ${strapiVersion}
898
1273
  */
899
1274
 
900
- import { defineAction, ActionError } from 'astro:actions';
901
- import { z } from 'astro:schema';
902
- import { ${serviceName} } from '${servicesImportPath}/${fileName}.service';
1275
+ ${imports.join("\n")}
903
1276
 
904
- /**
905
- * Pagination input schema
906
- */
907
- const paginationSchema = z.object({
908
- page: z.number().int().positive().optional().default(1),
909
- pageSize: z.number().int().positive().max(100).optional().default(25),
910
- }).optional();
1277
+ export interface FindManyOptions {
1278
+ ${findManyOptionsFields.join("\n")}
1279
+ }
911
1280
 
912
- /**
913
- * ${collection.displayName} actions
914
- */
915
- export const ${actionsName} = {
1281
+ export interface FindOneOptions {
1282
+ ${findOneOptionsFields.join("\n")}
1283
+ }
1284
+
1285
+ // Create typed collection helper
1286
+ const ${toCamelCase(collection.singularName)}Collection = collection<${typeName}>('${endpoint}');
1287
+
1288
+ export const ${serviceName} = {
916
1289
  /**
917
- * Get all ${collection.pluralName} with pagination
1290
+ * Find multiple ${collection.pluralName}
918
1291
  */
919
- getAll: defineAction({
920
- input: z.object({
921
- pagination: paginationSchema,
922
- sort: z.union([z.string(), z.array(z.string())]).optional(),
923
- }).optional(),
924
- handler: async (input) => {
925
- try {
926
- const result = await ${serviceName}.findMany({
927
- pagination: input?.pagination,
928
- sort: input?.sort,
929
- });
1292
+ async findMany(options: FindManyOptions = {}): Promise<{ data: ${typeName}[]; pagination: StrapiPagination }> {
1293
+ const response = await ${toCamelCase(collection.singularName)}Collection.find({
1294
+ ${findParams.join("\n")}
1295
+ });
930
1296
 
931
- return result;
932
- } catch (error) {
933
- throw new ActionError({
934
- code: 'INTERNAL_SERVER_ERROR',
935
- message: error instanceof Error ? error.message : 'Failed to fetch ${collection.pluralName}',
936
- });
937
- }
938
- },
939
- }),
1297
+ return {
1298
+ data: response.data,
1299
+ pagination: response.meta.pagination,
1300
+ };
1301
+ },
940
1302
 
941
1303
  /**
942
- * Get a single ${collection.singularName} by ${idComment}
1304
+ * Find all ${collection.pluralName} (handles pagination automatically)
943
1305
  */
944
- getOne: defineAction({
945
- input: z.object({
946
- ${idParamName}: ${idInputSchema},
947
- populate: z.union([z.string(), z.array(z.string())]).optional(),
948
- }),
949
- handler: async ({ ${idParamName}, populate }) => {
950
- try {
951
- const result = await ${serviceName}.findOne(${idParamName}, { populate });
1306
+ async findAll(options: Omit<FindManyOptions, 'pagination'> = {}): Promise<${typeName}[]> {
1307
+ const allItems: ${typeName}[] = [];
1308
+ let page = 1;
1309
+ let hasMore = true;
952
1310
 
953
- if (!result) {
954
- throw new ActionError({
955
- code: 'NOT_FOUND',
956
- message: '${collection.displayName} not found',
957
- });
958
- }
1311
+ while (hasMore) {
1312
+ const { data, pagination } = await this.findMany({
1313
+ ...options,
1314
+ pagination: { page, pageSize: 100 },
1315
+ });
959
1316
 
960
- return result;
961
- } catch (error) {
962
- if (error instanceof ActionError) throw error;
963
- throw new ActionError({
964
- code: 'INTERNAL_SERVER_ERROR',
965
- message: error instanceof Error ? error.message : 'Failed to fetch ${collection.singularName}',
966
- });
1317
+ allItems.push(...data);
1318
+ hasMore = page < pagination.pageCount;
1319
+ page++;
1320
+ }
1321
+
1322
+ return allItems;
1323
+ },
1324
+
1325
+ /**
1326
+ * Find one ${collection.singularName} by ${idName}
1327
+ */
1328
+ async findOne(${idParam}, options: FindOneOptions = {}): Promise<${typeName} | null> {
1329
+ try {
1330
+ const response = await ${toCamelCase(collection.singularName)}Collection.findOne(${idName}, {
1331
+ ${findOneParams.join("\n")}
1332
+ });
1333
+
1334
+ return response.data;
1335
+ } catch (error) {
1336
+ // Return null if not found
1337
+ if (error instanceof Error && error.message.includes('404')) {
1338
+ return null;
967
1339
  }
968
- },
969
- }),
1340
+ throw error;
1341
+ }
1342
+ },
970
1343
  ${hasSlug ? `
971
1344
  /**
972
- * Get a single ${collection.singularName} by slug
1345
+ * Find one ${collection.singularName} by slug
973
1346
  */
974
- getBySlug: defineAction({
975
- input: z.object({
976
- slug: z.string().min(1),
977
- populate: z.union([z.string(), z.array(z.string())]).optional(),
978
- }),
979
- handler: async ({ slug, populate }) => {
980
- try {
981
- const result = await ${serviceName}.findBySlug(slug, { populate });
982
-
983
- if (!result) {
984
- throw new ActionError({
985
- code: 'NOT_FOUND',
986
- message: '${collection.displayName} not found',
987
- });
988
- }
1347
+ async findBySlug(slug: string, options: FindOneOptions = {}): Promise<${typeName} | null> {
1348
+ const { data } = await this.findMany({
1349
+ filters: { slug: { $eq: slug } } as ${typeName}Filters,
1350
+ pagination: { pageSize: 1 },
1351
+ ${findBySlugParams.join("\n")}
1352
+ });
989
1353
 
990
- return result;
991
- } catch (error) {
992
- if (error instanceof ActionError) throw error;
993
- throw new ActionError({
994
- code: 'INTERNAL_SERVER_ERROR',
995
- message: error instanceof Error ? error.message : 'Failed to fetch ${collection.singularName}',
996
- });
997
- }
998
- },
999
- }),
1354
+ return data[0] || null;
1355
+ },
1000
1356
  ` : ""}
1001
1357
  /**
1002
1358
  * Create a new ${collection.singularName}
1003
1359
  */
1004
- create: defineAction({
1005
- input: z.object({
1006
- data: z.record(z.unknown()),
1007
- }),
1008
- handler: async ({ data }) => {
1009
- try {
1010
- const result = await ${serviceName}.create(data);
1011
- return result;
1012
- } catch (error) {
1013
- throw new ActionError({
1014
- code: 'INTERNAL_SERVER_ERROR',
1015
- message: error instanceof Error ? error.message : 'Failed to create ${collection.singularName}',
1016
- });
1017
- }
1018
- },
1019
- }),
1360
+ async create(data: Partial<Omit<${typeName}, ${omitFields}>>): Promise<${typeName}> {
1361
+ const response = await ${toCamelCase(collection.singularName)}Collection.create({ data });
1362
+ return response.data;
1363
+ },
1020
1364
 
1021
1365
  /**
1022
1366
  * Update a ${collection.singularName}
1023
1367
  */
1024
- update: defineAction({
1025
- input: z.object({
1026
- ${idParamName}: ${idInputSchema},
1027
- data: z.record(z.unknown()),
1028
- }),
1029
- handler: async ({ ${idParamName}, data }) => {
1030
- try {
1031
- const result = await ${serviceName}.update(${idParamName}, data);
1032
- return result;
1033
- } catch (error) {
1034
- throw new ActionError({
1035
- code: 'INTERNAL_SERVER_ERROR',
1036
- message: error instanceof Error ? error.message : 'Failed to update ${collection.singularName}',
1037
- });
1038
- }
1039
- },
1040
- }),
1368
+ async update(${idParam}, data: Partial<Omit<${typeName}, ${omitFields}>>): Promise<${typeName}> {
1369
+ const response = await ${toCamelCase(collection.singularName)}Collection.update(${idName}, { data });
1370
+ return response.data;
1371
+ },
1041
1372
 
1042
1373
  /**
1043
1374
  * Delete a ${collection.singularName}
1044
1375
  */
1045
- delete: defineAction({
1046
- input: z.object({
1047
- ${idParamName}: ${idInputSchema},
1048
- }),
1049
- handler: async ({ ${idParamName} }) => {
1050
- try {
1051
- await ${serviceName}.delete(${idParamName});
1052
- return { success: true };
1053
- } catch (error) {
1054
- throw new ActionError({
1055
- code: 'INTERNAL_SERVER_ERROR',
1056
- message: error instanceof Error ? error.message : 'Failed to delete ${collection.singularName}',
1057
- });
1058
- }
1059
- },
1060
- }),
1376
+ async delete(${idParam}): Promise<void> {
1377
+ await ${toCamelCase(collection.singularName)}Collection.delete(${idName});
1378
+ },
1061
1379
 
1062
1380
  /**
1063
1381
  * Count ${collection.pluralName}
1064
1382
  */
1065
- count: defineAction({
1066
- input: z.object({
1067
- filters: z.record(z.unknown()).optional(),
1068
- }).optional(),
1069
- handler: async (input) => {
1070
- try {
1071
- const count = await ${serviceName}.count(input?.filters as any);
1072
- return { count };
1073
- } catch (error) {
1074
- throw new ActionError({
1075
- code: 'INTERNAL_SERVER_ERROR',
1076
- message: error instanceof Error ? error.message : 'Failed to count ${collection.pluralName}',
1077
- });
1078
- }
1079
- },
1080
- }),
1383
+ async count(filters?: ${typeName}Filters): Promise<number> {
1384
+ const { pagination } = await this.findMany({
1385
+ filters,
1386
+ pagination: { pageSize: 1 },
1387
+ });
1388
+
1389
+ return pagination.total;
1390
+ },
1081
1391
  };
1082
1392
  `;
1083
1393
  }
1084
- function generateSingleActions(single, servicesImportPath) {
1394
+ function generateSingleService2(single, typesImportPath, strapiVersion) {
1395
+ const typeName = toPascalCase(single.singularName);
1085
1396
  const serviceName = toCamelCase(single.singularName) + "Service";
1086
- const actionsName = toCamelCase(single.singularName);
1087
1397
  const fileName = toKebabCase(single.singularName);
1398
+ const endpoint = single.singularName;
1399
+ const isV4 = strapiVersion === "v4";
1400
+ const omitFields = isV4 ? "'id' | 'createdAt' | 'updatedAt'" : "'id' | 'documentId' | 'createdAt' | 'updatedAt'";
1401
+ const { localized, draftAndPublish } = single;
1402
+ const imports = [
1403
+ `import { single } from '../client';`,
1404
+ `import type { ${typeName} } from '${typesImportPath}/collections/${fileName}';`
1405
+ ];
1406
+ if (localized) {
1407
+ imports.push(`import type { Locale } from '../locales';`);
1408
+ }
1409
+ const findOptionsFields = [
1410
+ ` populate?: string | string[] | Record<string, unknown>;`
1411
+ ];
1412
+ if (localized) {
1413
+ findOptionsFields.push(` locale?: Locale;`);
1414
+ }
1415
+ if (draftAndPublish) {
1416
+ findOptionsFields.push(` status?: 'draft' | 'published';`);
1417
+ }
1418
+ const findParams = [
1419
+ ` populate: options.populate,`
1420
+ ];
1421
+ if (localized) {
1422
+ findParams.push(` locale: options.locale,`);
1423
+ }
1424
+ if (draftAndPublish) {
1425
+ findParams.push(` status: options.status,`);
1426
+ }
1088
1427
  return `/**
1089
- * ${single.displayName} Actions (Single Type)
1428
+ * ${single.displayName} Service (Single Type)
1090
1429
  * ${single.description || ""}
1091
1430
  * Generated by strapi2front
1431
+ * Strapi version: ${strapiVersion}
1432
+ */
1433
+
1434
+ ${imports.join("\n")}
1435
+
1436
+ export interface FindOptions {
1437
+ ${findOptionsFields.join("\n")}
1438
+ }
1439
+
1440
+ // Create typed single helper
1441
+ const ${toCamelCase(single.singularName)}Single = single<${typeName}>('${endpoint}');
1442
+
1443
+ export const ${serviceName} = {
1444
+ /**
1445
+ * Get ${single.displayName}
1446
+ */
1447
+ async find(options: FindOptions = {}): Promise<${typeName} | null> {
1448
+ try {
1449
+ const response = await ${toCamelCase(single.singularName)}Single.find({
1450
+ ${findParams.join("\n")}
1451
+ });
1452
+
1453
+ return response.data;
1454
+ } catch (error) {
1455
+ if (error instanceof Error && error.message.includes('404')) {
1456
+ return null;
1457
+ }
1458
+ throw error;
1459
+ }
1460
+ },
1461
+
1462
+ /**
1463
+ * Update ${single.displayName}
1464
+ */
1465
+ async update(data: Partial<Omit<${typeName}, ${omitFields}>>): Promise<${typeName}> {
1466
+ const response = await ${toCamelCase(single.singularName)}Single.update({ data });
1467
+ return response.data;
1468
+ },
1469
+
1470
+ /**
1471
+ * Delete ${single.displayName}
1472
+ */
1473
+ async delete(): Promise<void> {
1474
+ await ${toCamelCase(single.singularName)}Single.delete();
1475
+ },
1476
+ };
1477
+ `;
1478
+ }
1479
+ function isAstroActionsSupported(astroVersion) {
1480
+ if (!astroVersion) return false;
1481
+ const match = astroVersion.replace(/^[\^~]/, "").match(/^(\d+)/);
1482
+ const majorVersion = match ? parseInt(match[1], 10) : null;
1483
+ return majorVersion !== null && majorVersion >= 4;
1484
+ }
1485
+ async function generateAstroActions(schema, options) {
1486
+ const { outputDir, servicesImportPath, strapiVersion = "v5" } = options;
1487
+ const generatedFiles = [];
1488
+ await ensureDir(outputDir);
1489
+ for (const collection of schema.collections) {
1490
+ const fileName = `${toKebabCase(collection.singularName)}.ts`;
1491
+ const filePath = path9.join(outputDir, fileName);
1492
+ const content = generateCollectionActions(collection, servicesImportPath, strapiVersion);
1493
+ await writeFile(filePath, await formatCode(content));
1494
+ generatedFiles.push(filePath);
1495
+ }
1496
+ for (const single of schema.singles) {
1497
+ const fileName = `${toKebabCase(single.singularName)}.ts`;
1498
+ const filePath = path9.join(outputDir, fileName);
1499
+ const content = generateSingleActions(single, servicesImportPath);
1500
+ await writeFile(filePath, await formatCode(content));
1501
+ generatedFiles.push(filePath);
1502
+ }
1503
+ return generatedFiles;
1504
+ }
1505
+ function generateCollectionActions(collection, servicesImportPath, strapiVersion) {
1506
+ const serviceName = toCamelCase(collection.singularName) + "Service";
1507
+ const actionsName = toCamelCase(collection.singularName);
1508
+ const fileName = toKebabCase(collection.singularName);
1509
+ const isV4 = strapiVersion === "v4";
1510
+ const idInputSchema = isV4 ? "z.number().int().positive()" : "z.string().min(1)";
1511
+ const idParamName = isV4 ? "id" : "documentId";
1512
+ const idComment = isV4 ? "id" : "documentId";
1513
+ const hasSlug = "slug" in collection.attributes;
1514
+ return `/**
1515
+ * ${collection.displayName} Actions
1516
+ * ${collection.description || ""}
1517
+ * Generated by strapi2front
1518
+ * Framework: Astro
1519
+ * Strapi version: ${strapiVersion}
1092
1520
  */
1093
1521
 
1094
1522
  import { defineAction, ActionError } from 'astro:actions';
@@ -1096,26 +1524,58 @@ import { z } from 'astro:schema';
1096
1524
  import { ${serviceName} } from '${servicesImportPath}/${fileName}.service';
1097
1525
 
1098
1526
  /**
1099
- * ${single.displayName} actions
1527
+ * Pagination input schema
1528
+ */
1529
+ const paginationSchema = z.object({
1530
+ page: z.number().int().positive().optional().default(1),
1531
+ pageSize: z.number().int().positive().max(100).optional().default(25),
1532
+ }).optional();
1533
+
1534
+ /**
1535
+ * ${collection.displayName} actions
1100
1536
  */
1101
1537
  export const ${actionsName} = {
1102
1538
  /**
1103
- * Get ${single.displayName}
1539
+ * Get all ${collection.pluralName} with pagination
1104
1540
  */
1105
- get: defineAction({
1541
+ getAll: defineAction({
1106
1542
  input: z.object({
1107
- populate: z.union([z.string(), z.array(z.string())]).optional(),
1543
+ pagination: paginationSchema,
1544
+ sort: z.union([z.string(), z.array(z.string())]).optional(),
1108
1545
  }).optional(),
1109
1546
  handler: async (input) => {
1110
1547
  try {
1111
- const result = await ${serviceName}.find({
1112
- populate: input?.populate,
1548
+ const result = await ${serviceName}.findMany({
1549
+ pagination: input?.pagination,
1550
+ sort: input?.sort,
1113
1551
  });
1114
1552
 
1553
+ return result;
1554
+ } catch (error) {
1555
+ throw new ActionError({
1556
+ code: 'INTERNAL_SERVER_ERROR',
1557
+ message: error instanceof Error ? error.message : 'Failed to fetch ${collection.pluralName}',
1558
+ });
1559
+ }
1560
+ },
1561
+ }),
1562
+
1563
+ /**
1564
+ * Get a single ${collection.singularName} by ${idComment}
1565
+ */
1566
+ getOne: defineAction({
1567
+ input: z.object({
1568
+ ${idParamName}: ${idInputSchema},
1569
+ populate: z.union([z.string(), z.array(z.string())]).optional(),
1570
+ }),
1571
+ handler: async ({ ${idParamName}, populate }) => {
1572
+ try {
1573
+ const result = await ${serviceName}.findOne(${idParamName}, { populate });
1574
+
1115
1575
  if (!result) {
1116
1576
  throw new ActionError({
1117
1577
  code: 'NOT_FOUND',
1118
- message: '${single.displayName} not found',
1578
+ message: '${collection.displayName} not found',
1119
1579
  });
1120
1580
  }
1121
1581
 
@@ -1124,22 +1584,185 @@ export const ${actionsName} = {
1124
1584
  if (error instanceof ActionError) throw error;
1125
1585
  throw new ActionError({
1126
1586
  code: 'INTERNAL_SERVER_ERROR',
1127
- message: error instanceof Error ? error.message : 'Failed to fetch ${single.singularName}',
1587
+ message: error instanceof Error ? error.message : 'Failed to fetch ${collection.singularName}',
1128
1588
  });
1129
1589
  }
1130
1590
  },
1131
1591
  }),
1132
-
1592
+ ${hasSlug ? `
1133
1593
  /**
1134
- * Update ${single.displayName}
1594
+ * Get a single ${collection.singularName} by slug
1135
1595
  */
1136
- update: defineAction({
1596
+ getBySlug: defineAction({
1137
1597
  input: z.object({
1138
- data: z.record(z.unknown()),
1598
+ slug: z.string().min(1),
1599
+ populate: z.union([z.string(), z.array(z.string())]).optional(),
1139
1600
  }),
1140
- handler: async ({ data }) => {
1601
+ handler: async ({ slug, populate }) => {
1141
1602
  try {
1142
- const result = await ${serviceName}.update(data);
1603
+ const result = await ${serviceName}.findBySlug(slug, { populate });
1604
+
1605
+ if (!result) {
1606
+ throw new ActionError({
1607
+ code: 'NOT_FOUND',
1608
+ message: '${collection.displayName} not found',
1609
+ });
1610
+ }
1611
+
1612
+ return result;
1613
+ } catch (error) {
1614
+ if (error instanceof ActionError) throw error;
1615
+ throw new ActionError({
1616
+ code: 'INTERNAL_SERVER_ERROR',
1617
+ message: error instanceof Error ? error.message : 'Failed to fetch ${collection.singularName}',
1618
+ });
1619
+ }
1620
+ },
1621
+ }),
1622
+ ` : ""}
1623
+ /**
1624
+ * Create a new ${collection.singularName}
1625
+ */
1626
+ create: defineAction({
1627
+ input: z.object({
1628
+ data: z.record(z.unknown()),
1629
+ }),
1630
+ handler: async ({ data }) => {
1631
+ try {
1632
+ const result = await ${serviceName}.create(data);
1633
+ return result;
1634
+ } catch (error) {
1635
+ throw new ActionError({
1636
+ code: 'INTERNAL_SERVER_ERROR',
1637
+ message: error instanceof Error ? error.message : 'Failed to create ${collection.singularName}',
1638
+ });
1639
+ }
1640
+ },
1641
+ }),
1642
+
1643
+ /**
1644
+ * Update a ${collection.singularName}
1645
+ */
1646
+ update: defineAction({
1647
+ input: z.object({
1648
+ ${idParamName}: ${idInputSchema},
1649
+ data: z.record(z.unknown()),
1650
+ }),
1651
+ handler: async ({ ${idParamName}, data }) => {
1652
+ try {
1653
+ const result = await ${serviceName}.update(${idParamName}, data);
1654
+ return result;
1655
+ } catch (error) {
1656
+ throw new ActionError({
1657
+ code: 'INTERNAL_SERVER_ERROR',
1658
+ message: error instanceof Error ? error.message : 'Failed to update ${collection.singularName}',
1659
+ });
1660
+ }
1661
+ },
1662
+ }),
1663
+
1664
+ /**
1665
+ * Delete a ${collection.singularName}
1666
+ */
1667
+ delete: defineAction({
1668
+ input: z.object({
1669
+ ${idParamName}: ${idInputSchema},
1670
+ }),
1671
+ handler: async ({ ${idParamName} }) => {
1672
+ try {
1673
+ await ${serviceName}.delete(${idParamName});
1674
+ return { success: true };
1675
+ } catch (error) {
1676
+ throw new ActionError({
1677
+ code: 'INTERNAL_SERVER_ERROR',
1678
+ message: error instanceof Error ? error.message : 'Failed to delete ${collection.singularName}',
1679
+ });
1680
+ }
1681
+ },
1682
+ }),
1683
+
1684
+ /**
1685
+ * Count ${collection.pluralName}
1686
+ */
1687
+ count: defineAction({
1688
+ input: z.object({
1689
+ filters: z.record(z.unknown()).optional(),
1690
+ }).optional(),
1691
+ handler: async (input) => {
1692
+ try {
1693
+ const count = await ${serviceName}.count(input?.filters as any);
1694
+ return { count };
1695
+ } catch (error) {
1696
+ throw new ActionError({
1697
+ code: 'INTERNAL_SERVER_ERROR',
1698
+ message: error instanceof Error ? error.message : 'Failed to count ${collection.pluralName}',
1699
+ });
1700
+ }
1701
+ },
1702
+ }),
1703
+ };
1704
+ `;
1705
+ }
1706
+ function generateSingleActions(single, servicesImportPath) {
1707
+ const serviceName = toCamelCase(single.singularName) + "Service";
1708
+ const actionsName = toCamelCase(single.singularName);
1709
+ const fileName = toKebabCase(single.singularName);
1710
+ return `/**
1711
+ * ${single.displayName} Actions (Single Type)
1712
+ * ${single.description || ""}
1713
+ * Generated by strapi2front
1714
+ * Framework: Astro
1715
+ */
1716
+
1717
+ import { defineAction, ActionError } from 'astro:actions';
1718
+ import { z } from 'astro:schema';
1719
+ import { ${serviceName} } from '${servicesImportPath}/${fileName}.service';
1720
+
1721
+ /**
1722
+ * ${single.displayName} actions
1723
+ */
1724
+ export const ${actionsName} = {
1725
+ /**
1726
+ * Get ${single.displayName}
1727
+ */
1728
+ get: defineAction({
1729
+ input: z.object({
1730
+ populate: z.union([z.string(), z.array(z.string())]).optional(),
1731
+ }).optional(),
1732
+ handler: async (input) => {
1733
+ try {
1734
+ const result = await ${serviceName}.find({
1735
+ populate: input?.populate,
1736
+ });
1737
+
1738
+ if (!result) {
1739
+ throw new ActionError({
1740
+ code: 'NOT_FOUND',
1741
+ message: '${single.displayName} not found',
1742
+ });
1743
+ }
1744
+
1745
+ return result;
1746
+ } catch (error) {
1747
+ if (error instanceof ActionError) throw error;
1748
+ throw new ActionError({
1749
+ code: 'INTERNAL_SERVER_ERROR',
1750
+ message: error instanceof Error ? error.message : 'Failed to fetch ${single.singularName}',
1751
+ });
1752
+ }
1753
+ },
1754
+ }),
1755
+
1756
+ /**
1757
+ * Update ${single.displayName}
1758
+ */
1759
+ update: defineAction({
1760
+ input: z.object({
1761
+ data: z.record(z.unknown()),
1762
+ }),
1763
+ handler: async ({ data }) => {
1764
+ try {
1765
+ const result = await ${serviceName}.update(data);
1143
1766
  return result;
1144
1767
  } catch (error) {
1145
1768
  throw new ActionError({
@@ -1152,11 +1775,20 @@ export const ${actionsName} = {
1152
1775
  };
1153
1776
  `;
1154
1777
  }
1778
+
1779
+ // src/actions/generator.ts
1780
+ async function generateActions(schema, options) {
1781
+ return generateAstroActions(schema, {
1782
+ outputDir: options.outputDir,
1783
+ servicesImportPath: options.servicesImportPath,
1784
+ strapiVersion: options.strapiVersion
1785
+ });
1786
+ }
1155
1787
  async function generateClient(options) {
1156
1788
  const { outputDir, strapiVersion = "v5", apiPrefix = "/api" } = options;
1157
1789
  const generatedFiles = [];
1158
1790
  await ensureDir(outputDir);
1159
- const filePath = path7.join(outputDir, "client.ts");
1791
+ const filePath = path9.join(outputDir, "client.ts");
1160
1792
  const content = generateClientFile(strapiVersion, apiPrefix);
1161
1793
  await writeFile(filePath, await formatCode(content));
1162
1794
  generatedFiles.push(filePath);
@@ -1426,7 +2058,7 @@ async function generateLocales(locales, options) {
1426
2058
  const { outputDir } = options;
1427
2059
  const generatedFiles = [];
1428
2060
  await ensureDir(outputDir);
1429
- const filePath = path7.join(outputDir, "locales.ts");
2061
+ const filePath = path9.join(outputDir, "locales.ts");
1430
2062
  const content = generateLocalesFile(locales);
1431
2063
  await writeFile(filePath, await formatCode(content));
1432
2064
  generatedFiles.push(filePath);
@@ -1469,203 +2101,1196 @@ export const locales = [${localeArray}] as const;
1469
2101
  */
1470
2102
  export type Locale = ${localeCodes};
1471
2103
 
1472
- /**
1473
- * Default locale
1474
- */
1475
- export const defaultLocale: Locale = '${defaultLocale}';
2104
+ /**
2105
+ * Default locale
2106
+ */
2107
+ export const defaultLocale: Locale = '${defaultLocale}';
2108
+
2109
+ /**
2110
+ * Locale display names
2111
+ */
2112
+ export const localeNames: Record<Locale, string> = {
2113
+ ${localeNames}
2114
+ };
2115
+
2116
+ /**
2117
+ * Check if a string is a valid locale
2118
+ */
2119
+ export function isValidLocale(code: string): code is Locale {
2120
+ return locales.includes(code as Locale);
2121
+ }
2122
+
2123
+ /**
2124
+ * Get locale name by code
2125
+ */
2126
+ export function getLocaleName(code: Locale): string {
2127
+ return localeNames[code] || code;
2128
+ }
2129
+ `;
2130
+ }
2131
+ async function generateByFeature(schema, locales, options) {
2132
+ const {
2133
+ outputDir,
2134
+ features,
2135
+ blocksRendererInstalled = false,
2136
+ strapiVersion = "v5",
2137
+ apiPrefix = "/api",
2138
+ outputFormat = "typescript"
2139
+ } = options;
2140
+ const generatedFiles = [];
2141
+ const ext = outputFormat === "jsdoc" ? "js" : "ts";
2142
+ await ensureDir(path9.join(outputDir, "collections"));
2143
+ await ensureDir(path9.join(outputDir, "singles"));
2144
+ await ensureDir(path9.join(outputDir, "components"));
2145
+ await ensureDir(path9.join(outputDir, "shared"));
2146
+ const sharedDir = path9.join(outputDir, "shared");
2147
+ const utilsPath = path9.join(sharedDir, `utils.${ext}`);
2148
+ const utilsContent = outputFormat === "jsdoc" ? generateUtilityTypesJSDoc(blocksRendererInstalled, strapiVersion) : generateUtilityTypes3(blocksRendererInstalled, strapiVersion);
2149
+ await writeFile(utilsPath, await formatCode(utilsContent));
2150
+ generatedFiles.push(utilsPath);
2151
+ const clientPath = path9.join(sharedDir, `client.${ext}`);
2152
+ const clientContent = outputFormat === "jsdoc" ? generateClientJSDoc(strapiVersion, apiPrefix) : generateClient2(strapiVersion, apiPrefix);
2153
+ await writeFile(clientPath, await formatCode(clientContent));
2154
+ generatedFiles.push(clientPath);
2155
+ const localesPath = path9.join(sharedDir, `locales.${ext}`);
2156
+ const localesContent = outputFormat === "jsdoc" ? generateLocalesFileJSDoc(locales) : generateLocalesFile2(locales);
2157
+ await writeFile(localesPath, await formatCode(localesContent));
2158
+ generatedFiles.push(localesPath);
2159
+ for (const collection of schema.collections) {
2160
+ const featureDir = path9.join(outputDir, "collections", toKebabCase(collection.singularName));
2161
+ await ensureDir(featureDir);
2162
+ if (features.types) {
2163
+ const typesPath = path9.join(featureDir, `types.${ext}`);
2164
+ const content = outputFormat === "jsdoc" ? generateCollectionTypesJSDoc(collection, schema) : generateCollectionTypes(collection, schema);
2165
+ await writeFile(typesPath, await formatCode(content));
2166
+ generatedFiles.push(typesPath);
2167
+ }
2168
+ if (features.services) {
2169
+ const servicePath = path9.join(featureDir, `service.${ext}`);
2170
+ const content = outputFormat === "jsdoc" ? generateCollectionServiceJSDoc(collection, strapiVersion) : generateCollectionService3(collection, strapiVersion);
2171
+ await writeFile(servicePath, await formatCode(content));
2172
+ generatedFiles.push(servicePath);
2173
+ }
2174
+ if (features.actions) {
2175
+ const actionsPath = path9.join(featureDir, `actions.${ext}`);
2176
+ await writeFile(actionsPath, await formatCode(generateCollectionActions2(collection, strapiVersion)));
2177
+ generatedFiles.push(actionsPath);
2178
+ }
2179
+ }
2180
+ for (const single of schema.singles) {
2181
+ const featureDir = path9.join(outputDir, "singles", toKebabCase(single.singularName));
2182
+ await ensureDir(featureDir);
2183
+ if (features.types) {
2184
+ const typesPath = path9.join(featureDir, `types.${ext}`);
2185
+ const content = outputFormat === "jsdoc" ? generateSingleTypesJSDoc(single, schema) : generateSingleTypes(single, schema);
2186
+ await writeFile(typesPath, await formatCode(content));
2187
+ generatedFiles.push(typesPath);
2188
+ }
2189
+ if (features.services) {
2190
+ const servicePath = path9.join(featureDir, `service.${ext}`);
2191
+ const content = outputFormat === "jsdoc" ? generateSingleServiceJSDoc(single, strapiVersion) : generateSingleService3(single, strapiVersion);
2192
+ await writeFile(servicePath, await formatCode(content));
2193
+ generatedFiles.push(servicePath);
2194
+ }
2195
+ }
2196
+ for (const component of schema.components) {
2197
+ const componentPath = path9.join(outputDir, "components", `${toKebabCase(component.name)}.${ext}`);
2198
+ const content = outputFormat === "jsdoc" ? generateComponentTypesJSDoc(component, schema) : generateComponentTypes(component, schema);
2199
+ await writeFile(componentPath, await formatCode(content));
2200
+ generatedFiles.push(componentPath);
2201
+ }
2202
+ return generatedFiles;
2203
+ }
2204
+ function generateUtilityTypes3(blocksRendererInstalled, strapiVersion) {
2205
+ const isV4 = strapiVersion === "v4";
2206
+ const blocksContentType = isV4 ? `/**
2207
+ * Rich text content (Strapi v4)
2208
+ * Can be markdown string or custom JSON structure
2209
+ */
2210
+ export type RichTextContent = string;` : blocksRendererInstalled ? `/**
2211
+ * Blocks content type (Strapi v5 rich text)
2212
+ * Re-exported from @strapi/blocks-react-renderer
2213
+ */
2214
+ export type { BlocksContent } from '@strapi/blocks-react-renderer';` : `/**
2215
+ * Blocks content type (Strapi v5 rich text)
2216
+ *
2217
+ * For full type support and rendering, install:
2218
+ * npm install @strapi/blocks-react-renderer
2219
+ *
2220
+ * Then re-run: npx strapi2front sync
2221
+ */
2222
+ export type BlocksContent = unknown[];`;
2223
+ const baseEntity = isV4 ? `export interface StrapiBaseEntity {
2224
+ id: number;
2225
+ createdAt: string;
2226
+ updatedAt: string;
2227
+ publishedAt: string | null;
2228
+ }` : `export interface StrapiBaseEntity {
2229
+ id: number;
2230
+ documentId: string;
2231
+ createdAt: string;
2232
+ updatedAt: string;
2233
+ publishedAt: string | null;
2234
+ }`;
2235
+ const mediaType = isV4 ? `export interface StrapiMedia {
2236
+ id: number;
2237
+ name: string;
2238
+ alternativeText: string | null;
2239
+ caption: string | null;
2240
+ width: number;
2241
+ height: number;
2242
+ formats: {
2243
+ thumbnail?: StrapiMediaFormat;
2244
+ small?: StrapiMediaFormat;
2245
+ medium?: StrapiMediaFormat;
2246
+ large?: StrapiMediaFormat;
2247
+ } | null;
2248
+ hash: string;
2249
+ ext: string;
2250
+ mime: string;
2251
+ size: number;
2252
+ url: string;
2253
+ previewUrl: string | null;
2254
+ provider: string;
2255
+ createdAt: string;
2256
+ updatedAt: string;
2257
+ }` : `export interface StrapiMedia {
2258
+ id: number;
2259
+ documentId: string;
2260
+ name: string;
2261
+ alternativeText: string | null;
2262
+ caption: string | null;
2263
+ width: number;
2264
+ height: number;
2265
+ formats: {
2266
+ thumbnail?: StrapiMediaFormat;
2267
+ small?: StrapiMediaFormat;
2268
+ medium?: StrapiMediaFormat;
2269
+ large?: StrapiMediaFormat;
2270
+ } | null;
2271
+ hash: string;
2272
+ ext: string;
2273
+ mime: string;
2274
+ size: number;
2275
+ url: string;
2276
+ previewUrl: string | null;
2277
+ provider: string;
2278
+ createdAt: string;
2279
+ updatedAt: string;
2280
+ }`;
2281
+ const v4RawResponseTypes = isV4 ? `
2282
+
2283
+ /**
2284
+ * Strapi v4 raw API response (with nested attributes)
2285
+ */
2286
+ export interface StrapiV4RawItem<T> {
2287
+ id: number;
2288
+ attributes: Omit<T, 'id'>;
2289
+ }
2290
+
2291
+ export interface StrapiV4RawResponse<T> {
2292
+ data: StrapiV4RawItem<T>;
2293
+ meta: Record<string, unknown>;
2294
+ }
2295
+
2296
+ export interface StrapiV4RawListResponse<T> {
2297
+ data: StrapiV4RawItem<T>[];
2298
+ meta: {
2299
+ pagination: StrapiPagination;
2300
+ };
2301
+ }
2302
+
2303
+ /**
2304
+ * Flatten Strapi v4 response item
2305
+ */
2306
+ export function flattenV4Response<T>(item: StrapiV4RawItem<T>): T {
2307
+ return { id: item.id, ...item.attributes } as T;
2308
+ }
2309
+
2310
+ /**
2311
+ * Flatten Strapi v4 list response
2312
+ */
2313
+ export function flattenV4ListResponse<T>(items: StrapiV4RawItem<T>[]): T[] {
2314
+ return items.map(item => flattenV4Response<T>(item));
2315
+ }` : "";
2316
+ return `/**
2317
+ * Strapi utility types
2318
+ * Generated by strapi2front
2319
+ * Strapi version: ${strapiVersion}
2320
+ */
2321
+
2322
+ ${mediaType}
2323
+
2324
+ export interface StrapiMediaFormat {
2325
+ name: string;
2326
+ hash: string;
2327
+ ext: string;
2328
+ mime: string;
2329
+ width: number;
2330
+ height: number;
2331
+ size: number;
2332
+ url: string;
2333
+ }
2334
+
2335
+ export interface StrapiPagination {
2336
+ page: number;
2337
+ pageSize: number;
2338
+ pageCount: number;
2339
+ total: number;
2340
+ }
2341
+
2342
+ export interface StrapiResponse<T> {
2343
+ data: T;
2344
+ meta: {
2345
+ pagination?: StrapiPagination;
2346
+ };
2347
+ }
2348
+
2349
+ export interface StrapiListResponse<T> {
2350
+ data: T[];
2351
+ meta: {
2352
+ pagination: StrapiPagination;
2353
+ };
2354
+ }
2355
+
2356
+ ${baseEntity}
2357
+ ${v4RawResponseTypes}
2358
+ ${blocksContentType}
2359
+ `;
2360
+ }
2361
+ function generateClient2(strapiVersion, apiPrefix = "/api") {
2362
+ const isV4 = strapiVersion === "v4";
2363
+ const normalizedPrefix = apiPrefix.startsWith("/") ? apiPrefix : "/" + apiPrefix;
2364
+ if (isV4) {
2365
+ return `/**
2366
+ * Strapi Client (v4)
2367
+ * Generated by strapi2front
2368
+ */
2369
+
2370
+ import Strapi from 'strapi-sdk-js';
2371
+ import type { StrapiPagination } from './utils';
2372
+
2373
+ // Initialize the Strapi client
2374
+ const strapiUrl = import.meta.env.STRAPI_URL || process.env.STRAPI_URL || 'http://localhost:1337';
2375
+ const strapiToken = import.meta.env.STRAPI_TOKEN || process.env.STRAPI_TOKEN;
2376
+ const strapiApiPrefix = import.meta.env.STRAPI_API_PREFIX || process.env.STRAPI_API_PREFIX || '${normalizedPrefix}';
2377
+
2378
+ export const strapi = new Strapi({
2379
+ url: strapiUrl,
2380
+ prefix: strapiApiPrefix,
2381
+ axiosOptions: {
2382
+ headers: strapiToken ? {
2383
+ Authorization: \`Bearer \${strapiToken}\`,
2384
+ } : {},
2385
+ },
2386
+ });
2387
+
2388
+ // Default pagination for fallback
2389
+ const defaultPagination: StrapiPagination = {
2390
+ page: 1,
2391
+ pageSize: 25,
2392
+ pageCount: 1,
2393
+ total: 0,
2394
+ };
2395
+
2396
+ // Strapi v4 raw response types (with nested attributes)
2397
+ interface StrapiV4RawItem<T> {
2398
+ id: number;
2399
+ attributes: Omit<T, 'id'>;
2400
+ }
2401
+
2402
+ interface StrapiV4RawListResponse<T> {
2403
+ data: StrapiV4RawItem<T>[];
2404
+ meta: {
2405
+ pagination?: StrapiPagination;
2406
+ };
2407
+ }
2408
+
2409
+ interface StrapiV4RawSingleResponse<T> {
2410
+ data: StrapiV4RawItem<T>;
2411
+ meta?: Record<string, unknown>;
2412
+ }
2413
+
2414
+ /**
2415
+ * Flatten a Strapi v4 response item (merges id with attributes)
2416
+ */
2417
+ function flattenItem<T>(item: StrapiV4RawItem<T>): T {
2418
+ return { id: item.id, ...item.attributes } as T;
2419
+ }
2420
+
2421
+ /**
2422
+ * Recursively flatten nested relations in Strapi v4 response
2423
+ */
2424
+ function flattenRelations<T>(data: T): T {
2425
+ if (data === null || data === undefined) return data;
2426
+ if (Array.isArray(data)) {
2427
+ return data.map(item => flattenRelations(item)) as unknown as T;
2428
+ }
2429
+ if (typeof data === 'object') {
2430
+ const result: Record<string, unknown> = {};
2431
+ for (const [key, value] of Object.entries(data as Record<string, unknown>)) {
2432
+ // Check if this is a Strapi v4 relation response { data: { id, attributes } }
2433
+ if (value && typeof value === 'object' && 'data' in value) {
2434
+ const relationData = (value as { data: unknown }).data;
2435
+ if (relationData === null) {
2436
+ result[key] = null;
2437
+ } else if (Array.isArray(relationData)) {
2438
+ // To-many relation
2439
+ result[key] = relationData.map((item: StrapiV4RawItem<unknown>) =>
2440
+ flattenRelations(flattenItem(item))
2441
+ );
2442
+ } else if (typeof relationData === 'object' && 'id' in relationData && 'attributes' in relationData) {
2443
+ // To-one relation
2444
+ result[key] = flattenRelations(flattenItem(relationData as StrapiV4RawItem<unknown>));
2445
+ } else {
2446
+ result[key] = flattenRelations(value);
2447
+ }
2448
+ } else {
2449
+ result[key] = flattenRelations(value);
2450
+ }
2451
+ }
2452
+ return result as T;
2453
+ }
2454
+ return data;
2455
+ }
2456
+
2457
+ // Helper to get typed collection
2458
+ export function collection<T>(pluralName: string) {
2459
+ return {
2460
+ async find(params?: Record<string, unknown>): Promise<{ data: T[]; meta: { pagination: StrapiPagination } }> {
2461
+ const response = await strapi.find(pluralName, params) as unknown as StrapiV4RawListResponse<T>;
2462
+ const flattenedData = Array.isArray(response.data)
2463
+ ? response.data.map(item => flattenRelations(flattenItem<T>(item)))
2464
+ : [];
2465
+ return {
2466
+ data: flattenedData,
2467
+ meta: {
2468
+ pagination: response.meta?.pagination || defaultPagination,
2469
+ },
2470
+ };
2471
+ },
2472
+ async findOne(id: number | string, params?: Record<string, unknown>): Promise<{ data: T }> {
2473
+ const response = await strapi.findOne(pluralName, String(id), params) as unknown as StrapiV4RawSingleResponse<T>;
2474
+ return { data: flattenRelations(flattenItem<T>(response.data)) };
2475
+ },
2476
+ async create(data: { data: Partial<T> }): Promise<{ data: T }> {
2477
+ const response = await strapi.create(pluralName, data.data as Record<string, unknown>) as unknown as StrapiV4RawSingleResponse<T>;
2478
+ return { data: flattenRelations(flattenItem<T>(response.data)) };
2479
+ },
2480
+ async update(id: number | string, data: { data: Partial<T> }): Promise<{ data: T }> {
2481
+ const response = await strapi.update(pluralName, String(id), data.data as Record<string, unknown>) as unknown as StrapiV4RawSingleResponse<T>;
2482
+ return { data: flattenRelations(flattenItem<T>(response.data)) };
2483
+ },
2484
+ async delete(id: number | string): Promise<void> {
2485
+ await strapi.delete(pluralName, String(id));
2486
+ },
2487
+ };
2488
+ }
2489
+
2490
+ // Helper to get typed single type
2491
+ export function single<T>(singularName: string) {
2492
+ return {
2493
+ async find(params?: Record<string, unknown>): Promise<{ data: T }> {
2494
+ const response = await strapi.find(singularName, params) as unknown as StrapiV4RawSingleResponse<T>;
2495
+ return { data: flattenRelations(flattenItem<T>(response.data)) };
2496
+ },
2497
+ async update(data: { data: Partial<T> }): Promise<{ data: T }> {
2498
+ const response = await strapi.update(singularName, 1 as unknown as string, data.data as Record<string, unknown>) as unknown as StrapiV4RawSingleResponse<T>;
2499
+ return { data: flattenRelations(flattenItem<T>(response.data)) };
2500
+ },
2501
+ async delete(): Promise<void> {
2502
+ await strapi.delete(singularName, 1 as unknown as string);
2503
+ },
2504
+ };
2505
+ }
2506
+ `;
2507
+ }
2508
+ return `/**
2509
+ * Strapi Client (v5)
2510
+ * Generated by strapi2front
2511
+ */
2512
+
2513
+ import Strapi from 'strapi-sdk-js';
2514
+ import type { StrapiPagination } from './utils';
2515
+
2516
+ // Initialize the Strapi client
2517
+ const strapiUrl = import.meta.env.STRAPI_URL || process.env.STRAPI_URL || 'http://localhost:1337';
2518
+ const strapiToken = import.meta.env.STRAPI_TOKEN || process.env.STRAPI_TOKEN;
2519
+ const strapiApiPrefix = import.meta.env.STRAPI_API_PREFIX || process.env.STRAPI_API_PREFIX || '${normalizedPrefix}';
2520
+
2521
+ export const strapi = new Strapi({
2522
+ url: strapiUrl,
2523
+ prefix: strapiApiPrefix,
2524
+ axiosOptions: {
2525
+ headers: strapiToken ? {
2526
+ Authorization: \`Bearer \${strapiToken}\`,
2527
+ } : {},
2528
+ },
2529
+ });
2530
+
2531
+ // Default pagination for fallback
2532
+ const defaultPagination: StrapiPagination = {
2533
+ page: 1,
2534
+ pageSize: 25,
2535
+ pageCount: 1,
2536
+ total: 0,
2537
+ };
2538
+
2539
+ // Response types from strapi-sdk-js
2540
+ interface StrapiListResponse<T> {
2541
+ data: T[];
2542
+ meta: {
2543
+ pagination?: StrapiPagination;
2544
+ };
2545
+ }
2546
+
2547
+ interface StrapiSingleResponse<T> {
2548
+ data: T;
2549
+ meta?: Record<string, unknown>;
2550
+ }
2551
+
2552
+ // Helper to get typed collection
2553
+ export function collection<T>(pluralName: string) {
2554
+ return {
2555
+ async find(params?: Record<string, unknown>): Promise<{ data: T[]; meta: { pagination: StrapiPagination } }> {
2556
+ const response = await strapi.find(pluralName, params) as unknown as StrapiListResponse<T>;
2557
+ return {
2558
+ data: Array.isArray(response.data) ? response.data : [],
2559
+ meta: {
2560
+ pagination: response.meta?.pagination || defaultPagination,
2561
+ },
2562
+ };
2563
+ },
2564
+ async findOne(documentId: string, params?: Record<string, unknown>): Promise<{ data: T }> {
2565
+ const response = await strapi.findOne(pluralName, documentId, params) as unknown as StrapiSingleResponse<T>;
2566
+ return { data: response.data };
2567
+ },
2568
+ async create(data: { data: Partial<T> }): Promise<{ data: T }> {
2569
+ const response = await strapi.create(pluralName, data.data as Record<string, unknown>) as unknown as StrapiSingleResponse<T>;
2570
+ return { data: response.data };
2571
+ },
2572
+ async update(documentId: string, data: { data: Partial<T> }): Promise<{ data: T }> {
2573
+ const response = await strapi.update(pluralName, documentId, data.data as Record<string, unknown>) as unknown as StrapiSingleResponse<T>;
2574
+ return { data: response.data };
2575
+ },
2576
+ async delete(documentId: string): Promise<void> {
2577
+ await strapi.delete(pluralName, documentId);
2578
+ },
2579
+ };
2580
+ }
2581
+
2582
+ // Helper to get typed single type
2583
+ export function single<T>(singularName: string) {
2584
+ return {
2585
+ async find(params?: Record<string, unknown>): Promise<{ data: T }> {
2586
+ const response = await strapi.find(singularName, params) as unknown as StrapiSingleResponse<T>;
2587
+ return { data: response.data };
2588
+ },
2589
+ async update(data: { data: Partial<T> }): Promise<{ data: T }> {
2590
+ const response = await strapi.update(singularName, 1 as unknown as string, data.data as Record<string, unknown>) as unknown as StrapiSingleResponse<T>;
2591
+ return { data: response.data };
2592
+ },
2593
+ async delete(): Promise<void> {
2594
+ await strapi.delete(singularName, 1 as unknown as string);
2595
+ },
2596
+ };
2597
+ }
2598
+ `;
2599
+ }
2600
+ function generateLocalesFile2(locales) {
2601
+ if (locales.length === 0) {
2602
+ return `/**
2603
+ * Strapi locales
2604
+ * Generated by strapi2front
2605
+ * Note: i18n is not enabled in Strapi
2606
+ */
2607
+
2608
+ export const locales = [] as const;
2609
+ export type Locale = string;
2610
+ export const defaultLocale: Locale = 'en';
2611
+ export const localeNames: Record<string, string> = {};
2612
+
2613
+ export function isValidLocale(_code: string): _code is Locale {
2614
+ return true;
2615
+ }
2616
+
2617
+ export function getLocaleName(code: string): string {
2618
+ return code;
2619
+ }
2620
+ `;
2621
+ }
2622
+ const localeCodes = locales.map((l) => l.code);
2623
+ const defaultLocale = locales.find((l) => l.isDefault)?.code || locales[0]?.code || "en";
2624
+ return `/**
2625
+ * Strapi locales
2626
+ * Generated by strapi2front
2627
+ */
2628
+
2629
+ export const locales = [${localeCodes.map((c) => `'${c}'`).join(", ")}] as const;
2630
+
2631
+ export type Locale = typeof locales[number];
2632
+
2633
+ export const defaultLocale: Locale = '${defaultLocale}';
2634
+
2635
+ export const localeNames: Record<Locale, string> = {
2636
+ ${locales.map((l) => ` '${l.code}': '${l.name}'`).join(",\n")}
2637
+ };
2638
+
2639
+ export function isValidLocale(code: string): code is Locale {
2640
+ return locales.includes(code as Locale);
2641
+ }
2642
+
2643
+ export function getLocaleName(code: Locale): string {
2644
+ return localeNames[code] || code;
2645
+ }
2646
+ `;
2647
+ }
2648
+ function generateCollectionTypes(collection, schema) {
2649
+ const typeName = toPascalCase(collection.singularName);
2650
+ const attributes = generateAttributes2(collection.attributes);
2651
+ const imports = generateTypeImports(collection.attributes, schema, "collection");
2652
+ return `/**
2653
+ * ${collection.displayName}
2654
+ * ${collection.description || ""}
2655
+ * Generated by strapi2front
2656
+ */
2657
+
2658
+ ${imports}
2659
+
2660
+ export interface ${typeName} extends StrapiBaseEntity {
2661
+ ${attributes}
2662
+ }
2663
+
2664
+ export interface ${typeName}Filters {
2665
+ id?: number | { $eq?: number; $ne?: number; $in?: number[]; $notIn?: number[] };
2666
+ documentId?: string | { $eq?: string; $ne?: string };
2667
+ createdAt?: string | { $eq?: string; $gt?: string; $gte?: string; $lt?: string; $lte?: string };
2668
+ updatedAt?: string | { $eq?: string; $gt?: string; $gte?: string; $lt?: string; $lte?: string };
2669
+ publishedAt?: string | null | { $eq?: string; $ne?: string; $null?: boolean };
2670
+ $and?: ${typeName}Filters[];
2671
+ $or?: ${typeName}Filters[];
2672
+ $not?: ${typeName}Filters;
2673
+ }
2674
+ `;
2675
+ }
2676
+ function generateSingleTypes(single, schema) {
2677
+ const typeName = toPascalCase(single.singularName);
2678
+ const attributes = generateAttributes2(single.attributes);
2679
+ const imports = generateTypeImports(single.attributes, schema, "single");
2680
+ return `/**
2681
+ * ${single.displayName}
2682
+ * ${single.description || ""}
2683
+ * Generated by strapi2front
2684
+ */
2685
+
2686
+ ${imports}
2687
+
2688
+ export interface ${typeName} extends StrapiBaseEntity {
2689
+ ${attributes}
2690
+ }
2691
+ `;
2692
+ }
2693
+ function generateComponentTypes(component, schema) {
2694
+ const typeName = toPascalCase(component.name);
2695
+ const attributes = generateAttributes2(component.attributes);
2696
+ const imports = generateTypeImports(component.attributes, schema, "component");
2697
+ return `/**
2698
+ * ${component.displayName} component
2699
+ * Category: ${component.category}
2700
+ * ${component.description || ""}
2701
+ * Generated by strapi2front
2702
+ */
2703
+
2704
+ ${imports}
2705
+
2706
+ export interface ${typeName} {
2707
+ id: number;
2708
+ ${attributes}
2709
+ }
2710
+ `;
2711
+ }
2712
+ function generateTypeImports(attributes, schema, context) {
2713
+ const utilsImports = [];
2714
+ const relationImports = /* @__PURE__ */ new Map();
2715
+ const componentImports = /* @__PURE__ */ new Map();
2716
+ const attributesStr = JSON.stringify(attributes);
2717
+ if (context !== "component") {
2718
+ utilsImports.push("StrapiBaseEntity");
2719
+ }
2720
+ if (attributesStr.includes('"type":"media"')) {
2721
+ utilsImports.push("StrapiMedia");
2722
+ }
2723
+ if (attributesStr.includes('"type":"blocks"')) {
2724
+ utilsImports.push("BlocksContent");
2725
+ }
2726
+ const relativePrefix = context === "component" ? ".." : "../..";
2727
+ for (const attr of Object.values(attributes)) {
2728
+ if (attr.type === "relation" && "target" in attr && attr.target) {
2729
+ const targetName = attr.target.split(".").pop() || "";
2730
+ if (targetName) {
2731
+ const typeName = toPascalCase(targetName);
2732
+ const fileName = toKebabCase(targetName);
2733
+ const isCollection = schema.collections.some((c) => c.singularName === targetName);
2734
+ const isSingle = schema.singles.some((s) => s.singularName === targetName);
2735
+ if (isCollection) {
2736
+ relationImports.set(typeName, `${relativePrefix}/collections/${fileName}/types`);
2737
+ } else if (isSingle) {
2738
+ relationImports.set(typeName, `${relativePrefix}/singles/${fileName}/types`);
2739
+ }
2740
+ }
2741
+ }
2742
+ if (attr.type === "component" && "component" in attr && attr.component) {
2743
+ const componentName = attr.component.split(".").pop() || "";
2744
+ if (componentName) {
2745
+ const typeName = toPascalCase(componentName);
2746
+ const fileName = toKebabCase(componentName);
2747
+ if (context === "component") {
2748
+ componentImports.set(typeName, `./${fileName}`);
2749
+ } else {
2750
+ componentImports.set(typeName, `../../components/${fileName}`);
2751
+ }
2752
+ }
2753
+ }
2754
+ if (attr.type === "dynamiczone" && "components" in attr && attr.components) {
2755
+ for (const comp of attr.components) {
2756
+ const componentName = comp.split(".").pop() || "";
2757
+ if (componentName) {
2758
+ const typeName = toPascalCase(componentName);
2759
+ const fileName = toKebabCase(componentName);
2760
+ if (context === "component") {
2761
+ componentImports.set(typeName, `./${fileName}`);
2762
+ } else {
2763
+ componentImports.set(typeName, `../../components/${fileName}`);
2764
+ }
2765
+ }
2766
+ }
2767
+ }
2768
+ }
2769
+ const lines = [];
2770
+ if (utilsImports.length > 0) {
2771
+ const utilsPath = context === "component" ? "../shared/utils" : "../../shared/utils";
2772
+ lines.push(`import type { ${utilsImports.join(", ")} } from '${utilsPath}';`);
2773
+ }
2774
+ for (const [typeName, importPath] of relationImports) {
2775
+ lines.push(`import type { ${typeName} } from '${importPath}';`);
2776
+ }
2777
+ for (const [typeName, importPath] of componentImports) {
2778
+ lines.push(`import type { ${typeName} } from '${importPath}';`);
2779
+ }
2780
+ return lines.join("\n");
2781
+ }
2782
+ function generateAttributes2(attributes) {
2783
+ const lines = [];
2784
+ for (const [name, attr] of Object.entries(attributes)) {
2785
+ const tsType = attributeToTsType(attr);
2786
+ const optional = attr.required ? "" : "?";
2787
+ lines.push(` ${name}${optional}: ${tsType};`);
2788
+ }
2789
+ return lines.join("\n");
2790
+ }
2791
+ function attributeToTsType(attr) {
2792
+ switch (attr.type) {
2793
+ case "string":
2794
+ case "text":
2795
+ case "richtext":
2796
+ case "email":
2797
+ case "password":
2798
+ case "uid":
2799
+ return "string";
2800
+ case "blocks":
2801
+ return "BlocksContent";
2802
+ case "integer":
2803
+ case "biginteger":
2804
+ case "float":
2805
+ case "decimal":
2806
+ return "number";
2807
+ case "boolean":
2808
+ return "boolean";
2809
+ case "date":
2810
+ case "time":
2811
+ case "datetime":
2812
+ case "timestamp":
2813
+ return "string";
2814
+ case "json":
2815
+ return "unknown";
2816
+ case "enumeration":
2817
+ if ("enum" in attr && attr.enum) {
2818
+ return attr.enum.map((v) => `'${v}'`).join(" | ");
2819
+ }
2820
+ return "string";
2821
+ case "media":
2822
+ if ("multiple" in attr && attr.multiple) {
2823
+ return "StrapiMedia[]";
2824
+ }
2825
+ return "StrapiMedia | null";
2826
+ case "relation":
2827
+ if ("target" in attr && attr.target) {
2828
+ const targetName = toPascalCase(attr.target.split(".").pop() || "unknown");
2829
+ const isMany = attr.relation === "oneToMany" || attr.relation === "manyToMany";
2830
+ return isMany ? `${targetName}[]` : `${targetName} | null`;
2831
+ }
2832
+ return "unknown";
2833
+ case "component":
2834
+ if ("component" in attr && attr.component) {
2835
+ const componentName = toPascalCase(attr.component.split(".").pop() || "unknown");
2836
+ if ("repeatable" in attr && attr.repeatable) {
2837
+ return `${componentName}[]`;
2838
+ }
2839
+ return `${componentName} | null`;
2840
+ }
2841
+ return "unknown";
2842
+ case "dynamiczone":
2843
+ if ("components" in attr && attr.components) {
2844
+ const types = attr.components.map((c) => toPascalCase(c.split(".").pop() || "unknown"));
2845
+ return `(${types.join(" | ")})[]`;
2846
+ }
2847
+ return "unknown[]";
2848
+ default:
2849
+ return "unknown";
2850
+ }
2851
+ }
2852
+ function generateCollectionService3(collection, strapiVersion) {
2853
+ const typeName = toPascalCase(collection.singularName);
2854
+ const serviceName = toCamelCase(collection.singularName) + "Service";
2855
+ const endpoint = collection.pluralName;
2856
+ const hasSlug = "slug" in collection.attributes;
2857
+ const { localized, draftAndPublish } = collection;
2858
+ const isV4 = strapiVersion === "v4";
2859
+ const idParam = isV4 ? "id: number" : "documentId: string";
2860
+ const idName = isV4 ? "id" : "documentId";
2861
+ const omitFields = isV4 ? "'id' | 'createdAt' | 'updatedAt' | 'publishedAt'" : "'id' | 'documentId' | 'createdAt' | 'updatedAt' | 'publishedAt'";
2862
+ const omitFieldsUpdate = isV4 ? "'id' | 'createdAt' | 'updatedAt'" : "'id' | 'documentId' | 'createdAt' | 'updatedAt'";
2863
+ const imports = [
2864
+ `import { collection } from '../../shared/client';`,
2865
+ `import type { ${typeName}, ${typeName}Filters } from './types';`,
2866
+ `import type { StrapiPagination } from '../../shared/utils';`
2867
+ ];
2868
+ if (localized) {
2869
+ imports.push(`import type { Locale } from '../../shared/locales';`);
2870
+ }
2871
+ const paginationFields = `
2872
+ /** Page number (1-indexed) - use with pageSize */
2873
+ page?: number;
2874
+ /** Number of items per page (default: 25) - use with page */
2875
+ pageSize?: number;
2876
+ /** Offset to start from (0-indexed) - use with limit */
2877
+ start?: number;
2878
+ /** Maximum number of items to return - use with start */
2879
+ limit?: number;`;
2880
+ let findManyOptionsFields = ` filters?: ${typeName}Filters;
2881
+ pagination?: {${paginationFields}
2882
+ };
2883
+ sort?: string | string[];
2884
+ populate?: string | string[] | Record<string, unknown>;`;
2885
+ let findOneOptionsFields = ` populate?: string | string[] | Record<string, unknown>;`;
2886
+ if (localized) {
2887
+ findManyOptionsFields += `
2888
+ locale?: Locale;`;
2889
+ findOneOptionsFields += `
2890
+ locale?: Locale;`;
2891
+ }
2892
+ if (draftAndPublish) {
2893
+ findManyOptionsFields += `
2894
+ status?: 'draft' | 'published';`;
2895
+ findOneOptionsFields += `
2896
+ status?: 'draft' | 'published';`;
2897
+ }
2898
+ let findParams = ` filters: options.filters,
2899
+ pagination: options.pagination,
2900
+ sort: options.sort,
2901
+ populate: options.populate,`;
2902
+ let findOneParams = ` populate: options.populate,`;
2903
+ if (localized) {
2904
+ findParams += `
2905
+ locale: options.locale,`;
2906
+ findOneParams += `
2907
+ locale: options.locale,`;
2908
+ }
2909
+ if (draftAndPublish) {
2910
+ findParams += `
2911
+ status: options.status,`;
2912
+ findOneParams += `
2913
+ status: options.status,`;
2914
+ }
2915
+ return `/**
2916
+ * ${collection.displayName} Service
2917
+ * ${collection.description || ""}
2918
+ * Generated by strapi2front
2919
+ * Strapi version: ${strapiVersion}
2920
+ */
2921
+
2922
+ ${imports.join("\n")}
2923
+
2924
+ export interface FindManyOptions {
2925
+ ${findManyOptionsFields}
2926
+ }
2927
+
2928
+ export interface FindOneOptions {
2929
+ ${findOneOptionsFields}
2930
+ }
2931
+
2932
+ // Create typed collection helper
2933
+ const ${toCamelCase(collection.singularName)}Collection = collection<${typeName}>('${endpoint}');
2934
+
2935
+ export const ${serviceName} = {
2936
+ async findMany(options: FindManyOptions = {}): Promise<{ data: ${typeName}[]; pagination: StrapiPagination }> {
2937
+ const response = await ${toCamelCase(collection.singularName)}Collection.find({
2938
+ ${findParams}
2939
+ });
2940
+
2941
+ return {
2942
+ data: response.data,
2943
+ pagination: response.meta.pagination,
2944
+ };
2945
+ },
2946
+
2947
+ async findAll(options: Omit<FindManyOptions, 'pagination'> = {}): Promise<${typeName}[]> {
2948
+ const allItems: ${typeName}[] = [];
2949
+ let page = 1;
2950
+ let hasMore = true;
2951
+
2952
+ while (hasMore) {
2953
+ const { data, pagination } = await this.findMany({
2954
+ ...options,
2955
+ pagination: { page, pageSize: 100 },
2956
+ });
2957
+
2958
+ allItems.push(...data);
2959
+ hasMore = page < pagination.pageCount;
2960
+ page++;
2961
+ }
2962
+
2963
+ return allItems;
2964
+ },
2965
+
2966
+ async findOne(${idParam}, options: FindOneOptions = {}): Promise<${typeName} | null> {
2967
+ try {
2968
+ const response = await ${toCamelCase(collection.singularName)}Collection.findOne(${idName}, {
2969
+ ${findOneParams}
2970
+ });
2971
+
2972
+ return response.data;
2973
+ } catch (error) {
2974
+ if (error instanceof Error && error.message.includes('404')) {
2975
+ return null;
2976
+ }
2977
+ throw error;
2978
+ }
2979
+ },
2980
+ ${hasSlug ? `
2981
+ async findBySlug(slug: string, options: FindOneOptions = {}): Promise<${typeName} | null> {
2982
+ const { data } = await this.findMany({
2983
+ filters: { slug: { $eq: slug } } as ${typeName}Filters,
2984
+ pagination: { pageSize: 1 },
2985
+ populate: options.populate,${localized ? "\n locale: options.locale," : ""}${draftAndPublish ? "\n status: options.status," : ""}
2986
+ });
2987
+
2988
+ return data[0] || null;
2989
+ },
2990
+ ` : ""}
2991
+ async create(data: Partial<Omit<${typeName}, ${omitFields}>>): Promise<${typeName}> {
2992
+ const response = await ${toCamelCase(collection.singularName)}Collection.create({ data });
2993
+ return response.data;
2994
+ },
2995
+
2996
+ async update(${idParam}, data: Partial<Omit<${typeName}, ${omitFieldsUpdate}>>): Promise<${typeName}> {
2997
+ const response = await ${toCamelCase(collection.singularName)}Collection.update(${idName}, { data });
2998
+ return response.data;
2999
+ },
3000
+
3001
+ async delete(${idParam}): Promise<void> {
3002
+ await ${toCamelCase(collection.singularName)}Collection.delete(${idName});
3003
+ },
3004
+
3005
+ async count(filters?: ${typeName}Filters): Promise<number> {
3006
+ const { pagination } = await this.findMany({
3007
+ filters,
3008
+ pagination: { pageSize: 1 },
3009
+ });
3010
+
3011
+ return pagination.total;
3012
+ },
3013
+ };
3014
+ `;
3015
+ }
3016
+ function generateSingleService3(single, strapiVersion) {
3017
+ const typeName = toPascalCase(single.singularName);
3018
+ const serviceName = toCamelCase(single.singularName) + "Service";
3019
+ const endpoint = single.singularName;
3020
+ const { localized, draftAndPublish } = single;
3021
+ const isV4 = strapiVersion === "v4";
3022
+ const omitFields = isV4 ? "'id' | 'createdAt' | 'updatedAt'" : "'id' | 'documentId' | 'createdAt' | 'updatedAt'";
3023
+ const imports = [
3024
+ `import { single } from '../../shared/client';`,
3025
+ `import type { ${typeName} } from './types';`
3026
+ ];
3027
+ if (localized) {
3028
+ imports.push(`import type { Locale } from '../../shared/locales';`);
3029
+ }
3030
+ let findOptionsFields = ` populate?: string | string[] | Record<string, unknown>;`;
3031
+ if (localized) {
3032
+ findOptionsFields += `
3033
+ locale?: Locale;`;
3034
+ }
3035
+ if (draftAndPublish) {
3036
+ findOptionsFields += `
3037
+ status?: 'draft' | 'published';`;
3038
+ }
3039
+ let findParams = ` populate: options.populate,`;
3040
+ if (localized) {
3041
+ findParams += `
3042
+ locale: options.locale,`;
3043
+ }
3044
+ if (draftAndPublish) {
3045
+ findParams += `
3046
+ status: options.status,`;
3047
+ }
3048
+ return `/**
3049
+ * ${single.displayName} Service (Single Type)
3050
+ * ${single.description || ""}
3051
+ * Generated by strapi2front
3052
+ * Strapi version: ${strapiVersion}
3053
+ */
3054
+
3055
+ ${imports.join("\n")}
3056
+
3057
+ export interface FindOptions {
3058
+ ${findOptionsFields}
3059
+ }
3060
+
3061
+ // Create typed single helper
3062
+ const ${toCamelCase(single.singularName)}Single = single<${typeName}>('${endpoint}');
3063
+
3064
+ export const ${serviceName} = {
3065
+ async find(options: FindOptions = {}): Promise<${typeName} | null> {
3066
+ try {
3067
+ const response = await ${toCamelCase(single.singularName)}Single.find({
3068
+ ${findParams}
3069
+ });
3070
+
3071
+ return response.data;
3072
+ } catch (error) {
3073
+ if (error instanceof Error && error.message.includes('404')) {
3074
+ return null;
3075
+ }
3076
+ throw error;
3077
+ }
3078
+ },
3079
+
3080
+ async update(data: Partial<Omit<${typeName}, ${omitFields}>>): Promise<${typeName}> {
3081
+ const response = await ${toCamelCase(single.singularName)}Single.update({ data });
3082
+ return response.data;
3083
+ },
3084
+
3085
+ async delete(): Promise<void> {
3086
+ await ${toCamelCase(single.singularName)}Single.delete();
3087
+ },
3088
+ };
3089
+ `;
3090
+ }
3091
+ function generateCollectionActions2(collection, strapiVersion) {
3092
+ const typeName = toPascalCase(collection.singularName);
3093
+ const serviceName = toCamelCase(collection.singularName) + "Service";
3094
+ const actionPrefix = toCamelCase(collection.singularName);
3095
+ const isV4 = strapiVersion === "v4";
3096
+ const idInputSchema = isV4 ? "z.number().int().positive()" : "z.string()";
3097
+ const idParamName = isV4 ? "id" : "documentId";
3098
+ return `/**
3099
+ * ${collection.displayName} Astro Actions
3100
+ * ${collection.description || ""}
3101
+ * Generated by strapi2front
3102
+ * Strapi version: ${strapiVersion}
3103
+ */
3104
+
3105
+ import { defineAction } from 'astro:actions';
3106
+ import { z } from 'astro:schema';
3107
+ import { ${serviceName} } from './service';
3108
+ import type { ${typeName} } from './types';
3109
+
3110
+ export const ${actionPrefix}Actions = {
3111
+ getMany: defineAction({
3112
+ input: z.object({
3113
+ page: z.number().optional(),
3114
+ pageSize: z.number().optional(),
3115
+ sort: z.string().optional(),
3116
+ }).optional(),
3117
+ handler: async (input) => {
3118
+ const { data, pagination } = await ${serviceName}.findMany({
3119
+ pagination: input ? { page: input.page, pageSize: input.pageSize } : undefined,
3120
+ sort: input?.sort,
3121
+ });
3122
+ return { data, pagination };
3123
+ },
3124
+ }),
3125
+
3126
+ getOne: defineAction({
3127
+ input: z.object({
3128
+ ${idParamName}: ${idInputSchema},
3129
+ }),
3130
+ handler: async (input) => {
3131
+ const data = await ${serviceName}.findOne(input.${idParamName});
3132
+ return { data };
3133
+ },
3134
+ }),
3135
+
3136
+ create: defineAction({
3137
+ input: z.object({
3138
+ data: z.record(z.unknown()),
3139
+ }),
3140
+ handler: async (input) => {
3141
+ const data = await ${serviceName}.create(input.data as Partial<${typeName}>);
3142
+ return { data };
3143
+ },
3144
+ }),
3145
+
3146
+ update: defineAction({
3147
+ input: z.object({
3148
+ ${idParamName}: ${idInputSchema},
3149
+ data: z.record(z.unknown()),
3150
+ }),
3151
+ handler: async (input) => {
3152
+ const data = await ${serviceName}.update(input.${idParamName}, input.data as Partial<${typeName}>);
3153
+ return { data };
3154
+ },
3155
+ }),
1476
3156
 
1477
- /**
1478
- * Locale display names
1479
- */
1480
- export const localeNames: Record<Locale, string> = {
1481
- ${localeNames}
3157
+ delete: defineAction({
3158
+ input: z.object({
3159
+ ${idParamName}: ${idInputSchema},
3160
+ }),
3161
+ handler: async (input) => {
3162
+ await ${serviceName}.delete(input.${idParamName});
3163
+ return { success: true };
3164
+ },
3165
+ }),
1482
3166
  };
1483
-
1484
- /**
1485
- * Check if a string is a valid locale
1486
- */
1487
- export function isValidLocale(code: string): code is Locale {
1488
- return locales.includes(code as Locale);
1489
- }
1490
-
1491
- /**
1492
- * Get locale name by code
1493
- */
1494
- export function getLocaleName(code: Locale): string {
1495
- return localeNames[code] || code;
1496
- }
1497
3167
  `;
1498
3168
  }
1499
- async function generateByFeature(schema, locales, options) {
1500
- const { outputDir, features, blocksRendererInstalled = false, strapiVersion = "v5", apiPrefix = "/api" } = options;
1501
- const generatedFiles = [];
1502
- await ensureDir(path7.join(outputDir, "collections"));
1503
- await ensureDir(path7.join(outputDir, "singles"));
1504
- await ensureDir(path7.join(outputDir, "components"));
1505
- await ensureDir(path7.join(outputDir, "shared"));
1506
- const sharedDir = path7.join(outputDir, "shared");
1507
- const utilsPath = path7.join(sharedDir, "utils.ts");
1508
- await writeFile(utilsPath, await formatCode(generateUtilityTypes2(blocksRendererInstalled, strapiVersion)));
1509
- generatedFiles.push(utilsPath);
1510
- const clientPath = path7.join(sharedDir, "client.ts");
1511
- await writeFile(clientPath, await formatCode(generateClient2(strapiVersion, apiPrefix)));
1512
- generatedFiles.push(clientPath);
1513
- const localesPath = path7.join(sharedDir, "locales.ts");
1514
- await writeFile(localesPath, await formatCode(generateLocalesFile2(locales)));
1515
- generatedFiles.push(localesPath);
1516
- for (const collection of schema.collections) {
1517
- const featureDir = path7.join(outputDir, "collections", toKebabCase(collection.singularName));
1518
- await ensureDir(featureDir);
1519
- if (features.types) {
1520
- const typesPath = path7.join(featureDir, "types.ts");
1521
- await writeFile(typesPath, await formatCode(generateCollectionTypes(collection, schema)));
1522
- generatedFiles.push(typesPath);
1523
- }
1524
- if (features.services) {
1525
- const servicePath = path7.join(featureDir, "service.ts");
1526
- await writeFile(servicePath, await formatCode(generateCollectionService2(collection, strapiVersion)));
1527
- generatedFiles.push(servicePath);
1528
- }
1529
- if (features.actions) {
1530
- const actionsPath = path7.join(featureDir, "actions.ts");
1531
- await writeFile(actionsPath, await formatCode(generateCollectionActions2(collection, strapiVersion)));
1532
- generatedFiles.push(actionsPath);
1533
- }
1534
- }
1535
- for (const single of schema.singles) {
1536
- const featureDir = path7.join(outputDir, "singles", toKebabCase(single.singularName));
1537
- await ensureDir(featureDir);
1538
- if (features.types) {
1539
- const typesPath = path7.join(featureDir, "types.ts");
1540
- await writeFile(typesPath, await formatCode(generateSingleTypes(single, schema)));
1541
- generatedFiles.push(typesPath);
1542
- }
1543
- if (features.services) {
1544
- const servicePath = path7.join(featureDir, "service.ts");
1545
- await writeFile(servicePath, await formatCode(generateSingleService2(single, strapiVersion)));
1546
- generatedFiles.push(servicePath);
1547
- }
1548
- }
1549
- for (const component of schema.components) {
1550
- const componentPath = path7.join(outputDir, "components", `${toKebabCase(component.name)}.ts`);
1551
- await writeFile(componentPath, await formatCode(generateComponentTypes(component, schema)));
1552
- generatedFiles.push(componentPath);
1553
- }
1554
- return generatedFiles;
1555
- }
1556
- function generateUtilityTypes2(blocksRendererInstalled, strapiVersion) {
3169
+ function generateUtilityTypesJSDoc(blocksRendererInstalled, strapiVersion) {
1557
3170
  const isV4 = strapiVersion === "v4";
1558
3171
  const blocksContentType = isV4 ? `/**
1559
3172
  * Rich text content (Strapi v4)
1560
3173
  * Can be markdown string or custom JSON structure
1561
- */
1562
- export type RichTextContent = string;` : blocksRendererInstalled ? `/**
1563
- * Blocks content type (Strapi v5 rich text)
1564
- * Re-exported from @strapi/blocks-react-renderer
1565
- */
1566
- export type { BlocksContent } from '@strapi/blocks-react-renderer';` : `/**
3174
+ * @typedef {string} RichTextContent
3175
+ */` : blocksRendererInstalled ? `// BlocksContent - import from '@strapi/blocks-react-renderer' for full type support` : `/**
1567
3176
  * Blocks content type (Strapi v5 rich text)
1568
3177
  *
1569
3178
  * For full type support and rendering, install:
1570
3179
  * npm install @strapi/blocks-react-renderer
1571
3180
  *
1572
3181
  * Then re-run: npx strapi2front sync
1573
- */
1574
- export type BlocksContent = unknown[];`;
1575
- const baseEntity = isV4 ? `export interface StrapiBaseEntity {
1576
- id: number;
1577
- createdAt: string;
1578
- updatedAt: string;
1579
- publishedAt: string | null;
1580
- }` : `export interface StrapiBaseEntity {
1581
- id: number;
1582
- documentId: string;
1583
- createdAt: string;
1584
- updatedAt: string;
1585
- publishedAt: string | null;
1586
- }`;
1587
- const mediaType = isV4 ? `export interface StrapiMedia {
1588
- id: number;
1589
- name: string;
1590
- alternativeText: string | null;
1591
- caption: string | null;
1592
- width: number;
1593
- height: number;
1594
- formats: {
1595
- thumbnail?: StrapiMediaFormat;
1596
- small?: StrapiMediaFormat;
1597
- medium?: StrapiMediaFormat;
1598
- large?: StrapiMediaFormat;
1599
- } | null;
1600
- hash: string;
1601
- ext: string;
1602
- mime: string;
1603
- size: number;
1604
- url: string;
1605
- previewUrl: string | null;
1606
- provider: string;
1607
- createdAt: string;
1608
- updatedAt: string;
1609
- }` : `export interface StrapiMedia {
1610
- id: number;
1611
- documentId: string;
1612
- name: string;
1613
- alternativeText: string | null;
1614
- caption: string | null;
1615
- width: number;
1616
- height: number;
1617
- formats: {
1618
- thumbnail?: StrapiMediaFormat;
1619
- small?: StrapiMediaFormat;
1620
- medium?: StrapiMediaFormat;
1621
- large?: StrapiMediaFormat;
1622
- } | null;
1623
- hash: string;
1624
- ext: string;
1625
- mime: string;
1626
- size: number;
1627
- url: string;
1628
- previewUrl: string | null;
1629
- provider: string;
1630
- createdAt: string;
1631
- updatedAt: string;
1632
- }`;
3182
+ * @typedef {Array<Object>} BlocksContent
3183
+ */`;
3184
+ const baseEntity = isV4 ? `/**
3185
+ * @typedef {Object} StrapiBaseEntity
3186
+ * @property {number} id
3187
+ * @property {string} createdAt
3188
+ * @property {string} updatedAt
3189
+ * @property {string|null} publishedAt
3190
+ */` : `/**
3191
+ * @typedef {Object} StrapiBaseEntity
3192
+ * @property {number} id
3193
+ * @property {string} documentId
3194
+ * @property {string} createdAt
3195
+ * @property {string} updatedAt
3196
+ * @property {string|null} publishedAt
3197
+ */`;
3198
+ const mediaType = isV4 ? `/**
3199
+ * @typedef {Object} StrapiMedia
3200
+ * @property {number} id
3201
+ * @property {string} name
3202
+ * @property {string|null} alternativeText
3203
+ * @property {string|null} caption
3204
+ * @property {number} width
3205
+ * @property {number} height
3206
+ * @property {Object|null} formats
3207
+ * @property {StrapiMediaFormat} [formats.thumbnail]
3208
+ * @property {StrapiMediaFormat} [formats.small]
3209
+ * @property {StrapiMediaFormat} [formats.medium]
3210
+ * @property {StrapiMediaFormat} [formats.large]
3211
+ * @property {string} hash
3212
+ * @property {string} ext
3213
+ * @property {string} mime
3214
+ * @property {number} size
3215
+ * @property {string} url
3216
+ * @property {string|null} previewUrl
3217
+ * @property {string} provider
3218
+ * @property {string} createdAt
3219
+ * @property {string} updatedAt
3220
+ */` : `/**
3221
+ * @typedef {Object} StrapiMedia
3222
+ * @property {number} id
3223
+ * @property {string} documentId
3224
+ * @property {string} name
3225
+ * @property {string|null} alternativeText
3226
+ * @property {string|null} caption
3227
+ * @property {number} width
3228
+ * @property {number} height
3229
+ * @property {Object|null} formats
3230
+ * @property {StrapiMediaFormat} [formats.thumbnail]
3231
+ * @property {StrapiMediaFormat} [formats.small]
3232
+ * @property {StrapiMediaFormat} [formats.medium]
3233
+ * @property {StrapiMediaFormat} [formats.large]
3234
+ * @property {string} hash
3235
+ * @property {string} ext
3236
+ * @property {string} mime
3237
+ * @property {number} size
3238
+ * @property {string} url
3239
+ * @property {string|null} previewUrl
3240
+ * @property {string} provider
3241
+ * @property {string} createdAt
3242
+ * @property {string} updatedAt
3243
+ */`;
1633
3244
  const v4RawResponseTypes = isV4 ? `
1634
3245
 
1635
3246
  /**
1636
- * Strapi v4 raw API response (with nested attributes)
3247
+ * Strapi v4 raw API response item (with nested attributes)
3248
+ * @template T
3249
+ * @typedef {Object} StrapiV4RawItem
3250
+ * @property {number} id
3251
+ * @property {Omit<T, 'id'>} attributes
1637
3252
  */
1638
- export interface StrapiV4RawItem<T> {
1639
- id: number;
1640
- attributes: Omit<T, 'id'>;
1641
- }
1642
3253
 
1643
- export interface StrapiV4RawResponse<T> {
1644
- data: StrapiV4RawItem<T>;
1645
- meta: Record<string, unknown>;
1646
- }
3254
+ /**
3255
+ * Strapi v4 raw API response
3256
+ * @template T
3257
+ * @typedef {Object} StrapiV4RawResponse
3258
+ * @property {StrapiV4RawItem<T>} data
3259
+ * @property {Object} meta
3260
+ */
1647
3261
 
1648
- export interface StrapiV4RawListResponse<T> {
1649
- data: StrapiV4RawItem<T>[];
1650
- meta: {
1651
- pagination: StrapiPagination;
1652
- };
1653
- }
3262
+ /**
3263
+ * Strapi v4 raw list response
3264
+ * @template T
3265
+ * @typedef {Object} StrapiV4RawListResponse
3266
+ * @property {StrapiV4RawItem<T>[]} data
3267
+ * @property {Object} meta
3268
+ * @property {StrapiPagination} meta.pagination
3269
+ */
1654
3270
 
1655
3271
  /**
1656
3272
  * Flatten Strapi v4 response item
3273
+ * @template T
3274
+ * @param {StrapiV4RawItem<T>} item
3275
+ * @returns {T}
1657
3276
  */
1658
- export function flattenV4Response<T>(item: StrapiV4RawItem<T>): T {
1659
- return { id: item.id, ...item.attributes } as T;
3277
+ function flattenV4Response(item) {
3278
+ return { id: item.id, ...item.attributes };
1660
3279
  }
1661
3280
 
1662
3281
  /**
1663
3282
  * Flatten Strapi v4 list response
3283
+ * @template T
3284
+ * @param {StrapiV4RawItem<T>[]} items
3285
+ * @returns {T[]}
1664
3286
  */
1665
- export function flattenV4ListResponse<T>(items: StrapiV4RawItem<T>[]): T[] {
1666
- return items.map(item => flattenV4Response<T>(item));
1667
- }` : "";
1668
- return `/**
3287
+ function flattenV4ListResponse(items) {
3288
+ return items.map(item => flattenV4Response(item));
3289
+ }
3290
+
3291
+ module.exports = { flattenV4Response, flattenV4ListResponse };` : "";
3292
+ return `// @ts-check
3293
+ /**
1669
3294
  * Strapi utility types
1670
3295
  * Generated by strapi2front
1671
3296
  * Strapi version: ${strapiVersion}
@@ -1673,61 +3298,67 @@ export function flattenV4ListResponse<T>(items: StrapiV4RawItem<T>[]): T[] {
1673
3298
 
1674
3299
  ${mediaType}
1675
3300
 
1676
- export interface StrapiMediaFormat {
1677
- name: string;
1678
- hash: string;
1679
- ext: string;
1680
- mime: string;
1681
- width: number;
1682
- height: number;
1683
- size: number;
1684
- url: string;
1685
- }
3301
+ /**
3302
+ * @typedef {Object} StrapiMediaFormat
3303
+ * @property {string} name
3304
+ * @property {string} hash
3305
+ * @property {string} ext
3306
+ * @property {string} mime
3307
+ * @property {number} width
3308
+ * @property {number} height
3309
+ * @property {number} size
3310
+ * @property {string} url
3311
+ */
1686
3312
 
1687
- export interface StrapiPagination {
1688
- page: number;
1689
- pageSize: number;
1690
- pageCount: number;
1691
- total: number;
1692
- }
3313
+ /**
3314
+ * @typedef {Object} StrapiPagination
3315
+ * @property {number} page
3316
+ * @property {number} pageSize
3317
+ * @property {number} pageCount
3318
+ * @property {number} total
3319
+ */
1693
3320
 
1694
- export interface StrapiResponse<T> {
1695
- data: T;
1696
- meta: {
1697
- pagination?: StrapiPagination;
1698
- };
1699
- }
3321
+ /**
3322
+ * @template T
3323
+ * @typedef {Object} StrapiResponse
3324
+ * @property {T} data
3325
+ * @property {Object} meta
3326
+ * @property {StrapiPagination} [meta.pagination]
3327
+ */
1700
3328
 
1701
- export interface StrapiListResponse<T> {
1702
- data: T[];
1703
- meta: {
1704
- pagination: StrapiPagination;
1705
- };
1706
- }
3329
+ /**
3330
+ * @template T
3331
+ * @typedef {Object} StrapiListResponse
3332
+ * @property {T[]} data
3333
+ * @property {Object} meta
3334
+ * @property {StrapiPagination} meta.pagination
3335
+ */
1707
3336
 
1708
3337
  ${baseEntity}
1709
3338
  ${v4RawResponseTypes}
1710
3339
  ${blocksContentType}
3340
+
3341
+ module.exports = {};
1711
3342
  `;
1712
3343
  }
1713
- function generateClient2(strapiVersion, apiPrefix = "/api") {
3344
+ function generateClientJSDoc(strapiVersion, apiPrefix = "/api") {
1714
3345
  const isV4 = strapiVersion === "v4";
1715
3346
  const normalizedPrefix = apiPrefix.startsWith("/") ? apiPrefix : "/" + apiPrefix;
1716
3347
  if (isV4) {
1717
- return `/**
3348
+ return `// @ts-check
3349
+ /**
1718
3350
  * Strapi Client (v4)
1719
3351
  * Generated by strapi2front
1720
3352
  */
1721
3353
 
1722
- import Strapi from 'strapi-sdk-js';
1723
- import type { StrapiPagination } from './utils';
3354
+ const Strapi = require('strapi-sdk-js').default;
1724
3355
 
1725
3356
  // Initialize the Strapi client
1726
- const strapiUrl = import.meta.env.STRAPI_URL || process.env.STRAPI_URL || 'http://localhost:1337';
1727
- const strapiToken = import.meta.env.STRAPI_TOKEN || process.env.STRAPI_TOKEN;
1728
- const strapiApiPrefix = import.meta.env.STRAPI_API_PREFIX || process.env.STRAPI_API_PREFIX || '${normalizedPrefix}';
3357
+ const strapiUrl = process.env.STRAPI_URL || 'http://localhost:1337';
3358
+ const strapiToken = process.env.STRAPI_TOKEN;
3359
+ const strapiApiPrefix = process.env.STRAPI_API_PREFIX || '${normalizedPrefix}';
1729
3360
 
1730
- export const strapi = new Strapi({
3361
+ const strapi = new Strapi({
1731
3362
  url: strapiUrl,
1732
3363
  prefix: strapiApiPrefix,
1733
3364
  axiosOptions: {
@@ -1737,63 +3368,49 @@ export const strapi = new Strapi({
1737
3368
  },
1738
3369
  });
1739
3370
 
1740
- // Default pagination for fallback
1741
- const defaultPagination: StrapiPagination = {
3371
+ /** @type {import('./utils').StrapiPagination} */
3372
+ const defaultPagination = {
1742
3373
  page: 1,
1743
3374
  pageSize: 25,
1744
3375
  pageCount: 1,
1745
3376
  total: 0,
1746
3377
  };
1747
3378
 
1748
- // Strapi v4 raw response types (with nested attributes)
1749
- interface StrapiV4RawItem<T> {
1750
- id: number;
1751
- attributes: Omit<T, 'id'>;
1752
- }
1753
-
1754
- interface StrapiV4RawListResponse<T> {
1755
- data: StrapiV4RawItem<T>[];
1756
- meta: {
1757
- pagination?: StrapiPagination;
1758
- };
1759
- }
1760
-
1761
- interface StrapiV4RawSingleResponse<T> {
1762
- data: StrapiV4RawItem<T>;
1763
- meta?: Record<string, unknown>;
1764
- }
1765
-
1766
3379
  /**
1767
3380
  * Flatten a Strapi v4 response item (merges id with attributes)
3381
+ * @template T
3382
+ * @param {{ id: number, attributes: Omit<T, 'id'> }} item
3383
+ * @returns {T}
1768
3384
  */
1769
- function flattenItem<T>(item: StrapiV4RawItem<T>): T {
1770
- return { id: item.id, ...item.attributes } as T;
3385
+ function flattenItem(item) {
3386
+ return { id: item.id, ...item.attributes };
1771
3387
  }
1772
3388
 
1773
3389
  /**
1774
3390
  * Recursively flatten nested relations in Strapi v4 response
3391
+ * @template T
3392
+ * @param {T} data
3393
+ * @returns {T}
1775
3394
  */
1776
- function flattenRelations<T>(data: T): T {
3395
+ function flattenRelations(data) {
1777
3396
  if (data === null || data === undefined) return data;
1778
3397
  if (Array.isArray(data)) {
1779
- return data.map(item => flattenRelations(item)) as unknown as T;
3398
+ return /** @type {T} */ (data.map(item => flattenRelations(item)));
1780
3399
  }
1781
3400
  if (typeof data === 'object') {
1782
- const result: Record<string, unknown> = {};
1783
- for (const [key, value] of Object.entries(data as Record<string, unknown>)) {
1784
- // Check if this is a Strapi v4 relation response { data: { id, attributes } }
3401
+ /** @type {Record<string, unknown>} */
3402
+ const result = {};
3403
+ for (const [key, value] of Object.entries(data)) {
1785
3404
  if (value && typeof value === 'object' && 'data' in value) {
1786
- const relationData = (value as { data: unknown }).data;
3405
+ const relationData = /** @type {{ data: unknown }} */ (value).data;
1787
3406
  if (relationData === null) {
1788
3407
  result[key] = null;
1789
3408
  } else if (Array.isArray(relationData)) {
1790
- // To-many relation
1791
- result[key] = relationData.map((item: StrapiV4RawItem<unknown>) =>
3409
+ result[key] = relationData.map((item) =>
1792
3410
  flattenRelations(flattenItem(item))
1793
3411
  );
1794
3412
  } else if (typeof relationData === 'object' && 'id' in relationData && 'attributes' in relationData) {
1795
- // To-one relation
1796
- result[key] = flattenRelations(flattenItem(relationData as StrapiV4RawItem<unknown>));
3413
+ result[key] = flattenRelations(flattenItem(/** @type {{ id: number, attributes: object }} */ (relationData)));
1797
3414
  } else {
1798
3415
  result[key] = flattenRelations(value);
1799
3416
  }
@@ -1801,76 +3418,148 @@ function flattenRelations<T>(data: T): T {
1801
3418
  result[key] = flattenRelations(value);
1802
3419
  }
1803
3420
  }
1804
- return result as T;
3421
+ return /** @type {T} */ (result);
1805
3422
  }
1806
3423
  return data;
1807
3424
  }
1808
3425
 
1809
- // Helper to get typed collection
1810
- export function collection<T>(pluralName: string) {
3426
+ /**
3427
+ * Helper to get typed collection
3428
+ * @template T
3429
+ * @param {string} pluralName
3430
+ */
3431
+ function collection(pluralName) {
1811
3432
  return {
1812
- async find(params?: Record<string, unknown>): Promise<{ data: T[]; meta: { pagination: StrapiPagination } }> {
1813
- const response = await strapi.find(pluralName, params) as unknown as StrapiV4RawListResponse<T>;
1814
- const flattenedData = Array.isArray(response.data)
1815
- ? response.data.map(item => flattenRelations(flattenItem<T>(item)))
3433
+ /**
3434
+ * @param {Record<string, unknown>} [params]
3435
+ * @returns {Promise<{ data: T[], meta: { pagination: import('./utils').StrapiPagination } }>}
3436
+ */
3437
+ async find(params) {
3438
+ const response = await strapi.find(pluralName, params);
3439
+ /** @type {any} */
3440
+ const rawData = response.data;
3441
+ /** @type {T[]} */
3442
+ const data = Array.isArray(rawData)
3443
+ ? rawData.map(item => flattenRelations(flattenItem(item)))
1816
3444
  : [];
1817
- return {
1818
- data: flattenedData,
1819
- meta: {
1820
- pagination: response.meta?.pagination || defaultPagination,
1821
- },
3445
+ /** @type {any} */
3446
+ const rawMeta = response.meta;
3447
+ /** @type {any} */
3448
+ const rawPag = rawMeta?.pagination;
3449
+ /** @type {import('./utils').StrapiPagination} */
3450
+ const pagination = {
3451
+ page: rawPag?.page ?? defaultPagination.page,
3452
+ pageSize: rawPag?.pageSize ?? defaultPagination.pageSize,
3453
+ pageCount: rawPag?.pageCount ?? defaultPagination.pageCount,
3454
+ total: rawPag?.total ?? defaultPagination.total,
1822
3455
  };
3456
+ return { data, meta: { pagination } };
1823
3457
  },
1824
- async findOne(id: number | string, params?: Record<string, unknown>): Promise<{ data: T }> {
1825
- const response = await strapi.findOne(pluralName, String(id), params) as unknown as StrapiV4RawSingleResponse<T>;
1826
- return { data: flattenRelations(flattenItem<T>(response.data)) };
3458
+ /**
3459
+ * @param {number|string} id
3460
+ * @param {Record<string, unknown>} [params]
3461
+ * @returns {Promise<{ data: T }>}
3462
+ */
3463
+ async findOne(id, params) {
3464
+ const response = await strapi.findOne(pluralName, String(id), params);
3465
+ /** @type {any} */
3466
+ const rawData = response.data;
3467
+ /** @type {T} */
3468
+ const data = flattenRelations(flattenItem(rawData));
3469
+ return { data };
1827
3470
  },
1828
- async create(data: { data: Partial<T> }): Promise<{ data: T }> {
1829
- const response = await strapi.create(pluralName, data.data as Record<string, unknown>) as unknown as StrapiV4RawSingleResponse<T>;
1830
- return { data: flattenRelations(flattenItem<T>(response.data)) };
3471
+ /**
3472
+ * @param {{ data: Partial<T> }} data
3473
+ * @returns {Promise<{ data: T }>}
3474
+ */
3475
+ async create(data) {
3476
+ const response = await strapi.create(pluralName, data.data);
3477
+ /** @type {any} */
3478
+ const rawData = response.data;
3479
+ /** @type {T} */
3480
+ const result = flattenRelations(flattenItem(rawData));
3481
+ return { data: result };
1831
3482
  },
1832
- async update(id: number | string, data: { data: Partial<T> }): Promise<{ data: T }> {
1833
- const response = await strapi.update(pluralName, String(id), data.data as Record<string, unknown>) as unknown as StrapiV4RawSingleResponse<T>;
1834
- return { data: flattenRelations(flattenItem<T>(response.data)) };
3483
+ /**
3484
+ * @param {number|string} id
3485
+ * @param {{ data: Partial<T> }} data
3486
+ * @returns {Promise<{ data: T }>}
3487
+ */
3488
+ async update(id, data) {
3489
+ const response = await strapi.update(pluralName, String(id), data.data);
3490
+ /** @type {any} */
3491
+ const rawData = response.data;
3492
+ /** @type {T} */
3493
+ const result = flattenRelations(flattenItem(rawData));
3494
+ return { data: result };
1835
3495
  },
1836
- async delete(id: number | string): Promise<void> {
3496
+ /**
3497
+ * @param {number|string} id
3498
+ * @returns {Promise<void>}
3499
+ */
3500
+ async delete(id) {
1837
3501
  await strapi.delete(pluralName, String(id));
1838
3502
  },
1839
3503
  };
1840
3504
  }
1841
3505
 
1842
- // Helper to get typed single type
1843
- export function single<T>(singularName: string) {
3506
+ /**
3507
+ * Helper to get typed single type
3508
+ * @template T
3509
+ * @param {string} singularName
3510
+ */
3511
+ function single(singularName) {
1844
3512
  return {
1845
- async find(params?: Record<string, unknown>): Promise<{ data: T }> {
1846
- const response = await strapi.find(singularName, params) as unknown as StrapiV4RawSingleResponse<T>;
1847
- return { data: flattenRelations(flattenItem<T>(response.data)) };
3513
+ /**
3514
+ * @param {Record<string, unknown>} [params]
3515
+ * @returns {Promise<{ data: T }>}
3516
+ */
3517
+ async find(params) {
3518
+ const response = await strapi.find(singularName, params);
3519
+ /** @type {any} */
3520
+ const rawData = response.data;
3521
+ /** @type {T} */
3522
+ const data = flattenRelations(flattenItem(rawData));
3523
+ return { data };
1848
3524
  },
1849
- async update(data: { data: Partial<T> }): Promise<{ data: T }> {
1850
- const response = await strapi.update(singularName, 1 as unknown as string, data.data as Record<string, unknown>) as unknown as StrapiV4RawSingleResponse<T>;
1851
- return { data: flattenRelations(flattenItem<T>(response.data)) };
3525
+ /**
3526
+ * @param {{ data: Partial<T> }} data
3527
+ * @returns {Promise<{ data: T }>}
3528
+ */
3529
+ async update(data) {
3530
+ const response = await strapi.update(singularName, String(1), data.data);
3531
+ /** @type {any} */
3532
+ const rawData = response.data;
3533
+ /** @type {T} */
3534
+ const result = flattenRelations(flattenItem(rawData));
3535
+ return { data: result };
1852
3536
  },
1853
- async delete(): Promise<void> {
1854
- await strapi.delete(singularName, 1 as unknown as string);
3537
+ /**
3538
+ * @returns {Promise<void>}
3539
+ */
3540
+ async delete() {
3541
+ await strapi.delete(singularName, String(1));
1855
3542
  },
1856
3543
  };
1857
3544
  }
3545
+
3546
+ module.exports = { strapi, collection, single };
1858
3547
  `;
1859
3548
  }
1860
- return `/**
3549
+ return `// @ts-check
3550
+ /**
1861
3551
  * Strapi Client (v5)
1862
3552
  * Generated by strapi2front
1863
3553
  */
1864
3554
 
1865
- import Strapi from 'strapi-sdk-js';
1866
- import type { StrapiPagination } from './utils';
3555
+ const Strapi = require('strapi-sdk-js').default;
1867
3556
 
1868
3557
  // Initialize the Strapi client
1869
- const strapiUrl = import.meta.env.STRAPI_URL || process.env.STRAPI_URL || 'http://localhost:1337';
1870
- const strapiToken = import.meta.env.STRAPI_TOKEN || process.env.STRAPI_TOKEN;
1871
- const strapiApiPrefix = import.meta.env.STRAPI_API_PREFIX || process.env.STRAPI_API_PREFIX || '${normalizedPrefix}';
3558
+ const strapiUrl = process.env.STRAPI_URL || 'http://localhost:1337';
3559
+ const strapiToken = process.env.STRAPI_TOKEN;
3560
+ const strapiApiPrefix = process.env.STRAPI_API_PREFIX || '${normalizedPrefix}';
1872
3561
 
1873
- export const strapi = new Strapi({
3562
+ const strapi = new Strapi({
1874
3563
  url: strapiUrl,
1875
3564
  prefix: strapiApiPrefix,
1876
3565
  axiosOptions: {
@@ -1880,267 +3569,304 @@ export const strapi = new Strapi({
1880
3569
  },
1881
3570
  });
1882
3571
 
1883
- // Default pagination for fallback
1884
- const defaultPagination: StrapiPagination = {
3572
+ /** @type {import('./utils').StrapiPagination} */
3573
+ const defaultPagination = {
1885
3574
  page: 1,
1886
3575
  pageSize: 25,
1887
3576
  pageCount: 1,
1888
3577
  total: 0,
1889
3578
  };
1890
3579
 
1891
- // Response types from strapi-sdk-js
1892
- interface StrapiListResponse<T> {
1893
- data: T[];
1894
- meta: {
1895
- pagination?: StrapiPagination;
1896
- };
1897
- }
1898
-
1899
- interface StrapiSingleResponse<T> {
1900
- data: T;
1901
- meta?: Record<string, unknown>;
1902
- }
1903
-
1904
- // Helper to get typed collection
1905
- export function collection<T>(pluralName: string) {
3580
+ /**
3581
+ * Helper to get typed collection
3582
+ * @template T
3583
+ * @param {string} pluralName
3584
+ */
3585
+ function collection(pluralName) {
1906
3586
  return {
1907
- async find(params?: Record<string, unknown>): Promise<{ data: T[]; meta: { pagination: StrapiPagination } }> {
1908
- const response = await strapi.find(pluralName, params) as unknown as StrapiListResponse<T>;
1909
- return {
1910
- data: Array.isArray(response.data) ? response.data : [],
1911
- meta: {
1912
- pagination: response.meta?.pagination || defaultPagination,
1913
- },
3587
+ /**
3588
+ * @param {Record<string, unknown>} [params]
3589
+ * @returns {Promise<{ data: T[], meta: { pagination: import('./utils').StrapiPagination } }>}
3590
+ */
3591
+ async find(params) {
3592
+ const response = await strapi.find(pluralName, params);
3593
+ /** @type {any} */
3594
+ const rawMeta = response.meta;
3595
+ /** @type {any} */
3596
+ const rawPag = rawMeta?.pagination;
3597
+ /** @type {import('./utils').StrapiPagination} */
3598
+ const pagination = {
3599
+ page: rawPag?.page ?? defaultPagination.page,
3600
+ pageSize: rawPag?.pageSize ?? defaultPagination.pageSize,
3601
+ pageCount: rawPag?.pageCount ?? defaultPagination.pageCount,
3602
+ total: rawPag?.total ?? defaultPagination.total,
1914
3603
  };
3604
+ /** @type {any} */
3605
+ const rawData = response.data;
3606
+ /** @type {T[]} */
3607
+ const data = Array.isArray(rawData) ? rawData : [];
3608
+ return { data, meta: { pagination } };
1915
3609
  },
1916
- async findOne(documentId: string, params?: Record<string, unknown>): Promise<{ data: T }> {
1917
- const response = await strapi.findOne(pluralName, documentId, params) as unknown as StrapiSingleResponse<T>;
1918
- return { data: response.data };
3610
+ /**
3611
+ * @param {string} documentId
3612
+ * @param {Record<string, unknown>} [params]
3613
+ * @returns {Promise<{ data: T }>}
3614
+ */
3615
+ async findOne(documentId, params) {
3616
+ const response = await strapi.findOne(pluralName, documentId, params);
3617
+ /** @type {any} */
3618
+ const rawData = response.data;
3619
+ /** @type {T} */
3620
+ const data = rawData;
3621
+ return { data };
1919
3622
  },
1920
- async create(data: { data: Partial<T> }): Promise<{ data: T }> {
1921
- const response = await strapi.create(pluralName, data.data as Record<string, unknown>) as unknown as StrapiSingleResponse<T>;
1922
- return { data: response.data };
3623
+ /**
3624
+ * @param {{ data: Partial<T> }} data
3625
+ * @returns {Promise<{ data: T }>}
3626
+ */
3627
+ async create(data) {
3628
+ const response = await strapi.create(pluralName, data.data);
3629
+ /** @type {any} */
3630
+ const rawData = response.data;
3631
+ /** @type {T} */
3632
+ const result = rawData;
3633
+ return { data: result };
1923
3634
  },
1924
- async update(documentId: string, data: { data: Partial<T> }): Promise<{ data: T }> {
1925
- const response = await strapi.update(pluralName, documentId, data.data as Record<string, unknown>) as unknown as StrapiSingleResponse<T>;
1926
- return { data: response.data };
3635
+ /**
3636
+ * @param {string} documentId
3637
+ * @param {{ data: Partial<T> }} data
3638
+ * @returns {Promise<{ data: T }>}
3639
+ */
3640
+ async update(documentId, data) {
3641
+ const response = await strapi.update(pluralName, documentId, data.data);
3642
+ /** @type {any} */
3643
+ const rawData = response.data;
3644
+ /** @type {T} */
3645
+ const result = rawData;
3646
+ return { data: result };
1927
3647
  },
1928
- async delete(documentId: string): Promise<void> {
3648
+ /**
3649
+ * @param {string} documentId
3650
+ * @returns {Promise<void>}
3651
+ */
3652
+ async delete(documentId) {
1929
3653
  await strapi.delete(pluralName, documentId);
1930
3654
  },
1931
3655
  };
1932
3656
  }
1933
3657
 
1934
- // Helper to get typed single type
1935
- export function single<T>(singularName: string) {
3658
+ /**
3659
+ * Helper to get typed single type
3660
+ * @template T
3661
+ * @param {string} singularName
3662
+ */
3663
+ function single(singularName) {
1936
3664
  return {
1937
- async find(params?: Record<string, unknown>): Promise<{ data: T }> {
1938
- const response = await strapi.find(singularName, params) as unknown as StrapiSingleResponse<T>;
1939
- return { data: response.data };
3665
+ /**
3666
+ * @param {Record<string, unknown>} [params]
3667
+ * @returns {Promise<{ data: T }>}
3668
+ */
3669
+ async find(params) {
3670
+ const response = await strapi.find(singularName, params);
3671
+ /** @type {any} */
3672
+ const rawData = response.data;
3673
+ /** @type {T} */
3674
+ const data = rawData;
3675
+ return { data };
1940
3676
  },
1941
- async update(data: { data: Partial<T> }): Promise<{ data: T }> {
1942
- const response = await strapi.update(singularName, 1 as unknown as string, data.data as Record<string, unknown>) as unknown as StrapiSingleResponse<T>;
1943
- return { data: response.data };
3677
+ /**
3678
+ * @param {{ data: Partial<T> }} data
3679
+ * @returns {Promise<{ data: T }>}
3680
+ */
3681
+ async update(data) {
3682
+ const response = await strapi.update(singularName, String(1), data.data);
3683
+ /** @type {any} */
3684
+ const rawData = response.data;
3685
+ /** @type {T} */
3686
+ const result = rawData;
3687
+ return { data: result };
1944
3688
  },
1945
- async delete(): Promise<void> {
1946
- await strapi.delete(singularName, 1 as unknown as string);
3689
+ /**
3690
+ * @returns {Promise<void>}
3691
+ */
3692
+ async delete() {
3693
+ await strapi.delete(singularName, String(1));
1947
3694
  },
1948
3695
  };
1949
3696
  }
3697
+
3698
+ module.exports = { strapi, collection, single };
1950
3699
  `;
1951
3700
  }
1952
- function generateLocalesFile2(locales) {
3701
+ function generateLocalesFileJSDoc(locales) {
1953
3702
  if (locales.length === 0) {
1954
- return `/**
3703
+ return `// @ts-check
3704
+ /**
1955
3705
  * Strapi locales
1956
3706
  * Generated by strapi2front
1957
3707
  * Note: i18n is not enabled in Strapi
1958
3708
  */
1959
3709
 
1960
- export const locales = [] as const;
1961
- export type Locale = string;
1962
- export const defaultLocale: Locale = 'en';
1963
- export const localeNames: Record<string, string> = {};
3710
+ /** @type {readonly string[]} */
3711
+ const locales = [];
1964
3712
 
1965
- export function isValidLocale(_code: string): _code is Locale {
3713
+ /** @typedef {string} Locale */
3714
+
3715
+ /** @type {string} */
3716
+ const defaultLocale = 'en';
3717
+
3718
+ /** @type {Record<string, string>} */
3719
+ const localeNames = {};
3720
+
3721
+ /**
3722
+ * @param {string} _code
3723
+ * @returns {boolean}
3724
+ */
3725
+ function isValidLocale(_code) {
1966
3726
  return true;
1967
3727
  }
1968
3728
 
1969
- export function getLocaleName(code: string): string {
3729
+ /**
3730
+ * @param {string} code
3731
+ * @returns {string}
3732
+ */
3733
+ function getLocaleName(code) {
1970
3734
  return code;
1971
3735
  }
3736
+
3737
+ module.exports = { locales, defaultLocale, localeNames, isValidLocale, getLocaleName };
1972
3738
  `;
1973
3739
  }
1974
3740
  const localeCodes = locales.map((l) => l.code);
1975
3741
  const defaultLocale = locales.find((l) => l.isDefault)?.code || locales[0]?.code || "en";
1976
- return `/**
3742
+ return `// @ts-check
3743
+ /**
1977
3744
  * Strapi locales
1978
3745
  * Generated by strapi2front
1979
3746
  */
1980
3747
 
1981
- export const locales = [${localeCodes.map((c) => `'${c}'`).join(", ")}] as const;
3748
+ /** @type {readonly [${localeCodes.map((c) => `'${c}'`).join(", ")}]} */
3749
+ const locales = [${localeCodes.map((c) => `'${c}'`).join(", ")}];
1982
3750
 
1983
- export type Locale = typeof locales[number];
3751
+ /** @typedef {${localeCodes.map((c) => `'${c}'`).join(" | ")}} Locale */
1984
3752
 
1985
- export const defaultLocale: Locale = '${defaultLocale}';
3753
+ /** @type {Locale} */
3754
+ const defaultLocale = '${defaultLocale}';
1986
3755
 
1987
- export const localeNames: Record<Locale, string> = {
3756
+ /** @type {Record<Locale, string>} */
3757
+ const localeNames = {
1988
3758
  ${locales.map((l) => ` '${l.code}': '${l.name}'`).join(",\n")}
1989
3759
  };
1990
3760
 
1991
- export function isValidLocale(code: string): code is Locale {
1992
- return locales.includes(code as Locale);
3761
+ /**
3762
+ * @param {string} code
3763
+ * @returns {boolean}
3764
+ */
3765
+ function isValidLocale(code) {
3766
+ /** @type {readonly string[]} */
3767
+ const localeList = locales;
3768
+ return localeList.includes(code);
1993
3769
  }
1994
3770
 
1995
- export function getLocaleName(code: Locale): string {
3771
+ /**
3772
+ * @param {Locale} code
3773
+ * @returns {string}
3774
+ */
3775
+ function getLocaleName(code) {
1996
3776
  return localeNames[code] || code;
1997
3777
  }
3778
+
3779
+ module.exports = { locales, defaultLocale, localeNames, isValidLocale, getLocaleName };
1998
3780
  `;
1999
3781
  }
2000
- function generateCollectionTypes(collection, schema) {
3782
+ function generateCollectionTypesJSDoc(collection, schema) {
2001
3783
  const typeName = toPascalCase(collection.singularName);
2002
- const attributes = generateAttributes2(collection.attributes);
2003
- const imports = generateTypeImports(collection.attributes, schema, "collection");
2004
- return `/**
3784
+ const attributes = generateJSDocAttributes(collection.attributes, schema, "collection");
3785
+ return `// @ts-check
3786
+ /**
2005
3787
  * ${collection.displayName}
2006
3788
  * ${collection.description || ""}
2007
3789
  * Generated by strapi2front
2008
3790
  */
2009
3791
 
2010
- ${imports}
3792
+ /**
3793
+ * @typedef {import('../../shared/utils').StrapiBaseEntity & ${typeName}Attributes} ${typeName}
3794
+ */
2011
3795
 
2012
- export interface ${typeName} extends StrapiBaseEntity {
3796
+ /**
3797
+ * @typedef {Object} ${typeName}Attributes
2013
3798
  ${attributes}
2014
- }
3799
+ */
2015
3800
 
2016
- export interface ${typeName}Filters {
2017
- id?: number | { $eq?: number; $ne?: number; $in?: number[]; $notIn?: number[] };
2018
- documentId?: string | { $eq?: string; $ne?: string };
2019
- createdAt?: string | { $eq?: string; $gt?: string; $gte?: string; $lt?: string; $lte?: string };
2020
- updatedAt?: string | { $eq?: string; $gt?: string; $gte?: string; $lt?: string; $lte?: string };
2021
- publishedAt?: string | null | { $eq?: string; $ne?: string; $null?: boolean };
2022
- $and?: ${typeName}Filters[];
2023
- $or?: ${typeName}Filters[];
2024
- $not?: ${typeName}Filters;
2025
- }
3801
+ /**
3802
+ * @typedef {Object} ${typeName}Filters
3803
+ * @property {number|{$eq?: number, $ne?: number, $in?: number[], $notIn?: number[]}} [id]
3804
+ * @property {string|{$eq?: string, $ne?: string}} [documentId]
3805
+ * @property {string|{$eq?: string, $gt?: string, $gte?: string, $lt?: string, $lte?: string}} [createdAt]
3806
+ * @property {string|{$eq?: string, $gt?: string, $gte?: string, $lt?: string, $lte?: string}} [updatedAt]
3807
+ * @property {string|null|{$eq?: string, $ne?: string, $null?: boolean}} [publishedAt]
3808
+ * @property {${typeName}Filters[]} [$and]
3809
+ * @property {${typeName}Filters[]} [$or]
3810
+ * @property {${typeName}Filters} [$not]
3811
+ */
3812
+
3813
+ module.exports = {};
2026
3814
  `;
2027
3815
  }
2028
- function generateSingleTypes(single, schema) {
3816
+ function generateSingleTypesJSDoc(single, schema) {
2029
3817
  const typeName = toPascalCase(single.singularName);
2030
- const attributes = generateAttributes2(single.attributes);
2031
- const imports = generateTypeImports(single.attributes, schema, "single");
2032
- return `/**
3818
+ const attributes = generateJSDocAttributes(single.attributes, schema, "single");
3819
+ return `// @ts-check
3820
+ /**
2033
3821
  * ${single.displayName}
2034
3822
  * ${single.description || ""}
2035
3823
  * Generated by strapi2front
2036
3824
  */
2037
3825
 
2038
- ${imports}
3826
+ /**
3827
+ * @typedef {import('../../shared/utils').StrapiBaseEntity & ${typeName}Attributes} ${typeName}
3828
+ */
2039
3829
 
2040
- export interface ${typeName} extends StrapiBaseEntity {
3830
+ /**
3831
+ * @typedef {Object} ${typeName}Attributes
2041
3832
  ${attributes}
2042
- }
3833
+ */
3834
+
3835
+ module.exports = {};
2043
3836
  `;
2044
3837
  }
2045
- function generateComponentTypes(component, schema) {
3838
+ function generateComponentTypesJSDoc(component, schema) {
2046
3839
  const typeName = toPascalCase(component.name);
2047
- const attributes = generateAttributes2(component.attributes);
2048
- const imports = generateTypeImports(component.attributes, schema, "component");
2049
- return `/**
3840
+ const attributes = generateJSDocAttributes(component.attributes, schema, "component");
3841
+ return `// @ts-check
3842
+ /**
2050
3843
  * ${component.displayName} component
2051
3844
  * Category: ${component.category}
2052
3845
  * ${component.description || ""}
2053
3846
  * Generated by strapi2front
2054
3847
  */
2055
3848
 
2056
- ${imports}
2057
-
2058
- export interface ${typeName} {
2059
- id: number;
3849
+ /**
3850
+ * @typedef {Object} ${typeName}
3851
+ * @property {number} id
2060
3852
  ${attributes}
2061
- }
3853
+ */
3854
+
3855
+ module.exports = {};
2062
3856
  `;
2063
3857
  }
2064
- function generateTypeImports(attributes, schema, context) {
2065
- const utilsImports = [];
2066
- const relationImports = /* @__PURE__ */ new Map();
2067
- const componentImports = /* @__PURE__ */ new Map();
2068
- const attributesStr = JSON.stringify(attributes);
2069
- if (context !== "component") {
2070
- utilsImports.push("StrapiBaseEntity");
2071
- }
2072
- if (attributesStr.includes('"type":"media"')) {
2073
- utilsImports.push("StrapiMedia");
2074
- }
2075
- if (attributesStr.includes('"type":"blocks"')) {
2076
- utilsImports.push("BlocksContent");
2077
- }
2078
- const relativePrefix = context === "component" ? ".." : "../..";
2079
- for (const attr of Object.values(attributes)) {
2080
- if (attr.type === "relation" && "target" in attr && attr.target) {
2081
- const targetName = attr.target.split(".").pop() || "";
2082
- if (targetName) {
2083
- const typeName = toPascalCase(targetName);
2084
- const fileName = toKebabCase(targetName);
2085
- const isCollection = schema.collections.some((c) => c.singularName === targetName);
2086
- const isSingle = schema.singles.some((s) => s.singularName === targetName);
2087
- if (isCollection) {
2088
- relationImports.set(typeName, `${relativePrefix}/collections/${fileName}/types`);
2089
- } else if (isSingle) {
2090
- relationImports.set(typeName, `${relativePrefix}/singles/${fileName}/types`);
2091
- }
2092
- }
2093
- }
2094
- if (attr.type === "component" && "component" in attr && attr.component) {
2095
- const componentName = attr.component.split(".").pop() || "";
2096
- if (componentName) {
2097
- const typeName = toPascalCase(componentName);
2098
- const fileName = toKebabCase(componentName);
2099
- if (context === "component") {
2100
- componentImports.set(typeName, `./${fileName}`);
2101
- } else {
2102
- componentImports.set(typeName, `../../components/${fileName}`);
2103
- }
2104
- }
2105
- }
2106
- if (attr.type === "dynamiczone" && "components" in attr && attr.components) {
2107
- for (const comp of attr.components) {
2108
- const componentName = comp.split(".").pop() || "";
2109
- if (componentName) {
2110
- const typeName = toPascalCase(componentName);
2111
- const fileName = toKebabCase(componentName);
2112
- if (context === "component") {
2113
- componentImports.set(typeName, `./${fileName}`);
2114
- } else {
2115
- componentImports.set(typeName, `../../components/${fileName}`);
2116
- }
2117
- }
2118
- }
2119
- }
2120
- }
2121
- const lines = [];
2122
- if (utilsImports.length > 0) {
2123
- const utilsPath = context === "component" ? "../shared/utils" : "../../shared/utils";
2124
- lines.push(`import type { ${utilsImports.join(", ")} } from '${utilsPath}';`);
2125
- }
2126
- for (const [typeName, importPath] of relationImports) {
2127
- lines.push(`import type { ${typeName} } from '${importPath}';`);
2128
- }
2129
- for (const [typeName, importPath] of componentImports) {
2130
- lines.push(`import type { ${typeName} } from '${importPath}';`);
2131
- }
2132
- return lines.join("\n");
2133
- }
2134
- function generateAttributes2(attributes) {
3858
+ function generateJSDocAttributes(attributes, schema, context) {
2135
3859
  const lines = [];
3860
+ const relativePrefix = context === "component" ? ".." : "../..";
2136
3861
  for (const [name, attr] of Object.entries(attributes)) {
2137
- const tsType = attributeToTsType2(attr);
2138
- const optional = attr.required ? "" : "?";
2139
- lines.push(` ${name}${optional}: ${tsType};`);
3862
+ const jsType = attributeToJSDocType(attr, schema, relativePrefix);
3863
+ const optional = attr.required ? "" : "[";
3864
+ const optionalEnd = attr.required ? "" : "]";
3865
+ lines.push(` * @property {${jsType}} ${optional}${name}${optionalEnd}`);
2140
3866
  }
2141
3867
  return lines.join("\n");
2142
3868
  }
2143
- function attributeToTsType2(attr) {
3869
+ function attributeToJSDocType(attr, schema, relativePrefix) {
2144
3870
  switch (attr.type) {
2145
3871
  case "string":
2146
3872
  case "text":
@@ -2150,7 +3876,7 @@ function attributeToTsType2(attr) {
2150
3876
  case "uid":
2151
3877
  return "string";
2152
3878
  case "blocks":
2153
- return "BlocksContent";
3879
+ return "Array<Object>";
2154
3880
  case "integer":
2155
3881
  case "biginteger":
2156
3882
  case "float":
@@ -2164,130 +3890,116 @@ function attributeToTsType2(attr) {
2164
3890
  case "timestamp":
2165
3891
  return "string";
2166
3892
  case "json":
2167
- return "unknown";
3893
+ return "Object";
2168
3894
  case "enumeration":
2169
3895
  if ("enum" in attr && attr.enum) {
2170
- return attr.enum.map((v) => `'${v}'`).join(" | ");
3896
+ return attr.enum.map((v) => `'${v}'`).join("|");
2171
3897
  }
2172
3898
  return "string";
2173
3899
  case "media":
2174
3900
  if ("multiple" in attr && attr.multiple) {
2175
- return "StrapiMedia[]";
3901
+ return `import("${relativePrefix}/shared/utils").StrapiMedia[]`;
2176
3902
  }
2177
- return "StrapiMedia | null";
3903
+ return `import("${relativePrefix}/shared/utils").StrapiMedia|null`;
2178
3904
  case "relation":
2179
3905
  if ("target" in attr && attr.target) {
2180
- const targetName = toPascalCase(attr.target.split(".").pop() || "unknown");
3906
+ const targetName = attr.target.split(".").pop() || "unknown";
3907
+ const typeName = toPascalCase(targetName);
3908
+ const fileName = toKebabCase(targetName);
2181
3909
  const isMany = attr.relation === "oneToMany" || attr.relation === "manyToMany";
2182
- return isMany ? `${targetName}[]` : `${targetName} | null`;
3910
+ const isCollection = schema.collections.some((c) => c.singularName === targetName);
3911
+ const isSingle = schema.singles.some((s) => s.singularName === targetName);
3912
+ let importPath;
3913
+ if (isCollection) {
3914
+ importPath = `${relativePrefix}/collections/${fileName}/types`;
3915
+ } else if (isSingle) {
3916
+ importPath = `${relativePrefix}/singles/${fileName}/types`;
3917
+ } else {
3918
+ importPath = `${relativePrefix}/collections/${fileName}/types`;
3919
+ }
3920
+ const importType = `import("${importPath}").${typeName}`;
3921
+ return isMany ? `${importType}[]` : `${importType}|null`;
2183
3922
  }
2184
- return "unknown";
3923
+ return "Object";
2185
3924
  case "component":
2186
3925
  if ("component" in attr && attr.component) {
2187
- const componentName = toPascalCase(attr.component.split(".").pop() || "unknown");
3926
+ const componentName = attr.component.split(".").pop() || "unknown";
3927
+ const typeName = toPascalCase(componentName);
3928
+ const fileName = toKebabCase(componentName);
3929
+ const importPath = `${relativePrefix}/components/${fileName}`;
3930
+ const importType = `import("${importPath}").${typeName}`;
2188
3931
  if ("repeatable" in attr && attr.repeatable) {
2189
- return `${componentName}[]`;
3932
+ return `${importType}[]`;
2190
3933
  }
2191
- return `${componentName} | null`;
3934
+ return `${importType}|null`;
2192
3935
  }
2193
- return "unknown";
3936
+ return "Object";
2194
3937
  case "dynamiczone":
2195
3938
  if ("components" in attr && attr.components) {
2196
- const types = attr.components.map((c) => toPascalCase(c.split(".").pop() || "unknown"));
2197
- return `(${types.join(" | ")})[]`;
3939
+ const types = attr.components.map((comp) => {
3940
+ const componentName = comp.split(".").pop() || "unknown";
3941
+ const typeName = toPascalCase(componentName);
3942
+ const fileName = toKebabCase(componentName);
3943
+ const importPath = `${relativePrefix}/components/${fileName}`;
3944
+ return `import("${importPath}").${typeName}`;
3945
+ });
3946
+ return `(${types.join("|")})[]`;
2198
3947
  }
2199
- return "unknown[]";
3948
+ return "Object[]";
2200
3949
  default:
2201
- return "unknown";
3950
+ return "Object";
2202
3951
  }
2203
3952
  }
2204
- function generateCollectionService2(collection, strapiVersion) {
3953
+ function generateCollectionServiceJSDoc(collection, strapiVersion) {
2205
3954
  const typeName = toPascalCase(collection.singularName);
2206
3955
  const serviceName = toCamelCase(collection.singularName) + "Service";
2207
3956
  const endpoint = collection.pluralName;
2208
3957
  const hasSlug = "slug" in collection.attributes;
2209
3958
  const { localized, draftAndPublish } = collection;
2210
3959
  const isV4 = strapiVersion === "v4";
2211
- const idParam = isV4 ? "id: number" : "documentId: string";
2212
- const idName = isV4 ? "id" : "documentId";
2213
- const omitFields = isV4 ? "'id' | 'createdAt' | 'updatedAt' | 'publishedAt'" : "'id' | 'documentId' | 'createdAt' | 'updatedAt' | 'publishedAt'";
2214
- const omitFieldsUpdate = isV4 ? "'id' | 'createdAt' | 'updatedAt'" : "'id' | 'documentId' | 'createdAt' | 'updatedAt'";
2215
- const imports = [
2216
- `import { collection } from '../../shared/client';`,
2217
- `import type { ${typeName}, ${typeName}Filters } from './types';`,
2218
- `import type { StrapiPagination } from '../../shared/utils';`
2219
- ];
2220
- if (localized) {
2221
- imports.push(`import type { Locale } from '../../shared/locales';`);
2222
- }
2223
- const paginationFields = `
2224
- /** Page number (1-indexed) - use with pageSize */
2225
- page?: number;
2226
- /** Number of items per page (default: 25) - use with page */
2227
- pageSize?: number;
2228
- /** Offset to start from (0-indexed) - use with limit */
2229
- start?: number;
2230
- /** Maximum number of items to return - use with start */
2231
- limit?: number;`;
2232
- let findManyOptionsFields = ` filters?: ${typeName}Filters;
2233
- pagination?: {${paginationFields}
2234
- };
2235
- sort?: string | string[];
2236
- populate?: string | string[] | Record<string, unknown>;`;
2237
- let findOneOptionsFields = ` populate?: string | string[] | Record<string, unknown>;`;
2238
- if (localized) {
2239
- findManyOptionsFields += `
2240
- locale?: Locale;`;
2241
- findOneOptionsFields += `
2242
- locale?: Locale;`;
2243
- }
2244
- if (draftAndPublish) {
2245
- findManyOptionsFields += `
2246
- status?: 'draft' | 'published';`;
2247
- findOneOptionsFields += `
2248
- status?: 'draft' | 'published';`;
2249
- }
2250
- let findParams = ` filters: options.filters,
2251
- pagination: options.pagination,
2252
- sort: options.sort,
2253
- populate: options.populate,`;
2254
- let findOneParams = ` populate: options.populate,`;
2255
- if (localized) {
2256
- findParams += `
2257
- locale: options.locale,`;
2258
- findOneParams += `
2259
- locale: options.locale,`;
2260
- }
2261
- if (draftAndPublish) {
2262
- findParams += `
2263
- status: options.status,`;
2264
- findOneParams += `
2265
- status: options.status,`;
2266
- }
2267
- return `/**
3960
+ const idParam = isV4 ? "id" : "documentId";
3961
+ const idType = isV4 ? "number" : "string";
3962
+ return `// @ts-check
3963
+ /**
2268
3964
  * ${collection.displayName} Service
2269
3965
  * ${collection.description || ""}
2270
3966
  * Generated by strapi2front
2271
3967
  * Strapi version: ${strapiVersion}
2272
3968
  */
2273
3969
 
2274
- ${imports.join("\n")}
3970
+ const { collection } = require('../../shared/client');
2275
3971
 
2276
- export interface FindManyOptions {
2277
- ${findManyOptionsFields}
2278
- }
3972
+ /**
3973
+ * @typedef {Object} FindManyOptions
3974
+ * @property {import('./types').${typeName}Filters} [filters]
3975
+ * @property {Object} [pagination]
3976
+ * @property {number} [pagination.page]
3977
+ * @property {number} [pagination.pageSize]
3978
+ * @property {number} [pagination.start]
3979
+ * @property {number} [pagination.limit]
3980
+ * @property {string|string[]} [sort]
3981
+ * @property {string|string[]|Record<string, unknown>} [populate]${localized ? "\n * @property {string} [locale]" : ""}${draftAndPublish ? "\n * @property {'draft'|'published'} [status]" : ""}
3982
+ */
2279
3983
 
2280
- export interface FindOneOptions {
2281
- ${findOneOptionsFields}
2282
- }
3984
+ /**
3985
+ * @typedef {Object} FindOneOptions
3986
+ * @property {string|string[]|Record<string, unknown>} [populate]${localized ? "\n * @property {string} [locale]" : ""}${draftAndPublish ? "\n * @property {'draft'|'published'} [status]" : ""}
3987
+ */
2283
3988
 
2284
- // Create typed collection helper
2285
- const ${toCamelCase(collection.singularName)}Collection = collection<${typeName}>('${endpoint}');
3989
+ /** @type {ReturnType<typeof collection<import('./types').${typeName}>>} */
3990
+ const ${toCamelCase(collection.singularName)}Collection = collection('${endpoint}');
2286
3991
 
2287
- export const ${serviceName} = {
2288
- async findMany(options: FindManyOptions = {}): Promise<{ data: ${typeName}[]; pagination: StrapiPagination }> {
3992
+ const ${serviceName} = {
3993
+ /**
3994
+ * @param {FindManyOptions} [options]
3995
+ * @returns {Promise<{ data: import('./types').${typeName}[], pagination: import('../../shared/utils').StrapiPagination }>}
3996
+ */
3997
+ async findMany(options = {}) {
2289
3998
  const response = await ${toCamelCase(collection.singularName)}Collection.find({
2290
- ${findParams}
3999
+ filters: options.filters,
4000
+ pagination: options.pagination,
4001
+ sort: options.sort,
4002
+ populate: options.populate,${localized ? "\n locale: options.locale," : ""}${draftAndPublish ? "\n status: options.status," : ""}
2291
4003
  });
2292
4004
 
2293
4005
  return {
@@ -2296,8 +4008,13 @@ ${findParams}
2296
4008
  };
2297
4009
  },
2298
4010
 
2299
- async findAll(options: Omit<FindManyOptions, 'pagination'> = {}): Promise<${typeName}[]> {
2300
- const allItems: ${typeName}[] = [];
4011
+ /**
4012
+ * @param {Omit<FindManyOptions, 'pagination'>} [options]
4013
+ * @returns {Promise<import('./types').${typeName}[]>}
4014
+ */
4015
+ async findAll(options = {}) {
4016
+ /** @type {import('./types').${typeName}[]} */
4017
+ const allItems = [];
2301
4018
  let page = 1;
2302
4019
  let hasMore = true;
2303
4020
 
@@ -2315,10 +4032,15 @@ ${findParams}
2315
4032
  return allItems;
2316
4033
  },
2317
4034
 
2318
- async findOne(${idParam}, options: FindOneOptions = {}): Promise<${typeName} | null> {
4035
+ /**
4036
+ * @param {${idType}} ${idParam}
4037
+ * @param {FindOneOptions} [options]
4038
+ * @returns {Promise<import('./types').${typeName}|null>}
4039
+ */
4040
+ async findOne(${idParam}, options = {}) {
2319
4041
  try {
2320
- const response = await ${toCamelCase(collection.singularName)}Collection.findOne(${idName}, {
2321
- ${findOneParams}
4042
+ const response = await ${toCamelCase(collection.singularName)}Collection.findOne(${idParam}, {
4043
+ populate: options.populate,${localized ? "\n locale: options.locale," : ""}${draftAndPublish ? "\n status: options.status," : ""}
2322
4044
  });
2323
4045
 
2324
4046
  return response.data;
@@ -2330,9 +4052,14 @@ ${findOneParams}
2330
4052
  }
2331
4053
  },
2332
4054
  ${hasSlug ? `
2333
- async findBySlug(slug: string, options: FindOneOptions = {}): Promise<${typeName} | null> {
4055
+ /**
4056
+ * @param {string} slug
4057
+ * @param {FindOneOptions} [options]
4058
+ * @returns {Promise<import('./types').${typeName}|null>}
4059
+ */
4060
+ async findBySlug(slug, options = {}) {
2334
4061
  const { data } = await this.findMany({
2335
- filters: { slug: { $eq: slug } } as ${typeName}Filters,
4062
+ filters: /** @type {import('./types').${typeName}Filters} */ ({ slug: { $eq: slug } }),
2336
4063
  pagination: { pageSize: 1 },
2337
4064
  populate: options.populate,${localized ? "\n locale: options.locale," : ""}${draftAndPublish ? "\n status: options.status," : ""}
2338
4065
  });
@@ -2340,21 +4067,38 @@ ${hasSlug ? `
2340
4067
  return data[0] || null;
2341
4068
  },
2342
4069
  ` : ""}
2343
- async create(data: Partial<Omit<${typeName}, ${omitFields}>>): Promise<${typeName}> {
4070
+ /**
4071
+ * @param {Partial<import('./types').${typeName}>} data
4072
+ * @returns {Promise<import('./types').${typeName}>}
4073
+ */
4074
+ async create(data) {
2344
4075
  const response = await ${toCamelCase(collection.singularName)}Collection.create({ data });
2345
4076
  return response.data;
2346
4077
  },
2347
4078
 
2348
- async update(${idParam}, data: Partial<Omit<${typeName}, ${omitFieldsUpdate}>>): Promise<${typeName}> {
2349
- const response = await ${toCamelCase(collection.singularName)}Collection.update(${idName}, { data });
4079
+ /**
4080
+ * @param {${idType}} ${idParam}
4081
+ * @param {Partial<import('./types').${typeName}>} data
4082
+ * @returns {Promise<import('./types').${typeName}>}
4083
+ */
4084
+ async update(${idParam}, data) {
4085
+ const response = await ${toCamelCase(collection.singularName)}Collection.update(${idParam}, { data });
2350
4086
  return response.data;
2351
4087
  },
2352
4088
 
2353
- async delete(${idParam}): Promise<void> {
2354
- await ${toCamelCase(collection.singularName)}Collection.delete(${idName});
4089
+ /**
4090
+ * @param {${idType}} ${idParam}
4091
+ * @returns {Promise<void>}
4092
+ */
4093
+ async delete(${idParam}) {
4094
+ await ${toCamelCase(collection.singularName)}Collection.delete(${idParam});
2355
4095
  },
2356
4096
 
2357
- async count(filters?: ${typeName}Filters): Promise<number> {
4097
+ /**
4098
+ * @param {import('./types').${typeName}Filters} [filters]
4099
+ * @returns {Promise<number>}
4100
+ */
4101
+ async count(filters) {
2358
4102
  const { pagination } = await this.findMany({
2359
4103
  filters,
2360
4104
  pagination: { pageSize: 1 },
@@ -2363,61 +4107,42 @@ ${hasSlug ? `
2363
4107
  return pagination.total;
2364
4108
  },
2365
4109
  };
4110
+
4111
+ module.exports = { ${serviceName} };
2366
4112
  `;
2367
4113
  }
2368
- function generateSingleService2(single, strapiVersion) {
4114
+ function generateSingleServiceJSDoc(single, strapiVersion) {
2369
4115
  const typeName = toPascalCase(single.singularName);
2370
4116
  const serviceName = toCamelCase(single.singularName) + "Service";
2371
4117
  const endpoint = single.singularName;
2372
4118
  const { localized, draftAndPublish } = single;
2373
- const isV4 = strapiVersion === "v4";
2374
- const omitFields = isV4 ? "'id' | 'createdAt' | 'updatedAt'" : "'id' | 'documentId' | 'createdAt' | 'updatedAt'";
2375
- const imports = [
2376
- `import { single } from '../../shared/client';`,
2377
- `import type { ${typeName} } from './types';`
2378
- ];
2379
- if (localized) {
2380
- imports.push(`import type { Locale } from '../../shared/locales';`);
2381
- }
2382
- let findOptionsFields = ` populate?: string | string[] | Record<string, unknown>;`;
2383
- if (localized) {
2384
- findOptionsFields += `
2385
- locale?: Locale;`;
2386
- }
2387
- if (draftAndPublish) {
2388
- findOptionsFields += `
2389
- status?: 'draft' | 'published';`;
2390
- }
2391
- let findParams = ` populate: options.populate,`;
2392
- if (localized) {
2393
- findParams += `
2394
- locale: options.locale,`;
2395
- }
2396
- if (draftAndPublish) {
2397
- findParams += `
2398
- status: options.status,`;
2399
- }
2400
- return `/**
4119
+ return `// @ts-check
4120
+ /**
2401
4121
  * ${single.displayName} Service (Single Type)
2402
4122
  * ${single.description || ""}
2403
4123
  * Generated by strapi2front
2404
4124
  * Strapi version: ${strapiVersion}
2405
4125
  */
2406
4126
 
2407
- ${imports.join("\n")}
4127
+ const { single } = require('../../shared/client');
2408
4128
 
2409
- export interface FindOptions {
2410
- ${findOptionsFields}
2411
- }
4129
+ /**
4130
+ * @typedef {Object} FindOptions
4131
+ * @property {string|string[]|Record<string, unknown>} [populate]${localized ? "\n * @property {string} [locale]" : ""}${draftAndPublish ? "\n * @property {'draft'|'published'} [status]" : ""}
4132
+ */
2412
4133
 
2413
- // Create typed single helper
2414
- const ${toCamelCase(single.singularName)}Single = single<${typeName}>('${endpoint}');
4134
+ /** @type {ReturnType<typeof single<import('./types').${typeName}>>} */
4135
+ const ${toCamelCase(single.singularName)}Single = single('${endpoint}');
2415
4136
 
2416
- export const ${serviceName} = {
2417
- async find(options: FindOptions = {}): Promise<${typeName} | null> {
4137
+ const ${serviceName} = {
4138
+ /**
4139
+ * @param {FindOptions} [options]
4140
+ * @returns {Promise<import('./types').${typeName}|null>}
4141
+ */
4142
+ async find(options = {}) {
2418
4143
  try {
2419
4144
  const response = await ${toCamelCase(single.singularName)}Single.find({
2420
- ${findParams}
4145
+ populate: options.populate,${localized ? "\n locale: options.locale," : ""}${draftAndPublish ? "\n status: options.status," : ""}
2421
4146
  });
2422
4147
 
2423
4148
  return response.data;
@@ -2429,96 +4154,60 @@ ${findParams}
2429
4154
  }
2430
4155
  },
2431
4156
 
2432
- async update(data: Partial<Omit<${typeName}, ${omitFields}>>): Promise<${typeName}> {
4157
+ /**
4158
+ * @param {Partial<import('./types').${typeName}>} data
4159
+ * @returns {Promise<import('./types').${typeName}>}
4160
+ */
4161
+ async update(data) {
2433
4162
  const response = await ${toCamelCase(single.singularName)}Single.update({ data });
2434
4163
  return response.data;
2435
4164
  },
2436
4165
 
2437
- async delete(): Promise<void> {
4166
+ /**
4167
+ * @returns {Promise<void>}
4168
+ */
4169
+ async delete() {
2438
4170
  await ${toCamelCase(single.singularName)}Single.delete();
2439
4171
  },
2440
4172
  };
4173
+
4174
+ module.exports = { ${serviceName} };
2441
4175
  `;
2442
4176
  }
2443
- function generateCollectionActions2(collection, strapiVersion) {
2444
- const typeName = toPascalCase(collection.singularName);
2445
- const serviceName = toCamelCase(collection.singularName) + "Service";
2446
- const actionPrefix = toCamelCase(collection.singularName);
2447
- const isV4 = strapiVersion === "v4";
2448
- const idInputSchema = isV4 ? "z.number().int().positive()" : "z.string()";
2449
- const idParamName = isV4 ? "id" : "documentId";
2450
- return `/**
2451
- * ${collection.displayName} Astro Actions
2452
- * ${collection.description || ""}
2453
- * Generated by strapi2front
2454
- * Strapi version: ${strapiVersion}
2455
- */
2456
-
2457
- import { defineAction } from 'astro:actions';
2458
- import { z } from 'astro:schema';
2459
- import { ${serviceName} } from './service';
2460
- import type { ${typeName} } from './types';
2461
-
2462
- export const ${actionPrefix}Actions = {
2463
- getMany: defineAction({
2464
- input: z.object({
2465
- page: z.number().optional(),
2466
- pageSize: z.number().optional(),
2467
- sort: z.string().optional(),
2468
- }).optional(),
2469
- handler: async (input) => {
2470
- const { data, pagination } = await ${serviceName}.findMany({
2471
- pagination: input ? { page: input.page, pageSize: input.pageSize } : undefined,
2472
- sort: input?.sort,
2473
- });
2474
- return { data, pagination };
2475
- },
2476
- }),
2477
-
2478
- getOne: defineAction({
2479
- input: z.object({
2480
- ${idParamName}: ${idInputSchema},
2481
- }),
2482
- handler: async (input) => {
2483
- const data = await ${serviceName}.findOne(input.${idParamName});
2484
- return { data };
2485
- },
2486
- }),
2487
4177
 
2488
- create: defineAction({
2489
- input: z.object({
2490
- data: z.record(z.unknown()),
2491
- }),
2492
- handler: async (input) => {
2493
- const data = await ${serviceName}.create(input.data as Partial<${typeName}>);
2494
- return { data };
2495
- },
2496
- }),
4178
+ // src/frameworks/nextjs/actions.ts
4179
+ async function generateNextJsActions(_schema, _options) {
4180
+ throw new Error(
4181
+ "Next.js Server Actions generator is not yet implemented. Coming soon! For now, you can use the generated services directly."
4182
+ );
4183
+ }
4184
+ function isNextJsActionsSupported(nextVersion) {
4185
+ if (!nextVersion) return false;
4186
+ const match = nextVersion.replace(/^[\^~]/, "").match(/^(\d+)/);
4187
+ const majorVersion = match ? parseInt(match[1], 10) : null;
4188
+ return majorVersion !== null && majorVersion >= 14;
4189
+ }
2497
4190
 
2498
- update: defineAction({
2499
- input: z.object({
2500
- ${idParamName}: ${idInputSchema},
2501
- data: z.record(z.unknown()),
2502
- }),
2503
- handler: async (input) => {
2504
- const data = await ${serviceName}.update(input.${idParamName}, input.data as Partial<${typeName}>);
2505
- return { data };
2506
- },
2507
- }),
4191
+ // src/frameworks/nuxt/server-routes.ts
4192
+ async function generateNuxtServerRoutes(_schema, _options) {
4193
+ throw new Error(
4194
+ "Nuxt Server Routes generator is not yet implemented. Coming soon! For now, you can use the generated services directly."
4195
+ );
4196
+ }
4197
+ function isNuxtServerRoutesSupported(nuxtVersion) {
4198
+ if (!nuxtVersion) return false;
4199
+ const match = nuxtVersion.replace(/^[\^~]/, "").match(/^(\d+)/);
4200
+ const majorVersion = match ? parseInt(match[1], 10) : null;
4201
+ return majorVersion !== null && majorVersion >= 3;
4202
+ }
2508
4203
 
2509
- delete: defineAction({
2510
- input: z.object({
2511
- ${idParamName}: ${idInputSchema},
2512
- }),
2513
- handler: async (input) => {
2514
- await ${serviceName}.delete(input.${idParamName});
2515
- return { success: true };
2516
- },
2517
- }),
4204
+ // src/frameworks/index.ts
4205
+ var frameworkSupport = {
4206
+ astro: { status: "stable", minVersion: "4.0.0" },
4207
+ nextjs: { status: "coming-soon", minVersion: "14.0.0" },
4208
+ nuxt: { status: "coming-soon", minVersion: "3.0.0" }
2518
4209
  };
2519
- `;
2520
- }
2521
4210
 
2522
- export { deleteFile, ensureDir, fileExists, formatCode, formatJson, generateActions, generateByFeature, generateClient, generateLocales, generateServices, generateTypes, listFiles, pluralize, readFile, toCamelCase, toKebabCase, toPascalCase, writeFile };
4211
+ export { deleteFile, ensureDir, fileExists, formatCode, formatJson, frameworkSupport, generateActions, generateAstroActions, generateByFeature, generateClient, generateJSDocServices, generateJSDocTypes, generateLocales, generateNextJsActions, generateNuxtServerRoutes, generateServices, generateTypeScriptTypes, generateTypes, isAstroActionsSupported, isNextJsActionsSupported, isNuxtServerRoutesSupported, listFiles, pluralize, readFile, toCamelCase, toKebabCase, toPascalCase, writeFile };
2523
4212
  //# sourceMappingURL=index.js.map
2524
4213
  //# sourceMappingURL=index.js.map