mirror of
https://github.com/strapi/strapi.git
synced 2025-11-01 02:16:03 +00:00
Update generator. Use typescript compiler API to generate definitions
This commit is contained in:
parent
44d216a028
commit
6c9b500586
@ -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
|
||||
|
||||
@ -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();
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
@ -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';
|
||||
`;
|
||||
},
|
||||
};
|
||||
@ -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'],
|
||||
});
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
@ -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 };
|
||||
|
||||
@ -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
|
||||
|
||||
7
packages/utils/typescript/lib/generators/index.js
Normal file
7
packages/utils/typescript/lib/generators/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const generateSchemasDefinitions = require('./schemas');
|
||||
|
||||
module.exports = {
|
||||
generateSchemasDefinitions,
|
||||
};
|
||||
241
packages/utils/typescript/lib/generators/schemas/attributes.js
Normal file
241
packages/utils/typescript/lib/generators/schemas/attributes.js
Normal 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;
|
||||
68
packages/utils/typescript/lib/generators/schemas/global.js
Normal file
68
packages/utils/typescript/lib/generators/schemas/global.js
Normal 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 };
|
||||
33
packages/utils/typescript/lib/generators/schemas/imports.js
Normal file
33
packages/utils/typescript/lib/generators/schemas/imports.js
Normal 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
|
||||
);
|
||||
},
|
||||
};
|
||||
173
packages/utils/typescript/lib/generators/schemas/index.js
Normal file
173
packages/utils/typescript/lib/generators/schemas/index.js
Normal 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;
|
||||
87
packages/utils/typescript/lib/generators/schemas/schema.js
Normal file
87
packages/utils/typescript/lib/generators/schemas/schema.js
Normal 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 };
|
||||
145
packages/utils/typescript/lib/generators/schemas/utils.js
Normal file
145
packages/utils/typescript/lib/generators/schemas/utils.js
Normal 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,
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user