From c92e8e634fa26957a1946c0a63fed6e98c81db5c Mon Sep 17 00:00:00 2001 From: Isac Petinate Date: Tue, 21 May 2024 11:03:15 -0300 Subject: [PATCH] feat: implementing advanced command #wip --- .clingon/presets/generator.json | 18 +++ .clingon/presets/util.json | 18 +++ src/actions/scaffold.js | 58 +++++++--- src/generators/scaffold-template.js | 136 ++++++++++++++++++++++ src/generators/scaffold-template.test.js | 13 +++ src/schemas/custom-template.js | 29 +++++ src/types.js | 30 ++++- src/utils/scaffold-action.js | 137 +++++++++++++++++++++++ src/utils/scaffold-action.test.js | 9 ++ src/validators/validator.js | 35 ++++++ 10 files changed, 468 insertions(+), 15 deletions(-) create mode 100644 .clingon/presets/generator.json create mode 100644 .clingon/presets/util.json create mode 100644 src/generators/scaffold-template.js create mode 100644 src/generators/scaffold-template.test.js create mode 100644 src/schemas/custom-template.js create mode 100644 src/utils/scaffold-action.js create mode 100644 src/utils/scaffold-action.test.js create mode 100644 src/validators/validator.js diff --git a/.clingon/presets/generator.json b/.clingon/presets/generator.json new file mode 100644 index 0000000..ec4e872 --- /dev/null +++ b/.clingon/presets/generator.json @@ -0,0 +1,18 @@ +{ + "framework": null, + "cssFramework": "css_vanilla", + "testFramework": "jest", + "version": null, + "name": "scaffold-template", + "resourcePath": "src/generators", + "testPath": "src/generators", + "storyPath": null, + "testPostfix": "test", + "storyPostfix": "stories", + "type": "function", + "typescript": false, + "withStory": false, + "withTest": true, + "withTestingLibrary": false, + "folderWrapper": false +} diff --git a/.clingon/presets/util.json b/.clingon/presets/util.json new file mode 100644 index 0000000..dbd3bfa --- /dev/null +++ b/.clingon/presets/util.json @@ -0,0 +1,18 @@ +{ + "framework": null, + "cssFramework": "css_vanilla", + "testFramework": "jest", + "version": null, + "name": "scaffold-template", + "resourcePath": "src/utils", + "testPath": "src/utils", + "storyPath": null, + "testPostfix": "test", + "storyPostfix": "stories", + "type": "function", + "typescript": false, + "withStory": false, + "withTest": true, + "withTestingLibrary": false, + "folderWrapper": false +} \ No newline at end of file diff --git a/src/actions/scaffold.js b/src/actions/scaffold.js index a273fde..5e84db6 100644 --- a/src/actions/scaffold.js +++ b/src/actions/scaffold.js @@ -1,20 +1,52 @@ -import { readFileContent } from '../utils/file.js' -import { parse as parseYaml } from 'yaml' +import { join } from 'node:path' -export function scaffoldAction(name, options) { - const template = getTemplateFomMetaFile(options.template) -} +import { buildFromTemplate } from '../generators/scaffold-template.js' + +import { + getTemplateFromMetaFile, + validateTemplate +} from '../utils/scaffold-action.js' + +/** + * Build resources from local custom templates + * + * @typedef {"template"} Options + * + * @param {string} name + * @param {Record} options + */ -export function getTemplateFomMetaFile() { - try { - const path = join(process.cwd(), '.clingon', 'templates', 'meta.yaml') +export async function scaffoldAction(name, options) { + /** + * Templates folder path + */ + const basePath = join(process.cwd(), '.clingon', 'templates') - const fileContent = readFileContent(path) + /** + * Templates from meta file + */ + const template = getTemplateFromMetaFile(options.template) + + /** + * Template already be validated and flow can continue + */ + const alreadyOkContinue = validateTemplate(template) + + if (!alreadyOkContinue) { + throw new Error( + 'Template has many errors, please, review your meta at: ', + basePath + ) + } - const parsedYamlFileContent = yaml + /** + * Resources already be created + */ + const created = await buildFromTemplate(name, template) - return parsedYamlFileContent - } catch (error) { - console.error(error) + if (created) { + console.info('Success') + } else { + console.info('Fail') } } diff --git a/src/generators/scaffold-template.js b/src/generators/scaffold-template.js new file mode 100644 index 0000000..aeb17aa --- /dev/null +++ b/src/generators/scaffold-template.js @@ -0,0 +1,136 @@ +import { compose } from '../utils/compose.js' +import { convertCase, splitPathString } from '../utils/string.js' +import { readFileContent } from '../utils/file.js' +import { replaceContentFromSideResource } from '../utils/scaffold-action.js' +import { checkDirectoriesTree } from '../utils/directory.js' + +/** + * @typedef {Record, string>} TemplatesContent + * + * @typedef {{name: string, template: import('../types').CustomTemplate, templatesContent: TemplatesContent}} CommomReturn + */ + +/** + * Build resources from template + * + * @param {string} name Resource name from arguments + * @param {import("../types").CustomTemplate} template Local template from meta file + * @returns {Promise} + */ +export async function buildFromTemplate(name, template) { + const result = compose( + getTemplatesData(name, template), + handleTemplateReplacements, + checkPaths, + createResources + ) + + return result +} + +/** + * Get templates from local dir + * + * @param {string} name Resource name from arguments + * @param {import('../types').CustomTemplate} template Template data from meta file + * + * @returns {CommomReturn} + */ +function getTemplatesData(name, template) { + return () => { + /** + * @type {TemplatesContent} + */ + const templatesContent = { + resource: readFileContent(template.resource.template) + } + + if (template.test) { + templatesContent.test = readFileContent(template.test.template) + } + + if (template.story) { + templatesContent.story = readFileContent(template.story.template) + } + + return { name, template, templatesContent } + } +} + +/** + * Replace text occurrences inside template + * + * @param {ReturnType} param0 - Template data and templates content + */ +function handleTemplateReplacements({ name, template, templatesContent }) { + name = convertCase('PascalCase', name) + + templatesContent.resource = templatesContent.resource.replace( + /ResourceName/g, + name + ) + + if (templatesContent.story) { + templatesContent.story = replaceContentFromSideResource( + name, + templatesContent.story, + template + ) + } + + if (templatesContent.test) { + templatesContent.test = replaceContentFromSideResource( + name, + templatesContent.test, + template + ) + } + + return { name, template, templatesContent } +} + +/** + * Check templates path and ask if not exists + * + * @param {ReturnType} param0 Template data from meta file + */ +function checkPaths({ name, template, templatesContent }) { + const targets = { + resource: false, + test: false, + story: false + } + + targets.resource = checkDirectoriesTree( + splitPathString(template.resource.path) + ) + + if (templatesContent.test) { + targets.test = checkDirectoriesTree(splitPathString(template.test.path)) + } + + if (templatesContent.story) { + targets.story = checkDirectoriesTree(splitPathString(template.story.path)) + } + + return { name, template, templatesContent, targets } +} + +/** + * + * @param {ReturnType} param0 Template data from meta file + * @returns {boolean} + */ +function createResources({ name, targets, template, templatesContent }) { + if (targets.resource) { + // TODO: generate file + } + if (targets.test) { + // TODO: generate file + } + if (targets.style) { + // TODO: generate file + } + + return true +} diff --git a/src/generators/scaffold-template.test.js b/src/generators/scaffold-template.test.js new file mode 100644 index 0000000..5bdf289 --- /dev/null +++ b/src/generators/scaffold-template.test.js @@ -0,0 +1,13 @@ +import assert from 'node:assert/strict' + +import { describe, it } from 'node:test' + +import { scaffoldTemplate } from './scaffold-template' + +describe('scaffoldTemplate', () => { + it('should works properly', () => { + const result = scaffoldTemplate() + + assert.strictEqual(result, true) + }) +}) diff --git a/src/schemas/custom-template.js b/src/schemas/custom-template.js new file mode 100644 index 0000000..cf2f745 --- /dev/null +++ b/src/schemas/custom-template.js @@ -0,0 +1,29 @@ +/** + * Define the type map for TemplateResource + * + * @type {Record} + */ +export const templateResourceTypeMap = { + index: 'boolean', + path: 'string', + template: 'string' +} + +/** + * Define the type map for CustomTemplate + * + * @type {Record } + */ +export const customTemplateTypeMap = { + identifier: 'string', + folderWrapper: 'boolean', + resource: templateResourceTypeMap, + test: { + path: 'string', + template: 'string' + }, + story: { + path: 'string', + template: 'string' + } +} diff --git a/src/types.js b/src/types.js index ad87db9..dff2207 100644 --- a/src/types.js +++ b/src/types.js @@ -1,7 +1,15 @@ import { ResourceTypeEnum } from 'enums/resource-type.js' import { FileExtensionEnum } from 'enums/file-extension.js' -import { FrameworkEnum, TestFrameworkEnum, CssFrameworkEnum } from 'enums/frameworks.js' -import { StoryPostfixEnum, TestPostfixEnum, StylePostfixEnum } from 'enums/postfixes.js' +import { + FrameworkEnum, + TestFrameworkEnum, + CssFrameworkEnum +} from 'enums/frameworks.js' +import { + StoryPostfixEnum, + TestPostfixEnum, + StylePostfixEnum +} from 'enums/postfixes.js' /** * @typedef {keyof typeof ResourceTypeEnum} Resource - Resource type @@ -83,6 +91,24 @@ import { StoryPostfixEnum, TestPostfixEnum, StylePostfixEnum } from 'enums/postf * @typedef {"options" | "setup"} VueApi - Vue api template syntax * * @typedef {"class" | "functional"} ReactComponentVariant - React component type variant (class or functional) + * + * @typedef {{ + * index?: boolean + * path: string + * template: string + * }} TemplateResource - Template resource definition + * + * @typedef {{ + * identifier: string + * folderWrapper: boolean + * resource: TemplateResource + * test?: Omit + * story?: Omit + * }} CustomTemplate - Custom template from meta file + * + * @typedef {"bigint" | "boolean" | "function" | "number" | "object" | "string" | "symbol" | "undefined"} Primitives - JS Primities */ +typeof 1 == '' + export default {} diff --git a/src/utils/scaffold-action.js b/src/utils/scaffold-action.js new file mode 100644 index 0000000..90bcd03 --- /dev/null +++ b/src/utils/scaffold-action.js @@ -0,0 +1,137 @@ +import { parse as parseYaml } from 'yaml' + +import { validateObject } from '../validators/validator.js' +import { customTemplateTypeMap } from '../schemas/custom-template.js' + +import { checkFileExists, readFileContent } from '../utils/file.js' +import { removePostfixAndExt } from './file-extension.js' + +/** + * Get template from meta file + * + * @param {string} templateName Template name to find inside meta templates + * @returns {CustomTemplate[] | Error} + */ +export function getTemplateFromMetaFile(templateName) { + try { + const { path, type } = getMetaFilePath() + + const fileContent = readFileContent(path) + + /** + * @type {import('../types.js').CustomTemplate[]} + */ + const templates = null + + switch (type) { + case 'json': + templates = JSON.parse(fileContent) + case 'yaml': + templates = parseYaml(fileContent) + default: + break + } + + const template = templates.find((templ) => { + return templ.identifier === templateName + }) + + return template + } catch (error) { + console.error(error) + } +} + +/** + * Get meta file path from `.clingon/templates` folder + * + * @returns {{ path: string, type: "yaml" | "json" }} + */ +export function getMetaFilePath() { + /** + * Templates folder path + */ + const basePath = join(process.cwd(), '.clingon', 'templates') + + try { + const yamlPath = join(basePath, 'meta.yaml') + const yamlExists = checkFileExists(yamlPath) + + if (yamlExists) return { path: yamlPath, type: 'yaml' } + + const jsonPath = join(basePath, 'meta.json') + const jsonExists = checkFileExists(jsonPath) + + if (jsonExists) return { path: jsonPath, type: 'json' } + + throw new Error( + 'Meta file is not defined, run `npx clingon init --template`' + ) + } catch (error) { + throw new Error(error) + } +} + +/** + * Validate template data + * + * @param {CustomTemplate} template + * @returns {boolean} - Returns true if the template is valid, false otherwise. + */ +export function validateTemplate(template) { + return validateObject(template, customTemplateTypeMap) +} + +/** + * Get file name from template path + * + * @param {string} templatePath Template path from meta file + */ +export function getFileMetadata(templatePath) { + const fileName = getLastItem(templatePath, '/') + const extension = getLastItem(fileName, '.') + + return { fileName, extension } +} + +/** + * Split a string based on pattern and return last item from array + * + * @param {string} pattern Pattern to split string + * @param {string} text Text to be splitted + * + * @returns {string} + */ +export function getLastItem(text, pattern) { + const pieces = text.split(pattern) + + return pieces[pieces.length - 1] +} + +export function replaceContentFromSideResource(name, content, template) { + content = content.replace(/ResourceName/g, name) + + const { extension } = getFileMetadata(template.story.template) + + const fullPath = join(template.story.path, name + extension) + + content = replaceResourcePath(fullPath) + + return content +} + +/** + * Replace resource path inside template content + * + * @param {string} fullPath Resource full path + * @param {string} fileContent File content from template + * + * @returns {string} + */ +export function replaceResourcePath(fullPath, fileContent) { + const resourcePath = removePostfixAndExt(fullPath) + + fileContent = fileContent.replace(/resourcePath/g, resourcePath) + + return fileContent +} diff --git a/src/utils/scaffold-action.test.js b/src/utils/scaffold-action.test.js new file mode 100644 index 0000000..9bd28a3 --- /dev/null +++ b/src/utils/scaffold-action.test.js @@ -0,0 +1,9 @@ +import { scaffoldAction } from 'src/utils/scaffold-action' + +describe('scaffoldAction', () => { + it('should works properly', () => { + const result = scaffoldAction() + + expect(result).toBeDefined() + }) +}) diff --git a/src/validators/validator.js b/src/validators/validator.js new file mode 100644 index 0000000..30b0eb0 --- /dev/null +++ b/src/validators/validator.js @@ -0,0 +1,35 @@ +/** + * Generic function to validate an object against a type map + * + * @param {Object} obj - The object to validate + * @param {Object} typeMap - The type map to validate against + * @param {boolean} [allowPartial=false] - Whether to allow partial validation + * + * @returns {boolean} - Returns true if the object is valid, false otherwise. + */ +export function validateObject(obj, typeMap, allowPartial = false) { + if (typeof obj !== 'object' || obj === null) { + return false + } + + for (const key in typeMap) { + const expectedType = typeMap[key] + const value = obj[key] + + if (value === undefined && allowPartial) { + continue + } + + if (typeof expectedType === 'object') { + if (!validateObject(value, expectedType)) { + return false + } + } else { + if (typeof value !== expectedType) { + return false + } + } + } + + return true +}