@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 +15 -0
- package/bin/spark-setup-theme.mjs +50 -21
- package/package.json +3 -2
- package/utils/setupTheme/createCSSTokenfile.js +95 -0
- package/utils/setupTheme/createTailwindThemeConfigFile.js +60 -0
- package/utils/setupTheme/index.js +2 -0
- package/utils/setupTheme/utils.js +30 -0
- package/utils.js +0 -22
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
|
|
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
|
|
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
|
|
28
|
-
|
|
29
|
-
if (!allowedExtensions.includes(
|
|
30
|
-
system.exit(`Your spark.theme.config file extension (${
|
|
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
|
|
34
|
-
const jsCode = transformSync(
|
|
36
|
+
const configFileContent = readFileSync(configFilePath, 'utf-8')
|
|
37
|
+
const jsCode = transformSync(configFileContent, { loader: 'ts' }).code
|
|
35
38
|
|
|
36
|
-
const jsFilePath =
|
|
37
|
-
const jsFileContents =
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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": "
|
|
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": "
|
|
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,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}`))
|