-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli-utils): add hook and utils template
- Loading branch information
Showing
37 changed files
with
424 additions
and
148 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { Logger } from './Logger.mjs' | ||
export { System } from './System.mjs' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { Generator } from './Generator.mjs' | ||
export { TemplateGenerator } from './TemplateGenerator.mjs' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.