@spark-ui/cli-utils 1.4.0 → 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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
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
+
6
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)
7
22
 
8
23
  ### Features
@@ -1,15 +1,18 @@
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'
8
+
9
+ import { Logger, System } from './core/index.mjs'
10
+ import { createCSSTokensFile, createTailwindThemeConfigFile } from '../utils/setupTheme/index.js'
7
11
 
8
12
  const logger = new Logger()
9
13
  const system = new System({ logger })
10
14
 
11
- const jsFileExtension = '.js'
12
-
15
+ const require = createRequire(import.meta.url)
13
16
  const configFile = readdirSync(process.cwd()).find(fileName =>
14
17
  /^(spark\.theme\.config)\.(js|ts|mjs|mts|cjs|cts)$/.test(fileName)
15
18
  )
@@ -20,30 +23,56 @@ if (!configFile) {
20
23
  )
21
24
  }
22
25
 
26
+ const configFilePath = join(process.cwd(), configFile)
23
27
  const configFileIsInJS = configFile === 'spark.theme.config.js'
24
- const filePath = join(process.cwd(), configFile)
25
28
 
26
29
  const allowedExtensions = ['.ts', '.mts', '.cts', '.js', '.cjs', '.mjs']
27
- const fileExtension = extname(filePath)
28
-
29
- if (!allowedExtensions.includes(fileExtension)) {
30
- system.exit(`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.`)
31
34
  }
32
35
 
33
- const tsCode = readFileSync(filePath, 'utf-8')
34
- const jsCode = transformSync(tsCode, { loader: 'ts' }).code
36
+ const configFileContent = readFileSync(configFilePath, 'utf-8')
37
+ const jsCode = transformSync(configFileContent, { loader: 'ts' }).code
35
38
 
36
- const jsFilePath = filePath.replace(/\.ts$|\.mts$|\.cts$|\.mjs|\.cjs$/, jsFileExtension)
37
- const jsFileContents = Buffer.from(jsCode, 'utf-8')
39
+ const jsFilePath = configFilePath.replace(/\.ts$|\.mts$|\.cts$|\.mjs|\.cjs$/, jsFileExtension)
40
+ const jsFileContents = jsCode
38
41
 
39
42
  if (!configFileIsInJS) writeFileSync(jsFilePath, jsFileContents)
40
43
 
41
- const child = spawn(process.execPath, [jsFilePath], {
42
- stdio: 'inherit',
43
- })
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
+ )
44
70
 
45
- child.on('exit', code => {
46
- if (!configFileIsInJS) unlinkSync(jsFilePath)
47
- logger.success('✨ Your Spark theme config files have been successfully created!')
48
- process.exit(code)
49
- })
71
+ process.exit(code)
72
+ })
73
+ })
74
+ .catch(err => {
75
+ unlinkSync(jsFilePath)
76
+ system.exit(`
77
+ Something went wrong while running ${configFilePath}: ${err}`)
78
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spark-ui/cli-utils",
3
- "version": "1.4.0",
3
+ "version": "2.0.0",
4
4
  "description": "Spark CLI utils",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -13,6 +13,7 @@
13
13
  "type": "module",
14
14
  "dependencies": {
15
15
  "@clack/prompts": "0.2.2",
16
+ "@spark-ui/theme-utils": "*",
16
17
  "camel-case": "4.1.2",
17
18
  "chalk": "5.2.0",
18
19
  "commander": "10.0.0",
@@ -26,5 +27,5 @@
26
27
  "url": "git@github.com:adevinta/spark.git",
27
28
  "directory": "packages/utils/cli"
28
29
  },
29
- "gitHead": "61d037acf7909f745ced1d1a0196fe6d2838850e"
30
+ "gitHead": "a7cfc5667178ab46c812a43423f57d10037a5c6a"
30
31
  }
@@ -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 }
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}`))