@strapi/typescript-utils
Version:
Typescript support for Strapi
217 lines (176 loc) • 5.21 kB
JavaScript
;
const path = require('path');
const assert = require('assert');
const ts = require('typescript');
const fse = require('fs-extra');
const chalk = require('chalk');
const { factory } = ts;
const MODULE_DECLARATION = '@strapi/strapi';
const PUBLIC_NAMESPACE = 'Public';
/**
* Aggregate the given TypeScript nodes into a single string
*
* @param {ts.Node[]} definitions
* @return {string}
*/
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({ omitTrailingSemicolon: true });
return printer.printList(ts.ListFormat.MultiLine, nodeArray, sourceFile);
};
/**
* Save the given string representation of TS nodes in a file
* If the given directory doesn't exist, it'll be created automatically
*
* @param {string} dir
* @param {string} file
* @param {string} content
*
* @return {Promise<string>} The path of the created file
*/
const saveDefinitionToFileSystem = async (dir, file, content) => {
const filepath = path.join(dir, file);
fse.ensureDirSync(dir);
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 {Promise<string>}
*/
const format = async (content) => {
// eslint-disable-next-line node/no-unsupported-features/es-syntax
const prettier = await import('prettier'); // ESM-only
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);
};
/**
* Generate the extension block for a shared component from strapi/strapi
*
* @param {string} registry The registry to extend
* @param {Array<{ uid: string; definition: ts.TypeNode }>} definitions
* @returns {ts.ModuleDeclaration}
*/
const generateSharedExtensionDefinition = (registry, definitions) => {
const properties = definitions.map(({ uid, definition }) =>
factory.createPropertySignature(
undefined,
factory.createStringLiteral(uid, true),
undefined,
factory.createTypeReferenceNode(factory.createIdentifier(definition.name.escapedText))
)
);
return factory.createModuleDeclaration(
[factory.createModifier(ts.SyntaxKind.DeclareKeyword)],
factory.createStringLiteral(MODULE_DECLARATION, true),
factory.createModuleBlock([
factory.createModuleDeclaration(
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
factory.createIdentifier(PUBLIC_NAMESPACE),
factory.createModuleBlock(
properties.length > 0
? [
factory.createInterfaceDeclaration(
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
factory.createIdentifier(registry),
undefined,
undefined,
properties
),
]
: []
)
),
]),
ts.NodeFlags.ExportContext
);
};
const createLogger = (options = {}) => {
const { silent = false, debug = false } = options;
const state = { errors: 0, warning: 0 };
return {
get warnings() {
return state.warning;
},
get errors() {
return state.errors;
},
debug(...args) {
if (silent || !debug) {
return;
}
console.log(chalk.cyan(`[DEBUG]\t[${new Date().toISOString()}] (Typegen)`), ...args);
},
info(...args) {
if (silent) {
return;
}
console.info(chalk.blue(`[INFO]\t[${new Date().toISOString()}] (Typegen)`), ...args);
},
warn(...args) {
state.warning += 1;
if (silent) {
return;
}
console.warn(chalk.yellow(`[WARN]\t[${new Date().toISOString()}] (Typegen)`), ...args);
},
error(...args) {
state.errors += 1;
if (silent) {
return;
}
console.error(chalk.red(`[ERROR]\t[${new Date().toISOString()}] (Typegen)`), ...args);
},
};
};
const timer = () => {
const state = {
start: null,
end: null,
};
return {
start() {
assert(state.start === null, 'The timer has already been started');
assert(state.end === null, 'The timer has already been ended');
state.start = Date.now();
return this;
},
end() {
assert(state.start !== null, 'The timer needs to be started before ending it');
assert(state.end === null, 'The timer has already been ended');
state.end = Date.now();
return this;
},
get duration() {
assert(state.start !== null, 'The timer has not been started');
return ((state.end ?? Date.now) - state.start) / 1000;
},
};
};
module.exports = {
emitDefinitions,
saveDefinitionToFileSystem,
format,
generateSharedExtensionDefinition,
createLogger,
timer,
};