@spark-ui/cli-utils 1.3.2 → 2.0.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/bin/core/Logger.mjs +19 -0
  3. package/bin/core/System.mjs +45 -0
  4. package/bin/core/index.mjs +2 -0
  5. package/bin/generators/Generator.mjs +5 -0
  6. package/bin/generators/TemplateGenerator.mjs +83 -0
  7. package/bin/generators/index.mjs +2 -0
  8. package/bin/spark-generate.mjs +35 -121
  9. package/bin/spark-setup-theme.mjs +52 -21
  10. package/bin/validators/DescriptionValidator.mjs +19 -0
  11. package/bin/validators/NameValidator.mjs +25 -0
  12. package/bin/validators/Validator.mjs +5 -0
  13. package/bin/validators/index.mjs +2 -0
  14. package/package.json +4 -2
  15. package/templates/{[.npmignore].js → component/[.npmignore].js} +0 -0
  16. package/templates/{[package.json].js → component/[package.json].js} +2 -2
  17. package/templates/{[tsconfig.json].js → component/[tsconfig.json].js} +0 -0
  18. package/templates/{[vite.config.ts].js → component/[vite.config.ts].js} +0 -0
  19. package/templates/{src → component/src}/[Component.stories.mdx].js +4 -4
  20. package/templates/{src → component/src}/[Component.stories.tsx].js +2 -2
  21. package/templates/{src → component/src}/[Component.test.tsx].js +2 -2
  22. package/templates/{src → component/src}/[Component.tsx].js +2 -2
  23. package/templates/component/src/[Component.variants.tsx].js +8 -0
  24. package/templates/{src → component/src}/[index.ts].js +2 -2
  25. package/templates/hook/[.npmignore].js +3 -0
  26. package/templates/hook/[package.json].js +15 -0
  27. package/templates/hook/[tsconfig.json].js +4 -0
  28. package/templates/hook/[vite.config.ts].js +7 -0
  29. package/templates/hook/src/[index.ts].js +8 -0
  30. package/templates/hook/src/[name.stories.mdx].js +42 -0
  31. package/templates/hook/src/[name.test.tsx].js +17 -0
  32. package/templates/hook/src/[name.tsx].js +10 -0
  33. package/templates/util/[.npmignore].js +1 -0
  34. package/templates/util/[package.json].js +15 -0
  35. package/templates/util/[tsconfig.json].js +5 -0
  36. package/templates/util/[vite.config.ts].js +23 -0
  37. package/templates/util/src/[index.ts].js +1 -0
  38. package/utils/setupTheme/createCSSTokenfile.js +95 -0
  39. package/utils/setupTheme/createTailwindThemeConfigFile.js +60 -0
  40. package/utils/setupTheme/index.js +2 -0
  41. package/utils/setupTheme/utils.js +30 -0
  42. package/templates/src/[Component.variants.tsx].js +0 -8
  43. package/utils.js +0 -22
package/CHANGELOG.md CHANGED
@@ -3,6 +3,27 @@
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
+ # [2.0.0](https://github.com/adevinta/spark/compare/@spark-ui/cli-utils@1.4.0...@spark-ui/cli-utils@2.0.0) (2023-02-22)
7
+
8
+ ### Code Refactoring
9
+
10
+ - **cli-utils:** clean up code following PR feedbacks ([724c83e](https://github.com/adevinta/spark/commit/724c83e771307addeabd1fcea45b810d56f41e2f))
11
+
12
+ ### Features
13
+
14
+ - **cli-utils:** optimize DS configuration for spark consumers ([8d18892](https://github.com/adevinta/spark/commit/8d188920cffc3d24f96b1d5a9398eb22149c1640))
15
+
16
+ ### BREAKING CHANGES
17
+
18
+ - **cli-utils:** update the way the cli spark-setup-theme command works
19
+ - **cli-utils:** update the way the cli spark-setup-theme command works
20
+
21
+ # [1.4.0](https://github.com/adevinta/spark/compare/@spark-ui/cli-utils@1.3.2...@spark-ui/cli-utils@1.4.0) (2023-02-22)
22
+
23
+ ### Features
24
+
25
+ - **cli-utils:** add hook and utils template ([e22d672](https://github.com/adevinta/spark/commit/e22d672e349909cc4bc8673312846d10d77e7ea5))
26
+
6
27
  ## [1.3.2](https://github.com/adevinta/spark/compare/@spark-ui/cli-utils@1.3.1...@spark-ui/cli-utils@1.3.2) (2023-02-22)
7
28
 
8
29
  **Note:** Version bump only for package @spark-ui/cli-utils
@@ -0,0 +1,19 @@
1
+ import chalk from 'chalk'
2
+
3
+ export class Logger {
4
+ success(message) {
5
+ console.log(chalk.green(message))
6
+ }
7
+
8
+ error(message) {
9
+ console.log(chalk.red(message))
10
+ }
11
+
12
+ info(message) {
13
+ console.log(chalk.yellow(message))
14
+ }
15
+
16
+ warning(message) {
17
+ console.log(chalk.green(message))
18
+ }
19
+ }
@@ -0,0 +1,45 @@
1
+ import fse from 'fs-extra'
2
+ import glob from 'glob'
3
+
4
+ export class System {
5
+ logger
6
+
7
+ constructor({ logger }) {
8
+ this.logger = logger
9
+ }
10
+
11
+ exit(message) {
12
+ this.logger.error(`✖ Error: ${message}\n`)
13
+ process.exit(1)
14
+ }
15
+
16
+ getBasePath() {
17
+ return process.cwd()
18
+ }
19
+
20
+ getPackageJSON() {
21
+ const basePath = this.getBasePath()
22
+
23
+ const raw = fse.readFileSync(`${basePath}/package.json`).toString()
24
+
25
+ return JSON.parse(raw)
26
+ }
27
+
28
+ isPackageCreated(name) {
29
+ const base = this.getBasePath()
30
+ const packageJSON = this.getPackageJSON()
31
+
32
+ return packageJSON.workspaces.some(workspace => {
33
+ const packages = glob.sync(`${base}/${workspace}/`)
34
+
35
+ return packages.some(path => path.endsWith(`/${name}/`))
36
+ })
37
+ }
38
+
39
+ writeFile({ path, content }) {
40
+ return fse
41
+ .outputFile(path, content)
42
+ .then(() => this.logger.info(`Created ${path}`))
43
+ .catch(error => this.exit(`Failed creating ${path}`))
44
+ }
45
+ }
@@ -0,0 +1,2 @@
1
+ export { Logger } from './Logger.mjs'
2
+ export { System } from './System.mjs'
@@ -0,0 +1,5 @@
1
+ export class Generator {
2
+ execute() {
3
+ throw new Error('execute method should be implemented')
4
+ }
5
+ }
@@ -0,0 +1,83 @@
1
+ import { join } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import glob from 'glob'
4
+ import { pascalCase } from 'pascal-case'
5
+ import { camelCase } from 'camel-case'
6
+
7
+ import { System } from '../core/index.mjs'
8
+ import { Generator } from './Generator.mjs'
9
+
10
+ export class TemplateGenerator extends Generator {
11
+ static TYPES = {
12
+ COMPONENT: 'component',
13
+ HOOK: 'hook',
14
+ UTIL: 'util',
15
+ }
16
+
17
+ static CONTEXTS = {
18
+ [TemplateGenerator.TYPES.COMPONENT]: 'components',
19
+ [TemplateGenerator.TYPES.HOOK]: 'components',
20
+ [TemplateGenerator.TYPES.UTIL]: 'utils',
21
+ }
22
+
23
+ constructor({ system }) {
24
+ super()
25
+ this.system = system
26
+ }
27
+
28
+ getDest({ type, name }) {
29
+ const basePath = this.system.getBasePath()
30
+ const context = TemplateGenerator.CONTEXTS[type]
31
+
32
+ return `${basePath}/packages/${context}/${name}`
33
+ }
34
+
35
+ getTemplatePaths({ type }) {
36
+ const pattern = fileURLToPath(new URL(`../../templates/${type}/**/*.js`, import.meta.url))
37
+
38
+ return new Promise((resolve, reject) => {
39
+ glob(pattern, async (error, paths) => {
40
+ if (error) {
41
+ return reject(error)
42
+ }
43
+
44
+ resolve(paths)
45
+ })
46
+ })
47
+ }
48
+
49
+ getTemplatePath({ path, name, type, dest }) {
50
+ const parsed = path
51
+ .replace(/(.*)\/templates\/([a-z-]+)\//, `${dest}/`)
52
+ .replaceAll(/\[|\]|\.js$/g, '')
53
+
54
+ if (type === TemplateGenerator.TYPES.COMPONENT) {
55
+ return parsed.replace('Component', pascalCase(name))
56
+ }
57
+
58
+ if (type === TemplateGenerator.TYPES.HOOK) {
59
+ return parsed.replace('name', camelCase(name))
60
+ }
61
+
62
+ return parsed
63
+ }
64
+
65
+ async execute({ type, name, description }) {
66
+ const dest = this.getDest({ type, name })
67
+ const paths = await this.getTemplatePaths({ type })
68
+
69
+ const promises = paths.map(path =>
70
+ import(path).then(module => ({
71
+ path: this.getTemplatePath({ path, name, type, dest }),
72
+ content: module.default({
73
+ name,
74
+ description,
75
+ }),
76
+ }))
77
+ )
78
+
79
+ const files = await Promise.all(promises)
80
+
81
+ return Promise.all(files.map(file => this.system.writeFile(file)))
82
+ }
83
+ }
@@ -0,0 +1,2 @@
1
+ export { Generator } from './Generator.mjs'
2
+ export { TemplateGenerator } from './TemplateGenerator.mjs'
@@ -1,157 +1,71 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import chalk from 'chalk'
4
- import fse from 'fs-extra'
5
3
  import * as prompt from '@clack/prompts'
6
- import { fileURLToPath } from 'node:url'
7
- import glob from 'glob'
8
- import { pascalCase } from 'pascal-case'
9
- import { log, showError, writeFile } from '../utils.js'
10
4
 
11
- const BASE_DIR = process.cwd()
12
- const rawRootPackageJSON = fse.readFileSync(`${BASE_DIR}/package.json`)
13
- let rootPackageJSON = JSON.parse(rawRootPackageJSON)
5
+ import { TemplateGenerator } from './generators/index.mjs'
6
+ import { Logger, System } from './core/index.mjs'
7
+ import { DescriptionValidator, NameValidator } from './validators/index.mjs'
14
8
 
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
-
42
- return existingPackages.some(path => path.endsWith(`/${name}/`))
43
- })
44
- },
45
- /** Retrieves the target folder of the generated package */
46
- getDirectory: (name, template) => `${WORKSPACES[template]}/${name}/`,
47
- /** Retrieves the full path to the folder of the generated package */
48
- getFullPath: (name, template) => `${BASE_DIR}${packageUtils.getDirectory(name, template)}`,
49
- }
9
+ const logger = new Logger()
10
+ const system = new System({ logger })
11
+ const generator = new TemplateGenerator({ system })
50
12
 
51
- async function promptPackageName() {
13
+ export const run = async () => {
52
14
  const name = await prompt.text({
53
15
  message: 'Package name (must contain letters and dash symbols only):',
54
16
  initialValue: '',
55
17
  validate(value) {
56
- if (value == null) return ERRORS.NO_PKG_NAME
57
- if (!packageUtils.hasValidName(value)) return ERRORS.INVALID_PKG_NAME
58
- if (packageUtils.alreadyExists(value)) return ERRORS.PKG_ALREADY_EXISTS
18
+ const validator = new NameValidator({ system })
19
+
20
+ return validator.validate(value)
59
21
  },
60
22
  })
61
23
 
62
- if (prompt.isCancel(name)) showError(ERRORS.ABORT)
63
-
64
- return name
65
- }
24
+ if (prompt.isCancel(name)) {
25
+ system.exit('Aborted package generation')
26
+ }
66
27
 
67
- async function promptPackageTemplate() {
68
- const template = await prompt.select({
28
+ const type = await prompt.select({
69
29
  message: 'Chose a template:',
70
- initialValue: TEMPLATE_TYPE.COMPONENT,
30
+ initialValue: 'component',
71
31
  options: [
72
32
  {
73
- value: TEMPLATE_TYPE.COMPONENT,
33
+ value: TemplateGenerator.TYPES.COMPONENT,
74
34
  label: 'Component',
75
- hint: 'Typescript dummy component with some tests, stories and config files',
35
+ hint: 'TypeScript component package',
76
36
  },
77
37
  {
78
- value: TEMPLATE_TYPE.HOOK,
38
+ value: TemplateGenerator.TYPES.HOOK,
79
39
  label: 'Hook',
80
- hint: 'Typescript hook with some tests, stories and config files',
40
+ hint: 'TypeScript hook package',
41
+ },
42
+ {
43
+ value: TemplateGenerator.TYPES.UTIL,
44
+ label: 'Utility',
45
+ hint: 'TypeScript utility package',
81
46
  },
82
47
  ],
83
48
  })
84
49
 
85
- if (prompt.isCancel(template)) showError(ERRORS.ABORT)
86
-
87
- return template
88
- }
50
+ if (prompt.isCancel(type)) {
51
+ system.exit('Aborted package generation')
52
+ }
89
53
 
90
- async function promptPackageDescription() {
91
54
  const description = await prompt.text({
92
55
  message: 'Describe your package (short description):',
93
56
  initialValue: '',
94
57
  validate(value) {
95
- if (!value) return `You package must have a description`
96
- if (value.length < 10) return ERRORS.INVALID_DESCRIPTION
58
+ const validator = new DescriptionValidator()
59
+
60
+ return validator.validate(value)
97
61
  },
98
62
  })
99
63
 
100
- if (prompt.isCancel(description)) showError(ERRORS.ABORT)
101
-
102
- return description
103
- }
104
-
105
- /**
106
- * Program starts here
107
- */
108
- prompt.intro(`Generate Spark package`)
109
-
110
- const name = await promptPackageName()
111
- const template = await promptPackageTemplate()
112
- const description = await promptPackageDescription()
113
-
114
- const packagePath = packageUtils.getFullPath(name, template)
115
-
116
- switch (template) {
117
- case TEMPLATE_TYPE.COMPONENT:
118
- generateComponentPackage(name, description)
119
- break
120
- case TEMPLATE_TYPE.HOOK:
121
- generateHookPackage(name, description)
122
- break
123
- }
124
-
125
- prompt.outro(`Generating package...`)
126
-
127
- function generateComponentPackage(name, description) {
128
- const templatesPattern = fileURLToPath(new URL('../templates/**/*.js', import.meta.url))
129
-
130
- glob(templatesPattern, async (err, res) => {
131
- if (err) showError(err)
132
- if (res) {
133
- const templateContents = res.map(templatePath =>
134
- import(templatePath).then(module => ({
135
- path: templatePath
136
- .replace(/(.*)\/templates\//, packagePath)
137
- .replace('Component', pascalCase(name))
138
- .replaceAll(/\[|\]|\.js$/g, ''),
139
- content: module.default({
140
- component: name,
141
- description: description,
142
- }),
143
- }))
144
- )
64
+ if (prompt.isCancel(description)) {
65
+ system.exit('Aborted package generation')
66
+ }
145
67
 
146
- const filesToWrite = await Promise.all(templateContents)
147
-
148
- Promise.all(filesToWrite.map(writeFile)).then(() => {
149
- log.success('All package files has been properly written!')
150
- })
151
- }
152
- })
68
+ generator.execute({ name, type, description })
153
69
  }
154
70
 
155
- function generateHookPackage(name, description) {
156
- showError('Todo: template for hook packages is not ready yet.')
157
- }
71
+ run()
@@ -1,47 +1,78 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn } from 'child_process'
4
- import { join, extname, parse, sep } from 'path'
5
- import { readFileSync, readdirSync, writeFileSync, unlinkSync } from 'fs'
4
+ import { join, extname } from 'node:path'
5
+ import { readFileSync, readdirSync, writeFileSync, unlinkSync } from 'node:fs'
6
6
  import { transformSync } from 'esbuild'
7
+ import { createRequire } from 'node:module'
7
8
 
8
- import { log, showError } from '../utils.js'
9
+ import { Logger, System } from './core/index.mjs'
10
+ import { createCSSTokensFile, createTailwindThemeConfigFile } from '../utils/setupTheme/index.js'
9
11
 
10
- const jsFileExtension = '.js'
12
+ const logger = new Logger()
13
+ const system = new System({ logger })
11
14
 
15
+ const require = createRequire(import.meta.url)
12
16
  const configFile = readdirSync(process.cwd()).find(fileName =>
13
17
  /^(spark\.theme\.config)\.(js|ts|mjs|mts|cjs|cts)$/.test(fileName)
14
18
  )
15
19
 
16
20
  if (!configFile) {
17
- showError(
21
+ system.exit(
18
22
  "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"
19
23
  )
20
24
  }
21
25
 
26
+ const configFilePath = join(process.cwd(), configFile)
22
27
  const configFileIsInJS = configFile === 'spark.theme.config.js'
23
- const filePath = join(process.cwd(), configFile)
24
28
 
25
29
  const allowedExtensions = ['.ts', '.mts', '.cts', '.js', '.cjs', '.mjs']
26
- const fileExtension = extname(filePath)
27
- if (!allowedExtensions.includes(fileExtension)) {
28
- showError(`Your spark.theme.config file extension (${fileExtension}) is not supported.`)
30
+ const jsFileExtension = '.js'
31
+ const configFileExtension = extname(configFilePath)
32
+ if (!allowedExtensions.includes(configFileExtension)) {
33
+ system.exit(`Your spark.theme.config file extension (${configFileExtension}) is not supported.`)
29
34
  }
30
35
 
31
- const tsCode = readFileSync(filePath, 'utf-8')
32
- const jsCode = transformSync(tsCode, { loader: 'ts' }).code
36
+ const configFileContent = readFileSync(configFilePath, 'utf-8')
37
+ const jsCode = transformSync(configFileContent, { loader: 'ts' }).code
33
38
 
34
- const jsFilePath = filePath.replace(/\.ts$|\.mts$|\.cts$|\.mjs|\.cjs$/, jsFileExtension)
35
- const jsFileContents = Buffer.from(jsCode, 'utf-8')
39
+ const jsFilePath = configFilePath.replace(/\.ts$|\.mts$|\.cts$|\.mjs|\.cjs$/, jsFileExtension)
40
+ const jsFileContents = jsCode
36
41
 
37
42
  if (!configFileIsInJS) writeFileSync(jsFilePath, jsFileContents)
38
43
 
39
- const child = spawn(process.execPath, [jsFilePath], {
40
- stdio: 'inherit',
41
- })
44
+ import(jsFilePath)
45
+ .then(module => {
46
+ const { tailwindThemeConfigFilePath, CSSTokens } = module.default
47
+
48
+ createTailwindThemeConfigFile(tailwindThemeConfigFilePath)
49
+ createCSSTokensFile(CSSTokens.filePath, CSSTokens.themes)
50
+
51
+ const child = spawn(process.execPath, [jsFilePath], {
52
+ stdio: 'inherit',
53
+ })
54
+
55
+ child.on('exit', code => {
56
+ if (!configFileIsInJS) unlinkSync(jsFilePath)
57
+ logger.success(
58
+ `✨ Your Spark Tailwind theme config file has been successfully created: ${join(
59
+ process.cwd(),
60
+ tailwindThemeConfigFilePath
61
+ )}`
62
+ )
63
+
64
+ logger.success(
65
+ `✨ Your Spark Tailwind CSS Tokens file file has been successfully created: ${join(
66
+ process.cwd(),
67
+ CSSTokens.filePath
68
+ )}`
69
+ )
42
70
 
43
- child.on('exit', code => {
44
- if (!configFileIsInJS) unlinkSync(jsFilePath)
45
- log.success('✨ Your Spark theme config files have been successfully created!')
46
- process.exit(code)
47
- })
71
+ process.exit(code)
72
+ })
73
+ })
74
+ .catch(err => {
75
+ unlinkSync(jsFilePath)
76
+ system.exit(`
77
+ Something went wrong while running ${configFilePath}: ${err}`)
78
+ })
@@ -0,0 +1,19 @@
1
+ import { Validator } from './Validator.mjs'
2
+
3
+ export class DescriptionValidator extends Validator {
4
+ constructor() {
5
+ super()
6
+ }
7
+
8
+ validate(description) {
9
+ if (!description) {
10
+ return 'You package must have a description'
11
+ }
12
+
13
+ if (description.length < 10) {
14
+ return 'Description is too short (minimum is 10 chars)'
15
+ }
16
+
17
+ return undefined
18
+ }
19
+ }
@@ -0,0 +1,25 @@
1
+ import { System } from '../core/index.mjs'
2
+ import { Validator } from './Validator.mjs'
3
+
4
+ export class NameValidator extends Validator {
5
+ system
6
+
7
+ constructor({ system }) {
8
+ super()
9
+ this.system = system
10
+ }
11
+
12
+ validate(name) {
13
+ if (!name) {
14
+ return 'Package name must me defined'
15
+ }
16
+
17
+ if (!/^[a-z-]*$/.test(name)) {
18
+ return 'Name name must contain letters and dash symbols only (ex: "my-package")'
19
+ }
20
+
21
+ if (this.system.isPackageCreated(name)) {
22
+ return 'A package with that name already exists. Either delete it manually or use another name.'
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,5 @@
1
+ export class Validator {
2
+ validate() {
3
+ throw new Error('validate method should be implemented')
4
+ }
5
+ }
@@ -0,0 +1,2 @@
1
+ export { NameValidator } from './NameValidator.mjs'
2
+ export { DescriptionValidator } from './DescriptionValidator.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spark-ui/cli-utils",
3
- "version": "1.3.2",
3
+ "version": "2.0.0",
4
4
  "description": "Spark CLI utils",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -13,6 +13,8 @@
13
13
  "type": "module",
14
14
  "dependencies": {
15
15
  "@clack/prompts": "0.2.2",
16
+ "@spark-ui/theme-utils": "*",
17
+ "camel-case": "4.1.2",
16
18
  "chalk": "5.2.0",
17
19
  "commander": "10.0.0",
18
20
  "esbuild": "0.17.8",
@@ -25,5 +27,5 @@
25
27
  "url": "git@github.com:adevinta/spark.git",
26
28
  "directory": "packages/utils/cli"
27
29
  },
28
- "gitHead": "d31d127a513beafe224e16ba94347cf0b968c486"
30
+ "gitHead": "a7cfc5667178ab46c812a43423f57d10037a5c6a"
29
31
  }
@@ -1,5 +1,5 @@
1
- export default ({ component, description }) => `{
2
- "name": "@spark-ui/${component}",
1
+ export default ({ name, description }) => `{
2
+ "name": "@spark-ui/${name}",
3
3
  "version": "1.0.0",
4
4
  "description": "${description}",
5
5
  "publishConfig": {
@@ -1,7 +1,7 @@
1
1
  import { pascalCase } from 'pascal-case'
2
2
 
3
- export default ({ component, description }) => {
4
- const componentName = pascalCase(component)
3
+ export default ({ name, description }) => {
4
+ const componentName = pascalCase(name)
5
5
 
6
6
  return `import { ArgsTable, Meta, Story } from '@storybook/addon-docs'
7
7
  import { ReactLiveBlock } from '@docs/helpers/ReactLiveBlock'
@@ -20,13 +20,13 @@ ${description}
20
20
  <StoryHeading label="Install" />
21
21
 
22
22
  \`\`\`
23
- npm install @spark-ui/${component}
23
+ npm install @spark-ui/${name}
24
24
  \`\`\`
25
25
 
26
26
  <StoryHeading label="Import" />
27
27
 
28
28
  \`\`\`
29
- import { ${componentName} } from "@spark-ui/${component}"
29
+ import { ${componentName} } from "@spark-ui/${name}"
30
30
  \`\`\`
31
31
 
32
32
  <StoryHeading label="Props" />
@@ -1,7 +1,7 @@
1
1
  import { pascalCase } from 'pascal-case'
2
2
 
3
- export default ({ component, description }) => {
4
- const componentName = pascalCase(component)
3
+ export default ({ name, description }) => {
4
+ const componentName = pascalCase(name)
5
5
 
6
6
  return `import { ReactLiveBlock } from '@docs/helpers/ReactLiveBlock'
7
7
 
@@ -1,7 +1,7 @@
1
1
  import { pascalCase } from 'pascal-case'
2
2
 
3
- export default ({ component }) => {
4
- const componentName = pascalCase(component)
3
+ export default ({ name }) => {
4
+ const componentName = pascalCase(name)
5
5
 
6
6
  return `import { render, screen } from '@testing-library/react'
7
7
  import userEvent from '@testing-library/user-event'
@@ -1,7 +1,7 @@
1
1
  import { pascalCase } from 'pascal-case'
2
2
 
3
- export default ({ component }) => {
4
- const componentName = pascalCase(component)
3
+ export default ({ name }) => {
4
+ const componentName = pascalCase(name)
5
5
 
6
6
  return `import { ComponentPropsWithoutRef, PropsWithChildren } from 'react'
7
7
 
@@ -0,0 +1,8 @@
1
+ import { pascalCase } from 'pascal-case'
2
+
3
+ export default ({ name }) => {
4
+ const componentName = pascalCase(name)
5
+
6
+ return `
7
+ `
8
+ }
@@ -1,7 +1,7 @@
1
1
  import { pascalCase } from 'pascal-case'
2
2
 
3
- export default ({ component }) => {
4
- const componentName = pascalCase(component)
3
+ export default ({ name }) => {
4
+ const componentName = pascalCase(name)
5
5
 
6
6
  return `export { ${componentName} } from './${componentName}'
7
7
  `
@@ -0,0 +1,3 @@
1
+ export default () => `src
2
+ **/*.stories.*
3
+ `
@@ -0,0 +1,15 @@
1
+ export default ({ name, description }) => `{
2
+ "name": "@spark-ui/${name}",
3
+ "version": "1.0.0",
4
+ "description": "${description}",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.mjs",
10
+ "types": "./dist/index.d.ts",
11
+ "scripts": {
12
+ "build": "vite build"
13
+ }
14
+ }
15
+ `
@@ -0,0 +1,4 @@
1
+ export default () => `{
2
+ "extends": "../../../tsconfig.json",
3
+ "include": ["src/**/*", "../../../global.d.ts"]
4
+ }`
@@ -0,0 +1,7 @@
1
+ export default () => `import path from 'path'
2
+ import { getComponentConfiguration } from '../../../config/index'
3
+
4
+ const { name } = require(path.resolve(__dirname, 'package.json'))
5
+
6
+ export default getComponentConfiguration(name)
7
+ `
@@ -0,0 +1,8 @@
1
+ import { camelCase } from 'camel-case'
2
+
3
+ export default ({ name }) => {
4
+ const hookName = camelCase(name)
5
+
6
+ return `export { ${hookName} } from './${hookName}'
7
+ `
8
+ }
@@ -0,0 +1,42 @@
1
+ import { camelCase } from 'camel-case'
2
+
3
+ export default ({ name, description }) => {
4
+ const hookName = camelCase(name)
5
+
6
+ return `import { ArgsTable, Meta, Story } from '@storybook/addon-docs'
7
+ import { ReactLiveBlock } from '@docs/helpers/ReactLiveBlock'
8
+ import { StoryHeading } from '@docs/helpers/StoryHeading'
9
+
10
+ import { ${hookName} } from '.'
11
+
12
+ import * as stories from './${hookName}.stories'
13
+
14
+ <Meta title="Hooks/${hookName}" />
15
+
16
+ # ${hookName}
17
+
18
+ ${description}
19
+
20
+ <StoryHeading label="Install" />
21
+
22
+ \`\`\`
23
+ npm install @spark-ui/${name}
24
+ \`\`\`
25
+
26
+ <StoryHeading label="Import" />
27
+
28
+ \`\`\`
29
+ import { ${hookName} } from "@spark-ui/${name}"
30
+ \`\`\`
31
+
32
+ <StoryHeading label="Usage" />
33
+
34
+ <ArgsTable of={${hookName}} />
35
+
36
+ \`\`\`jsx
37
+ import { ${hookName} } from "@spark-ui/${name}"
38
+
39
+ const Demo = () => {}
40
+ \`\`\`
41
+ `
42
+ }
@@ -0,0 +1,17 @@
1
+ import { camelCase } from 'camel-case'
2
+
3
+ export default ({ name }) => {
4
+ const hookName = camelCase(name)
5
+
6
+ return `import { renderHook } from '@testing-library/react'
7
+ import { describe, expect, it } from 'vitest'
8
+
9
+ import { ${hookName} } from './${hookName}'
10
+
11
+ describe('${hookName}', () => {
12
+ it('should be defined', () => {
13
+ expect(${hookName}).toBeDefined()
14
+ })
15
+ })
16
+ `
17
+ }
@@ -0,0 +1,10 @@
1
+ import { camelCase } from 'camel-case'
2
+
3
+ export default ({ name }) => {
4
+ const hookName = camelCase(name)
5
+
6
+ return `export function ${hookName}() {
7
+ return null
8
+ }
9
+ `
10
+ }
@@ -0,0 +1 @@
1
+ export default () => 'src'
@@ -0,0 +1,15 @@
1
+ export default ({ name, description }) => `{
2
+ "name": "@spark-ui/${name}",
3
+ "version": "1.0.0",
4
+ "description": "${description}",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.mjs",
10
+ "types": "./dist/index.d.ts",
11
+ "scripts": {
12
+ "build": "vite build"
13
+ }
14
+ }
15
+ `
@@ -0,0 +1,5 @@
1
+ export default () => `{
2
+ "extends": "../../../tsconfig.json",
3
+ "include": ["src/**/*", "../../global.d.ts"]
4
+ }
5
+ `
@@ -0,0 +1,23 @@
1
+ export default () => `import { terser } from 'rollup-plugin-terser'
2
+ import dts from 'vite-plugin-dts'
3
+
4
+ export default {
5
+ build: {
6
+ target: 'es2015',
7
+ lib: {
8
+ entry: 'src/index.ts',
9
+ formats: ['es', 'cjs'],
10
+ fileName: 'index',
11
+ },
12
+ rollupOptions: {
13
+ external: ['node:path', 'node:fs'],
14
+ plugins: [terser()],
15
+ },
16
+ },
17
+ plugins: [
18
+ dts({
19
+ entryRoot: './src',
20
+ }),
21
+ ],
22
+ }
23
+ `
@@ -0,0 +1 @@
1
+ export default () => ''
@@ -0,0 +1,95 @@
1
+ import { appendFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import hexRgb from 'hex-rgb'
5
+
6
+ import { isHex, isStringOrNumber, toKebabCase } from './utils.js'
7
+
8
+ function flattenTheme(theme, className) {
9
+ const flattenedTheme = {}
10
+
11
+ function flatten(obj, path) {
12
+ Object.entries(obj).forEach(([key, value]) => {
13
+ if (value !== null && typeof value === 'object') {
14
+ const formattedPath = path ? `--${path}-${key}` : `--${key}`
15
+ flatten(value, toKebabCase(formattedPath.replace(/-{3,}/, '--')))
16
+
17
+ return
18
+ }
19
+
20
+ if (isStringOrNumber(value)) {
21
+ const getFormattedValue = () => {
22
+ if (isHex(value)) {
23
+ const { red, green, blue } = hexRgb(value)
24
+
25
+ return `${red} ${green} ${blue}`
26
+ }
27
+
28
+ return value
29
+ }
30
+
31
+ flattenedTheme[`${path}-${toKebabCase(key)}`] = getFormattedValue()
32
+ }
33
+ })
34
+ }
35
+
36
+ flatten(theme)
37
+
38
+ return {
39
+ ...flattenedTheme,
40
+ className,
41
+ }
42
+ }
43
+
44
+ const toStringifiedTheme = theme =>
45
+ Object.entries(theme)
46
+ .map(([k, v]) => `${k}:${v}`)
47
+ .join(';')
48
+
49
+ const getStringifiedThemes = themeRecord =>
50
+ Object.keys(themeRecord).map(key => {
51
+ const { className, ...rest } = flattenTheme(themeRecord[key], key)
52
+
53
+ return key === 'default'
54
+ ? `:root{${toStringifiedTheme(rest)}}`
55
+ : `.${className}{${toStringifiedTheme(rest)}}`
56
+ })
57
+
58
+ /**
59
+ * Creates a CSS file containing theme tokens represented as CSS custom properties
60
+ *
61
+ * @param {string} path - The file path where the CSS file will be created.
62
+ * @param {Record<string, Theme>} themeRecord - A record (with a required key of "default") of themes that will be included in the CSS Tokens file.
63
+ *
64
+ * @example
65
+ *
66
+ * const defaultTheme: Theme = { ... }
67
+ * const darkTheme: Theme = { ... }
68
+ * const otherTheme: Theme = { ... }
69
+ *
70
+ * const themes = {
71
+ * default: defaultTheme,
72
+ * dark: darkTheme
73
+ * other: otherTheme
74
+ * }
75
+ *
76
+ * createCSSTokensFile('somePath.css', themes) // will generate a "somePath.css" file in the relative location from which it was called
77
+ */
78
+ export function createCSSTokensFile(path, themeRecord) {
79
+ try {
80
+ appendFileSync(
81
+ join(process.cwd(), path),
82
+ `
83
+ @tailwind base;
84
+ @tailwind components;
85
+ @tailwind utilities;
86
+ @layer base {${getStringifiedThemes(themeRecord).join('')}}
87
+ `,
88
+ {
89
+ flag: 'w',
90
+ }
91
+ )
92
+ } catch (error) {
93
+ console.error('Failed to create the CSS token file', error)
94
+ }
95
+ }
@@ -0,0 +1,60 @@
1
+ import { writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import { defaultTheme } from '@spark-ui/theme-utils'
5
+
6
+ import { isHex, isStringOrNumber, toKebabCase, toKebabCaseKeys } from './utils.js'
7
+
8
+ function toTailwindConfig(theme) {
9
+ const themeCpy = JSON.parse(JSON.stringify(theme))
10
+
11
+ function flatten(obj, path) {
12
+ Object.entries(obj).forEach(([key, value]) => {
13
+ if (value !== null && typeof value === 'object') {
14
+ const formattedPath = path ? `--${path}-${key}` : `--${key}`
15
+ flatten(value, toKebabCase(formattedPath.replace(/-{3,}/, '--')))
16
+
17
+ return
18
+ }
19
+
20
+ /* eslint-disable */
21
+ if (isStringOrNumber(value)) {
22
+ const formattedPath =
23
+ /--colors/.test(path || '') && isHex(value)
24
+ ? `rgb(var(${path}-${toKebabCase(key)}) / <alpha-value>)`
25
+ : `var(${path}-${toKebabCase(key)})`
26
+
27
+ /* @ts-ignore */
28
+ obj[key] = formattedPath
29
+ /* eslint-enable */
30
+ }
31
+ })
32
+ }
33
+
34
+ flatten(themeCpy)
35
+
36
+ return toKebabCaseKeys(themeCpy)
37
+ }
38
+
39
+ /**
40
+ * Creates a Tailwind config file that links the [theme options](https://tailwindcss.com/docs/theme#configuration-reference) provided by Tailwind with the CSS custom property values generated using the "createCSSTokensFile" function
41
+ *
42
+ * @param {string} path - The file path where the Tailwind config file will be created.
43
+ *
44
+ * @example
45
+ *
46
+ * createTailwindThemeConfigFile('./tailwind.theme.js') // will generate a "tailwind.theme.js" in the relative location from which it was called
47
+ */
48
+ export function createTailwindThemeConfigFile(path) {
49
+ try {
50
+ writeFileSync(
51
+ join(process.cwd(), path),
52
+ `module.exports = ${JSON.stringify(toTailwindConfig(defaultTheme))}`,
53
+ {
54
+ flag: 'w',
55
+ }
56
+ )
57
+ } catch (error) {
58
+ console.error('Failed to create the Tailwind theme config file', error)
59
+ }
60
+ }
@@ -0,0 +1,2 @@
1
+ export { createCSSTokensFile } from './createCSSTokenfile.js'
2
+ export { createTailwindThemeConfigFile } from './createTailwindThemeConfigFile.js'
@@ -0,0 +1,30 @@
1
+ function toKebabCase(v) {
2
+ return v.replace(/[A-Z]/g, e => `-${e.toLocaleLowerCase()}`)
3
+ }
4
+
5
+ function isHex(value) {
6
+ if (typeof value === 'number') {
7
+ return false
8
+ }
9
+
10
+ const regexp = /^#[0-9a-fA-F]+$/
11
+
12
+ return regexp.test(value)
13
+ }
14
+
15
+ function isStringOrNumber(value) {
16
+ return typeof value === 'string' || typeof value === 'number'
17
+ }
18
+
19
+ // eslint-disable-next-line @typescript-eslint/ban-types
20
+ function toKebabCaseKeys(obj, level = 1) {
21
+ const result = {}
22
+ for (const key in obj) {
23
+ const value = typeof obj[key] === 'object' ? toKebabCaseKeys(obj[key], level + 1) : obj[key]
24
+ result[level > 1 ? key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() : key] = value
25
+ }
26
+
27
+ return result
28
+ }
29
+
30
+ export { toKebabCase, isHex, toKebabCaseKeys, isStringOrNumber }
@@ -1,8 +0,0 @@
1
- import { pascalCase } from 'pascal-case'
2
-
3
- export default ({ component }) => {
4
- const componentName = pascalCase(component)
5
-
6
- return `
7
- `
8
- }
package/utils.js DELETED
@@ -1,22 +0,0 @@
1
- import fs from 'node:fs'
2
-
3
- import chalk from 'chalk'
4
- import fse from 'fs-extra'
5
-
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
- }
12
-
13
- export const showError = (msg, foreignProgram) => {
14
- log.error(`✖ Error: ${msg}\n`)
15
- process.exit(1)
16
- }
17
-
18
- export const writeFile = ({ path, content }) =>
19
- fse
20
- .outputFile(path, content)
21
- .then(() => log.info(`Created ${path}`))
22
- .catch(() => showError(`Failed creating ${path}`))