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,110 @@
1
+ import type { CustomTypeModel, CustomTypeModelField } from "@prismicio/types";
2
+ import type {
3
+ SourceFile,
4
+ TypeAliasDeclaration,
5
+ InterfaceDeclaration,
6
+ } from "ts-morph";
7
+ import { CUSTOM_TYPES_DOCUMENTATION_URL } from "../constants";
8
+ import { FieldConfigs } from "../types";
9
+
10
+ import { addInterfacePropertiesForFields } from "./addInterfacePropertiesForFields";
11
+ import { pascalCase } from "./pascalCase";
12
+
13
+ const collectCustomTypeFields = (
14
+ model: CustomTypeModel,
15
+ ): Record<string, CustomTypeModelField> => {
16
+ return Object.assign({}, ...Object.values(model.json));
17
+ };
18
+
19
+ type AddTypeAliasForCustomTypeConfig = {
20
+ model: CustomTypeModel;
21
+ sourceFile: SourceFile;
22
+ localeIDs: string[];
23
+ fieldConfigs: FieldConfigs;
24
+ };
25
+
26
+ export const addTypeAliasForCustomType = ({
27
+ model,
28
+ sourceFile,
29
+ localeIDs,
30
+ fieldConfigs,
31
+ }: AddTypeAliasForCustomTypeConfig): TypeAliasDeclaration => {
32
+ const { uid: uidField, ...fields } = collectCustomTypeFields(model);
33
+ const hasDataFields = Object.keys(fields).length > 0;
34
+ const hasUIDField = Boolean(uidField);
35
+
36
+ let dataInterface: InterfaceDeclaration | TypeAliasDeclaration;
37
+ if (hasDataFields) {
38
+ dataInterface = sourceFile.addInterface({
39
+ name: pascalCase(`${model.id} Document Data`),
40
+ docs: [
41
+ {
42
+ description: `Content for ${model.label} documents`,
43
+ },
44
+ ],
45
+ });
46
+ addInterfacePropertiesForFields({
47
+ fields,
48
+ interface: dataInterface,
49
+ sourceFile: sourceFile,
50
+ path: [
51
+ {
52
+ id: model.id,
53
+ model: model,
54
+ },
55
+ ],
56
+ fieldConfigs,
57
+ });
58
+ } else {
59
+ dataInterface = sourceFile.addTypeAlias({
60
+ name: pascalCase(`${model.id} Document Data`),
61
+ type: `Record<string, never>`,
62
+ docs: [
63
+ {
64
+ description: `Content for ${model.label} documents`,
65
+ },
66
+ ],
67
+ });
68
+ }
69
+
70
+ return sourceFile.addTypeAlias({
71
+ name: pascalCase(`${model.id} Document`),
72
+ typeParameters: [
73
+ {
74
+ name: "Lang",
75
+ constraint: "string",
76
+ default:
77
+ localeIDs.length > 0
78
+ ? localeIDs.map((localeID) => `"${localeID}"`).join(" | ")
79
+ : "string",
80
+ },
81
+ ],
82
+ type: hasUIDField
83
+ ? `prismicT.PrismicDocumentWithUID<Simplify<${dataInterface.getName()}>, "${
84
+ model.id
85
+ }", Lang>`
86
+ : `prismicT.PrismicDocumentWithoutUID<Simplify<${dataInterface.getName()}>, "${
87
+ model.id
88
+ }", Lang>`,
89
+ docs: [
90
+ {
91
+ description: (writer) => {
92
+ writer.writeLine(`${model.label} document from Prismic`);
93
+ writer.blankLine();
94
+ writer.writeLine(`- **API ID**: \`${model.id}\``);
95
+ writer.writeLine(`- **Repeatable**: \`${model.repeatable}\``);
96
+ writer.writeLine(
97
+ `- **Documentation**: ${CUSTOM_TYPES_DOCUMENTATION_URL}`,
98
+ );
99
+ },
100
+ tags: [
101
+ {
102
+ tagName: "typeParam",
103
+ text: "Lang - Language API ID of the document.",
104
+ },
105
+ ],
106
+ },
107
+ ],
108
+ isExported: true,
109
+ });
110
+ };
@@ -0,0 +1,200 @@
1
+ import type { SharedSliceModel } from "@prismicio/types";
2
+ import type {
3
+ InterfaceDeclaration,
4
+ SourceFile,
5
+ TypeAliasDeclaration,
6
+ } from "ts-morph";
7
+
8
+ import { addInterfacePropertiesForFields } from "./addInterfacePropertiesForFields";
9
+ import { buildSharedSliceInterfaceName } from "./buildSharedSliceInterfaceName";
10
+ import { pascalCase } from "./pascalCase";
11
+ import { getHumanReadableFieldPath } from "./getHumanReadableFieldPath";
12
+ import { SHARED_SLICES_DOCUMENTATION_URL } from "../constants";
13
+ import { FieldConfigs } from "../types";
14
+
15
+ type AddTypeAliasForSharedSliceConfig = {
16
+ model: SharedSliceModel;
17
+ sourceFile: SourceFile;
18
+ fieldConfigs: FieldConfigs;
19
+ };
20
+
21
+ export const addTypeAliasForSharedSlice = (
22
+ config: AddTypeAliasForSharedSliceConfig,
23
+ ): TypeAliasDeclaration => {
24
+ const variationTypeNames: string[] = [];
25
+
26
+ for (const variation of config.model.variations) {
27
+ let primaryInterface: InterfaceDeclaration | undefined;
28
+ if (Object.keys(variation.primary).length > 0) {
29
+ primaryInterface = config.sourceFile.addInterface({
30
+ name: pascalCase(
31
+ `${buildSharedSliceInterfaceName({ id: config.model.id })} ${
32
+ variation.id
33
+ } Primary`,
34
+ ),
35
+ docs: [
36
+ {
37
+ description: (writer) => {
38
+ const humanReadablePath = getHumanReadableFieldPath({
39
+ path: [
40
+ {
41
+ id: config.model.id,
42
+ model: config.model,
43
+ },
44
+ {
45
+ id: "primary",
46
+ label: "Primary",
47
+ },
48
+ ],
49
+ });
50
+
51
+ writer.writeLine(`Primary content in ${humanReadablePath}`);
52
+ },
53
+ },
54
+ ],
55
+ });
56
+ addInterfacePropertiesForFields({
57
+ interface: primaryInterface,
58
+ sourceFile: config.sourceFile,
59
+ fields: variation.primary,
60
+ path: [
61
+ {
62
+ id: config.model.id,
63
+ model: config.model,
64
+ },
65
+ {
66
+ id: "primary",
67
+ label: "Primary",
68
+ },
69
+ ],
70
+ fieldConfigs: config.fieldConfigs,
71
+ });
72
+ }
73
+
74
+ let itemInterface: InterfaceDeclaration | undefined;
75
+ if (Object.keys(variation.items).length > 0) {
76
+ itemInterface = config.sourceFile.addInterface({
77
+ name: pascalCase(
78
+ `${buildSharedSliceInterfaceName({ id: config.model.id })} ${
79
+ variation.id
80
+ } Item`,
81
+ ),
82
+ docs: [
83
+ {
84
+ description: (writer) => {
85
+ const humanReadablePath = getHumanReadableFieldPath({
86
+ path: [
87
+ {
88
+ id: config.model.id,
89
+ model: config.model,
90
+ },
91
+ {
92
+ id: "items",
93
+ label: "Items",
94
+ },
95
+ ],
96
+ });
97
+
98
+ writer.writeLine(`Item in ${humanReadablePath}`);
99
+ },
100
+ },
101
+ ],
102
+ });
103
+ addInterfacePropertiesForFields({
104
+ interface: itemInterface,
105
+ sourceFile: config.sourceFile,
106
+ fields: variation.items,
107
+ path: [
108
+ {
109
+ id: config.model.id,
110
+ model: config.model,
111
+ },
112
+ {
113
+ id: "items",
114
+ label: "Items",
115
+ },
116
+ ],
117
+ fieldConfigs: config.fieldConfigs,
118
+ });
119
+ }
120
+
121
+ const variationType = config.sourceFile.addTypeAlias({
122
+ name: pascalCase(
123
+ `${buildSharedSliceInterfaceName({
124
+ id: config.model.id,
125
+ })} ${variation.id}`,
126
+ ),
127
+ type: `prismicT.SharedSliceVariation<"${variation.id}", ${
128
+ primaryInterface
129
+ ? `Simplify<${primaryInterface.getName()}>`
130
+ : "Record<string, never>"
131
+ }, ${itemInterface ? `Simplify<${itemInterface.getName()}>` : "never"}>`,
132
+ docs: [
133
+ {
134
+ description: (writer) => {
135
+ writer.writeLine(
136
+ `${variation.name} variation for ${config.model.name} Slice`,
137
+ );
138
+ writer.blankLine();
139
+ writer.writeLine(`- **API ID**: \`${variation.id}\``);
140
+ writer.writeLine(`- **Description**: \`${variation.description}\``);
141
+ writer.writeLine(
142
+ `- **Documentation**: ${SHARED_SLICES_DOCUMENTATION_URL}`,
143
+ );
144
+ },
145
+ },
146
+ ],
147
+ });
148
+
149
+ variationTypeNames.push(variationType.getName());
150
+ }
151
+
152
+ const variationsType = config.sourceFile.addTypeAlias({
153
+ name: pascalCase(`${config.model.id} Slice Variation`),
154
+ type:
155
+ variationTypeNames.length > 0 ? variationTypeNames.join(" | ") : "never",
156
+ docs: [
157
+ {
158
+ description: (writer) => {
159
+ const humanReadablePath = getHumanReadableFieldPath({
160
+ path: [
161
+ {
162
+ id: config.model.id,
163
+ model: config.model,
164
+ },
165
+ ],
166
+ });
167
+
168
+ writer.writeLine(`Slice variation for *${humanReadablePath}*`);
169
+ },
170
+ },
171
+ ],
172
+ });
173
+
174
+ return config.sourceFile.addTypeAlias({
175
+ name: pascalCase(
176
+ buildSharedSliceInterfaceName({
177
+ id: config.model.id,
178
+ }),
179
+ ),
180
+ type: `prismicT.SharedSlice<"${
181
+ config.model.id
182
+ }", ${variationsType.getName()}>`,
183
+ docs: [
184
+ {
185
+ description: (writer) => {
186
+ writer.writeLine(`${config.model.name} Shared Slice`);
187
+ writer.blankLine();
188
+ writer.writeLine(`- **API ID**: \`${config.model.id}\``);
189
+ writer.writeLine(
190
+ `- **Description**: \`${config.model.description}\``,
191
+ );
192
+ writer.writeLine(
193
+ `- **Documentation**: ${SHARED_SLICES_DOCUMENTATION_URL}`,
194
+ );
195
+ },
196
+ },
197
+ ],
198
+ isExported: true,
199
+ });
200
+ };
@@ -0,0 +1,71 @@
1
+ import type {
2
+ CustomTypeModel,
3
+ CustomTypeModelField,
4
+ CustomTypeModelSlice,
5
+ SharedSliceModel,
6
+ } from "@prismicio/types";
7
+ import type { JSDocableNodeStructure } from "ts-morph";
8
+
9
+ import { PathElement } from "../types";
10
+ import { getAPIIDPath } from "./getAPIIDPath";
11
+ import { getFieldDocumentationURL } from "./getFieldDocumentationURL";
12
+ import { getHumanReadableFieldPath } from "./getHumanReadableFieldPath";
13
+ import { getHumanReadableFieldType } from "./getHumanReadableFieldType";
14
+ import { getHumanReadableModelName } from "./getHumanReadableModelName";
15
+ import { writeFieldDocsDescription } from "./writeFieldDocsDescription";
16
+
17
+ type BuildFieldDocsConfig = {
18
+ id: string;
19
+ model: CustomTypeModelField;
20
+ path: [
21
+ PathElement<CustomTypeModel | SharedSliceModel>,
22
+ ...PathElement<CustomTypeModelField | CustomTypeModelSlice>[]
23
+ ];
24
+ };
25
+
26
+ export const buildFieldDocs = (
27
+ config: BuildFieldDocsConfig,
28
+ ): NonNullable<JSDocableNodeStructure["docs"]> => {
29
+ return [
30
+ {
31
+ description: (writer) => {
32
+ const name = getHumanReadableModelName({
33
+ id: config.id,
34
+ model: config.model,
35
+ });
36
+ const humanReadablePath = getHumanReadableFieldPath({
37
+ path: config.path,
38
+ });
39
+
40
+ const fieldType = getHumanReadableFieldType({
41
+ model: config.model,
42
+ });
43
+
44
+ const placeholder =
45
+ "placeholder" in config.model.config &&
46
+ config.model.config.placeholder
47
+ ? config.model.config.placeholder
48
+ : `*None*`;
49
+
50
+ const defaultValue =
51
+ "default_value" in config.model.config
52
+ ? config.model.config.default_value
53
+ : undefined;
54
+
55
+ const apiIDPath = getAPIIDPath({
56
+ path: [...config.path, { id: config.id, model: config.model }],
57
+ });
58
+
59
+ writeFieldDocsDescription({
60
+ writer,
61
+ description: `${name} field in *${humanReadablePath}*`,
62
+ fieldType,
63
+ placeholder,
64
+ defaultValue,
65
+ apiIDPath,
66
+ documentationURL: getFieldDocumentationURL({ model: config.model }),
67
+ });
68
+ },
69
+ },
70
+ ];
71
+ };
@@ -0,0 +1,11 @@
1
+ import { pascalCase } from "./pascalCase";
2
+
3
+ type BuildSharedSliceInterfaceNameConfig = {
4
+ id: string;
5
+ };
6
+
7
+ export const buildSharedSliceInterfaceName = (
8
+ config: BuildSharedSliceInterfaceNameConfig,
9
+ ): string => {
10
+ return pascalCase(`${config.id} Slice`);
11
+ };
@@ -0,0 +1,46 @@
1
+ import type {
2
+ CustomTypeModel,
3
+ CustomTypeModelField,
4
+ CustomTypeModelSlice,
5
+ SharedSliceModel,
6
+ } from "@prismicio/types";
7
+ import { CustomTypeModelFieldType } from "@prismicio/types";
8
+
9
+ import { PathElement } from "../types";
10
+ import { isCustomTypeModelField } from "./isCustomTypeModelField";
11
+ import { isCustomTypeModelSlice } from "./isCustomTypeModelSlice";
12
+ import { isSharedSliceModel } from "./isSharedSliceModel";
13
+
14
+ type GetAPIIDPathConfig = {
15
+ path: [
16
+ PathElement<CustomTypeModel | SharedSliceModel>,
17
+ ...PathElement<CustomTypeModelField | CustomTypeModelSlice>[]
18
+ ];
19
+ };
20
+
21
+ export const getAPIIDPath = (config: GetAPIIDPathConfig): string => {
22
+ return config.path
23
+ .map((element, i) => {
24
+ if (
25
+ isCustomTypeModelField(element.model) &&
26
+ (element.model.type === CustomTypeModelFieldType.Group ||
27
+ element.model.type === CustomTypeModelFieldType.Slices)
28
+ ) {
29
+ return `${element.id}[]`;
30
+ } else {
31
+ if (element.id === "items") {
32
+ const previousElement = config.path[i - 1];
33
+
34
+ if (
35
+ isCustomTypeModelSlice(previousElement.model) ||
36
+ isSharedSliceModel(previousElement.model)
37
+ ) {
38
+ return `${element.id}[]`;
39
+ }
40
+ }
41
+
42
+ return element.id;
43
+ }
44
+ })
45
+ .join(".");
46
+ };
@@ -0,0 +1,13 @@
1
+ import type { CustomTypeModelField } from "@prismicio/types";
2
+
3
+ import { FIELD_DOCUMENTATION_URLS } from "../constants";
4
+
5
+ type GetFieldDocumentationURLConfig = {
6
+ model: CustomTypeModelField;
7
+ };
8
+
9
+ export const getFieldDocumentationURL = (
10
+ config: GetFieldDocumentationURLConfig,
11
+ ): string => {
12
+ return FIELD_DOCUMENTATION_URLS[config.model.type];
13
+ };
@@ -0,0 +1,35 @@
1
+ import type {
2
+ CustomTypeModel,
3
+ CustomTypeModelField,
4
+ CustomTypeModelSlice,
5
+ SharedSliceModel,
6
+ } from "@prismicio/types";
7
+
8
+ import { PathElement } from "../types";
9
+ import { getHumanReadableModelName } from "./getHumanReadableModelName";
10
+
11
+ type GetHumanReadableFieldPathConfig = {
12
+ path: [
13
+ PathElement<CustomTypeModel | SharedSliceModel>,
14
+ ...PathElement<CustomTypeModelField | CustomTypeModelSlice>[]
15
+ ];
16
+ };
17
+
18
+ export const getHumanReadableFieldPath = (
19
+ config: GetHumanReadableFieldPathConfig,
20
+ ): string => {
21
+ return config.path
22
+ .map((element) => {
23
+ if (element.label) {
24
+ return element.label;
25
+ } else if (element.model) {
26
+ return getHumanReadableModelName({
27
+ id: element.id,
28
+ model: element.model,
29
+ });
30
+ } else {
31
+ return element.id;
32
+ }
33
+ })
34
+ .join(" → ");
35
+ };
@@ -0,0 +1,50 @@
1
+ import type { CustomTypeModelField } from "@prismicio/types";
2
+ import { CustomTypeModelLinkSelectType } from "@prismicio/types";
3
+
4
+ type GetFieldHumanReadableTypeConfig = {
5
+ model: CustomTypeModelField;
6
+ };
7
+
8
+ export const getHumanReadableFieldType = (
9
+ config: GetFieldHumanReadableTypeConfig,
10
+ ): string => {
11
+ switch (config.model.type) {
12
+ case "StructuredText": {
13
+ const isTitleField =
14
+ "single" in config.model.config &&
15
+ config.model.config.single
16
+ .split(",")
17
+ .every((blockType) => /heading/.test(blockType));
18
+
19
+ return isTitleField ? "Title" : "Rich Text";
20
+ }
21
+
22
+ case "IntegrationFields": {
23
+ return `Integration Fields (Catalog: \`${config.model.config.catalog}\`)`;
24
+ }
25
+
26
+ case "Link": {
27
+ switch (config.model.config.select) {
28
+ case CustomTypeModelLinkSelectType.Document: {
29
+ return "Content Relationship";
30
+ }
31
+
32
+ case CustomTypeModelLinkSelectType.Media: {
33
+ return "Link to Media";
34
+ }
35
+
36
+ default: {
37
+ return "Link";
38
+ }
39
+ }
40
+ }
41
+
42
+ case "Slices": {
43
+ return "Slice Zone";
44
+ }
45
+
46
+ default: {
47
+ return config.model.type;
48
+ }
49
+ }
50
+ };
@@ -0,0 +1,40 @@
1
+ import type {
2
+ CustomTypeModel,
3
+ CustomTypeModelField,
4
+ CustomTypeModelSlice,
5
+ SharedSliceModel,
6
+ } from "@prismicio/types";
7
+
8
+ import { isCustomTypeModel } from "./isCustomTypeModel";
9
+ import { isCustomTypeModelField } from "./isCustomTypeModelField";
10
+ import { isCustomTypeModelSlice } from "./isCustomTypeModelSlice";
11
+ import { isSharedSliceModel } from "./isSharedSliceModel";
12
+
13
+ type GetModelHumanNameConfig = {
14
+ id: string;
15
+ model:
16
+ | CustomTypeModel
17
+ | SharedSliceModel
18
+ | CustomTypeModelField
19
+ | CustomTypeModelSlice;
20
+ };
21
+
22
+ export const getHumanReadableModelName = (
23
+ config: GetModelHumanNameConfig,
24
+ ): string => {
25
+ if (isCustomTypeModel(config.model)) {
26
+ return config.model.label;
27
+ } else if (isSharedSliceModel(config.model)) {
28
+ return config.model.name;
29
+ } else if (
30
+ isCustomTypeModelField(config.model) &&
31
+ "label" in config.model.config
32
+ ) {
33
+ return config.model.config.label;
34
+ } else if (isCustomTypeModelSlice(config.model)) {
35
+ return config.model.fieldset;
36
+ } else {
37
+ // Slice Zone
38
+ return `Slice Zone (\`${config.id}\`)`;
39
+ }
40
+ };
@@ -0,0 +1,14 @@
1
+ import * as tsm from "ts-morph";
2
+ // import prettier from "prettier";
3
+
4
+ import { BLANK_LINE_IDENTIFIER } from "../constants";
5
+
6
+ export const getSourceFileText = (sourceFile: tsm.SourceFile): string => {
7
+ const text = sourceFile
8
+ .print()
9
+ .replace(new RegExp(BLANK_LINE_IDENTIFIER, "g"), "");
10
+
11
+ return text;
12
+
13
+ // return prettier.format(text, { parser: "typescript" });
14
+ };
@@ -0,0 +1,5 @@
1
+ import type { CustomTypeModel } from "@prismicio/types";
2
+
3
+ export const isCustomTypeModel = (model: unknown): model is CustomTypeModel => {
4
+ return typeof model === "object" && model !== null && "json" in model;
5
+ };
@@ -0,0 +1,12 @@
1
+ import type { CustomTypeModelField } from "@prismicio/types";
2
+
3
+ export const isCustomTypeModelField = (
4
+ model: unknown,
5
+ ): model is CustomTypeModelField => {
6
+ return (
7
+ typeof model === "object" &&
8
+ model !== null &&
9
+ "type" in model &&
10
+ "config" in model
11
+ );
12
+ };
@@ -0,0 +1,12 @@
1
+ import type { CustomTypeModelSlice } from "@prismicio/types";
2
+
3
+ export const isCustomTypeModelSlice = (
4
+ model: unknown,
5
+ ): model is CustomTypeModelSlice => {
6
+ return (
7
+ typeof model === "object" &&
8
+ model !== null &&
9
+ "non-repeat" in model &&
10
+ "repeat" in model
11
+ );
12
+ };
@@ -0,0 +1,7 @@
1
+ import type { SharedSliceModel } from "@prismicio/types";
2
+
3
+ export const isSharedSliceModel = (
4
+ model: unknown,
5
+ ): model is SharedSliceModel => {
6
+ return typeof model === "object" && model !== null && "variations" in model;
7
+ };
@@ -0,0 +1,9 @@
1
+ import {
2
+ pascalCase as pascalCaseBase,
3
+ pascalCaseTransformMerge,
4
+ } from "pascal-case";
5
+
6
+ export const pascalCase = (input: string): string =>
7
+ pascalCaseBase(input, {
8
+ transform: pascalCaseTransformMerge,
9
+ });
@@ -0,0 +1,46 @@
1
+ import type { CodeBlockWriter } from "ts-morph";
2
+
3
+ type BuildFieldDocsConfig = {
4
+ writer: CodeBlockWriter;
5
+ description?: string;
6
+ fieldType?: string;
7
+ defaultValue?: string | boolean;
8
+ placeholder?: string;
9
+ apiIDPath?: string;
10
+ documentationURL?: string;
11
+ };
12
+
13
+ export const writeFieldDocsDescription = (
14
+ config: BuildFieldDocsConfig,
15
+ ): void => {
16
+ if (config.description) {
17
+ config.writer.writeLine(config.description);
18
+ }
19
+
20
+ config.writer.blankLineIfLastNot();
21
+
22
+ if (config.fieldType) {
23
+ config.writer.writeLine(`- **Field Type**: ${config.fieldType}`);
24
+ }
25
+
26
+ if (config.placeholder) {
27
+ config.writer.writeLine(`- **Placeholder**: ${config.placeholder}`);
28
+ }
29
+
30
+ if (config.defaultValue !== undefined) {
31
+ const stringifiedDefaultValue =
32
+ typeof config.defaultValue === "boolean"
33
+ ? `${config.defaultValue}`
34
+ : config.defaultValue;
35
+
36
+ config.writer.writeLine(`- **Default Value**: ${stringifiedDefaultValue}`);
37
+ }
38
+
39
+ if (config.apiIDPath) {
40
+ config.writer.writeLine(`- **API ID Path**: ${config.apiIDPath}`);
41
+ }
42
+
43
+ if (config.documentationURL) {
44
+ config.writer.writeLine(`- **Documentation**: ${config.documentationURL}`);
45
+ }
46
+ };