@spark-ui/cli-utils 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [1.2.0](https://github.com/adevinta/spark/compare/@spark-ui/cli-utils@1.1.1...@spark-ui/cli-utils@1.2.0) (2023-02-15)
7
+
8
+ ### Features
9
+
10
+ - **cli-utils:** prompt system for spark generate command ([12aec9b](https://github.com/adevinta/spark/commit/12aec9bb2af9aacdf2337e0de92e97ea608db962)), closes [#237](https://github.com/adevinta/spark/issues/237) [#237](https://github.com/adevinta/spark/issues/237)
11
+
6
12
  ## [1.1.1](https://github.com/adevinta/spark/compare/@spark-ui/cli-utils@1.1.0...@spark-ui/cli-utils@1.1.1) (2023-02-15)
7
13
 
8
14
  ### Bug Fixes
@@ -1,56 +1,156 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import chalk from 'chalk'
4
+ import fse from 'fs-extra'
5
+ import * as prompt from '@clack/prompts'
3
6
  import { fileURLToPath } from 'node:url'
4
- import { program } from 'commander'
5
7
  import glob from 'glob'
6
8
  import { pascalCase } from 'pascal-case'
7
9
  import { log, showError, writeFile } from '../utils.js'
8
10
 
9
11
  const BASE_DIR = process.cwd()
10
- const WORDS_ONLY_REGEX = /^[A-Za-z-]*$/
11
-
12
- program
13
- .option('-D, --description <description>', 'Description of this component')
14
- .on('--help', () => {
15
- console.log('Examples:')
16
- console.log(' $ spark generate button')
17
- console.log(' $ spark generate button -D "My button description"')
18
- console.log(' $ spark generate button --description "My button description"')
19
- console.log(' $ spark generate --help')
12
+ const rawRootPackageJSON = fse.readFileSync(`${BASE_DIR}/package.json`)
13
+ let rootPackageJSON = JSON.parse(rawRootPackageJSON)
14
+
15
+ const TEMPLATE_TYPE = {
16
+ COMPONENT: 'component',
17
+ HOOK: 'hook',
18
+ }
19
+
20
+ const WORKSPACES = {
21
+ [TEMPLATE_TYPE.COMPONENT]: '/packages/components',
22
+ [TEMPLATE_TYPE.HOOK]: '/packages/hooks',
23
+ }
24
+
25
+ const ERRORS = {
26
+ ABORT: 'Aborted package generation',
27
+ NO_PKG_NAME: 'Package name must me defined',
28
+ INVALID_PKG_NAME: 'Name name must contain letters and dash symbols only (ex: "my-package")',
29
+ INVALID_DESCRIPTION: 'Description is too short (minimum is 10 chars)',
30
+ PKG_ALREADY_EXISTS:
31
+ 'A package with that name already exists. Either delete it manually or use another name.',
32
+ }
33
+
34
+ const packageUtils = {
35
+ /** Validate the format of the package name (kebab case format) */
36
+ hasValidName: name => /^[a-z-]*$/.test(name),
37
+ /** Check that a package of the same name does not exists across all workspaces */
38
+ alreadyExists: name => {
39
+ return rootPackageJSON.workspaces.some(workspace => {
40
+ const existingPackages = glob.sync(`${BASE_DIR}/${workspace}/`)
41
+ return existingPackages.some(path => path.endsWith(`/${name}/`))
42
+ })
43
+ },
44
+ /** Retrieves the target folder of the generated package */
45
+ getDirectory: (name, template) => `${WORKSPACES[template]}/${name}/`,
46
+ /** Retrieves the full path to the folder of the generated package */
47
+ getFullPath: (name, template) => `${BASE_DIR}${packageUtils.getDirectory(name, template)}`,
48
+ }
49
+
50
+ async function promptPackageName() {
51
+ const name = await prompt.text({
52
+ message: 'Package name (must contain letters and dash symbols only):',
53
+ initialValue: '',
54
+ validate(value) {
55
+ if (value == null) return ERRORS.NO_PKG_NAME
56
+ if (!packageUtils.hasValidName(value)) return ERRORS.INVALID_PKG_NAME
57
+ if (packageUtils.alreadyExists(value)) return ERRORS.PKG_ALREADY_EXISTS
58
+ },
59
+ })
60
+
61
+ if (prompt.isCancel(name)) showError(ERRORS.ABORT)
62
+
63
+ return name
64
+ }
65
+
66
+ async function promptPackageTemplate() {
67
+ const template = await prompt.select({
68
+ message: 'Chose a template:',
69
+ initialValue: TEMPLATE_TYPE.COMPONENT,
70
+ options: [
71
+ {
72
+ value: TEMPLATE_TYPE.COMPONENT,
73
+ label: 'Component',
74
+ hint: 'Typescript dummy component with some tests, stories and config files',
75
+ },
76
+ {
77
+ value: TEMPLATE_TYPE.HOOK,
78
+ label: 'Hook',
79
+ hint: 'Typescript hook with some tests, stories and config files',
80
+ },
81
+ ],
20
82
  })
21
- .parse(process.argv)
22
83
 
23
- const [component] = program.args
24
- const { description = '' } = program.opts()
84
+ if (prompt.isCancel(template)) showError(ERRORS.ABORT)
25
85
 
26
- if (!component) {
27
- showError('Argument "component" must be defined!')
86
+ return template
28
87
  }
29
- if (!WORDS_ONLY_REGEX.test(component)) {
30
- showError('"component" name must contain letters and dash symbols only')
88
+
89
+ async function promptPackageDescription() {
90
+ const description = await prompt.text({
91
+ message: 'Describe your package (short description):',
92
+ initialValue: '',
93
+ validate(value) {
94
+ if (!value) return `You package must have a description`
95
+ if (value.length < 10) return ERRORS.INVALID_DESCRIPTION
96
+ },
97
+ })
98
+
99
+ if (prompt.isCancel(description)) showError(ERRORS.ABORT)
100
+
101
+ return description
31
102
  }
32
103
 
33
- const componentDir = `/packages/components/${component}/`
34
- const componentPath = `${BASE_DIR}${componentDir}`
35
- const templatesPattern = fileURLToPath(new URL('../templates/**/*.js', import.meta.url))
104
+ /**
105
+ * Program starts here
106
+ */
107
+ prompt.intro(`Generate Spark package`)
36
108
 
37
- glob(templatesPattern, async (err, res) => {
38
- if (err) showError(err)
39
- if (res) {
40
- const templateContents = res.map(templatePath =>
41
- import(templatePath).then(module => ({
42
- path: templatePath
43
- .replace(/(.*)\/templates\//, componentPath)
44
- .replace('Component', pascalCase(component))
45
- .replaceAll(/\[|\]|\.js$/g, ''),
46
- content: module.default({ component, description }),
47
- }))
48
- )
109
+ const name = await promptPackageName()
110
+ const template = await promptPackageTemplate()
111
+ const description = await promptPackageDescription()
49
112
 
50
- const filesToWrite = await Promise.all(templateContents)
113
+ const packagePath = packageUtils.getFullPath(name, template)
51
114
 
52
- Promise.all(filesToWrite.map(writeFile)).then(() => {
53
- log('All component files have been properly written!')
54
- })
55
- }
56
- })
115
+ switch (template) {
116
+ case TEMPLATE_TYPE.COMPONENT:
117
+ generateComponentPackage(name, description)
118
+ break
119
+ case TEMPLATE_TYPE.HOOK:
120
+ generateHookPackage(name, description)
121
+ break
122
+ }
123
+
124
+ prompt.outro(`Generating package...`)
125
+
126
+ function generateComponentPackage(name, description) {
127
+ const templatesPattern = fileURLToPath(new URL('../templates/**/*.js', import.meta.url))
128
+
129
+ glob(templatesPattern, async (err, res) => {
130
+ if (err) showError(err)
131
+ if (res) {
132
+ const templateContents = res.map(templatePath =>
133
+ import(templatePath).then(module => ({
134
+ path: templatePath
135
+ .replace(/(.*)\/templates\//, packagePath)
136
+ .replace('Component', pascalCase(name))
137
+ .replaceAll(/\[|\]|\.js$/g, ''),
138
+ content: module.default({
139
+ component: name,
140
+ description: description,
141
+ }),
142
+ }))
143
+ )
144
+
145
+ const filesToWrite = await Promise.all(templateContents)
146
+
147
+ Promise.all(filesToWrite.map(writeFile)).then(() => {
148
+ log.success('All package files has been properly written!')
149
+ })
150
+ }
151
+ })
152
+ }
153
+
154
+ function generateHookPackage(name, description) {
155
+ showError('Todo: template for hook packages is not ready yet.')
156
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spark-ui/cli-utils",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Spark CLI utils",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -11,11 +11,12 @@
11
11
  },
12
12
  "type": "module",
13
13
  "devDependencies": {
14
+ "@clack/prompts": "^0.2.2",
14
15
  "chalk": "5.2.0",
15
16
  "commander": "10.0.0",
16
17
  "fs-extra": "11.1.0",
17
18
  "glob": "8.1.0",
18
19
  "pascal-case": "3.1.2"
19
20
  },
20
- "gitHead": "1857690fc7d399de062fcdee6b7b03feacbc791f"
21
+ "gitHead": "3df014946de14807674052ae9a6624a11b9c725b"
21
22
  }
package/utils.js CHANGED
@@ -3,15 +3,20 @@ import fs from 'node:fs'
3
3
  import chalk from 'chalk'
4
4
  import fse from 'fs-extra'
5
5
 
6
- export const log = msg => console.log(chalk.gray(msg)) // eslint-disable-line no-console
6
+ export const log = {
7
+ success: msg => console.log(chalk.green(msg)), // eslint-disable-line no-console
8
+ error: msg => console.log(chalk.red(msg)), // eslint-disable-line no-console
9
+ info: msg => console.log(chalk.yellow(msg)), // eslint-disable-line no-console
10
+ warning: msg => console.log(chalk.orange(msg)), // eslint-disable-line no-console
11
+ }
7
12
 
8
13
  export const showError = (msg, foreignProgram) => {
9
- console.error(chalk.red(`✖ Error: ${msg}\n`))
14
+ log.error(`✖ Error: ${msg}\n`)
10
15
  process.exit(1)
11
16
  }
12
17
 
13
18
  export const writeFile = ({ path, content }) =>
14
19
  fse
15
20
  .outputFile(path, content)
16
- .then(() => log(`Created ${path}`))
21
+ .then(() => log.info(`Created ${path}`))
17
22
  .catch(() => showError(`Failed creating ${path}`))