@strapi2front/generators 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,55 @@ 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
+ /** @type {any} */
3387
+ const merged = { id: item.id, ...item.attributes };
3388
+ return merged;
1771
3389
  }
1772
3390
 
1773
3391
  /**
1774
3392
  * Recursively flatten nested relations in Strapi v4 response
3393
+ * @template T
3394
+ * @param {T} data
3395
+ * @returns {T}
1775
3396
  */
1776
- function flattenRelations<T>(data: T): T {
3397
+ function flattenRelations(data) {
1777
3398
  if (data === null || data === undefined) return data;
1778
3399
  if (Array.isArray(data)) {
1779
- return data.map(item => flattenRelations(item)) as unknown as T;
3400
+ /** @type {any} */
3401
+ const mapped = data.map(item => flattenRelations(item));
3402
+ return mapped;
1780
3403
  }
1781
3404
  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 } }
3405
+ /** @type {Record<string, unknown>} */
3406
+ const result = {};
3407
+ for (const [key, value] of Object.entries(data)) {
1785
3408
  if (value && typeof value === 'object' && 'data' in value) {
1786
- const relationData = (value as { data: unknown }).data;
3409
+ /** @type {any} */
3410
+ const wrapper = value;
3411
+ const relationData = wrapper.data;
1787
3412
  if (relationData === null) {
1788
3413
  result[key] = null;
1789
3414
  } else if (Array.isArray(relationData)) {
1790
- // To-many relation
1791
- result[key] = relationData.map((item: StrapiV4RawItem<unknown>) =>
3415
+ result[key] = relationData.map((item) =>
1792
3416
  flattenRelations(flattenItem(item))
1793
3417
  );
1794
- } else if (typeof relationData === 'object' && 'id' in relationData && 'attributes' in relationData) {
1795
- // To-one relation
1796
- result[key] = flattenRelations(flattenItem(relationData as StrapiV4RawItem<unknown>));
3418
+ } else if (typeof relationData === 'object' && relationData !== null && 'id' in relationData && 'attributes' in relationData) {
3419
+ result[key] = flattenRelations(flattenItem(relationData));
1797
3420
  } else {
1798
3421
  result[key] = flattenRelations(value);
1799
3422
  }
@@ -1801,76 +3424,150 @@ function flattenRelations<T>(data: T): T {
1801
3424
  result[key] = flattenRelations(value);
1802
3425
  }
1803
3426
  }
1804
- return result as T;
3427
+ /** @type {any} */
3428
+ const typed = result;
3429
+ return typed;
1805
3430
  }
1806
3431
  return data;
1807
3432
  }
1808
3433
 
1809
- // Helper to get typed collection
1810
- export function collection<T>(pluralName: string) {
3434
+ /**
3435
+ * Helper to get typed collection
3436
+ * @template T
3437
+ * @param {string} pluralName
3438
+ */
3439
+ function collection(pluralName) {
1811
3440
  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)))
3441
+ /**
3442
+ * @param {Record<string, unknown>} [params]
3443
+ * @returns {Promise<{ data: T[], meta: { pagination: import('./utils').StrapiPagination } }>}
3444
+ */
3445
+ async find(params) {
3446
+ const response = await strapi.find(pluralName, params);
3447
+ /** @type {any} */
3448
+ const rawData = response.data;
3449
+ /** @type {T[]} */
3450
+ const data = Array.isArray(rawData)
3451
+ ? rawData.map(item => flattenRelations(flattenItem(item)))
1816
3452
  : [];
1817
- return {
1818
- data: flattenedData,
1819
- meta: {
1820
- pagination: response.meta?.pagination || defaultPagination,
1821
- },
3453
+ /** @type {any} */
3454
+ const rawMeta = response.meta;
3455
+ /** @type {any} */
3456
+ const rawPag = rawMeta?.pagination;
3457
+ /** @type {import('./utils').StrapiPagination} */
3458
+ const pagination = {
3459
+ page: rawPag?.page ?? defaultPagination.page,
3460
+ pageSize: rawPag?.pageSize ?? defaultPagination.pageSize,
3461
+ pageCount: rawPag?.pageCount ?? defaultPagination.pageCount,
3462
+ total: rawPag?.total ?? defaultPagination.total,
1822
3463
  };
3464
+ return { data, meta: { pagination } };
1823
3465
  },
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)) };
3466
+ /**
3467
+ * @param {number|string} id
3468
+ * @param {Record<string, unknown>} [params]
3469
+ * @returns {Promise<{ data: T }>}
3470
+ */
3471
+ async findOne(id, params) {
3472
+ const response = await strapi.findOne(pluralName, String(id), params);
3473
+ /** @type {any} */
3474
+ const rawData = response.data;
3475
+ /** @type {T} */
3476
+ const data = flattenRelations(flattenItem(rawData));
3477
+ return { data };
1827
3478
  },
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)) };
3479
+ /**
3480
+ * @param {{ data: Partial<T> }} data
3481
+ * @returns {Promise<{ data: T }>}
3482
+ */
3483
+ async create(data) {
3484
+ const response = await strapi.create(pluralName, data.data);
3485
+ /** @type {any} */
3486
+ const rawData = response.data;
3487
+ /** @type {T} */
3488
+ const result = flattenRelations(flattenItem(rawData));
3489
+ return { data: result };
1831
3490
  },
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)) };
3491
+ /**
3492
+ * @param {number|string} id
3493
+ * @param {{ data: Partial<T> }} data
3494
+ * @returns {Promise<{ data: T }>}
3495
+ */
3496
+ async update(id, data) {
3497
+ const response = await strapi.update(pluralName, String(id), data.data);
3498
+ /** @type {any} */
3499
+ const rawData = response.data;
3500
+ /** @type {T} */
3501
+ const result = flattenRelations(flattenItem(rawData));
3502
+ return { data: result };
1835
3503
  },
1836
- async delete(id: number | string): Promise<void> {
3504
+ /**
3505
+ * @param {number|string} id
3506
+ * @returns {Promise<void>}
3507
+ */
3508
+ async delete(id) {
1837
3509
  await strapi.delete(pluralName, String(id));
1838
3510
  },
1839
3511
  };
1840
3512
  }
1841
3513
 
1842
- // Helper to get typed single type
1843
- export function single<T>(singularName: string) {
3514
+ /**
3515
+ * Helper to get typed single type
3516
+ * @template T
3517
+ * @param {string} singularName
3518
+ */
3519
+ function single(singularName) {
1844
3520
  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)) };
3521
+ /**
3522
+ * @param {Record<string, unknown>} [params]
3523
+ * @returns {Promise<{ data: T }>}
3524
+ */
3525
+ async find(params) {
3526
+ const response = await strapi.find(singularName, params);
3527
+ /** @type {any} */
3528
+ const rawData = response.data;
3529
+ /** @type {T} */
3530
+ const data = flattenRelations(flattenItem(rawData));
3531
+ return { data };
1848
3532
  },
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)) };
3533
+ /**
3534
+ * @param {{ data: Partial<T> }} data
3535
+ * @returns {Promise<{ data: T }>}
3536
+ */
3537
+ async update(data) {
3538
+ const response = await strapi.update(singularName, String(1), data.data);
3539
+ /** @type {any} */
3540
+ const rawData = response.data;
3541
+ /** @type {T} */
3542
+ const result = flattenRelations(flattenItem(rawData));
3543
+ return { data: result };
1852
3544
  },
1853
- async delete(): Promise<void> {
1854
- await strapi.delete(singularName, 1 as unknown as string);
3545
+ /**
3546
+ * @returns {Promise<void>}
3547
+ */
3548
+ async delete() {
3549
+ await strapi.delete(singularName, String(1));
1855
3550
  },
1856
3551
  };
1857
3552
  }
3553
+
3554
+ module.exports = { strapi, collection, single };
1858
3555
  `;
1859
3556
  }
1860
- return `/**
3557
+ return `// @ts-check
3558
+ /**
1861
3559
  * Strapi Client (v5)
1862
3560
  * Generated by strapi2front
1863
3561
  */
1864
3562
 
1865
- import Strapi from 'strapi-sdk-js';
1866
- import type { StrapiPagination } from './utils';
3563
+ const Strapi = require('strapi-sdk-js').default;
1867
3564
 
1868
3565
  // 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}';
3566
+ const strapiUrl = process.env.STRAPI_URL || 'http://localhost:1337';
3567
+ const strapiToken = process.env.STRAPI_TOKEN;
3568
+ const strapiApiPrefix = process.env.STRAPI_API_PREFIX || '${normalizedPrefix}';
1872
3569
 
1873
- export const strapi = new Strapi({
3570
+ const strapi = new Strapi({
1874
3571
  url: strapiUrl,
1875
3572
  prefix: strapiApiPrefix,
1876
3573
  axiosOptions: {
@@ -1880,267 +3577,304 @@ export const strapi = new Strapi({
1880
3577
  },
1881
3578
  });
1882
3579
 
1883
- // Default pagination for fallback
1884
- const defaultPagination: StrapiPagination = {
3580
+ /** @type {import('./utils').StrapiPagination} */
3581
+ const defaultPagination = {
1885
3582
  page: 1,
1886
3583
  pageSize: 25,
1887
3584
  pageCount: 1,
1888
3585
  total: 0,
1889
3586
  };
1890
3587
 
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) {
3588
+ /**
3589
+ * Helper to get typed collection
3590
+ * @template T
3591
+ * @param {string} pluralName
3592
+ */
3593
+ function collection(pluralName) {
1906
3594
  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
- },
3595
+ /**
3596
+ * @param {Record<string, unknown>} [params]
3597
+ * @returns {Promise<{ data: T[], meta: { pagination: import('./utils').StrapiPagination } }>}
3598
+ */
3599
+ async find(params) {
3600
+ const response = await strapi.find(pluralName, params);
3601
+ /** @type {any} */
3602
+ const rawMeta = response.meta;
3603
+ /** @type {any} */
3604
+ const rawPag = rawMeta?.pagination;
3605
+ /** @type {import('./utils').StrapiPagination} */
3606
+ const pagination = {
3607
+ page: rawPag?.page ?? defaultPagination.page,
3608
+ pageSize: rawPag?.pageSize ?? defaultPagination.pageSize,
3609
+ pageCount: rawPag?.pageCount ?? defaultPagination.pageCount,
3610
+ total: rawPag?.total ?? defaultPagination.total,
1914
3611
  };
3612
+ /** @type {any} */
3613
+ const rawData = response.data;
3614
+ /** @type {T[]} */
3615
+ const data = Array.isArray(rawData) ? rawData : [];
3616
+ return { data, meta: { pagination } };
1915
3617
  },
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 };
3618
+ /**
3619
+ * @param {string} documentId
3620
+ * @param {Record<string, unknown>} [params]
3621
+ * @returns {Promise<{ data: T }>}
3622
+ */
3623
+ async findOne(documentId, params) {
3624
+ const response = await strapi.findOne(pluralName, documentId, params);
3625
+ /** @type {any} */
3626
+ const rawData = response.data;
3627
+ /** @type {T} */
3628
+ const data = rawData;
3629
+ return { data };
1919
3630
  },
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 };
3631
+ /**
3632
+ * @param {{ data: Partial<T> }} data
3633
+ * @returns {Promise<{ data: T }>}
3634
+ */
3635
+ async create(data) {
3636
+ const response = await strapi.create(pluralName, data.data);
3637
+ /** @type {any} */
3638
+ const rawData = response.data;
3639
+ /** @type {T} */
3640
+ const result = rawData;
3641
+ return { data: result };
1923
3642
  },
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 };
3643
+ /**
3644
+ * @param {string} documentId
3645
+ * @param {{ data: Partial<T> }} data
3646
+ * @returns {Promise<{ data: T }>}
3647
+ */
3648
+ async update(documentId, data) {
3649
+ const response = await strapi.update(pluralName, documentId, data.data);
3650
+ /** @type {any} */
3651
+ const rawData = response.data;
3652
+ /** @type {T} */
3653
+ const result = rawData;
3654
+ return { data: result };
1927
3655
  },
1928
- async delete(documentId: string): Promise<void> {
3656
+ /**
3657
+ * @param {string} documentId
3658
+ * @returns {Promise<void>}
3659
+ */
3660
+ async delete(documentId) {
1929
3661
  await strapi.delete(pluralName, documentId);
1930
3662
  },
1931
3663
  };
1932
3664
  }
1933
3665
 
1934
- // Helper to get typed single type
1935
- export function single<T>(singularName: string) {
3666
+ /**
3667
+ * Helper to get typed single type
3668
+ * @template T
3669
+ * @param {string} singularName
3670
+ */
3671
+ function single(singularName) {
1936
3672
  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 };
3673
+ /**
3674
+ * @param {Record<string, unknown>} [params]
3675
+ * @returns {Promise<{ data: T }>}
3676
+ */
3677
+ async find(params) {
3678
+ const response = await strapi.find(singularName, params);
3679
+ /** @type {any} */
3680
+ const rawData = response.data;
3681
+ /** @type {T} */
3682
+ const data = rawData;
3683
+ return { data };
1940
3684
  },
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 };
3685
+ /**
3686
+ * @param {{ data: Partial<T> }} data
3687
+ * @returns {Promise<{ data: T }>}
3688
+ */
3689
+ async update(data) {
3690
+ const response = await strapi.update(singularName, String(1), data.data);
3691
+ /** @type {any} */
3692
+ const rawData = response.data;
3693
+ /** @type {T} */
3694
+ const result = rawData;
3695
+ return { data: result };
1944
3696
  },
1945
- async delete(): Promise<void> {
1946
- await strapi.delete(singularName, 1 as unknown as string);
3697
+ /**
3698
+ * @returns {Promise<void>}
3699
+ */
3700
+ async delete() {
3701
+ await strapi.delete(singularName, String(1));
1947
3702
  },
1948
3703
  };
1949
3704
  }
3705
+
3706
+ module.exports = { strapi, collection, single };
1950
3707
  `;
1951
3708
  }
1952
- function generateLocalesFile2(locales) {
3709
+ function generateLocalesFileJSDoc(locales) {
1953
3710
  if (locales.length === 0) {
1954
- return `/**
3711
+ return `// @ts-check
3712
+ /**
1955
3713
  * Strapi locales
1956
3714
  * Generated by strapi2front
1957
3715
  * Note: i18n is not enabled in Strapi
1958
3716
  */
1959
3717
 
1960
- export const locales = [] as const;
1961
- export type Locale = string;
1962
- export const defaultLocale: Locale = 'en';
1963
- export const localeNames: Record<string, string> = {};
3718
+ /** @type {readonly string[]} */
3719
+ const locales = [];
1964
3720
 
1965
- export function isValidLocale(_code: string): _code is Locale {
3721
+ /** @typedef {string} Locale */
3722
+
3723
+ /** @type {string} */
3724
+ const defaultLocale = 'en';
3725
+
3726
+ /** @type {Record<string, string>} */
3727
+ const localeNames = {};
3728
+
3729
+ /**
3730
+ * @param {string} _code
3731
+ * @returns {boolean}
3732
+ */
3733
+ function isValidLocale(_code) {
1966
3734
  return true;
1967
3735
  }
1968
3736
 
1969
- export function getLocaleName(code: string): string {
3737
+ /**
3738
+ * @param {string} code
3739
+ * @returns {string}
3740
+ */
3741
+ function getLocaleName(code) {
1970
3742
  return code;
1971
3743
  }
3744
+
3745
+ module.exports = { locales, defaultLocale, localeNames, isValidLocale, getLocaleName };
1972
3746
  `;
1973
3747
  }
1974
3748
  const localeCodes = locales.map((l) => l.code);
1975
3749
  const defaultLocale = locales.find((l) => l.isDefault)?.code || locales[0]?.code || "en";
1976
- return `/**
3750
+ return `// @ts-check
3751
+ /**
1977
3752
  * Strapi locales
1978
3753
  * Generated by strapi2front
1979
3754
  */
1980
3755
 
1981
- export const locales = [${localeCodes.map((c) => `'${c}'`).join(", ")}] as const;
3756
+ /** @type {readonly [${localeCodes.map((c) => `'${c}'`).join(", ")}]} */
3757
+ const locales = [${localeCodes.map((c) => `'${c}'`).join(", ")}];
1982
3758
 
1983
- export type Locale = typeof locales[number];
3759
+ /** @typedef {${localeCodes.map((c) => `'${c}'`).join(" | ")}} Locale */
1984
3760
 
1985
- export const defaultLocale: Locale = '${defaultLocale}';
3761
+ /** @type {Locale} */
3762
+ const defaultLocale = '${defaultLocale}';
1986
3763
 
1987
- export const localeNames: Record<Locale, string> = {
3764
+ /** @type {Record<Locale, string>} */
3765
+ const localeNames = {
1988
3766
  ${locales.map((l) => ` '${l.code}': '${l.name}'`).join(",\n")}
1989
3767
  };
1990
3768
 
1991
- export function isValidLocale(code: string): code is Locale {
1992
- return locales.includes(code as Locale);
3769
+ /**
3770
+ * @param {string} code
3771
+ * @returns {boolean}
3772
+ */
3773
+ function isValidLocale(code) {
3774
+ /** @type {readonly string[]} */
3775
+ const localeList = locales;
3776
+ return localeList.includes(code);
1993
3777
  }
1994
3778
 
1995
- export function getLocaleName(code: Locale): string {
3779
+ /**
3780
+ * @param {Locale} code
3781
+ * @returns {string}
3782
+ */
3783
+ function getLocaleName(code) {
1996
3784
  return localeNames[code] || code;
1997
3785
  }
3786
+
3787
+ module.exports = { locales, defaultLocale, localeNames, isValidLocale, getLocaleName };
1998
3788
  `;
1999
3789
  }
2000
- function generateCollectionTypes(collection, schema) {
3790
+ function generateCollectionTypesJSDoc(collection, schema) {
2001
3791
  const typeName = toPascalCase(collection.singularName);
2002
- const attributes = generateAttributes2(collection.attributes);
2003
- const imports = generateTypeImports(collection.attributes, schema, "collection");
2004
- return `/**
3792
+ const attributes = generateJSDocAttributes(collection.attributes, schema, "collection");
3793
+ return `// @ts-check
3794
+ /**
2005
3795
  * ${collection.displayName}
2006
3796
  * ${collection.description || ""}
2007
3797
  * Generated by strapi2front
2008
3798
  */
2009
3799
 
2010
- ${imports}
3800
+ /**
3801
+ * @typedef {import('../../shared/utils').StrapiBaseEntity & ${typeName}Attributes} ${typeName}
3802
+ */
2011
3803
 
2012
- export interface ${typeName} extends StrapiBaseEntity {
3804
+ /**
3805
+ * @typedef {Object} ${typeName}Attributes
2013
3806
  ${attributes}
2014
- }
3807
+ */
2015
3808
 
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
- }
3809
+ /**
3810
+ * @typedef {Object} ${typeName}Filters
3811
+ * @property {number|{$eq?: number, $ne?: number, $in?: number[], $notIn?: number[]}} [id]
3812
+ * @property {string|{$eq?: string, $ne?: string}} [documentId]
3813
+ * @property {string|{$eq?: string, $gt?: string, $gte?: string, $lt?: string, $lte?: string}} [createdAt]
3814
+ * @property {string|{$eq?: string, $gt?: string, $gte?: string, $lt?: string, $lte?: string}} [updatedAt]
3815
+ * @property {string|null|{$eq?: string, $ne?: string, $null?: boolean}} [publishedAt]
3816
+ * @property {${typeName}Filters[]} [$and]
3817
+ * @property {${typeName}Filters[]} [$or]
3818
+ * @property {${typeName}Filters} [$not]
3819
+ */
3820
+
3821
+ module.exports = {};
2026
3822
  `;
2027
3823
  }
2028
- function generateSingleTypes(single, schema) {
3824
+ function generateSingleTypesJSDoc(single, schema) {
2029
3825
  const typeName = toPascalCase(single.singularName);
2030
- const attributes = generateAttributes2(single.attributes);
2031
- const imports = generateTypeImports(single.attributes, schema, "single");
2032
- return `/**
3826
+ const attributes = generateJSDocAttributes(single.attributes, schema, "single");
3827
+ return `// @ts-check
3828
+ /**
2033
3829
  * ${single.displayName}
2034
3830
  * ${single.description || ""}
2035
3831
  * Generated by strapi2front
2036
3832
  */
2037
3833
 
2038
- ${imports}
3834
+ /**
3835
+ * @typedef {import('../../shared/utils').StrapiBaseEntity & ${typeName}Attributes} ${typeName}
3836
+ */
2039
3837
 
2040
- export interface ${typeName} extends StrapiBaseEntity {
3838
+ /**
3839
+ * @typedef {Object} ${typeName}Attributes
2041
3840
  ${attributes}
2042
- }
3841
+ */
3842
+
3843
+ module.exports = {};
2043
3844
  `;
2044
3845
  }
2045
- function generateComponentTypes(component, schema) {
3846
+ function generateComponentTypesJSDoc(component, schema) {
2046
3847
  const typeName = toPascalCase(component.name);
2047
- const attributes = generateAttributes2(component.attributes);
2048
- const imports = generateTypeImports(component.attributes, schema, "component");
2049
- return `/**
3848
+ const attributes = generateJSDocAttributes(component.attributes, schema, "component");
3849
+ return `// @ts-check
3850
+ /**
2050
3851
  * ${component.displayName} component
2051
3852
  * Category: ${component.category}
2052
3853
  * ${component.description || ""}
2053
3854
  * Generated by strapi2front
2054
3855
  */
2055
3856
 
2056
- ${imports}
2057
-
2058
- export interface ${typeName} {
2059
- id: number;
3857
+ /**
3858
+ * @typedef {Object} ${typeName}
3859
+ * @property {number} id
2060
3860
  ${attributes}
2061
- }
3861
+ */
3862
+
3863
+ module.exports = {};
2062
3864
  `;
2063
3865
  }
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) {
3866
+ function generateJSDocAttributes(attributes, schema, context) {
2135
3867
  const lines = [];
3868
+ const relativePrefix = context === "component" ? ".." : "../..";
2136
3869
  for (const [name, attr] of Object.entries(attributes)) {
2137
- const tsType = attributeToTsType2(attr);
2138
- const optional = attr.required ? "" : "?";
2139
- lines.push(` ${name}${optional}: ${tsType};`);
3870
+ const jsType = attributeToJSDocType(attr, schema, relativePrefix);
3871
+ const optional = attr.required ? "" : "[";
3872
+ const optionalEnd = attr.required ? "" : "]";
3873
+ lines.push(` * @property {${jsType}} ${optional}${name}${optionalEnd}`);
2140
3874
  }
2141
3875
  return lines.join("\n");
2142
3876
  }
2143
- function attributeToTsType2(attr) {
3877
+ function attributeToJSDocType(attr, schema, relativePrefix) {
2144
3878
  switch (attr.type) {
2145
3879
  case "string":
2146
3880
  case "text":
@@ -2150,7 +3884,7 @@ function attributeToTsType2(attr) {
2150
3884
  case "uid":
2151
3885
  return "string";
2152
3886
  case "blocks":
2153
- return "BlocksContent";
3887
+ return "Array<Object>";
2154
3888
  case "integer":
2155
3889
  case "biginteger":
2156
3890
  case "float":
@@ -2164,130 +3898,116 @@ function attributeToTsType2(attr) {
2164
3898
  case "timestamp":
2165
3899
  return "string";
2166
3900
  case "json":
2167
- return "unknown";
3901
+ return "Object";
2168
3902
  case "enumeration":
2169
3903
  if ("enum" in attr && attr.enum) {
2170
- return attr.enum.map((v) => `'${v}'`).join(" | ");
3904
+ return attr.enum.map((v) => `'${v}'`).join("|");
2171
3905
  }
2172
3906
  return "string";
2173
3907
  case "media":
2174
3908
  if ("multiple" in attr && attr.multiple) {
2175
- return "StrapiMedia[]";
3909
+ return `import("${relativePrefix}/shared/utils").StrapiMedia[]`;
2176
3910
  }
2177
- return "StrapiMedia | null";
3911
+ return `import("${relativePrefix}/shared/utils").StrapiMedia|null`;
2178
3912
  case "relation":
2179
3913
  if ("target" in attr && attr.target) {
2180
- const targetName = toPascalCase(attr.target.split(".").pop() || "unknown");
3914
+ const targetName = attr.target.split(".").pop() || "unknown";
3915
+ const typeName = toPascalCase(targetName);
3916
+ const fileName = toKebabCase(targetName);
2181
3917
  const isMany = attr.relation === "oneToMany" || attr.relation === "manyToMany";
2182
- return isMany ? `${targetName}[]` : `${targetName} | null`;
3918
+ const isCollection = schema.collections.some((c) => c.singularName === targetName);
3919
+ const isSingle = schema.singles.some((s) => s.singularName === targetName);
3920
+ let importPath;
3921
+ if (isCollection) {
3922
+ importPath = `${relativePrefix}/collections/${fileName}/types`;
3923
+ } else if (isSingle) {
3924
+ importPath = `${relativePrefix}/singles/${fileName}/types`;
3925
+ } else {
3926
+ importPath = `${relativePrefix}/collections/${fileName}/types`;
3927
+ }
3928
+ const importType = `import("${importPath}").${typeName}`;
3929
+ return isMany ? `${importType}[]` : `${importType}|null`;
2183
3930
  }
2184
- return "unknown";
3931
+ return "Object";
2185
3932
  case "component":
2186
3933
  if ("component" in attr && attr.component) {
2187
- const componentName = toPascalCase(attr.component.split(".").pop() || "unknown");
3934
+ const componentName = attr.component.split(".").pop() || "unknown";
3935
+ const typeName = toPascalCase(componentName);
3936
+ const fileName = toKebabCase(componentName);
3937
+ const importPath = `${relativePrefix}/components/${fileName}`;
3938
+ const importType = `import("${importPath}").${typeName}`;
2188
3939
  if ("repeatable" in attr && attr.repeatable) {
2189
- return `${componentName}[]`;
3940
+ return `${importType}[]`;
2190
3941
  }
2191
- return `${componentName} | null`;
3942
+ return `${importType}|null`;
2192
3943
  }
2193
- return "unknown";
3944
+ return "Object";
2194
3945
  case "dynamiczone":
2195
3946
  if ("components" in attr && attr.components) {
2196
- const types = attr.components.map((c) => toPascalCase(c.split(".").pop() || "unknown"));
2197
- return `(${types.join(" | ")})[]`;
3947
+ const types = attr.components.map((comp) => {
3948
+ const componentName = comp.split(".").pop() || "unknown";
3949
+ const typeName = toPascalCase(componentName);
3950
+ const fileName = toKebabCase(componentName);
3951
+ const importPath = `${relativePrefix}/components/${fileName}`;
3952
+ return `import("${importPath}").${typeName}`;
3953
+ });
3954
+ return `(${types.join("|")})[]`;
2198
3955
  }
2199
- return "unknown[]";
3956
+ return "Object[]";
2200
3957
  default:
2201
- return "unknown";
3958
+ return "Object";
2202
3959
  }
2203
3960
  }
2204
- function generateCollectionService2(collection, strapiVersion) {
3961
+ function generateCollectionServiceJSDoc(collection, strapiVersion) {
2205
3962
  const typeName = toPascalCase(collection.singularName);
2206
3963
  const serviceName = toCamelCase(collection.singularName) + "Service";
2207
3964
  const endpoint = collection.pluralName;
2208
3965
  const hasSlug = "slug" in collection.attributes;
2209
3966
  const { localized, draftAndPublish } = collection;
2210
3967
  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 `/**
3968
+ const idParam = isV4 ? "id" : "documentId";
3969
+ const idType = isV4 ? "number" : "string";
3970
+ return `// @ts-check
3971
+ /**
2268
3972
  * ${collection.displayName} Service
2269
3973
  * ${collection.description || ""}
2270
3974
  * Generated by strapi2front
2271
3975
  * Strapi version: ${strapiVersion}
2272
3976
  */
2273
3977
 
2274
- ${imports.join("\n")}
3978
+ const { collection } = require('../../shared/client');
2275
3979
 
2276
- export interface FindManyOptions {
2277
- ${findManyOptionsFields}
2278
- }
3980
+ /**
3981
+ * @typedef {Object} FindManyOptions
3982
+ * @property {import('./types').${typeName}Filters} [filters]
3983
+ * @property {Object} [pagination]
3984
+ * @property {number} [pagination.page]
3985
+ * @property {number} [pagination.pageSize]
3986
+ * @property {number} [pagination.start]
3987
+ * @property {number} [pagination.limit]
3988
+ * @property {string|string[]} [sort]
3989
+ * @property {string|string[]|Record<string, unknown>} [populate]${localized ? "\n * @property {string} [locale]" : ""}${draftAndPublish ? "\n * @property {'draft'|'published'} [status]" : ""}
3990
+ */
2279
3991
 
2280
- export interface FindOneOptions {
2281
- ${findOneOptionsFields}
2282
- }
3992
+ /**
3993
+ * @typedef {Object} FindOneOptions
3994
+ * @property {string|string[]|Record<string, unknown>} [populate]${localized ? "\n * @property {string} [locale]" : ""}${draftAndPublish ? "\n * @property {'draft'|'published'} [status]" : ""}
3995
+ */
2283
3996
 
2284
- // Create typed collection helper
2285
- const ${toCamelCase(collection.singularName)}Collection = collection<${typeName}>('${endpoint}');
3997
+ /** @type {ReturnType<typeof collection<import('./types').${typeName}>>} */
3998
+ const ${toCamelCase(collection.singularName)}Collection = collection('${endpoint}');
2286
3999
 
2287
- export const ${serviceName} = {
2288
- async findMany(options: FindManyOptions = {}): Promise<{ data: ${typeName}[]; pagination: StrapiPagination }> {
4000
+ const ${serviceName} = {
4001
+ /**
4002
+ * @param {FindManyOptions} [options]
4003
+ * @returns {Promise<{ data: import('./types').${typeName}[], pagination: import('../../shared/utils').StrapiPagination }>}
4004
+ */
4005
+ async findMany(options = {}) {
2289
4006
  const response = await ${toCamelCase(collection.singularName)}Collection.find({
2290
- ${findParams}
4007
+ filters: options.filters,
4008
+ pagination: options.pagination,
4009
+ sort: options.sort,
4010
+ populate: options.populate,${localized ? "\n locale: options.locale," : ""}${draftAndPublish ? "\n status: options.status," : ""}
2291
4011
  });
2292
4012
 
2293
4013
  return {
@@ -2296,8 +4016,13 @@ ${findParams}
2296
4016
  };
2297
4017
  },
2298
4018
 
2299
- async findAll(options: Omit<FindManyOptions, 'pagination'> = {}): Promise<${typeName}[]> {
2300
- const allItems: ${typeName}[] = [];
4019
+ /**
4020
+ * @param {Omit<FindManyOptions, 'pagination'>} [options]
4021
+ * @returns {Promise<import('./types').${typeName}[]>}
4022
+ */
4023
+ async findAll(options = {}) {
4024
+ /** @type {import('./types').${typeName}[]} */
4025
+ const allItems = [];
2301
4026
  let page = 1;
2302
4027
  let hasMore = true;
2303
4028
 
@@ -2315,10 +4040,15 @@ ${findParams}
2315
4040
  return allItems;
2316
4041
  },
2317
4042
 
2318
- async findOne(${idParam}, options: FindOneOptions = {}): Promise<${typeName} | null> {
4043
+ /**
4044
+ * @param {${idType}} ${idParam}
4045
+ * @param {FindOneOptions} [options]
4046
+ * @returns {Promise<import('./types').${typeName}|null>}
4047
+ */
4048
+ async findOne(${idParam}, options = {}) {
2319
4049
  try {
2320
- const response = await ${toCamelCase(collection.singularName)}Collection.findOne(${idName}, {
2321
- ${findOneParams}
4050
+ const response = await ${toCamelCase(collection.singularName)}Collection.findOne(${idParam}, {
4051
+ populate: options.populate,${localized ? "\n locale: options.locale," : ""}${draftAndPublish ? "\n status: options.status," : ""}
2322
4052
  });
2323
4053
 
2324
4054
  return response.data;
@@ -2330,9 +4060,14 @@ ${findOneParams}
2330
4060
  }
2331
4061
  },
2332
4062
  ${hasSlug ? `
2333
- async findBySlug(slug: string, options: FindOneOptions = {}): Promise<${typeName} | null> {
4063
+ /**
4064
+ * @param {string} slug
4065
+ * @param {FindOneOptions} [options]
4066
+ * @returns {Promise<import('./types').${typeName}|null>}
4067
+ */
4068
+ async findBySlug(slug, options = {}) {
2334
4069
  const { data } = await this.findMany({
2335
- filters: { slug: { $eq: slug } } as ${typeName}Filters,
4070
+ filters: /** @type {import('./types').${typeName}Filters} */ ({ slug: { $eq: slug } }),
2336
4071
  pagination: { pageSize: 1 },
2337
4072
  populate: options.populate,${localized ? "\n locale: options.locale," : ""}${draftAndPublish ? "\n status: options.status," : ""}
2338
4073
  });
@@ -2340,21 +4075,38 @@ ${hasSlug ? `
2340
4075
  return data[0] || null;
2341
4076
  },
2342
4077
  ` : ""}
2343
- async create(data: Partial<Omit<${typeName}, ${omitFields}>>): Promise<${typeName}> {
4078
+ /**
4079
+ * @param {Partial<import('./types').${typeName}>} data
4080
+ * @returns {Promise<import('./types').${typeName}>}
4081
+ */
4082
+ async create(data) {
2344
4083
  const response = await ${toCamelCase(collection.singularName)}Collection.create({ data });
2345
4084
  return response.data;
2346
4085
  },
2347
4086
 
2348
- async update(${idParam}, data: Partial<Omit<${typeName}, ${omitFieldsUpdate}>>): Promise<${typeName}> {
2349
- const response = await ${toCamelCase(collection.singularName)}Collection.update(${idName}, { data });
4087
+ /**
4088
+ * @param {${idType}} ${idParam}
4089
+ * @param {Partial<import('./types').${typeName}>} data
4090
+ * @returns {Promise<import('./types').${typeName}>}
4091
+ */
4092
+ async update(${idParam}, data) {
4093
+ const response = await ${toCamelCase(collection.singularName)}Collection.update(${idParam}, { data });
2350
4094
  return response.data;
2351
4095
  },
2352
4096
 
2353
- async delete(${idParam}): Promise<void> {
2354
- await ${toCamelCase(collection.singularName)}Collection.delete(${idName});
4097
+ /**
4098
+ * @param {${idType}} ${idParam}
4099
+ * @returns {Promise<void>}
4100
+ */
4101
+ async delete(${idParam}) {
4102
+ await ${toCamelCase(collection.singularName)}Collection.delete(${idParam});
2355
4103
  },
2356
4104
 
2357
- async count(filters?: ${typeName}Filters): Promise<number> {
4105
+ /**
4106
+ * @param {import('./types').${typeName}Filters} [filters]
4107
+ * @returns {Promise<number>}
4108
+ */
4109
+ async count(filters) {
2358
4110
  const { pagination } = await this.findMany({
2359
4111
  filters,
2360
4112
  pagination: { pageSize: 1 },
@@ -2363,61 +4115,42 @@ ${hasSlug ? `
2363
4115
  return pagination.total;
2364
4116
  },
2365
4117
  };
4118
+
4119
+ module.exports = { ${serviceName} };
2366
4120
  `;
2367
4121
  }
2368
- function generateSingleService2(single, strapiVersion) {
4122
+ function generateSingleServiceJSDoc(single, strapiVersion) {
2369
4123
  const typeName = toPascalCase(single.singularName);
2370
4124
  const serviceName = toCamelCase(single.singularName) + "Service";
2371
4125
  const endpoint = single.singularName;
2372
4126
  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 `/**
4127
+ return `// @ts-check
4128
+ /**
2401
4129
  * ${single.displayName} Service (Single Type)
2402
4130
  * ${single.description || ""}
2403
4131
  * Generated by strapi2front
2404
4132
  * Strapi version: ${strapiVersion}
2405
4133
  */
2406
4134
 
2407
- ${imports.join("\n")}
4135
+ const { single } = require('../../shared/client');
2408
4136
 
2409
- export interface FindOptions {
2410
- ${findOptionsFields}
2411
- }
4137
+ /**
4138
+ * @typedef {Object} FindOptions
4139
+ * @property {string|string[]|Record<string, unknown>} [populate]${localized ? "\n * @property {string} [locale]" : ""}${draftAndPublish ? "\n * @property {'draft'|'published'} [status]" : ""}
4140
+ */
2412
4141
 
2413
- // Create typed single helper
2414
- const ${toCamelCase(single.singularName)}Single = single<${typeName}>('${endpoint}');
4142
+ /** @type {ReturnType<typeof single<import('./types').${typeName}>>} */
4143
+ const ${toCamelCase(single.singularName)}Single = single('${endpoint}');
2415
4144
 
2416
- export const ${serviceName} = {
2417
- async find(options: FindOptions = {}): Promise<${typeName} | null> {
4145
+ const ${serviceName} = {
4146
+ /**
4147
+ * @param {FindOptions} [options]
4148
+ * @returns {Promise<import('./types').${typeName}|null>}
4149
+ */
4150
+ async find(options = {}) {
2418
4151
  try {
2419
4152
  const response = await ${toCamelCase(single.singularName)}Single.find({
2420
- ${findParams}
4153
+ populate: options.populate,${localized ? "\n locale: options.locale," : ""}${draftAndPublish ? "\n status: options.status," : ""}
2421
4154
  });
2422
4155
 
2423
4156
  return response.data;
@@ -2429,96 +4162,60 @@ ${findParams}
2429
4162
  }
2430
4163
  },
2431
4164
 
2432
- async update(data: Partial<Omit<${typeName}, ${omitFields}>>): Promise<${typeName}> {
4165
+ /**
4166
+ * @param {Partial<import('./types').${typeName}>} data
4167
+ * @returns {Promise<import('./types').${typeName}>}
4168
+ */
4169
+ async update(data) {
2433
4170
  const response = await ${toCamelCase(single.singularName)}Single.update({ data });
2434
4171
  return response.data;
2435
4172
  },
2436
4173
 
2437
- async delete(): Promise<void> {
4174
+ /**
4175
+ * @returns {Promise<void>}
4176
+ */
4177
+ async delete() {
2438
4178
  await ${toCamelCase(single.singularName)}Single.delete();
2439
4179
  },
2440
4180
  };
4181
+
4182
+ module.exports = { ${serviceName} };
2441
4183
  `;
2442
4184
  }
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
4185
 
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
- }),
4186
+ // src/frameworks/nextjs/actions.ts
4187
+ async function generateNextJsActions(_schema, _options) {
4188
+ throw new Error(
4189
+ "Next.js Server Actions generator is not yet implemented. Coming soon! For now, you can use the generated services directly."
4190
+ );
4191
+ }
4192
+ function isNextJsActionsSupported(nextVersion) {
4193
+ if (!nextVersion) return false;
4194
+ const match = nextVersion.replace(/^[\^~]/, "").match(/^(\d+)/);
4195
+ const majorVersion = match ? parseInt(match[1], 10) : null;
4196
+ return majorVersion !== null && majorVersion >= 14;
4197
+ }
2497
4198
 
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
- }),
4199
+ // src/frameworks/nuxt/server-routes.ts
4200
+ async function generateNuxtServerRoutes(_schema, _options) {
4201
+ throw new Error(
4202
+ "Nuxt Server Routes generator is not yet implemented. Coming soon! For now, you can use the generated services directly."
4203
+ );
4204
+ }
4205
+ function isNuxtServerRoutesSupported(nuxtVersion) {
4206
+ if (!nuxtVersion) return false;
4207
+ const match = nuxtVersion.replace(/^[\^~]/, "").match(/^(\d+)/);
4208
+ const majorVersion = match ? parseInt(match[1], 10) : null;
4209
+ return majorVersion !== null && majorVersion >= 3;
4210
+ }
2508
4211
 
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
- }),
4212
+ // src/frameworks/index.ts
4213
+ var frameworkSupport = {
4214
+ astro: { status: "stable", minVersion: "4.0.0" },
4215
+ nextjs: { status: "coming-soon", minVersion: "14.0.0" },
4216
+ nuxt: { status: "coming-soon", minVersion: "3.0.0" }
2518
4217
  };
2519
- `;
2520
- }
2521
4218
 
2522
- export { deleteFile, ensureDir, fileExists, formatCode, formatJson, generateActions, generateByFeature, generateClient, generateLocales, generateServices, generateTypes, listFiles, pluralize, readFile, toCamelCase, toKebabCase, toPascalCase, writeFile };
4219
+ 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
4220
  //# sourceMappingURL=index.js.map
2524
4221
  //# sourceMappingURL=index.js.map