Skip to content

Commit

Permalink
feat(cli-utils): add hook and utils template
Browse files Browse the repository at this point in the history
  • Loading branch information
andresz1 committed Feb 22, 2023
1 parent 39124f5 commit e22d672
Show file tree
Hide file tree
Showing 37 changed files with 424 additions and 148 deletions.
19 changes: 19 additions & 0 deletions packages/utils/cli/bin/core/Logger.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import chalk from 'chalk'

export class Logger {
success(message) {
console.log(chalk.green(message))
}

error(message) {
console.log(chalk.red(message))
}

info(message) {
console.log(chalk.yellow(message))
}

warning(message) {
console.log(chalk.green(message))
}
}
45 changes: 45 additions & 0 deletions packages/utils/cli/bin/core/System.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import fse from 'fs-extra'
import glob from 'glob'

export class System {
logger

constructor({ logger }) {
this.logger = logger
}

exit(message) {
this.logger.error(`✖ Error: ${message}\n`)
process.exit(1)
}

getBasePath() {
return process.cwd()
}

getPackageJSON() {
const basePath = this.getBasePath()

const raw = fse.readFileSync(`${basePath}/package.json`).toString()

return JSON.parse(raw)
}

isPackageCreated(name) {
const base = this.getBasePath()
const packageJSON = this.getPackageJSON()

return packageJSON.workspaces.some(workspace => {
const packages = glob.sync(`${base}/${workspace}/`)

return packages.some(path => path.endsWith(`/${name}/`))
})
}

writeFile({ path, content }) {
return fse
.outputFile(path, content)
.then(() => this.logger.info(`Created ${path}`))
.catch(error => this.exit(`Failed creating ${path}`))
}
}
2 changes: 2 additions & 0 deletions packages/utils/cli/bin/core/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Logger } from './Logger.mjs'
export { System } from './System.mjs'
5 changes: 5 additions & 0 deletions packages/utils/cli/bin/generators/Generator.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class Generator {
execute() {
throw new Error('execute method should be implemented')
}
}
83 changes: 83 additions & 0 deletions packages/utils/cli/bin/generators/TemplateGenerator.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import glob from 'glob'
import { pascalCase } from 'pascal-case'
import { camelCase } from 'camel-case'

import { System } from '../core/index.mjs'
import { Generator } from './Generator.mjs'

export class TemplateGenerator extends Generator {
static TYPES = {
COMPONENT: 'component',
HOOK: 'hook',
UTIL: 'util',
}

static CONTEXTS = {
[TemplateGenerator.TYPES.COMPONENT]: 'components',
[TemplateGenerator.TYPES.HOOK]: 'components',
[TemplateGenerator.TYPES.UTIL]: 'utils',
}

constructor({ system }) {
super()
this.system = system
}

getDest({ type, name }) {
const basePath = this.system.getBasePath()
const context = TemplateGenerator.CONTEXTS[type]

return `${basePath}/packages/${context}/${name}`
}

getTemplatePaths({ type }) {
const pattern = fileURLToPath(new URL(`../../templates/${type}/**/*.js`, import.meta.url))

return new Promise((resolve, reject) => {
glob(pattern, async (error, paths) => {
if (error) {
return reject(error)
}

resolve(paths)
})
})
}

getTemplatePath({ path, name, type, dest }) {
const parsed = path
.replace(/(.*)\/templates\/([a-z-]+)\//, `${dest}/`)
.replaceAll(/\[|\]|\.js$/g, '')

if (type === TemplateGenerator.TYPES.COMPONENT) {
return parsed.replace('Component', pascalCase(name))
}

if (type === TemplateGenerator.TYPES.HOOK) {
return parsed.replace('name', camelCase(name))
}

return parsed
}

async execute({ type, name, description }) {
const dest = this.getDest({ type, name })
const paths = await this.getTemplatePaths({ type })

const promises = paths.map(path =>
import(path).then(module => ({
path: this.getTemplatePath({ path, name, type, dest }),
content: module.default({
name,
description,
}),
}))
)

const files = await Promise.all(promises)

return Promise.all(files.map(file => this.system.writeFile(file)))
}
}
2 changes: 2 additions & 0 deletions packages/utils/cli/bin/generators/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Generator } from './Generator.mjs'
export { TemplateGenerator } from './TemplateGenerator.mjs'
155 changes: 35 additions & 120 deletions packages/utils/cli/bin/spark-generate.mjs
Original file line number Diff line number Diff line change
@@ -1,156 +1,71 @@
#!/usr/bin/env node

import chalk from 'chalk'
import fse from 'fs-extra'
import * as prompt from '@clack/prompts'
import { fileURLToPath } from 'node:url'
import glob from 'glob'
import { pascalCase } from 'pascal-case'
import { log, showError, writeFile } from '../utils.js'

const BASE_DIR = process.cwd()
const rawRootPackageJSON = fse.readFileSync(`${BASE_DIR}/package.json`)
let rootPackageJSON = JSON.parse(rawRootPackageJSON)
import { TemplateGenerator } from './generators/index.mjs'
import { Logger, System } from './core/index.mjs'
import { DescriptionValidator, NameValidator } from './validators/index.mjs'

const TEMPLATE_TYPE = {
COMPONENT: 'component',
HOOK: 'hook',
}

const WORKSPACES = {
[TEMPLATE_TYPE.COMPONENT]: '/packages/components',
[TEMPLATE_TYPE.HOOK]: '/packages/hooks',
}

const ERRORS = {
ABORT: 'Aborted package generation',
NO_PKG_NAME: 'Package name must me defined',
INVALID_PKG_NAME: 'Name name must contain letters and dash symbols only (ex: "my-package")',
INVALID_DESCRIPTION: 'Description is too short (minimum is 10 chars)',
PKG_ALREADY_EXISTS:
'A package with that name already exists. Either delete it manually or use another name.',
}

const packageUtils = {
/** Validate the format of the package name (kebab case format) */
hasValidName: name => /^[a-z-]*$/.test(name),
/** Check that a package of the same name does not exists across all workspaces */
alreadyExists: name => {
return rootPackageJSON.workspaces.some(workspace => {
const existingPackages = glob.sync(`${BASE_DIR}/${workspace}/`)
return existingPackages.some(path => path.endsWith(`/${name}/`))
})
},
/** Retrieves the target folder of the generated package */
getDirectory: (name, template) => `${WORKSPACES[template]}/${name}/`,
/** Retrieves the full path to the folder of the generated package */
getFullPath: (name, template) => `${BASE_DIR}${packageUtils.getDirectory(name, template)}`,
}
const logger = new Logger()
const system = new System({ logger })
const generator = new TemplateGenerator({ system })

async function promptPackageName() {
export const run = async () => {
const name = await prompt.text({
message: 'Package name (must contain letters and dash symbols only):',
initialValue: '',
validate(value) {
if (value == null) return ERRORS.NO_PKG_NAME
if (!packageUtils.hasValidName(value)) return ERRORS.INVALID_PKG_NAME
if (packageUtils.alreadyExists(value)) return ERRORS.PKG_ALREADY_EXISTS
const validator = new NameValidator({ system })

return validator.validate(value)
},
})

if (prompt.isCancel(name)) showError(ERRORS.ABORT)

return name
}
if (prompt.isCancel(name)) {
system.exit('Aborted package generation')
}

async function promptPackageTemplate() {
const template = await prompt.select({
const type = await prompt.select({
message: 'Chose a template:',
initialValue: TEMPLATE_TYPE.COMPONENT,
initialValue: 'component',
options: [
{
value: TEMPLATE_TYPE.COMPONENT,
value: TemplateGenerator.TYPES.COMPONENT,
label: 'Component',
hint: 'Typescript dummy component with some tests, stories and config files',
hint: 'TypeScript component package',
},
{
value: TEMPLATE_TYPE.HOOK,
value: TemplateGenerator.TYPES.HOOK,
label: 'Hook',
hint: 'Typescript hook with some tests, stories and config files',
hint: 'TypeScript hook package',
},
{
value: TemplateGenerator.TYPES.UTIL,
label: 'Utility',
hint: 'TypeScript utility package',
},
],
})

if (prompt.isCancel(template)) showError(ERRORS.ABORT)

return template
}
if (prompt.isCancel(type)) {
system.exit('Aborted package generation')
}

async function promptPackageDescription() {
const description = await prompt.text({
message: 'Describe your package (short description):',
initialValue: '',
validate(value) {
if (!value) return `You package must have a description`
if (value.length < 10) return ERRORS.INVALID_DESCRIPTION
const validator = new DescriptionValidator()

return validator.validate(value)
},
})

if (prompt.isCancel(description)) showError(ERRORS.ABORT)

return description
}

/**
* Program starts here
*/
prompt.intro(`Generate Spark package`)

const name = await promptPackageName()
const template = await promptPackageTemplate()
const description = await promptPackageDescription()

const packagePath = packageUtils.getFullPath(name, template)

switch (template) {
case TEMPLATE_TYPE.COMPONENT:
generateComponentPackage(name, description)
break
case TEMPLATE_TYPE.HOOK:
generateHookPackage(name, description)
break
}

prompt.outro(`Generating package...`)

function generateComponentPackage(name, description) {
const templatesPattern = fileURLToPath(new URL('../templates/**/*.js', import.meta.url))

glob(templatesPattern, async (err, res) => {
if (err) showError(err)
if (res) {
const templateContents = res.map(templatePath =>
import(templatePath).then(module => ({
path: templatePath
.replace(/(.*)\/templates\//, packagePath)
.replace('Component', pascalCase(name))
.replaceAll(/\[|\]|\.js$/g, ''),
content: module.default({
component: name,
description: description,
}),
}))
)
if (prompt.isCancel(description)) {
system.exit('Aborted package generation')
}

const filesToWrite = await Promise.all(templateContents)

Promise.all(filesToWrite.map(writeFile)).then(() => {
log.success('All package files has been properly written!')
})
}
})
generator.execute({ name, type, description })
}

function generateHookPackage(name, description) {
showError('Todo: template for hook packages is not ready yet.')
}
run()
10 changes: 6 additions & 4 deletions packages/utils/cli/bin/spark-setup-theme.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { join, extname, parse, sep } from 'path'
import { readFileSync, readdirSync, writeFileSync, unlinkSync } from 'fs'
import { transformSync } from 'esbuild'

import { log, showError } from '../utils.js'
const logger = new Logger()
const system = new System({ logger })

const jsFileExtension = '.js'

Expand All @@ -14,7 +15,7 @@ const configFile = readdirSync(process.cwd()).find(fileName =>
)

if (!configFile) {
showError(
system.exit(
"We couldn't find a `spark.theme.config` file in this folder. Please make sure that the file is located in the root folder of your project"
)
}
Expand All @@ -24,8 +25,9 @@ const filePath = join(process.cwd(), configFile)

const allowedExtensions = ['.ts', '.mts', '.cts', '.js', '.cjs', '.mjs']
const fileExtension = extname(filePath)

if (!allowedExtensions.includes(fileExtension)) {
showError(`Your spark.theme.config file extension (${fileExtension}) is not supported.`)
system.exit(`Your spark.theme.config file extension (${fileExtension}) is not supported.`)
}

const tsCode = readFileSync(filePath, 'utf-8')
Expand All @@ -42,6 +44,6 @@ const child = spawn(process.execPath, [jsFilePath], {

child.on('exit', code => {
if (!configFileIsInJS) unlinkSync(jsFilePath)
log.success('✨ Your Spark theme config files have been successfully created!')
logger.success('✨ Your Spark theme config files have been successfully created!')
process.exit(code)
})
Loading

0 comments on commit e22d672

Please sign in to comment.