Update generator. Use typescript compiler API to generate definitions

This commit is contained in:
Convly 2022-06-17 19:00:27 +02:00
parent 44d216a028
commit 6c9b500586
20 changed files with 791 additions and 489 deletions

View File

@ -221,7 +221,7 @@ program
'Specify a relative directory in which the schemas definitions will be generated'
)
.option('-f, --file <file>', 'Specify a filename to store the schemas definitions')
.option('-s, --silence', `Don't display debug information`, false)
.option('-d, --debug', `Display debug information`, false)
.action(getLocalScript('content-types/generate-types'));
program

View File

@ -0,0 +1,20 @@
'use strict';
const tsUtils = require('@strapi/typescript-utils');
const strapi = require('../../index');
module.exports = async function({ outDir, file, debug }) {
const appContext = await strapi.compile();
const app = await strapi(appContext).register();
await tsUtils.generators.generateSchemasDefinitions({
strapi: app,
outDir: outDir || appContext.appDir,
file,
dirs: appContext,
debug,
});
app.destroy();
};

View File

@ -1,145 +0,0 @@
'use strict';
const fp = require('lodash/fp');
const { addImport } = require('./imports');
const { logWarning, toType } = require('./utils');
const generateAttributesDefinition = (attributes, uid) => {
const attributesDefinitions = [];
for (const [attributeName, attribute] of Object.entries(attributes)) {
const type = getAttributeType(attributeName, attribute, uid);
attributesDefinitions.push([attributeName, type]);
}
const formattedDefinitions = attributesDefinitions
.map(([name, attributeType]) => ` ${name}: ${attributeType};`)
.join('\n');
return ` attributes: {
${formattedDefinitions}
}`;
};
const getAttributeType = (attributeName, attribute, uid) => {
const mappers = {
string() {
return ['StringAttribute', null];
},
text() {
return ['TextAttribute', null];
},
richtext() {
return ['RichTextAttribute', null];
},
password() {
return ['PasswordAttribute', null];
},
email() {
return ['EmailAttribute', null];
},
date() {
return ['DateAttribute', null];
},
time() {
return ['TimeAttribute', null];
},
datetime() {
return ['DateTimeAttribute', null];
},
timestamp() {
return ['TimestampAttribute', null];
},
integer() {
return ['IntegerAttribute', null];
},
biginteger() {
return ['BigIntegerAttribute', null];
},
float() {
return ['FloatAttribute', null];
},
decimal() {
return ['DecimalAttribute', null];
},
uid() {
return ['UIDAttribute', null];
},
enumeration() {
return ['EnumerationAttribute', null];
},
boolean() {
return ['BooleanAttribute', null];
},
json() {
return ['JsonAttribute', null];
},
media() {
return ['MediaAttribute', null];
},
relation() {
const { relation, target } = attribute;
if (relation.includes('morph') | relation.includes('Morph')) {
return ['PolymorphicRelationAttribute', [`'${uid}'`, `'${relation}'`]];
}
return ['RelationAttribute', [`'${uid}'`, `'${relation}'`, `'${target}'`]];
},
component() {
const target = attribute.component;
const params = [`'${target}'`];
if (attribute.repeatable) {
params.push(true);
}
return ['ComponentAttribute', params];
},
dynamiczone() {
const components = JSON.stringify(attribute.components);
return ['DynamicZoneAttribute', [components]];
},
};
if (!Object.keys(mappers).includes(attribute.type)) {
logWarning(
`"${attributeName}" attribute from "${uid}" has an invalid type: "${attribute.type}"`
);
return null;
}
let [attributeType, typeParams] = mappers[attribute.type]();
addImport(attributeType);
let type = typeParams ? `${attributeType}<${typeParams.join(', ')}>` : attributeType;
if (attribute.required) {
addImport('RequiredAttribute');
type = `${type} & RequiredAttribute`;
}
if (attribute.pluginOptions && !fp.isEmpty(attribute.pluginOptions)) {
addImport('PluginOptionsAttribute');
const pluginOptionsType = toType(attribute.pluginOptions, {
inline: true,
indent: 0,
});
type = `${type} & PluginOptionsAttribute<${pluginOptionsType}>`;
}
return type;
};
module.exports = {
generateAttributesDefinition,
getAttributeType,
};

View File

@ -1,21 +0,0 @@
'use strict';
const generateGlobalDefinition = definitions => {
const formattedSchemasDefinitions = definitions
.map(({ uid, type }) => ` '${uid}': ${type}`)
.join('\n');
return `
declare global {
namespace Strapi {
interface Schemas {
${formattedSchemasDefinitions}
}
}
}
`;
};
module.exports = {
generateGlobalDefinition,
};

View File

@ -1,26 +0,0 @@
'use strict';
const imports = [];
module.exports = {
getImports() {
return imports;
},
addImport(type) {
const hasType = imports.includes(type);
if (!hasType) {
imports.push(type);
}
},
generateImports() {
const formattedImports = imports.map(p => ` ${p}`).join(',\n');
return `import {
${formattedImports}
} from '@strapi/strapi';
`;
},
};

View File

@ -1,167 +0,0 @@
'use strict';
const path = require('path');
const CLITable = require('cli-table3');
const chalk = require('chalk');
const fp = require('lodash/fp');
const fse = require('fs-extra');
const tsUtils = require('@strapi/typescript-utils');
const createStrapiInstance = require('../../../index');
const { generateSchemaDefinition } = require('./schemas');
const { generateGlobalDefinition } = require('./global');
const { generateImports } = require('./imports');
const { logWarning, getSchemaTypeName } = require('./utils');
module.exports = async function({ outDir, file, silence }) {
const [app, dirs] = await setup();
const schemas = getAllStrapiSchemas(app);
const table = createInfoTable();
const definitions = generateTypesDefinitions(schemas);
const globalDefinition = generateGlobalDefinition(definitions);
const imports = generateImports();
const fullDefinition = [
imports + '\n',
definitions.map(fp.get('definition')).join('\n'),
globalDefinition,
].join('');
await generateSchemaFile(outDir || dirs.app, fullDefinition, file);
for (const definition of definitions) {
const isValidDefinition = definition.definition !== null;
const validateAndTransform = isValidDefinition ? fp.identity : chalk.redBright;
table.push([
validateAndTransform(definition.kind),
validateAndTransform(definition.uid),
validateAndTransform(definition.type),
isValidDefinition ? chalk.greenBright('✓') : chalk.redBright('✗'),
]);
}
const successfullDefinition = fp.filter(d => !fp.isNil(d.definition), definitions);
const skippedDefinition = fp.filter(d => fp.isNil(d.definition), definitions);
if (!silence) {
console.log(table.toString());
console.log(
chalk.greenBright(
`Generated ${fp.size(
successfullDefinition
)} type definition for your Strapi application's schemas.`
)
);
const skippedAmount = fp.size(skippedDefinition);
if (skippedAmount > 0) {
console.log(
chalk.redBright(
`Skipped ${skippedAmount} (${skippedDefinition.map(d => d.uid).join(', ')})`
)
);
}
}
app.destroy();
};
/**
* Setup a Strapi application based on the current process directory
* @returns {Promise<[Strapi, { app: string, dist: string }]>}
*/
const setup = async () => {
const dirs = { app: process.cwd(), dist: process.cwd() };
const isTSProject = await tsUtils.isUsingTypeScript(dirs.app);
if (isTSProject) {
await tsUtils.compile(dirs.app, { configOptions: { options: { incremental: true } } });
dirs.dist = await tsUtils.resolveOutDir(dirs.app);
}
const app = await createStrapiInstance({ appDir: dirs.app, distDir: dirs.dist }).register();
return [app, dirs];
};
/**
* Generate all the TypeScript definitions for the app schemas
* @param {Object} schemas
*/
const generateTypesDefinitions = schemas => {
const definitions = [];
for (const [uid, schema] of Object.entries(schemas)) {
const { modelType, kind } = schema;
// Schema UID -> Interface Name
const type = getSchemaTypeName(uid);
let definition;
const isComponent = modelType === 'component';
const isContentType =
modelType === 'contentType' && ['singleType', 'collectionType'].includes(kind);
// Handle components and content types
if (isComponent || isContentType) {
definition = generateSchemaDefinition(uid, schema, type) + '\n';
}
// Other
else {
logWarning(`"${uid}" has an invalid model definition. Skipping...`);
definition = null;
}
// Add the generated definition to the list
definitions.push({
type,
uid,
schema,
definition,
kind: fp.upperFirst(modelType === 'component' ? 'component' : kind),
});
}
return definitions;
};
/**
* Generate the content-types definitions file based on the given arguments
* @param {string} dir
* @param {string} definition
* @param {string} [file]
*/
const generateSchemaFile = async (dir, definition, file) => {
const filePath = path.join(dir, file || 'schemas.d.ts');
await fse.writeFile(filePath, definition);
};
/**
* Get all content types and components loaded in the Strapi instance
*
* @param {Strapi} app
* @returns {Object}
*/
const getAllStrapiSchemas = app => ({ ...app.contentTypes, ...app.components });
/**
* Create a new info table for the content types definitions
*/
const createInfoTable = () => {
return new CLITable({
head: [chalk.green('Model Type'), chalk.blue('UID'), chalk.blue('Type'), chalk.gray('Status')],
colAligns: ['center', 'left', 'left', 'center'],
});
};

View File

@ -1,45 +0,0 @@
'use strict';
const fp = require('lodash/fp');
const { generateAttributesDefinition } = require('./attributes');
const { addImport } = require('./imports');
const { toType } = require('./utils');
const getBaseSchema = schema => {
const { modelType, kind } = schema;
// Component
if (modelType === 'component') {
return 'ComponentSchema';
}
// Content Type
else if (modelType === 'contentType') {
return `${fp.upperFirst(kind)}Schema`;
}
return null;
};
const getFormatOptions = prefix => ({ indentStart: 2, suffix: ';', prefix });
const generateSchemaDefinition = (uid, schema, type) => {
const baseInterface = getBaseSchema(schema);
addImport(baseInterface);
const propertiesKeys = ['info', 'options', 'pluginOptions'];
const definitionBody = propertiesKeys
.map(key => toType(fp.get(key, schema), getFormatOptions(key)))
.concat(generateAttributesDefinition(schema.attributes, uid))
.filter(def => !fp.isNil(def))
.join('');
return `interface ${type} extends ${baseInterface} {\n${definitionBody}\n}`;
};
module.exports = {
generateSchemaDefinition,
};

View File

@ -1,79 +0,0 @@
'use strict';
const chalk = require('chalk');
const fp = require('lodash/fp');
const logWarning = message => {
console.log(chalk.yellow(`[${new Date().toLocaleTimeString()}] (warning):\t${message}`));
};
const getSchemaTypeName = fp.flow(fp.replace(/(:.)/, ' '), fp.camelCase, fp.upperFirst);
const toType = (object, formatOptions = {}) => {
const {
prefix = null,
suffix = null,
inline = false,
indent = 2,
indentStart = 0,
} = formatOptions;
if (fp.isNil(object) || fp.isEmpty(object)) {
return null;
}
let definition = '';
const properties = Object.entries(object);
const lineBreak = (s = '') => (inline ? s : '\n');
const offset = (m = 0) => ' '.repeat(indentStart + indent * m);
const getPrefix = (s = '') => (prefix ? `${prefix}: ` : s);
const getSuffix = (s = '') => suffix || s;
for (const [key, value] of properties) {
const validKey = key.includes('-') ? `'${key}'` : key;
let row;
// TODO: Handle arrays types
// Handle recursive types (objects)
if (fp.isObject(value) && !fp.isEmpty(value)) {
row = toType(value, {
prefix: validKey,
indentStart: indentStart + indent,
suffix,
inline,
indent,
});
}
// All non-recursive types are handled there
else {
let type = value;
if (fp.isString(value)) {
type = `'${value}'`;
}
row = `${offset(1)}${validKey}: ${type};${lineBreak(' ')}`;
}
definition += row;
}
if (!inline) {
definition = definition.slice(0, definition.length - 1);
}
return `${offset()}${getPrefix()}{${lineBreak(' ')}${definition}${lineBreak(
''
)}${offset()}}${getSuffix()}${lineBreak()}`;
};
module.exports = {
logWarning,
getSchemaTypeName,
toType,
};

View File

@ -36,7 +36,7 @@ export type SetMinMax<T extends MinMaxOption<U>, U = number> = T;
export type SetMinMaxLength<T extends MinMaxLengthOption> = T;
// pluginOptions
export type SetAttributePluginOptions<T extends object = object> = { pluginOptions?: T };
export type SetPluginOptions<T extends object = object> = { pluginOptions?: T };
// default
export type DefaultTo<T> = { default: T };

View File

@ -26,7 +26,7 @@ export interface PolymorphicRelationAttributeProperties<
export type RelationAttribute<
S extends SchemaUID,
R extends RelationsType,
T extends SchemaUID
T extends R extends PolymorphicRelationsType ? never: SchemaUID = never
> = Attribute<'relation'> &
// Properties
(R extends BasicRelationsType

View File

@ -0,0 +1,7 @@
'use strict';
const generateSchemasDefinitions = require('./schemas');
module.exports = {
generateSchemasDefinitions,
};

View File

@ -0,0 +1,241 @@
'use strict';
const { factory } = require('typescript');
const fp = require('lodash/fp');
const { addImport } = require('./imports');
const { getTypeNode, toTypeLitteral } = require('./utils');
/**
* Generate a property signature node for a given attribute
*
* @param {object} schema
* @param {string} attributeName
* @param {object} attribute
* @returns {object}
*/
const attributeToPropertySignature = (schema, attributeName, attribute) => {
const baseType = getAttributeType(attributeName, attribute, schema.uid);
const modifiers = getAttributeModifiers(attributeName, attribute);
const nodes = [baseType, ...modifiers];
return factory.createPropertySignature(
undefined,
factory.createIdentifier(attributeName),
undefined,
factory.createIntersectionTypeNode(nodes)
);
};
/**
* Create the base type node for a given attribute
*
* @param {string} attributeName
* @param {object} attribute
* @param {string} uid
* @returns {object}
*/
const getAttributeType = (attributeName, attribute, uid) => {
const mappers = {
string() {
return ['StringAttribute'];
},
text() {
return ['TextAttribute'];
},
richtext() {
return ['RichTextAttribute'];
},
password() {
return ['PasswordAttribute'];
},
email() {
return ['EmailAttribute'];
},
date() {
return ['DateAttribute'];
},
time() {
return ['TimeAttribute'];
},
datetime() {
return ['DateTimeAttribute'];
},
timestamp() {
return ['TimestampAttribute'];
},
integer() {
return ['IntegerAttribute'];
},
biginteger() {
return ['BigIntegerAttribute'];
},
float() {
return ['FloatAttribute'];
},
decimal() {
return ['DecimalAttribute'];
},
uid() {
return ['UIDAttribute'];
},
enumeration() {
return ['EnumerationAttribute'];
},
boolean() {
return ['BooleanAttribute'];
},
json() {
return ['JSONAttribute'];
},
media() {
return ['MediaAttribute'];
},
relation() {
const { relation, target } = attribute;
if (relation.includes('morph') | relation.includes('Morph')) {
return [
'RelationAttribute',
[factory.createStringLiteral(uid, true), factory.createStringLiteral(relation, true)],
];
}
return [
'RelationAttribute',
[
factory.createStringLiteral(uid, true),
factory.createStringLiteral(relation, true),
factory.createStringLiteral(target, true),
],
];
},
component() {
const target = attribute.component;
const params = [factory.createStringLiteral(target, true)];
if (attribute.repeatable) {
params.push(factory.createTrue());
}
return ['ComponentAttribute', params];
},
dynamiczone() {
const componentsParam = factory.createTupleTypeNode(
attribute.components.map(component => factory.createStringLiteral(component))
);
return ['DynamicZoneAttribute', [componentsParam]];
},
};
if (!Object.keys(mappers).includes(attribute.type)) {
console.warning(
`"${attributeName}" attribute from "${uid}" has an invalid type: "${attribute.type}"`
);
return null;
}
const [attributeType, typeParams] = mappers[attribute.type]();
addImport(attributeType);
return getTypeNode(attributeType, typeParams);
};
/**
* Collect every modifier node from an attribute
*
* @param {string} _attributeName
* @param {object} attribute
* @returns {object[]}
*/
const getAttributeModifiers = (_attributeName, attribute) => {
const modifiers = [];
// Required
if (attribute.required) {
addImport('RequiredAttribute');
modifiers.push(factory.createTypeReferenceNode(factory.createIdentifier('RequiredAttribute')));
}
// Private
if (attribute.private) {
addImport('PrivateAttribute');
modifiers.push(factory.createTypeReferenceNode(factory.createIdentifier('PrivateAttribute')));
}
// Unique
if (attribute.unique) {
addImport('UniqueAttribute');
modifiers.push(factory.createTypeReferenceNode(factory.createIdentifier('UniqueAttribute')));
}
// Configurable
if (attribute.configurable) {
addImport('ConfigurableAttribute');
modifiers.push(
factory.createTypeReferenceNode(factory.createIdentifier('ConfigurableAttribute'))
);
}
// Plugin Options
if (!fp.isEmpty(attribute.pluginOptions)) {
addImport('SetPluginOptions');
modifiers.push(
factory.createTypeReferenceNode(
factory.createIdentifier('SetPluginOptions'),
// Transform the pluginOptions object into an object litteral expression
[toTypeLitteral(attribute.pluginOptions)]
)
);
}
// Min / Max
if (!fp.isNil(attribute.min) || !fp.isNil(attribute.max)) {
addImport('SetMinMax');
const minMaxProperties = fp.pick(['min', 'max'], attribute);
modifiers.push(
factory.createTypeReferenceNode(factory.createIdentifier('SetMinMax'), [
toTypeLitteral(minMaxProperties),
])
);
}
// Min length / Max length
if (!fp.isNil(attribute.minLength) || !fp.isNil(attribute.maxLength)) {
addImport('SetMinMaxLength');
const minMaxProperties = fp.pick(['minLength', 'maxLength'], attribute);
modifiers.push(
factory.createTypeReferenceNode(factory.createIdentifier('SetMinMaxLength'), [
toTypeLitteral(minMaxProperties),
])
);
}
// Default
if (!fp.isNil(attribute.default)) {
addImport('DefaultTo');
const defaultLitteral = toTypeLitteral(attribute.default);
modifiers.push(
factory.createTypeReferenceNode(factory.createIdentifier('DefaultTo'), [defaultLitteral])
);
}
return modifiers;
};
module.exports = attributeToPropertySignature;

View File

@ -0,0 +1,68 @@
'use strict';
const ts = require('typescript');
const { factory } = require('typescript');
const { getSchemaInterfaceName } = require('./utils');
/**
* Generate the global module augmentation block
*
* @param {Array<{ schema: object; definition: ts.TypeNode }>} schemasDefinitions
* @returns {ts.ModuleDeclaration}
*/
const generateGlobalDefinition = (schemasDefinitions = []) => {
const properties = schemasDefinitions.map(schemaDefinitionToPropertySignature);
return factory.createModuleDeclaration(
undefined,
[factory.createModifier(ts.SyntaxKind.DeclareKeyword)],
factory.createIdentifier('global'),
factory.createModuleBlock([
factory.createModuleDeclaration(
undefined,
undefined,
factory.createIdentifier('Strapi'),
factory.createModuleBlock([
factory.createInterfaceDeclaration(
undefined,
undefined,
factory.createIdentifier('Schemas'),
undefined,
undefined,
properties
),
]),
ts.NodeFlags.Namespace |
ts.NodeFlags.ExportContext |
ts.NodeFlags.Ambient |
ts.NodeFlags.ContextFlags
),
]),
ts.NodeFlags.ExportContext |
ts.NodeFlags.GlobalAugmentation |
ts.NodeFlags.Ambient |
ts.NodeFlags.ContextFlags
);
};
/**
*
* @param {object} schemaDefinition
* @param {ts.InterfaceDeclaration} schemaDefinition.definition
* @param {object} schemaDefinition.schema
*/
const schemaDefinitionToPropertySignature = ({ schema }) => {
const { uid } = schema;
const interfaceTypeName = getSchemaInterfaceName(uid);
return factory.createPropertySignature(
undefined,
factory.createStringLiteral(uid, true),
undefined,
factory.createTypeReferenceNode(factory.createIdentifier(interfaceTypeName))
);
};
module.exports = { generateGlobalDefinition };

View File

@ -0,0 +1,33 @@
'use strict';
const { factory } = require('typescript');
const imports = [];
module.exports = {
getImports() {
return imports;
},
addImport(type) {
const hasType = imports.includes(type);
if (!hasType) {
imports.push(type);
}
},
generateImportDefinition() {
const formattedImports = imports.map(key =>
factory.createImportSpecifier(false, undefined, factory.createIdentifier(key))
);
return factory.createImportDeclaration(
undefined,
undefined,
factory.createImportClause(false, undefined, factory.createNamedImports(formattedImports)),
factory.createStringLiteral('@strapi/strapi'),
undefined
);
},
};

View File

@ -0,0 +1,173 @@
'use strict';
const path = require('path');
const ts = require('typescript');
const { factory } = require('typescript');
const fp = require('lodash/fp');
const fse = require('fs-extra');
const prettier = require('prettier');
const chalk = require('chalk');
const CLITable = require('cli-table3');
const { generateImportDefinition } = require('./imports');
const { generateSchemaDefinition } = require('./schema');
const { generateGlobalDefinition } = require('./global');
const {
getAllStrapiSchemas,
getSchemaInterfaceName,
getSchemaModelType,
getDefinitionAttributesCount,
} = require('./utils');
const DEFAULT_OUT_FILENAME = 'schemas.d.ts';
/**
* Generate type definitions for Strapi schemas
*
* @param {object} options
* @param {Strapi} options.strapi
* @param {{ distDir: string; appDir: string; }} options.dirs
* @param {string} [options.outDir]
* @param {string} [options.file]
* @param {boolean} [options.debug]
*/
const generateSchemasDefinitions = async (options = {}) => {
const { strapi, outDir = process.cwd(), file = DEFAULT_OUT_FILENAME, debug = true } = options;
const schemas = getAllStrapiSchemas(strapi);
const schemasDefinitions = Object.values(schemas).map(schema => ({
schema,
definition: generateSchemaDefinition(schema),
}));
const allDefinitions = [
// Imports
generateImportDefinition(),
// Add a newline after the import statement
factory.createIdentifier('\n'),
// Schemas
...schemasDefinitions.reduce(
(acc, def) => [
...acc,
def.definition,
// Add a newline between each interface declaration
factory.createIdentifier('\n'),
],
[]
),
// Global
generateGlobalDefinition(schemasDefinitions),
];
const output = emitDefinitions(allDefinitions);
const formattedOutput = await format(output);
const definitionFilepath = await saveDefinitionToFileSystem(outDir, file, formattedOutput);
if (debug) {
logDebugInformation(schemasDefinitions, { filepath: definitionFilepath });
}
};
const emitDefinitions = definitions => {
const nodeArray = factory.createNodeArray(definitions);
const sourceFile = ts.createSourceFile(
'placeholder.ts',
'',
ts.ScriptTarget.ESNext,
true,
ts.ScriptKind.TS
);
const printer = ts.createPrinter({ newLine: true, omitTrailingSemicolon: true });
return printer.printList(ts.ListFormat.MultiLine, nodeArray, sourceFile);
};
const saveDefinitionToFileSystem = async (dir, file, content) => {
const filepath = path.join(dir, file);
await fse.writeFile(filepath, content);
return filepath;
};
/**
* Format the given definitions.
* Uses the existing config if one is defined in the project.
*
* @param {string} content
* @returns {string}
*/
const format = async content => {
const configFile = await prettier.resolveConfigFile();
const config = configFile
? await prettier.resolveConfig(configFile)
: // Default config
{
singleQuote: true,
useTabs: false,
tabWidth: 2,
};
Object.assign(config, { parser: 'typescript' });
return prettier.format(content, config);
};
const logDebugInformation = (definitions, options = {}) => {
const { filepath } = options;
const table = new CLITable({
head: [
chalk.bold(chalk.green('Model Type')),
chalk.bold(chalk.blue('UID')),
chalk.bold(chalk.blue('Type')),
chalk.bold(chalk.gray('Attributes Count')),
],
colAligns: ['center', 'left', 'left', 'center'],
});
const sortedDefinitions = definitions.map(def => ({
...def,
attributesCount: getDefinitionAttributesCount(def.definition),
}));
for (const { schema, attributesCount } of sortedDefinitions) {
const modelType = fp.upperFirst(getSchemaModelType(schema));
const interfaceType = getSchemaInterfaceName(schema.uid);
table.push([
chalk.greenBright(modelType),
chalk.blue(schema.uid),
chalk.blue(interfaceType),
chalk.grey(fp.isNil(attributesCount) ? 'N/A' : attributesCount),
]);
}
// Table
console.log(table.toString());
// Metrics
console.log(
chalk.greenBright(
`Generated ${definitions.length} type definition for your Strapi application's schemas.`
)
);
// Filepath
const relativePath = path.relative(process.cwd(), filepath);
console.log(
chalk.grey(`The definitions file has been generated here: ${chalk.bold(relativePath)}`)
);
};
module.exports = generateSchemasDefinitions;

View File

@ -0,0 +1,87 @@
'use strict';
const ts = require('typescript');
const { factory } = require('typescript');
const { isEmpty } = require('lodash/fp');
const { getSchemaExtendsTypeName, getSchemaInterfaceName, toTypeLitteral } = require('./utils');
const attributeToPropertySignature = require('./attributes');
const { addImport } = require('./imports');
/**
* Generate an interface declaration for a given schema
*
* @param {object} schema
* @returns {ts.InterfaceDeclaration}
*/
const generateSchemaDefinition = schema => {
const { uid } = schema;
// Resolve the different interface names needed to declare the schema's interface
const interfaceName = getSchemaInterfaceName(uid);
const parentType = getSchemaExtendsTypeName(schema);
// Make sure the extended interface are imported
addImport(parentType);
// Properties whose values can be mapped to a litteral type expression
const litteralPropertiesDefinitions = ['info', 'options', 'pluginOptions']
// Ignore non-existent or empty declarations
.filter(key => !isEmpty(schema[key]))
// Generate litteral definition for each property
.map(generatePropertyLitteralDefinitionFactory(schema));
// Generate the `attributes` litteral type definition
const attributesProp = generateAttributePropertySignature(schema);
// Merge every schema's definition in a single list
const schemaProperties = [...litteralPropertiesDefinitions, attributesProp];
// Generate the schema's interface declaration
const schemaType = factory.createInterfaceDeclaration(
undefined,
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
factory.createIdentifier(interfaceName),
undefined,
[
factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [
factory.createIdentifier(parentType),
]),
],
schemaProperties
);
return schemaType;
};
/**
* Generate a property signature for the schema's `attributes` field
*
* @param {object} schema
* @returns {ts.PropertySignature}
*/
const generateAttributePropertySignature = schema => {
const { attributes } = schema;
const properties = Object.entries(attributes).map(([attributeName, attribute]) => {
return attributeToPropertySignature(schema, attributeName, attribute);
});
return factory.createPropertySignature(
undefined,
factory.createIdentifier('attributes'),
undefined,
factory.createTypeLiteralNode(properties)
);
};
const generatePropertyLitteralDefinitionFactory = schema => key => {
return factory.createPropertySignature(
undefined,
factory.createIdentifier(key),
undefined,
toTypeLitteral(schema[key])
);
};
module.exports = { generateSchemaDefinition };

View File

@ -0,0 +1,145 @@
'use strict';
const { factory } = require('typescript');
const {
flow,
replace,
camelCase,
upperFirst,
isObject,
isString,
isNumber,
isArray,
isBoolean,
propEq,
} = require('lodash/fp');
/**
* Get all components and content-types in a Strapi application
*
* @param {Strapi} strapi
* @returns {object}
*/
const getAllStrapiSchemas = strapi => ({ ...strapi.contentTypes, ...strapi.components });
/**
* Extract a valid interface name from a schema uid
*
* @param {string} uid
* @returns {string}
*/
const getSchemaInterfaceName = flow(replace(/(:.)/, ' '), camelCase, upperFirst);
/**
* Get the parent type name to extend based on the schema's nature
*
* @param {object} schema
* @returns {string}
*/
const getSchemaExtendsTypeName = schema => {
const base = getSchemaModelType(schema);
return upperFirst(base) + 'Schema';
};
const getSchemaModelType = schema => {
const { modelType, kind } = schema;
// Components
if (modelType === 'component') {
return 'component';
}
// Content-Types
else if (modelType === 'contentType') {
return kind;
}
return null;
};
/**
* Get a type node based on a type and its params
*
* @param {string} typeName
* @param {ts.TypeNode[]} [params]
* @returns
*/
const getTypeNode = (typeName, params = []) => {
return factory.createTypeReferenceNode(factory.createIdentifier(typeName), params);
};
/**
* Transform a regular JavaScript object into an object litteral expression
* @param {object} data
* @returns {ts.ObjectLiteralExpression}
*/
const toTypeLitteral = data => {
if (isString(data)) {
return factory.createStringLiteral(data, true);
}
if (isNumber(data)) {
return factory.createNumericLiteral(data);
}
if (isBoolean(data)) {
return data ? factory.createTrue() : factory.createFalse();
}
if (isArray(data)) {
return factory.createTupleTypeNode(data.map(item => toTypeLitteral(item)));
}
if (!isObject(data)) {
throw new Error('Cannot convert to object litteral. Unknown type', typeof data);
}
const entries = Object.entries(data);
const props = entries.reduce((acc, [key, value]) => {
// Handle keys such as content-type-builder & co.
const identifier = key.includes('-')
? factory.createStringLiteral(key, true)
: factory.createIdentifier(key);
return [
...acc,
factory.createPropertyDeclaration(
undefined,
undefined,
identifier,
undefined,
toTypeLitteral(value)
),
];
}, []);
return factory.createTypeLiteralNode(props);
};
/**
* Get the number of attributes generated for a given schema definition
*
* @param {ts.TypeNode} definition
* @returns {number | null}
*/
const getDefinitionAttributesCount = definition => {
const attributesNode = definition.members.find(propEq('name.escapedText', 'attributes'));
if (!attributesNode) {
return null;
}
return attributesNode.type.members.length;
};
module.exports = {
getAllStrapiSchemas,
getSchemaInterfaceName,
getSchemaExtendsTypeName,
getSchemaModelType,
getDefinitionAttributesCount,
getTypeNode,
toTypeLitteral,
};

View File

@ -4,11 +4,13 @@ const compile = require('./compile');
const compilers = require('./compilers');
const admin = require('./admin');
const utils = require('./utils');
const generators = require('./generators');
module.exports = {
compile,
compilers,
admin,
generators,
...utils,
};

View File

@ -24,9 +24,13 @@
"lib": "./lib"
},
"dependencies": {
"lodash": "4.17.21",
"@strapi/strapi": "4.2.0",
"typescript": "4.6.2",
"chalk": "4.1.2",
"cli-table3": "0.6.2",
"fs-extra": "10.0.1",
"typescript": "4.6.2"
"lodash": "4.17.21",
"prettier": "2.7.1"
},
"engines": {
"node": ">=12.22.0 <=16.x.x",

View File

@ -9150,7 +9150,7 @@ cli-table3@0.6.1:
optionalDependencies:
colors "1.4.0"
cli-table3@^0.6.1:
cli-table3@0.6.2, cli-table3@^0.6.1:
version "0.6.2"
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.2.tgz#aaf5df9d8b5bf12634dc8b3040806a0c07120d2a"
integrity sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==
@ -19341,6 +19341,11 @@ prettier@1.19.1:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
prettier@2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
"prettier@>=2.2.1 <=2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18"