From 7d72a63f440a2418491ad751a30a8378dfc64d47 Mon Sep 17 00:00:00 2001 From: Justineo Date: Wed, 27 Dec 2023 21:28:42 +0800 Subject: [PATCH 1/3] feat: add theme ai --- packages/less-plugin-dls/README.md | 31 +++++++++- .../less-plugin-dls/lib/utils/evaluate.js | 15 ++++- packages/less-plugin-dls/scripts/vars.js | 24 +++++--- packages/less-plugin-dls/src/inject.js | 25 +++++++- packages/less-plugin-dls/test/run.js | 18 ++++-- packages/less-plugin-dls/test/specs/ai/ai.css | 46 +++++++++++++++ .../less-plugin-dls/test/specs/ai/ai.less | 46 +++++++++++++++ .../less-plugin-dls/tokens/themes/ai.less | 58 +++++++++++++++++++ 8 files changed, 243 insertions(+), 20 deletions(-) create mode 100644 packages/less-plugin-dls/test/specs/ai/ai.css create mode 100644 packages/less-plugin-dls/test/specs/ai/ai.less create mode 100644 packages/less-plugin-dls/tokens/themes/ai.less diff --git a/packages/less-plugin-dls/README.md b/packages/less-plugin-dls/README.md index e6099e6b..22fd2ae8 100644 --- a/packages/less-plugin-dls/README.md +++ b/packages/less-plugin-dls/README.md @@ -32,7 +32,9 @@ With [less-loader](https://github.com/webpack-contrib/less-loader): ```less @import "~less-plugin-dls/tokens/index.less"; -a { color: @dls-link-font-color-normal; } +a { + color: @dls-link-font-color-normal; +} ``` ### Use CLI argument @@ -41,6 +43,29 @@ a { color: @dls-link-font-color-normal; } lessc style.less --dls ``` +## Using metadata + +Variable values/metadata are provided in three formats for each theme. + +| File | Description | +| -- | -- | +| `variables.js` | The raw variable values in JavaScript ESM format. Token names are transformed from `@dls-color-brand-7` to `dlsColorBrand7` as named exports. | +| `variables.json` | The raw variable values in JSON format. | +| `variables.less` | The variable values in Less format. | + +```ts +// types for the JSON format: +interface Variables { + [tokenName: string]: { + value: string + type: 'color' | 'font' | 'numeric' | 'length' | 'complex' + global: boolean + } +} +``` + +There are also themed versions of the above files, which are named with the theme name as the suffix, namely `variables..js`, `variables..json` and `variables..less`. Currently the only supported theme is `ai`. + ## Custom functions ### `dls-contextual(@color, @type)` @@ -138,6 +163,10 @@ The absolute length of the line-height. ## Options +### `theme: 'ai' | undefined` + +The theme of the DLS. If not specified, the default theme will be used. + ### `reduceCalc: boolean` Indicates whether to reduce `calc` expression to the simplest form or not. diff --git a/packages/less-plugin-dls/lib/utils/evaluate.js b/packages/less-plugin-dls/lib/utils/evaluate.js index 57c67722..41394f01 100644 --- a/packages/less-plugin-dls/lib/utils/evaluate.js +++ b/packages/less-plugin-dls/lib/utils/evaluate.js @@ -6,9 +6,14 @@ import { VariablesOutputVisitor } from './visitors' const SELECTOR = 'DLS_VARS' export async function getVariables(path) { + if (!fs.existsSync(path)) { + return [] + } + const visitor = new VariablesOutputVisitor() await less.render(fs.readFileSync(path, 'utf-8'), { + filename: path, plugins: [ dls({ inject: false @@ -25,15 +30,15 @@ export async function getVariables(path) { return visitor.variables.map((v) => v.slice(1)) } -export async function getTuples(variables) { +export async function getTuples(variables, { theme }) { const src = [ `${SELECTOR}{`, - variables.map((v) => `${v}: @${v}`).join(';'), + dedupe(variables).map((v) => `${v}: @${v}`).join(';'), '}' ].join('') const { css } = await less.render(src, { - plugins: [dls()] + plugins: [dls({ theme })] }) return css @@ -43,3 +48,7 @@ export async function getTuples(variables) { .filter((v) => v) .map((decl) => decl.split(/:\s*/)) } + +function dedupe (arr) { + return Array.from(new Set(arr)) +} diff --git a/packages/less-plugin-dls/scripts/vars.js b/packages/less-plugin-dls/scripts/vars.js index cff0c531..f2e0f41e 100644 --- a/packages/less-plugin-dls/scripts/vars.js +++ b/packages/less-plugin-dls/scripts/vars.js @@ -4,22 +4,27 @@ import camelCase from 'lodash.camelcase' import { parse } from 'postcss-values-parser' import { getVariables, getTuples } from '../lib/utils/evaluate' -async function generate() { +async function generate({ theme } = {}) { try { const allVariables = await getVariables('./tokens/index.less') const globalVariables = await getVariables('./tokens/global.less') + const themeVariables = await getVariables(`./tokens/themes/${theme}.less`) - const tuples = await getTuples(allVariables) + const tuples = await getTuples(allVariables.concat(themeVariables), { + theme + }) + + const themeTail = theme ? `.${theme}` : '' // generate variables.less fs.writeFileSync( - path.resolve(__dirname, '..', 'variables.less'), + path.resolve(__dirname, '..', `variables${themeTail}.less`), tuples.map(([key, value]) => `@${key}: ${value};`).join('\n') + '\n', 'utf8' ) fs.writeFileSync( - path.resolve(__dirname, '..', 'variables.js'), + path.resolve(__dirname, '..', `variables${themeTail}.js`), tuples .map(([key, value]) => `export const ${camelCase(key)} = '${value}'`) .join('\n') + '\n', @@ -28,7 +33,7 @@ async function generate() { // generate variables.json fs.writeFileSync( - path.resolve(__dirname, '..', 'variables.json'), + path.resolve(__dirname, '..', `variables${themeTail}.json`), JSON.stringify( tuples .map(([key, value]) => ({ @@ -48,7 +53,7 @@ async function generate() { 'utf8' ) - console.log(`${tuples.length} variables generated.`) + console.log(`${tuples.length} variables generated for theme [${theme || 'default'}].`) } catch (e) { console.error(e) } @@ -129,4 +134,9 @@ function getTypeByValue(value) { return 'unknown' } -generate() +async function run() { + await generate() + await generate({ theme: 'ai' }) +} + +run() diff --git a/packages/less-plugin-dls/src/inject.js b/packages/less-plugin-dls/src/inject.js index 147cebd1..9a6bf55e 100644 --- a/packages/less-plugin-dls/src/inject.js +++ b/packages/less-plugin-dls/src/inject.js @@ -1,9 +1,15 @@ import path from 'path' +import fs from 'fs' const SELF_MODULE_PATH = path.resolve(__dirname, '..') const ENTRY_LESS = path.resolve(__dirname, '../tokens/index.less') +const THEME_DIR = path.resolve(__dirname, '../tokens/themes') class Injector { + constructor ({ theme }) { + this.theme = theme + } + process(src, extra) { // Don't inject self if ( @@ -19,11 +25,24 @@ class Injector { path.dirname(extra.fileInfo.filename), ENTRY_LESS ) + + const themeLess = path.resolve(THEME_DIR, `${this.theme}.less`) + let themeRelative = fs.existsSync(themeLess) ? path.relative( + path.dirname(extra.fileInfo.filename), + themeLess + ) : null + // less requires relative path to starts with ./ if (relative.charAt(0) !== '.') { relative = `./${relative}` } - const injected = `@import "${relative}";\n` + if (themeRelative && themeRelative.charAt(0) !== '.') { + themeRelative = `./${themeRelative}` + } + + let injected = `@import "${relative}";\n` + injected += themeRelative ? `@import "${themeRelative}";\n` : '' + const ignored = extra.imports.contentsIgnoredChars const fileInfo = extra.fileInfo ignored[fileInfo.filename] = ignored[fileInfo.filename] || 0 @@ -33,9 +52,9 @@ class Injector { } export default function inject(_, pluginManager) { - const { inject = true } = this.options || {} + const { inject = true, theme } = this.options || {} if (inject) { - pluginManager.addPreProcessor(new Injector()) + pluginManager.addPreProcessor(new Injector({ theme })) } } diff --git a/packages/less-plugin-dls/test/run.js b/packages/less-plugin-dls/test/run.js index 8331ef36..4dcf1f95 100644 --- a/packages/less-plugin-dls/test/run.js +++ b/packages/less-plugin-dls/test/run.js @@ -21,7 +21,7 @@ const INCLUDE_PATH = path.resolve(__dirname, '../src') const SPEC_DIR = path.resolve(__dirname, 'specs') const SRC_DIR = path.resolve(__dirname, '../tokens') const SRC_COMPONENTS_DIR = path.resolve(SRC_DIR, 'components') -const MANUAL_SPEC_MODULES = ['functions', 'global'] +const MANUAL_SPEC_MODULES = ['functions', 'global', 'ai'] const VAR_DEF_RE = /@([a-z]+(?:-[a-z0-9]+)*)\s*:/g function extractVarDefs (file) { @@ -114,11 +114,16 @@ function getTests () { } const specFile = path.resolve(moduleDir, partFile) - if (args['--update-snapshots']) { - const srcFile = !MANUAL_SPEC_MODULES.includes(module) - ? path.resolve(SRC_COMPONENTS_DIR, module + '.less') - : module === 'global' ? path.resolve(SRC_DIR, 'global.less') : null + const srcFile = !MANUAL_SPEC_MODULES.includes(module) + ? path.resolve(SRC_COMPONENTS_DIR, module + '.less') + : module === 'global' + ? path.resolve(SRC_DIR, 'global.less') + : module === 'ai' + ? path.resolve(SRC_DIR, 'themes/ai.less') + : null + + if (args['--update-snapshots']) { if (srcFile) { const vars = extractVarDefs(srcFile) if (vars.length) { @@ -138,6 +143,7 @@ function getTests () { module, part, src, + theme: module === 'ai' ? 'ai' : null, expected }) } @@ -165,7 +171,7 @@ function getSuite (name, tests, { less }) { .render(test.src, { paths: [INCLUDE_PATH], javascriptEnabled: true, - plugins: [dls()] + plugins: [dls({ theme: test.theme })] }) .then( result => { diff --git a/packages/less-plugin-dls/test/specs/ai/ai.css b/packages/less-plugin-dls/test/specs/ai/ai.css new file mode 100644 index 00000000..e4891fef --- /dev/null +++ b/packages/less-plugin-dls/test/specs/ai/ai.css @@ -0,0 +1,46 @@ +div { + -dls-color-brand: #4d79ff; + -dls-color-brand-0: #fff; + -dls-color-brand-1: #f2f7ff; + -dls-color-brand-2: #e3edff; + -dls-color-brand-3: #c4daff; + -dls-color-brand-4: #99beff; + -dls-color-brand-5: #729cfe; + -dls-color-brand-6: #4d79ff; + -dls-color-brand-7: #3a5bfd; + -dls-color-brand-8: #333fe6; + -dls-color-brand-9: #0f2dbd; + -dls-color-brand-10: #1c244a; + -dls-color-brand-11: #000; + -dls-color-info: #4d79ff; + -dls-color-info-0: #fff; + -dls-color-info-1: #f2f7ff; + -dls-color-info-2: #e3edff; + -dls-color-info-3: #c4daff; + -dls-color-info-4: #99beff; + -dls-color-info-5: #729cfe; + -dls-color-info-6: #4d79ff; + -dls-color-info-7: #3a5bfd; + -dls-color-info-8: #333fe6; + -dls-color-info-9: #0f2dbd; + -dls-color-info-10: #1c244a; + -dls-color-info-11: #000; + -dls-color-success: #3ac222; + -dls-color-warning: #fea800; + -dls-color-error: #f53f3f; + -dls-height-xs: 28px; + -dls-height-s: 32px; + -dls-height-m: 36px; + -dls-height-l: 40px; + -dls-height-xl: 48px; + -dls-border-radius-0: 4px; + -dls-border-radius-1: 6px; + -dls-border-radius-2: 10px; + -dls-border-radius-3: 16px; + -dls-border-radius-4: 26px; + -dls-shadow-color: #0047c2; + -dls-checkbox-border-radius: 4px; + -dls-tag-color-turquoise: #00bbd1; + -dls-tag-color-violet: #824fe7; + -dls-tag-color-green: #3ac222; +} diff --git a/packages/less-plugin-dls/test/specs/ai/ai.less b/packages/less-plugin-dls/test/specs/ai/ai.less new file mode 100644 index 00000000..6627ed59 --- /dev/null +++ b/packages/less-plugin-dls/test/specs/ai/ai.less @@ -0,0 +1,46 @@ +div { + -dls-color-brand: @dls-color-brand; + -dls-color-brand-0: @dls-color-brand-0; + -dls-color-brand-1: @dls-color-brand-1; + -dls-color-brand-2: @dls-color-brand-2; + -dls-color-brand-3: @dls-color-brand-3; + -dls-color-brand-4: @dls-color-brand-4; + -dls-color-brand-5: @dls-color-brand-5; + -dls-color-brand-6: @dls-color-brand-6; + -dls-color-brand-7: @dls-color-brand-7; + -dls-color-brand-8: @dls-color-brand-8; + -dls-color-brand-9: @dls-color-brand-9; + -dls-color-brand-10: @dls-color-brand-10; + -dls-color-brand-11: @dls-color-brand-11; + -dls-color-info: @dls-color-info; + -dls-color-info-0: @dls-color-info-0; + -dls-color-info-1: @dls-color-info-1; + -dls-color-info-2: @dls-color-info-2; + -dls-color-info-3: @dls-color-info-3; + -dls-color-info-4: @dls-color-info-4; + -dls-color-info-5: @dls-color-info-5; + -dls-color-info-6: @dls-color-info-6; + -dls-color-info-7: @dls-color-info-7; + -dls-color-info-8: @dls-color-info-8; + -dls-color-info-9: @dls-color-info-9; + -dls-color-info-10: @dls-color-info-10; + -dls-color-info-11: @dls-color-info-11; + -dls-color-success: @dls-color-success; + -dls-color-warning: @dls-color-warning; + -dls-color-error: @dls-color-error; + -dls-height-xs: @dls-height-xs; + -dls-height-s: @dls-height-s; + -dls-height-m: @dls-height-m; + -dls-height-l: @dls-height-l; + -dls-height-xl: @dls-height-xl; + -dls-border-radius-0: @dls-border-radius-0; + -dls-border-radius-1: @dls-border-radius-1; + -dls-border-radius-2: @dls-border-radius-2; + -dls-border-radius-3: @dls-border-radius-3; + -dls-border-radius-4: @dls-border-radius-4; + -dls-shadow-color: @dls-shadow-color; + -dls-checkbox-border-radius: @dls-checkbox-border-radius; + -dls-tag-color-turquoise: @dls-tag-color-turquoise; + -dls-tag-color-violet: @dls-tag-color-violet; + -dls-tag-color-green: @dls-tag-color-green; +} diff --git a/packages/less-plugin-dls/tokens/themes/ai.less b/packages/less-plugin-dls/tokens/themes/ai.less new file mode 100644 index 00000000..fb17efe7 --- /dev/null +++ b/packages/less-plugin-dls/tokens/themes/ai.less @@ -0,0 +1,58 @@ +@import "../global.less"; + +// brand color +@dls-color-brand: #4d79ff; +@dls-color-brand-0: #fff; +@dls-color-brand-1: #f2f7ff; +@dls-color-brand-2: #e3edff; +@dls-color-brand-3: #c4daff; +@dls-color-brand-4: #99beff; +@dls-color-brand-5: #729cfe; +@dls-color-brand-6: #4d79ff; +@dls-color-brand-7: #3a5bfd; +@dls-color-brand-8: #333fe6; +@dls-color-brand-9: #0f2dbd; +@dls-color-brand-10: #1c244a; +@dls-color-brand-11: #000; + +// info color +@dls-color-info: @dls-color-brand; +@dls-color-info-0: @dls-color-brand-0; +@dls-color-info-1: @dls-color-brand-1; +@dls-color-info-2: @dls-color-brand-2; +@dls-color-info-3: @dls-color-brand-3; +@dls-color-info-4: @dls-color-brand-4; +@dls-color-info-5: @dls-color-brand-5; +@dls-color-info-6: @dls-color-brand-6; +@dls-color-info-7: @dls-color-brand-7; +@dls-color-info-8: @dls-color-brand-8; +@dls-color-info-9: @dls-color-brand-9; +@dls-color-info-10: @dls-color-brand-10; +@dls-color-info-11: @dls-color-brand-11; + +@dls-color-success: #3ac222; +@dls-color-warning: #fea800; +@dls-color-error: #f53f3f; + +// component height +@dls-height-xs: @dls-height-unit * 7; +@dls-height-s: @dls-height-unit * 8; +@dls-height-m: @dls-height-unit * 9; +@dls-height-l: @dls-height-unit * 10; +@dls-height-xl: @dls-height-unit * 12; + +// border radius +@dls-border-radius-0: 4px; +@dls-border-radius-1: 6px; +@dls-border-radius-2: 10px; +@dls-border-radius-3: 16px; +@dls-border-radius-4: 26px; + +// shadow color +@dls-shadow-color: #0047c2; + +// component overrides +@dls-checkbox-border-radius: @dls-border-radius-0; +@dls-tag-color-turquoise: #00bbd1; +@dls-tag-color-violet: #824fe7; +@dls-tag-color-green: @dls-color-success-7; From 64a870f3651cb44402acdbf5d75fb84db4243e78 Mon Sep 17 00:00:00 2001 From: Justineo Date: Thu, 28 Dec 2023 10:37:16 +0800 Subject: [PATCH 2/3] build: add more output metadata files --- packages/less-plugin-dls/README.md | 7 +- packages/less-plugin-dls/scripts/vars.js | 110 +++++++++++++------- packages/less-plugin-dls/tokens/global.less | 1 + 3 files changed, 77 insertions(+), 41 deletions(-) diff --git a/packages/less-plugin-dls/README.md b/packages/less-plugin-dls/README.md index 22fd2ae8..0f436090 100644 --- a/packages/less-plugin-dls/README.md +++ b/packages/less-plugin-dls/README.md @@ -64,7 +64,12 @@ interface Variables { } ``` -There are also themed versions of the above files, which are named with the theme name as the suffix, namely `variables..js`, `variables..json` and `variables..less`. Currently the only supported theme is `ai`. +We also included a set of metadata files for different themes and scopes. Currently the only supported theme is `ai` and supported scopes are `global` and `palette`. The dist file names are in the format of `variables...`, where `` and `` are both optional. eg. + +```sh +variables.global.less # global variables, w/o component variables +variables.palette.ai.less # global color palette variables in AI theme +``` ## Custom functions diff --git a/packages/less-plugin-dls/scripts/vars.js b/packages/less-plugin-dls/scripts/vars.js index f2e0f41e..32a24095 100644 --- a/packages/less-plugin-dls/scripts/vars.js +++ b/packages/less-plugin-dls/scripts/vars.js @@ -4,56 +4,86 @@ import camelCase from 'lodash.camelcase' import { parse } from 'postcss-values-parser' import { getVariables, getTuples } from '../lib/utils/evaluate' +const paletteRe = /^dls-color-(?:brand|info|success|warning|error|gray|translucent)(?:-\d+)?$/ + +function getFilePath (type, { scope, theme } = {}) { + return path.resolve(__dirname, '..', `variables${scope ? `.${scope}` : ''}${theme ? `.${theme}` : ''}.${type}`) +} + +function genLess (tuples, options) { + const filePath = getFilePath('less', options) + + fs.writeFileSync( + filePath, + tuples.map(([key, value]) => `@${key}: ${value};`).join('\n') + '\n', + 'utf8' + ) +} + +function genJS (tuples, options) { + const filePath = getFilePath('js', options) + + fs.writeFileSync( + filePath, + tuples + .map(([key, value]) => `export const ${camelCase(key)} = '${value}'`) + .join('\n') + '\n', + 'utf8' + ) +} + +function genJSON (tuples, options) { + const filePath = getFilePath('json', options) + fs.writeFileSync( + filePath, + JSON.stringify( + tuples + .map(([key, value]) => ({ + [key]: { + value, + type: getTypeByName(key) || getTypeByValue(value), + global: options.globals.includes(key) + } + })) + .reduce((acc, cur) => { + Object.assign(acc, cur) + return acc + }, {}), + null, + ' ' + ), + 'utf8' + ) +} + async function generate({ theme } = {}) { try { const allVariables = await getVariables('./tokens/index.less') const globalVariables = await getVariables('./tokens/global.less') - const themeVariables = await getVariables(`./tokens/themes/${theme}.less`) + const paletteVariables = globalVariables.filter(v => paletteRe.test(v)) - const tuples = await getTuples(allVariables.concat(themeVariables), { - theme - }) + const allTuples = await getTuples(allVariables, { theme }) + const globalTuples = await getTuples(globalVariables, { theme }) + const paletteTuples = await getTuples(paletteVariables, { theme }) - const themeTail = theme ? `.${theme}` : '' + genLess(allTuples, { theme }) + genLess(globalTuples, { scope: 'global', theme }) + genLess(paletteTuples, { scope: 'palette', theme }) - // generate variables.less - fs.writeFileSync( - path.resolve(__dirname, '..', `variables${themeTail}.less`), - tuples.map(([key, value]) => `@${key}: ${value};`).join('\n') + '\n', - 'utf8' - ) + genJS(allTuples, { theme }) + genJS(globalTuples, { scope: 'global', theme }) + genJS(paletteTuples, { scope: 'palette', theme }) - fs.writeFileSync( - path.resolve(__dirname, '..', `variables${themeTail}.js`), - tuples - .map(([key, value]) => `export const ${camelCase(key)} = '${value}'`) - .join('\n') + '\n', - 'utf8' - ) - // generate variables.json - fs.writeFileSync( - path.resolve(__dirname, '..', `variables${themeTail}.json`), - JSON.stringify( - tuples - .map(([key, value]) => ({ - [key]: { - value, - type: getTypeByName(key) || getTypeByValue(value), - global: globalVariables.includes(key) - } - })) - .reduce((acc, cur) => { - Object.assign(acc, cur) - return acc - }, {}), - null, - ' ' - ), - 'utf8' - ) + genJSON(allTuples, { theme, globals: globalVariables }) + genJSON(globalTuples, { scope: 'global', theme, globals: globalVariables }) + genJSON(paletteTuples, { scope: 'palette', theme, globals: paletteVariables }) - console.log(`${tuples.length} variables generated for theme [${theme || 'default'}].`) + console.log( + `${allTuples.length} variables generated for theme [${ + theme || 'default' + }].` + ) } catch (e) { console.error(e) } diff --git a/packages/less-plugin-dls/tokens/global.less b/packages/less-plugin-dls/tokens/global.less index e73757be..a7c9baad 100644 --- a/packages/less-plugin-dls/tokens/global.less +++ b/packages/less-plugin-dls/tokens/global.less @@ -381,6 +381,7 @@ @dls-border-radius-1: 4px; @dls-border-radius-2: 6px; @dls-border-radius-3: 10px; +@dls-border-radius-4: 16px; /* Shadows */ @dls-shadow-color: @dls-color-gray-11; From 489857c48f20845bac8cc90968728cfa9adc562b Mon Sep 17 00:00:00 2001 From: Justineo Date: Thu, 28 Dec 2023 10:40:12 +0800 Subject: [PATCH 3/3] chore: update stylelint config --- .stylelintrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.stylelintrc b/.stylelintrc index 24f1cf35..c4196a60 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -6,6 +6,7 @@ "no-empty-source": null, "number-leading-zero": "always", "declaration-colon-newline-after": null, - "at-rule-no-unknown": null + "at-rule-no-unknown": null, + "import-notation": "string" } }