@strapi/typescript-utils 4.2.0-beta.4 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,281 @@
1
+ 'use strict';
2
+
3
+ const ts = require('typescript');
4
+ const { factory } = require('typescript');
5
+ const _ = require('lodash/fp');
6
+
7
+ const { addImport } = require('./imports');
8
+ const { getTypeNode, toTypeLiteral } = require('./utils');
9
+
10
+ /**
11
+ * Generate a property signature node for a given attribute
12
+ *
13
+ * @param {object} schema
14
+ * @param {string} attributeName
15
+ * @param {object} attribute
16
+ * @returns {object}
17
+ */
18
+ const attributeToPropertySignature = (schema, attributeName, attribute) => {
19
+ const baseType = getAttributeType(attributeName, attribute, schema.uid);
20
+
21
+ if (baseType === null) {
22
+ return null;
23
+ }
24
+
25
+ const modifiers = getAttributeModifiers(attribute);
26
+
27
+ const nodes = [baseType, ...modifiers];
28
+
29
+ return factory.createPropertySignature(
30
+ undefined,
31
+ factory.createIdentifier(attributeName),
32
+ undefined,
33
+ factory.createIntersectionTypeNode(nodes)
34
+ );
35
+ };
36
+
37
+ /**
38
+ * Create the base type node for a given attribute
39
+ *
40
+ * @param {string} attributeName
41
+ * @param {object} attribute
42
+ * @param {string} uid
43
+ * @returns {object}
44
+ */
45
+ const getAttributeType = (attributeName, attribute, uid) => {
46
+ if (!Object.keys(mappers).includes(attribute.type)) {
47
+ console.warn(
48
+ `"${attributeName}" attribute from "${uid}" has an invalid type: "${attribute.type}"`
49
+ );
50
+
51
+ return null;
52
+ }
53
+
54
+ const [attributeType, typeParams] = mappers[attribute.type]({ uid, attribute, attributeName });
55
+
56
+ addImport(attributeType);
57
+
58
+ return getTypeNode(attributeType, typeParams);
59
+ };
60
+
61
+ /**
62
+ * Collect every modifier node from an attribute
63
+ *
64
+ * @param {object} attribute
65
+ * @returns {object[]}
66
+ */
67
+ const getAttributeModifiers = attribute => {
68
+ const modifiers = [];
69
+
70
+ // Required
71
+ if (attribute.required) {
72
+ addImport('RequiredAttribute');
73
+
74
+ modifiers.push(factory.createTypeReferenceNode(factory.createIdentifier('RequiredAttribute')));
75
+ }
76
+
77
+ // Private
78
+ if (attribute.private) {
79
+ addImport('PrivateAttribute');
80
+
81
+ modifiers.push(factory.createTypeReferenceNode(factory.createIdentifier('PrivateAttribute')));
82
+ }
83
+
84
+ // Unique
85
+ if (attribute.unique) {
86
+ addImport('UniqueAttribute');
87
+
88
+ modifiers.push(factory.createTypeReferenceNode(factory.createIdentifier('UniqueAttribute')));
89
+ }
90
+
91
+ // Configurable
92
+ if (attribute.configurable) {
93
+ addImport('ConfigurableAttribute');
94
+
95
+ modifiers.push(
96
+ factory.createTypeReferenceNode(factory.createIdentifier('ConfigurableAttribute'))
97
+ );
98
+ }
99
+
100
+ // Plugin Options
101
+ if (!_.isEmpty(attribute.pluginOptions)) {
102
+ addImport('SetPluginOptions');
103
+
104
+ modifiers.push(
105
+ factory.createTypeReferenceNode(
106
+ factory.createIdentifier('SetPluginOptions'),
107
+ // Transform the pluginOptions object into an object literal expression
108
+ [toTypeLiteral(attribute.pluginOptions)]
109
+ )
110
+ );
111
+ }
112
+
113
+ // Min / Max
114
+ // TODO: Always provide a second type argument for min/max (ie: resolve the attribute scalar type with a `GetAttributeType<${mappers[attribute][0]}>` (useful for biginter (string values)))
115
+ if (!_.isNil(attribute.min) || !_.isNil(attribute.max)) {
116
+ addImport('SetMinMax');
117
+
118
+ const minMaxProperties = _.pick(['min', 'max'], attribute);
119
+
120
+ modifiers.push(
121
+ factory.createTypeReferenceNode(factory.createIdentifier('SetMinMax'), [
122
+ toTypeLiteral(minMaxProperties),
123
+ ])
124
+ );
125
+ }
126
+
127
+ // Min length / Max length
128
+ if (!_.isNil(attribute.minLength) || !_.isNil(attribute.maxLength)) {
129
+ addImport('SetMinMaxLength');
130
+
131
+ const minMaxProperties = _.pick(['minLength', 'maxLength'], attribute);
132
+
133
+ modifiers.push(
134
+ factory.createTypeReferenceNode(factory.createIdentifier('SetMinMaxLength'), [
135
+ toTypeLiteral(minMaxProperties),
136
+ ])
137
+ );
138
+ }
139
+
140
+ // Default
141
+ if (!_.isNil(attribute.default)) {
142
+ addImport('DefaultTo');
143
+
144
+ const defaultLiteral = toTypeLiteral(attribute.default);
145
+
146
+ modifiers.push(
147
+ factory.createTypeReferenceNode(factory.createIdentifier('DefaultTo'), [defaultLiteral])
148
+ );
149
+ }
150
+
151
+ return modifiers;
152
+ };
153
+
154
+ const mappers = {
155
+ string() {
156
+ return ['StringAttribute'];
157
+ },
158
+ text() {
159
+ return ['TextAttribute'];
160
+ },
161
+ richtext() {
162
+ return ['RichTextAttribute'];
163
+ },
164
+ password() {
165
+ return ['PasswordAttribute'];
166
+ },
167
+ email() {
168
+ return ['EmailAttribute'];
169
+ },
170
+ date() {
171
+ return ['DateAttribute'];
172
+ },
173
+ time() {
174
+ return ['TimeAttribute'];
175
+ },
176
+ datetime() {
177
+ return ['DateTimeAttribute'];
178
+ },
179
+ timestamp() {
180
+ return ['TimestampAttribute'];
181
+ },
182
+ integer() {
183
+ return ['IntegerAttribute'];
184
+ },
185
+ biginteger() {
186
+ return ['BigIntegerAttribute'];
187
+ },
188
+ float() {
189
+ return ['FloatAttribute'];
190
+ },
191
+ decimal() {
192
+ return ['DecimalAttribute'];
193
+ },
194
+ uid({ attribute, uid }) {
195
+ const { targetField, options } = attribute;
196
+
197
+ // If there are no params to compute, then return the attribute type alone
198
+ if (targetField === undefined && options === undefined) {
199
+ return ['UIDAttribute'];
200
+ }
201
+
202
+ const params = [];
203
+
204
+ // If the targetField property is defined, then reference it,
205
+ // otherwise, put `undefined` keyword type nodes as placeholders
206
+ const targetFieldParams = _.isUndefined(targetField)
207
+ ? [
208
+ factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword),
209
+ factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword),
210
+ ]
211
+ : [factory.createStringLiteral(uid), factory.createStringLiteral(targetField)];
212
+
213
+ params.push(...targetFieldParams);
214
+
215
+ // If the options property is defined, transform it to
216
+ // a type literral node and add it to the params list
217
+ if (_.isObject(options)) {
218
+ params.push(toTypeLiteral(options));
219
+ }
220
+
221
+ return ['UIDAttribute', params];
222
+ },
223
+ enumeration({ attribute }) {
224
+ const { enum: enumValues } = attribute;
225
+
226
+ return ['EnumerationAttribute', [toTypeLiteral(enumValues)]];
227
+ },
228
+ boolean() {
229
+ return ['BooleanAttribute'];
230
+ },
231
+ json() {
232
+ return ['JSONAttribute'];
233
+ },
234
+ media() {
235
+ return ['MediaAttribute'];
236
+ },
237
+ relation({ uid, attribute }) {
238
+ const { relation, target } = attribute;
239
+
240
+ const isMorphRelation = relation.toLowerCase().includes('morph');
241
+
242
+ if (isMorphRelation) {
243
+ return [
244
+ 'RelationAttribute',
245
+ [factory.createStringLiteral(uid, true), factory.createStringLiteral(relation, true)],
246
+ ];
247
+ }
248
+
249
+ return [
250
+ 'RelationAttribute',
251
+ [
252
+ factory.createStringLiteral(uid, true),
253
+ factory.createStringLiteral(relation, true),
254
+ factory.createStringLiteral(target, true),
255
+ ],
256
+ ];
257
+ },
258
+ component({ attribute }) {
259
+ const target = attribute.component;
260
+ const params = [factory.createStringLiteral(target, true)];
261
+
262
+ if (attribute.repeatable) {
263
+ params.push(factory.createTrue());
264
+ }
265
+
266
+ return ['ComponentAttribute', params];
267
+ },
268
+ dynamiczone({ attribute }) {
269
+ const componentsParam = factory.createTupleTypeNode(
270
+ attribute.components.map(component => factory.createStringLiteral(component))
271
+ );
272
+
273
+ return ['DynamicZoneAttribute', [componentsParam]];
274
+ },
275
+ };
276
+
277
+ module.exports = attributeToPropertySignature;
278
+
279
+ module.exports.mappers = mappers;
280
+ module.exports.getAttributeType = getAttributeType;
281
+ module.exports.getAttributeModifiers = getAttributeModifiers;
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ const ts = require('typescript');
4
+ const { factory } = require('typescript');
5
+
6
+ const { getSchemaInterfaceName } = require('./utils');
7
+
8
+ /**
9
+ * Generate the global module augmentation block
10
+ *
11
+ * @param {Array<{ schema: object; definition: ts.TypeNode }>} schemasDefinitions
12
+ * @returns {ts.ModuleDeclaration}
13
+ */
14
+ const generateGlobalDefinition = (schemasDefinitions = []) => {
15
+ const properties = schemasDefinitions.map(schemaDefinitionToPropertySignature);
16
+
17
+ return factory.createModuleDeclaration(
18
+ undefined,
19
+ [factory.createModifier(ts.SyntaxKind.DeclareKeyword)],
20
+ factory.createIdentifier('global'),
21
+ factory.createModuleBlock([
22
+ factory.createModuleDeclaration(
23
+ undefined,
24
+ undefined,
25
+ factory.createIdentifier('Strapi'),
26
+ factory.createModuleBlock([
27
+ factory.createInterfaceDeclaration(
28
+ undefined,
29
+ undefined,
30
+ factory.createIdentifier('Schemas'),
31
+ undefined,
32
+ undefined,
33
+ properties
34
+ ),
35
+ ]),
36
+ ts.NodeFlags.Namespace |
37
+ ts.NodeFlags.ExportContext |
38
+ ts.NodeFlags.Ambient |
39
+ ts.NodeFlags.ContextFlags
40
+ ),
41
+ ]),
42
+ ts.NodeFlags.ExportContext |
43
+ ts.NodeFlags.GlobalAugmentation |
44
+ ts.NodeFlags.Ambient |
45
+ ts.NodeFlags.ContextFlags
46
+ );
47
+ };
48
+
49
+ /**
50
+ *
51
+ * @param {object} schemaDefinition
52
+ * @param {ts.InterfaceDeclaration} schemaDefinition.definition
53
+ * @param {object} schemaDefinition.schema
54
+ */
55
+ const schemaDefinitionToPropertySignature = ({ schema }) => {
56
+ const { uid } = schema;
57
+
58
+ const interfaceTypeName = getSchemaInterfaceName(uid);
59
+
60
+ return factory.createPropertySignature(
61
+ undefined,
62
+ factory.createStringLiteral(uid, true),
63
+ undefined,
64
+ factory.createTypeReferenceNode(factory.createIdentifier(interfaceTypeName))
65
+ );
66
+ };
67
+
68
+ module.exports = { generateGlobalDefinition };
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const { factory } = require('typescript');
4
+
5
+ const imports = [];
6
+
7
+ module.exports = {
8
+ getImports() {
9
+ return imports;
10
+ },
11
+
12
+ addImport(type) {
13
+ const hasType = imports.includes(type);
14
+
15
+ if (!hasType) {
16
+ imports.push(type);
17
+ }
18
+ },
19
+
20
+ generateImportDefinition() {
21
+ const formattedImports = imports.map(key =>
22
+ factory.createImportSpecifier(false, undefined, factory.createIdentifier(key))
23
+ );
24
+
25
+ return factory.createImportDeclaration(
26
+ undefined,
27
+ undefined,
28
+ factory.createImportClause(false, undefined, factory.createNamedImports(formattedImports)),
29
+ factory.createStringLiteral('@strapi/strapi'),
30
+ undefined
31
+ );
32
+ },
33
+ };
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const ts = require('typescript');
6
+ const { factory } = require('typescript');
7
+
8
+ const fp = require('lodash/fp');
9
+ const fse = require('fs-extra');
10
+ const prettier = require('prettier');
11
+ const chalk = require('chalk');
12
+ const CLITable = require('cli-table3');
13
+
14
+ const { generateImportDefinition } = require('./imports');
15
+ const { generateSchemaDefinition } = require('./schema');
16
+ const { generateGlobalDefinition } = require('./global');
17
+ const {
18
+ getAllStrapiSchemas,
19
+ getSchemaInterfaceName,
20
+ getSchemaModelType,
21
+ getDefinitionAttributesCount,
22
+ } = require('./utils');
23
+
24
+ const DEFAULT_OUT_FILENAME = 'schemas.d.ts';
25
+
26
+ /**
27
+ * Generate type definitions for Strapi schemas
28
+ *
29
+ * @param {object} options
30
+ * @param {Strapi} options.strapi
31
+ * @param {{ distDir: string; appDir: string; }} options.dirs
32
+ * @param {string} [options.outDir]
33
+ * @param {string} [options.file]
34
+ * @param {boolean} [options.verbose]
35
+ */
36
+ const generateSchemasDefinitions = async (options = {}) => {
37
+ const {
38
+ strapi,
39
+ outDir = process.cwd(),
40
+ file = DEFAULT_OUT_FILENAME,
41
+ verbose = false,
42
+ silent = false,
43
+ } = options;
44
+
45
+ const schemas = getAllStrapiSchemas(strapi);
46
+
47
+ const schemasDefinitions = Object.values(schemas).map(schema => ({
48
+ schema,
49
+ definition: generateSchemaDefinition(schema),
50
+ }));
51
+
52
+ const formattedSchemasDefinitions = schemasDefinitions.reduce((acc, def) => {
53
+ acc.push(
54
+ // Definition
55
+ def.definition,
56
+
57
+ // Add a newline between each interface declaration
58
+ factory.createIdentifier('\n')
59
+ );
60
+
61
+ return acc;
62
+ }, []);
63
+
64
+ const allDefinitions = [
65
+ // Imports
66
+ generateImportDefinition(),
67
+
68
+ // Add a newline after the import statement
69
+ factory.createIdentifier('\n'),
70
+
71
+ // Schemas
72
+ ...formattedSchemasDefinitions,
73
+
74
+ // Global
75
+ generateGlobalDefinition(schemasDefinitions),
76
+ ];
77
+
78
+ const output = emitDefinitions(allDefinitions);
79
+ const formattedOutput = await format(output);
80
+
81
+ const definitionFilepath = await saveDefinitionToFileSystem(outDir, file, formattedOutput);
82
+
83
+ logDebugInformation(schemasDefinitions, { filepath: definitionFilepath, verbose, silent });
84
+ };
85
+
86
+ const emitDefinitions = definitions => {
87
+ const nodeArray = factory.createNodeArray(definitions);
88
+
89
+ const sourceFile = ts.createSourceFile(
90
+ 'placeholder.ts',
91
+ '',
92
+ ts.ScriptTarget.ESNext,
93
+ true,
94
+ ts.ScriptKind.TS
95
+ );
96
+
97
+ const printer = ts.createPrinter({ newLine: true, omitTrailingSemicolon: true });
98
+
99
+ return printer.printList(ts.ListFormat.MultiLine, nodeArray, sourceFile);
100
+ };
101
+
102
+ const saveDefinitionToFileSystem = async (dir, file, content) => {
103
+ const filepath = path.join(dir, file);
104
+
105
+ await fse.writeFile(filepath, content);
106
+
107
+ return filepath;
108
+ };
109
+
110
+ /**
111
+ * Format the given definitions.
112
+ * Uses the existing config if one is defined in the project.
113
+ *
114
+ * @param {string} content
115
+ * @returns {Promise<string>}
116
+ */
117
+ const format = async content => {
118
+ const configFile = await prettier.resolveConfigFile();
119
+ const config = configFile
120
+ ? await prettier.resolveConfig(configFile)
121
+ : // Default config
122
+ {
123
+ singleQuote: true,
124
+ useTabs: false,
125
+ tabWidth: 2,
126
+ };
127
+
128
+ Object.assign(config, { parser: 'typescript' });
129
+
130
+ return prettier.format(content, config);
131
+ };
132
+
133
+ const logDebugInformation = (definitions, options = {}) => {
134
+ const { filepath, verbose, silent } = options;
135
+
136
+ if (verbose) {
137
+ const table = new CLITable({
138
+ head: [
139
+ chalk.bold(chalk.green('Model Type')),
140
+ chalk.bold(chalk.blue('UID')),
141
+ chalk.bold(chalk.blue('Type')),
142
+ chalk.bold(chalk.gray('Attributes Count')),
143
+ ],
144
+ colAligns: ['center', 'left', 'left', 'center'],
145
+ });
146
+
147
+ const sortedDefinitions = definitions.map(def => ({
148
+ ...def,
149
+ attributesCount: getDefinitionAttributesCount(def.definition),
150
+ }));
151
+
152
+ for (const { schema, attributesCount } of sortedDefinitions) {
153
+ const modelType = fp.upperFirst(getSchemaModelType(schema));
154
+ const interfaceType = getSchemaInterfaceName(schema.uid);
155
+
156
+ table.push([
157
+ chalk.greenBright(modelType),
158
+ chalk.blue(schema.uid),
159
+ chalk.blue(interfaceType),
160
+ chalk.grey(fp.isNil(attributesCount) ? 'N/A' : attributesCount),
161
+ ]);
162
+ }
163
+
164
+ // Table
165
+ console.log(table.toString());
166
+ }
167
+
168
+ if (!silent) {
169
+ // Metrics
170
+ console.log(
171
+ chalk.greenBright(
172
+ `Generated ${definitions.length} type definition for your Strapi application's schemas.`
173
+ )
174
+ );
175
+
176
+ // Filepath
177
+ const relativePath = path.relative(process.cwd(), filepath);
178
+
179
+ console.log(
180
+ chalk.grey(`The definitions file has been generated here: ${chalk.bold(relativePath)}`)
181
+ );
182
+ }
183
+ };
184
+
185
+ module.exports = generateSchemasDefinitions;
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ const ts = require('typescript');
4
+ const { factory } = require('typescript');
5
+ const { isEmpty } = require('lodash/fp');
6
+
7
+ const { getSchemaExtendsTypeName, getSchemaInterfaceName, toTypeLiteral } = require('./utils');
8
+ const attributeToPropertySignature = require('./attributes');
9
+ const { addImport } = require('./imports');
10
+
11
+ /**
12
+ * Generate an interface declaration for a given schema
13
+ *
14
+ * @param {object} schema
15
+ * @returns {ts.InterfaceDeclaration}
16
+ */
17
+ const generateSchemaDefinition = schema => {
18
+ const { uid } = schema;
19
+
20
+ // Resolve the different interface names needed to declare the schema's interface
21
+ const interfaceName = getSchemaInterfaceName(uid);
22
+ const parentType = getSchemaExtendsTypeName(schema);
23
+
24
+ // Make sure the extended interface are imported
25
+ addImport(parentType);
26
+
27
+ // Properties whose values can be mapped to a literal type expression
28
+ const literalPropertiesDefinitions = ['info', 'options', 'pluginOptions']
29
+ // Ignore non-existent or empty declarations
30
+ .filter(key => !isEmpty(schema[key]))
31
+ // Generate literal definition for each property
32
+ .map(generatePropertyLiteralDefinitionFactory(schema));
33
+
34
+ // Generate the `attributes` literal type definition
35
+ const attributesProp = generateAttributePropertySignature(schema);
36
+
37
+ // Merge every schema's definition in a single list
38
+ const schemaProperties = [...literalPropertiesDefinitions, attributesProp];
39
+
40
+ // Generate the schema's interface declaration
41
+ const schemaType = factory.createInterfaceDeclaration(
42
+ undefined,
43
+ [factory.createModifier(ts.SyntaxKind.ExportKeyword)],
44
+ factory.createIdentifier(interfaceName),
45
+ undefined,
46
+ [
47
+ factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [
48
+ factory.createIdentifier(parentType),
49
+ ]),
50
+ ],
51
+ schemaProperties
52
+ );
53
+
54
+ return schemaType;
55
+ };
56
+
57
+ /**
58
+ * Generate a property signature for the schema's `attributes` field
59
+ *
60
+ * @param {object} schema
61
+ * @returns {ts.PropertySignature}
62
+ */
63
+ const generateAttributePropertySignature = schema => {
64
+ const { attributes } = schema;
65
+
66
+ const properties = Object.entries(attributes).map(([attributeName, attribute]) => {
67
+ return attributeToPropertySignature(schema, attributeName, attribute);
68
+ });
69
+
70
+ return factory.createPropertySignature(
71
+ undefined,
72
+ factory.createIdentifier('attributes'),
73
+ undefined,
74
+ factory.createTypeLiteralNode(properties)
75
+ );
76
+ };
77
+
78
+ const generatePropertyLiteralDefinitionFactory = schema => key => {
79
+ return factory.createPropertySignature(
80
+ undefined,
81
+ factory.createIdentifier(key),
82
+ undefined,
83
+ toTypeLiteral(schema[key])
84
+ );
85
+ };
86
+
87
+ module.exports = { generateSchemaDefinition };