prismic-ts-codegen 0.0.2

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.
@@ -0,0 +1,65 @@
1
+ import { Project } from "ts-morph";
2
+ import type { CustomTypeModel, SharedSliceModel } from "@prismicio/types";
3
+
4
+ import { BLANK_LINE_IDENTIFIER, NON_EDITABLE_FILE_HEADER } from "./constants";
5
+ import { addTypeAliasForCustomType } from "./lib/addTypeAliasForCustomType";
6
+ import { addTypeAliasForSharedSlice } from "./lib/addTypeAliasForSharedSlice";
7
+ import { getSourceFileText } from "./lib/getSourceFileText";
8
+ import { FieldConfigs } from "./types";
9
+
10
+ export type GenerateTypesConfig = {
11
+ customTypeModels?: CustomTypeModel[];
12
+ sharedSliceModels?: SharedSliceModel[];
13
+ localeIDs?: string[];
14
+ fieldConfigs?: FieldConfigs;
15
+ };
16
+
17
+ export const generateTypes = (config: GenerateTypesConfig = {}) => {
18
+ const project = new Project({
19
+ useInMemoryFileSystem: true,
20
+ });
21
+
22
+ const sourceFile = project.createSourceFile("types.d.ts");
23
+
24
+ sourceFile.addImportDeclaration({
25
+ moduleSpecifier: "@prismicio/types",
26
+ namespaceImport: "prismicT",
27
+ isTypeOnly: true,
28
+ leadingTrivia: NON_EDITABLE_FILE_HEADER,
29
+ });
30
+
31
+ sourceFile.addStatements(BLANK_LINE_IDENTIFIER);
32
+
33
+ sourceFile.addTypeAlias({
34
+ name: "Simplify",
35
+ typeParameters: [
36
+ {
37
+ name: "T",
38
+ },
39
+ ],
40
+ type: `{ [KeyType in keyof T]: T[KeyType] }`,
41
+ });
42
+
43
+ if (config.customTypeModels) {
44
+ for (const model of config.customTypeModels) {
45
+ addTypeAliasForCustomType({
46
+ model,
47
+ sourceFile,
48
+ localeIDs: config.localeIDs || [],
49
+ fieldConfigs: config.fieldConfigs || {},
50
+ });
51
+ }
52
+ }
53
+
54
+ if (config.sharedSliceModels) {
55
+ for (const model of config.sharedSliceModels) {
56
+ addTypeAliasForSharedSlice({
57
+ model,
58
+ sourceFile,
59
+ fieldConfigs: config.fieldConfigs || {},
60
+ });
61
+ }
62
+ }
63
+
64
+ return getSourceFileText(sourceFile);
65
+ };
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { generateTypes } from "./generateTypes";
2
+ export type { GenerateTypesConfig } from "./generateTypes";
3
+
4
+ // The CLI configuration type is exported at the root level for ease of access.
5
+ // It is not used in the the library's code.
6
+ export type { Config } from "./cli/types";
@@ -0,0 +1,595 @@
1
+ import type {
2
+ CustomTypeModel,
3
+ CustomTypeModelField,
4
+ CustomTypeModelSlice,
5
+ SharedSliceModel,
6
+ } from "@prismicio/types";
7
+ import {
8
+ CustomTypeModelSliceType,
9
+ CustomTypeModelLinkSelectType,
10
+ } from "@prismicio/types";
11
+ import type { InterfaceDeclaration, SourceFile } from "ts-morph";
12
+
13
+ import { FieldConfigs, PathElement } from "../types";
14
+ import { buildFieldDocs } from "./buildFieldDocs";
15
+ import { buildSharedSliceInterfaceName } from "./buildSharedSliceInterfaceName";
16
+ import { getHumanReadableFieldPath } from "./getHumanReadableFieldPath";
17
+ import { pascalCase } from "./pascalCase";
18
+
19
+ type AddInterfacePropertyFromFieldConfig = {
20
+ id: string;
21
+ model: CustomTypeModelField;
22
+ interface: InterfaceDeclaration;
23
+ sourceFile: SourceFile;
24
+ path: [
25
+ PathElement<CustomTypeModel | SharedSliceModel>,
26
+ ...PathElement<CustomTypeModelField | CustomTypeModelSlice>[]
27
+ ];
28
+ fieldConfigs: FieldConfigs;
29
+ };
30
+
31
+ const addInterfacePropertyFromField = (
32
+ config: AddInterfacePropertyFromFieldConfig,
33
+ ) => {
34
+ switch (config.model.type) {
35
+ case "UID": {
36
+ // UID fields are not included in Data.
37
+ break;
38
+ }
39
+
40
+ case "Boolean": {
41
+ config.interface.addProperty({
42
+ name: config.id,
43
+ type: "prismicT.BooleanField",
44
+ docs: buildFieldDocs({
45
+ id: config.id,
46
+ model: config.model,
47
+ path: config.path,
48
+ }),
49
+ });
50
+
51
+ break;
52
+ }
53
+
54
+ case "Color": {
55
+ config.interface.addProperty({
56
+ name: config.id,
57
+ type: "prismicT.ColorField",
58
+ docs: buildFieldDocs({
59
+ id: config.id,
60
+ model: config.model,
61
+ path: config.path,
62
+ }),
63
+ });
64
+
65
+ break;
66
+ }
67
+
68
+ case "Date": {
69
+ config.interface.addProperty({
70
+ name: config.id,
71
+ type: "prismicT.DateField",
72
+ docs: buildFieldDocs({
73
+ id: config.id,
74
+ model: config.model,
75
+ path: config.path,
76
+ }),
77
+ });
78
+
79
+ break;
80
+ }
81
+
82
+ case "Embed": {
83
+ const providerTypes: string[] = [];
84
+
85
+ if (config.fieldConfigs.embed?.providerTypes) {
86
+ for (const providerType in config.fieldConfigs.embed?.providerTypes) {
87
+ const configuredProviderType =
88
+ config.fieldConfigs.embed?.providerTypes[providerType];
89
+
90
+ providerTypes.push(
91
+ `({ provider_name: "${providerType}" } & ${configuredProviderType})`,
92
+ );
93
+ }
94
+ }
95
+
96
+ config.interface.addProperty({
97
+ name: config.id,
98
+ type:
99
+ providerTypes.length > 0
100
+ ? `prismicT.EmbedField<prismicT.AnyOEmbed & prismicT.OEmbedExtra & (${providerTypes.join(
101
+ " | ",
102
+ )})>`
103
+ : "prismicT.EmbedField",
104
+ docs: buildFieldDocs({
105
+ id: config.id,
106
+ model: config.model,
107
+ path: config.path,
108
+ }),
109
+ });
110
+
111
+ break;
112
+ }
113
+
114
+ case "GeoPoint": {
115
+ config.interface.addProperty({
116
+ name: config.id,
117
+ type: "prismicT.GeoPointField",
118
+ docs: buildFieldDocs({
119
+ id: config.id,
120
+ model: config.model,
121
+ path: config.path,
122
+ }),
123
+ });
124
+
125
+ break;
126
+ }
127
+
128
+ case "Image": {
129
+ if (config.model.config.thumbnails.length > 0) {
130
+ const thumbnailNames = config.model.config.thumbnails
131
+ .map((thumbnail) => `"${thumbnail.name}"`)
132
+ .join(" | ");
133
+
134
+ config.interface.addProperty({
135
+ name: config.id,
136
+ type: `prismicT.ImageField<${thumbnailNames}>`,
137
+ docs: buildFieldDocs({
138
+ id: config.id,
139
+ model: config.model,
140
+ path: config.path,
141
+ }),
142
+ });
143
+ } else {
144
+ config.interface.addProperty({
145
+ name: config.id,
146
+ type: "prismicT.ImageField<null>",
147
+ docs: buildFieldDocs({
148
+ id: config.id,
149
+ model: config.model,
150
+ path: config.path,
151
+ }),
152
+ });
153
+ }
154
+
155
+ break;
156
+ }
157
+
158
+ case "IntegrationFields": {
159
+ const catalogType =
160
+ config.fieldConfigs.integrationFields?.catalogTypes?.[
161
+ config.model.config.catalog
162
+ ];
163
+
164
+ config.interface.addProperty({
165
+ name: config.id,
166
+ type: catalogType
167
+ ? `prismicT.IntegrationFields<${catalogType}>`
168
+ : "prismicT.IntegrationFields",
169
+ docs: buildFieldDocs({
170
+ id: config.id,
171
+ model: config.model,
172
+ path: config.path,
173
+ }),
174
+ });
175
+
176
+ break;
177
+ }
178
+
179
+ case "Link": {
180
+ switch (config.model.config.select) {
181
+ case CustomTypeModelLinkSelectType.Document: {
182
+ config.interface.addProperty({
183
+ name: config.id,
184
+ type:
185
+ config.model.config.customtypes &&
186
+ config.model.config.customtypes.length > 0
187
+ ? `prismicT.RelationField<${config.model.config.customtypes
188
+ .map((type) => `"${type}"`)
189
+ .join(" | ")}>`
190
+ : "prismicT.RelationField",
191
+ docs: buildFieldDocs({
192
+ id: config.id,
193
+ model: config.model,
194
+ path: config.path,
195
+ }),
196
+ });
197
+
198
+ break;
199
+ }
200
+
201
+ case CustomTypeModelLinkSelectType.Media: {
202
+ config.interface.addProperty({
203
+ name: config.id,
204
+ type: "prismicT.LinkToMediaField",
205
+ docs: buildFieldDocs({
206
+ id: config.id,
207
+ model: config.model,
208
+ path: config.path,
209
+ }),
210
+ });
211
+
212
+ break;
213
+ }
214
+
215
+ default: {
216
+ config.interface.addProperty({
217
+ name: config.id,
218
+ type: "prismicT.LinkField",
219
+ docs: buildFieldDocs({
220
+ id: config.id,
221
+ model: config.model,
222
+ path: config.path,
223
+ }),
224
+ });
225
+ }
226
+ }
227
+
228
+ break;
229
+ }
230
+
231
+ case "Number": {
232
+ config.interface.addProperty({
233
+ name: config.id,
234
+ type: "prismicT.NumberField",
235
+ docs: buildFieldDocs({
236
+ id: config.id,
237
+ model: config.model,
238
+ path: config.path,
239
+ }),
240
+ });
241
+
242
+ break;
243
+ }
244
+
245
+ case "StructuredText": {
246
+ const isTitleField =
247
+ "single" in config.model.config &&
248
+ config.model.config.single
249
+ .split(",")
250
+ .every((blockType) => /heading/.test(blockType));
251
+
252
+ if (isTitleField) {
253
+ config.interface.addProperty({
254
+ name: config.id,
255
+ type: "prismicT.TitleField",
256
+ docs: buildFieldDocs({
257
+ id: config.id,
258
+ model: config.model,
259
+ path: config.path,
260
+ }),
261
+ });
262
+ } else {
263
+ config.interface.addProperty({
264
+ name: config.id,
265
+ type: "prismicT.RichTextField",
266
+ docs: buildFieldDocs({
267
+ id: config.id,
268
+ model: config.model,
269
+ path: config.path,
270
+ }),
271
+ });
272
+ }
273
+
274
+ break;
275
+ }
276
+
277
+ case "Select": {
278
+ const options = config.model.config.options
279
+ .map((option) => `"${option}"`)
280
+ .join(" | ");
281
+ const hasDefault = Boolean(config.model.config.default_value);
282
+
283
+ if (hasDefault) {
284
+ config.interface.addProperty({
285
+ name: config.id,
286
+ type: `prismicT.SelectField<${options}, "filled">`,
287
+ docs: buildFieldDocs({
288
+ id: config.id,
289
+ model: config.model,
290
+ path: config.path,
291
+ }),
292
+ });
293
+ } else {
294
+ config.interface.addProperty({
295
+ name: config.id,
296
+ type: `prismicT.SelectField<${options}>`,
297
+ docs: buildFieldDocs({
298
+ id: config.id,
299
+ model: config.model,
300
+ path: config.path,
301
+ }),
302
+ });
303
+ }
304
+
305
+ break;
306
+ }
307
+
308
+ case "Text": {
309
+ config.interface.addProperty({
310
+ name: config.id,
311
+ type: "prismicT.KeyTextField",
312
+ docs: buildFieldDocs({
313
+ id: config.id,
314
+ model: config.model,
315
+ path: config.path,
316
+ }),
317
+ });
318
+
319
+ break;
320
+ }
321
+
322
+ case "Timestamp": {
323
+ config.interface.addProperty({
324
+ name: config.id,
325
+ type: "prismicT.TimestampField",
326
+ docs: buildFieldDocs({
327
+ id: config.id,
328
+ model: config.model,
329
+ path: config.path,
330
+ }),
331
+ });
332
+
333
+ break;
334
+ }
335
+
336
+ case "Group": {
337
+ const itemInterface = config.sourceFile.addInterface({
338
+ name: pascalCase(
339
+ `${config.path[0].id} Document Data ${config.id} Item`,
340
+ ),
341
+ docs: [
342
+ {
343
+ description: (writer) => {
344
+ const humanReadablePath = getHumanReadableFieldPath({
345
+ path: [
346
+ ...config.path,
347
+ {
348
+ id: config.id,
349
+ model: config.model,
350
+ },
351
+ ],
352
+ });
353
+
354
+ writer.writeLine(`Item in ${humanReadablePath}`);
355
+ },
356
+ },
357
+ ],
358
+ });
359
+ addInterfacePropertiesForFields({
360
+ interface: itemInterface,
361
+ sourceFile: config.sourceFile,
362
+ fields: config.model.config.fields,
363
+ path: [
364
+ ...config.path,
365
+ {
366
+ id: config.id,
367
+ model: config.model,
368
+ },
369
+ ],
370
+ fieldConfigs: config.fieldConfigs,
371
+ });
372
+
373
+ config.interface.addProperty({
374
+ name: config.id,
375
+ type: `prismicT.GroupField<Simplify<${itemInterface.getName()}>>`,
376
+ docs: buildFieldDocs({
377
+ id: config.id,
378
+ model: config.model,
379
+ path: config.path,
380
+ }),
381
+ });
382
+
383
+ break;
384
+ }
385
+
386
+ case "Slices": {
387
+ const choiceInterfaceNames: string[] = [];
388
+
389
+ for (const choiceId in config.model.config.choices) {
390
+ const choice = config.model.config.choices[choiceId];
391
+
392
+ if (choice.type === CustomTypeModelSliceType.SharedSlice) {
393
+ choiceInterfaceNames.push(
394
+ buildSharedSliceInterfaceName({ id: choiceId }),
395
+ );
396
+ } else if (choice.type === CustomTypeModelSliceType.Slice) {
397
+ let primaryInterface: InterfaceDeclaration | undefined;
398
+ if (Object.keys(choice["non-repeat"]).length > 0) {
399
+ primaryInterface = config.sourceFile.addInterface({
400
+ name: pascalCase(
401
+ `${config.path[0].id} Document Data ${config.id} ${choiceId} Slice Primary`,
402
+ ),
403
+ docs: [
404
+ {
405
+ description: (writer) => {
406
+ const humanReadablePath = getHumanReadableFieldPath({
407
+ path: [
408
+ ...config.path,
409
+ {
410
+ id: config.id,
411
+ model: config.model,
412
+ },
413
+ {
414
+ id: choiceId,
415
+ model: choice,
416
+ },
417
+ {
418
+ id: "primary",
419
+ label: "Primary",
420
+ },
421
+ ],
422
+ });
423
+
424
+ writer.writeLine(`Primary content in ${humanReadablePath}`);
425
+ },
426
+ },
427
+ ],
428
+ });
429
+ addInterfacePropertiesForFields({
430
+ interface: primaryInterface,
431
+ sourceFile: config.sourceFile,
432
+ fields: choice["non-repeat"],
433
+ path: [
434
+ ...config.path,
435
+ {
436
+ id: config.id,
437
+ model: config.model,
438
+ },
439
+ {
440
+ id: choiceId,
441
+ model: choice,
442
+ },
443
+ {
444
+ id: "primary",
445
+ label: "Primary",
446
+ },
447
+ ],
448
+ fieldConfigs: config.fieldConfigs,
449
+ });
450
+ }
451
+
452
+ let itemInterface: InterfaceDeclaration | undefined;
453
+ if (Object.keys(choice.repeat).length > 0) {
454
+ itemInterface = config.sourceFile.addInterface({
455
+ name: pascalCase(
456
+ `${config.path[0].id} Document Data ${config.id} ${choiceId} Slice Item`,
457
+ ),
458
+ docs: [
459
+ {
460
+ description: (writer) => {
461
+ const humanReadablePath = getHumanReadableFieldPath({
462
+ path: [
463
+ ...config.path,
464
+ {
465
+ id: config.id,
466
+ model: config.model,
467
+ },
468
+ {
469
+ id: choiceId,
470
+ model: choice,
471
+ },
472
+ {
473
+ id: "items",
474
+ label: "Items",
475
+ },
476
+ ],
477
+ });
478
+
479
+ writer.writeLine(`Item in ${humanReadablePath}`);
480
+ },
481
+ },
482
+ ],
483
+ });
484
+ addInterfacePropertiesForFields({
485
+ interface: itemInterface,
486
+ sourceFile: config.sourceFile,
487
+ fields: choice.repeat,
488
+ path: [
489
+ ...config.path,
490
+ {
491
+ id: config.id,
492
+ model: config.model,
493
+ },
494
+ {
495
+ id: choiceId,
496
+ model: choice,
497
+ },
498
+ {
499
+ id: "items",
500
+ label: "Items",
501
+ },
502
+ ],
503
+ fieldConfigs: config.fieldConfigs,
504
+ });
505
+ }
506
+
507
+ const sliceType = config.sourceFile.addTypeAlias({
508
+ name: pascalCase(
509
+ `${config.path[0].id} Document Data ${config.id} ${choiceId} Slice`,
510
+ ),
511
+ type: `prismicT.Slice<"${choiceId}", ${
512
+ primaryInterface
513
+ ? `Simplify<${primaryInterface.getName()}>`
514
+ : "Record<string, never>"
515
+ }, ${
516
+ itemInterface ? `Simplify<${itemInterface.getName()}>` : "never"
517
+ }>`,
518
+ });
519
+
520
+ choiceInterfaceNames.push(sliceType.getName());
521
+ }
522
+ }
523
+
524
+ const slicesType = config.sourceFile.addTypeAlias({
525
+ name: pascalCase(
526
+ `${config.path[0].id} Document Data ${config.id} Slice`,
527
+ ),
528
+ type:
529
+ choiceInterfaceNames.length > 0
530
+ ? choiceInterfaceNames.join(" | ")
531
+ : "never",
532
+ docs: [
533
+ {
534
+ description: (writer) => {
535
+ const humanReadablePath = getHumanReadableFieldPath({
536
+ path: [
537
+ ...config.path,
538
+ {
539
+ id: config.id,
540
+ model: config.model,
541
+ },
542
+ ],
543
+ });
544
+
545
+ writer.writeLine(`Slice for *${humanReadablePath}*`);
546
+ },
547
+ },
548
+ ],
549
+ });
550
+
551
+ config.interface.addProperty({
552
+ name: config.id,
553
+ type: `prismicT.SliceZone<${slicesType.getName()}>`,
554
+ docs: buildFieldDocs({
555
+ id: config.id,
556
+ model: config.model,
557
+ path: config.path,
558
+ }),
559
+ });
560
+
561
+ break;
562
+ }
563
+
564
+ default: {
565
+ config.interface.addProperty({
566
+ name: config.id,
567
+ type: "unknown",
568
+ docs: buildFieldDocs({
569
+ id: config.id,
570
+ model: config.model,
571
+ path: config.path,
572
+ }),
573
+ });
574
+ }
575
+ }
576
+ };
577
+
578
+ type AddInterfacePropertiesForFieldsConfig = Omit<
579
+ AddInterfacePropertyFromFieldConfig,
580
+ "id" | "model"
581
+ > & {
582
+ fields: Record<string, AddInterfacePropertyFromFieldConfig["model"]>;
583
+ };
584
+
585
+ export const addInterfacePropertiesForFields = (
586
+ config: AddInterfacePropertiesForFieldsConfig,
587
+ ) => {
588
+ for (const name in config.fields) {
589
+ addInterfacePropertyFromField({
590
+ ...config,
591
+ id: name,
592
+ model: config.fields[name],
593
+ });
594
+ }
595
+ };