diff --git a/Makefile b/Makefile index a1fae49f06..d6dc6ff320 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ build: rm -rf dist/**/__snapshots__ rm -rf dist/__mocks__ rm -rf dist/setupTest.js - node build-scss.js + ./bin/paragon-scripts.js build-scss export TRANSIFEX_RESOURCE = paragon transifex_langs = "ar,ca,es_419,fr,he,id,ko_KR,pl,pt_BR,ru,th,uk,zh_CN,es_AR,es_ES,pt_PT,tr_TR,it_IT" diff --git a/README.md b/README.md index cddd191354..95f7d78473 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,10 @@ The Paragon CLI (Command Line Interface) is a tool that provides various utility ### Available Commands -- `paragon install-theme [theme]`: Installs the specific @edx/brand package. +- `paragon install-theme [theme]`: Installs the specific [brand package](https://github.com/openedx/brand-openedx). +- `paragon build-tokens`: Build Paragon's design tokens. +- `paragon replace-variables`: Replace SCSS variables usages or definitions to CSS variables and vice versa in `.scss` files. +- `paragon build-scss`: Compile Paragon's core and themes SCSS into CSS. Use `paragon help` to see more information. diff --git a/bin/paragon-scripts.js b/bin/paragon-scripts.js index 47ef258f0b..dbc9329699 100755 --- a/bin/paragon-scripts.js +++ b/bin/paragon-scripts.js @@ -2,8 +2,9 @@ const chalk = require('chalk'); const themeCommand = require('../lib/install-theme'); const helpCommand = require('../lib/help'); - -const HELP_COMMAND = 'help'; +const buildTokensCommand = require('../lib/build-tokens'); +const replaceVariablesCommand = require('../lib/replace-variables'); +const buildScssCommand = require('../lib/build-scss'); const COMMANDS = { /** @@ -25,6 +26,9 @@ const COMMANDS = { * { * name: '--optionName', * description: 'optionDescription', + * choices: 'optionChoices', + * defaultValue: 'optionDefaultValue', + * required: true/false, * }, * ... * ], @@ -43,14 +47,130 @@ const COMMANDS = { }, ], }, + 'build-tokens': { + executor: buildTokensCommand, + description: 'CLI to build Paragon design tokens.', + options: [ + { + name: '-s, --source', + description: 'Specify the source directory for design tokens.', + defaultValue: '\'\'', + }, + { + name: '-b, --build-dir', + description: 'Specify the build directory for the generated tokens.', + defaultValue: './build/', + }, + { + name: '--source-tokens-only', + description: 'Include only source design tokens in the build.', + defaultValue: false, + }, + { + name: '-t, --themes', + description: 'Specify themes to include in the token build.', + defaultValue: 'light', + }, + ], + }, + 'replace-variables': { + executor: replaceVariablesCommand, + description: 'CLI to replace SCSS variables usages or definitions to CSS variables and vice versa in .scss files.', + parameters: [ + { + name: '-p, --filePath', + description: 'Path to the file or directory where to replace variables.', + defaultValue: '\'\'', + }, + ], + options: [ + { + name: '-s, --source', + description: 'Type of replacement: usage or definition. If set to "definition" the command will only update SCSS variables definitions with CSS variables, if set to "usage" - all occurrences of SCSS variables will we replaced', + defaultValue: '\'\'', + }, + { + name: '-t, --replacementType', + description: 'Type of replacement: usage or definition. If set to "definition" the command will only update SCSS variables definitions with CSS variables, if set to "usage" - all occurrences of SCSS variables will we replaced', + choices: '[usage|definition]', + defaultValue: 'definition', + }, + { + name: '-d, --direction', + description: 'Map direction: css-to-scss or scss-to-css, if replacement type parameter is set to "definition" this has no effect.', + choices: '[scss-to-css|css-to-scss]', + defaultValue: 'scss-to-css', + }, + ], + }, + 'build-scss': { + executor: buildScssCommand, + description: 'CLI to compile Paragon\'s core and themes SCSS into CSS.', + options: [ + { + name: '--corePath', + description: 'Path to the theme\'s core SCSS file, defaults to Paragon\'s core.scss.', + defaultValue: 'styles/scss/core/core.scss', + }, + { + name: '--themesPath', + description: `Path to the directory that contains themes' files. Expects directory to have following structure: + themes/ + light/ + │ ├─ index.css + │ ├─ other_css_files + dark/ + │ ├─ index.css + │ ├─ other_css_files + some_other_custom_theme/ + │ ├─ index.css + │ ├─ other_css_files + ... + + where index.css has imported all other CSS files in the theme's subdirectory. The script will output + light.css, dark.css and some_other_custom_theme.css files (together with maps and minified versions). + You can provide any amount of themes. Default to paragon's themes. + `, + defaultValue: 'styles/css/themes', + }, + { + name: '--outDir', + description: 'Specifies directory where to out resulting CSS files.', + defaultValue: './dist', + }, + { + name: '--defaultThemeVariants', + description: `Specifies default theme variants. Defaults to a single 'light' theme variant. + You can provide multiple default theme variants by passing multiple values, for + example: \`--defaultThemeVariants light dark\` + `, + defaultValue: 'light', + }, + ], + }, help: { - executor: helpCommand, + executor: (args) => helpCommand(COMMANDS, args), + parameters: [ + { + name: 'command', + description: 'Specifies command name.', + defaultValue: '\'\'', + choices: '[install-theme|build-tokens|replace-variables|build-scss]', + required: false, + }, + ], description: 'Displays help for available commands.', }, }; +/** + * Executes a Paragon CLI command based on the provided command-line arguments. + * + * @async + * @function executeParagonCommand + */ (async () => { - const [command] = process.argv.slice(2); + const [command, ...commandArgs] = process.argv.slice(2); const executor = COMMANDS[command]; if (!executor) { @@ -59,13 +179,8 @@ const COMMANDS = { return; } - if (command === HELP_COMMAND) { - helpCommand(COMMANDS); - return; - } - try { - await executor.executor(); + await executor.executor(commandArgs); } catch (error) { // eslint-disable-next-line no-console console.error(chalk.red.bold('An error occurred:', error.message)); diff --git a/build-scss.js b/lib/build-scss.js similarity index 66% rename from build-scss.js rename to lib/build-scss.js index be80a49373..9125cb8166 100755 --- a/build-scss.js +++ b/lib/build-scss.js @@ -1,4 +1,3 @@ -#!/usr/bin/env node const fs = require('fs'); const sass = require('sass'); const postCSS = require('postcss'); @@ -8,7 +7,10 @@ const postCSSMinify = require('postcss-minify'); const combineSelectors = require('postcss-combine-duplicated-selectors'); const { pathToFileURL } = require('url'); const path = require('path'); -const { program, Option } = require('commander'); +const minimist = require('minimist'); +const chalk = require('chalk'); +const ora = require('ora'); +const { capitalize } = require('./utils'); const paragonThemeOutputFilename = 'theme-urls.json'; @@ -92,17 +94,19 @@ const compileAndWriteStyleSheets = ({ }, }], }); + const commonPostCssPlugins = [ postCSSImport(), postCSSCustomMedia({ preserve: true }), combineSelectors({ removeDuplicatedProperties: true }), ]; + const postCSSCompilation = ora(`Compilation for ${capitalize(name)} stylesheet...`).start(); postCSS(commonPostCssPlugins) .process(compiledStyleSheet.css, { from: stylesPath, map: false }) .then((result) => { + postCSSCompilation.succeed(`Successfully compiled ${capitalize(name)} theme stylesheet`); fs.writeFileSync(`${outDir}/${name}.css`, result.css); - postCSS([postCSSMinify()]) .process(result.css, { from: `${name}.css`, map: { inline: false } }) .then((minifiedResult) => { @@ -129,83 +133,56 @@ const compileAndWriteStyleSheets = ({ isDefaultThemeVariant, }); } + fs.writeFileSync(`${outDir}/${paragonThemeOutputFilename}`, `${JSON.stringify(paragonThemeOutput, null, 2)}\n`); + }) + .then(() => { + ora().succeed(chalk.underline.bold.green(`Successfully built stylesheet for ${capitalize(name)} theme!\n`)); + }) + .catch((error) => { + ora().fail(chalk.bold(`Failed to build stylesheets for ${capitalize(name)}: ${error.message}`)); }); }; -program - .version('0.0.1') - .description('CLI to compile Paragon\'s core and themes\' SCSS into CSS.') - .addOption( - new Option( - '--corePath ', - 'Path to the theme\'s core SCSS file, defaults to Paragon\'s core.scss.', - ), - ) - .addOption( - new Option( - '--themesPath ', - `Path to the directory that contains themes' files. Expects directory to have following structure: - themes/ - light/ - │ ├─ index.css - │ ├─ other_css_files - dark/ - │ ├─ index.css - │ ├─ other_css_files - some_other_custom_theme/ - │ ├─ index.css - │ ├─ other_css_files - ... - - where index.css has imported all other CSS files in the theme's subdirectory. The script will output - light.css, dark.css and some_other_custom_theme.css files (together with maps and minified versions). - You can provide any amount of themes. Default to paragon's themes. - `, - ), - ) - .addOption( - new Option( - '--outDir ', - 'Specifies directory where to out resulting CSS files.', - ), - ) - .addOption( - new Option( - '--defaultThemeVariants ', - `Specifies default theme variants. Defaults to a single 'light' theme variant. - You can provide multiple default theme variants by passing multiple values, for - example: \`--defaultThemeVariants light dark\` - `, - ), - ); - -program.parse(process.argv); +/** + * Builds SCSS stylesheets based on the provided command arguments. + * + * @param {Array} commandArgs - Command line arguments for building SCSS stylesheets. + */ +function buildScssCommand(commandArgs) { + const defaultArgs = { + corePath: path.resolve(process.cwd(), 'styles/scss/core/core.scss'), + themesPath: path.resolve(process.cwd(), 'styles/css/themes'), + outDir: './dist', + defaultThemeVariants: 'light', + }; -const options = program.opts(); -const { - corePath = path.resolve(__dirname, 'styles/scss/core/core.scss'), - themesPath = path.resolve(__dirname, 'styles/css/themes'), - outDir = './dist', - defaultThemeVariants = ['light'], -} = options; + const { + corePath, + themesPath, + outDir, + defaultThemeVariants, + } = minimist(commandArgs, { default: defaultArgs }); -// Core CSS -compileAndWriteStyleSheets({ - name: 'core', - stylesPath: corePath, - outDir, -}); + // Core CSS + compileAndWriteStyleSheets({ + name: 'core', + stylesPath: corePath, + outDir, + }); -// Theme Variants CSS -fs.readdirSync(themesPath, { withFileTypes: true }) - .filter((item) => item.isDirectory()) - .forEach((themeDir) => { - compileAndWriteStyleSheets({ - name: themeDir.name, - stylesPath: `${themesPath}/${themeDir.name}/index.css`, - outDir, - isThemeVariant: true, - isDefaultThemeVariant: defaultThemeVariants.includes(themeDir.name), + // Theme Variants CSS + fs.readdirSync(themesPath, { withFileTypes: true }) + .filter((item) => item.isDirectory()) + .forEach((themeDir) => { + compileAndWriteStyleSheets({ + name: themeDir.name, + stylesPath: `${themesPath}/${themeDir.name}/index.css`, + outDir, + isThemeVariant: true, + isDefaultThemeVariant: defaultThemeVariants.includes(themeDir.name), + }); }); - }); +} + +module.exports = buildScssCommand; diff --git a/lib/build-tokens.js b/lib/build-tokens.js new file mode 100755 index 0000000000..b9916ddb3d --- /dev/null +++ b/lib/build-tokens.js @@ -0,0 +1,119 @@ +const path = require('path'); +const minimist = require('minimist'); +const { StyleDictionary, colorTransform, createCustomCSSVariables } = require('../tokens/style-dictionary'); +const { createIndexCssFile } = require('../tokens/utils'); + +/** + * Builds tokens for CSS styles from JSON source files. + * + * @param {string[]} commandArgs - Command line arguments for building tokens. + * @param {string} [commandArgs.build-dir='./build/'] - The directory where the build output will be placed. + * @param {string} [commandArgs.source] - The source directory containing JSON token files. + * @param {boolean} [commandArgs.source-tokens-only=false] - Indicates whether to include only source tokens. + * @param {string|string[]} [commandArgs.themes=['light']] - The themes (variants) for which to build tokens. + */ +async function buildTokensCommand(commandArgs) { + const defaultParams = { + themes: ['light'], + 'build-dir': './build/', + }; + + const alias = { + 'build-dir': 'b', + themes: 't', + }; + + const { + 'build-dir': buildDir, + source: tokensSource, + 'source-tokens-only': hasSourceTokensOnly, + themes, + } = minimist(commandArgs, { alias, default: defaultParams, boolean: 'source-tokens-only' }); + + const coreConfig = { + include: [path.resolve(__dirname, '../tokens/src/core/**/*.json')], + source: tokensSource ? [`${tokensSource}/core/**/*.json`] : [], + platforms: { + css: { + prefix: 'pgn', + transformGroup: 'css', + // NOTE: buildPath must end with a slash + buildPath: buildDir.slice(-1) === '/' ? buildDir : `${buildDir}/`, + files: [ + { + format: 'css/custom-variables', + destination: 'core/variables.css', + filter: hasSourceTokensOnly ? 'isSource' : undefined, + options: { + outputReferences: !hasSourceTokensOnly, + }, + }, + { + format: 'css/custom-media-breakpoints', + destination: 'core/custom-media-breakpoints.css', + filter: hasSourceTokensOnly ? 'isSource' : undefined, + options: { + outputReferences: !hasSourceTokensOnly, + }, + }, + ], + transforms: StyleDictionary.transformGroup.css.filter(item => item !== 'size/rem').concat('color/sass-color-functions', 'str-replace'), + options: { + fileHeader: 'customFileHeader', + }, + }, + }, + }; + + const getStyleDictionaryConfig = (themeVariant) => ({ + ...coreConfig, + include: [...coreConfig.include, path.resolve(__dirname, `../tokens/src/themes/${themeVariant}/**/*.json`)], + source: tokensSource ? [`${tokensSource}/themes/${themeVariant}/**/*.json`] : [], + transform: { + 'color/sass-color-functions': { + ...StyleDictionary.transform['color/sass-color-functions'], + transformer: (token) => colorTransform(token, themeVariant), + }, + }, + format: { + 'css/custom-variables': formatterArgs => createCustomCSSVariables({ + formatterArgs, + themeVariant, + }), + }, + platforms: { + css: { + ...coreConfig.platforms.css, + files: [ + { + format: 'css/custom-variables', + destination: `themes/${themeVariant}/variables.css`, + filter: hasSourceTokensOnly ? 'isSource' : undefined, + options: { + outputReferences: !hasSourceTokensOnly, + }, + }, + { + format: 'css/utility-classes', + destination: `themes/${themeVariant}/utility-classes.css`, + filter: hasSourceTokensOnly ? 'isSource' : undefined, + options: { + outputReferences: !hasSourceTokensOnly, + }, + }, + ], + }, + }, + }); + + StyleDictionary.extend(coreConfig).buildAllPlatforms(); + createIndexCssFile({ buildDir, isTheme: false }); + + themes.forEach((themeVariant) => { + const config = getStyleDictionaryConfig(themeVariant); + StyleDictionary.extend(config).buildAllPlatforms(); + createIndexCssFile({ buildDir, isTheme: true, themeVariant }); + }); +} + +module.exports = buildTokensCommand; diff --git a/lib/help.js b/lib/help.js index 9491995a18..17e68b33b9 100644 --- a/lib/help.js +++ b/lib/help.js @@ -1,52 +1,65 @@ /* eslint-disable no-console */ const chalk = require('chalk'); -const DESCRIPTION_PAD = 20; - /** - * Pads a description string to align with a specified offset string. + * Finds a command based on the given name in the commands object. * - * @param {string} description - The description to pad. - * @param {string} offsetString - The offset string that the description should align with. - * @returns {string} - The padded description. + * @param {Array} commandName - The name to find the command. + * @param {Object} commands - The object containing commands to search in. + * @returns {Object|null} - The found command or null if the command is not found. */ -function padLeft(description, offsetString) { - // Calculate the necessary padding based on the offsetString length - const padding = ' '.repeat(Math.max(0, DESCRIPTION_PAD - offsetString.length)); - return `${padding}${description}`; -} +const findCommandByName = (commandName, commands) => ((commandName in commands) + ? { [commandName]: commands[commandName] } : null); /** * Displays a help message for available commands, including descriptions, parameters, and options. * * @param {Object} commands - An object containing information about available commands. + * @param {Array} commandArgs - An array containing the command name. */ -function helpCommand(commands) { +function helpCommand(commands, commandArgs) { + const retrievedCommands = commandArgs.length ? findCommandByName(commandArgs, commands) : commands; + if (!retrievedCommands) { + console.error(chalk.red.bold('Unknown command. Usage: paragon help .')); + return; + } + console.log(chalk.yellow.bold('Paragon Help')); console.log(); - console.log('Available commands:'); + + if (!commandArgs.length) { + console.log('Available commands:'); + } + console.log(); - Object.entries(commands).forEach(([command, { parameters, description, options }]) => { - console.log(` ${chalk.green.bold(command)}`); + Object.entries(retrievedCommands).forEach(([command, { parameters, description, options }]) => { + console.log(` ${chalk.green.underline.bold(command)}`); if (description) { - console.log(` ${description}`); + console.log(` ${description}`); } if (parameters && parameters.length > 0) { - console.log(` ${chalk.cyan('Parameters: ')}`); + console.log(''); + console.log(` ${chalk.bold.cyan('Parameters: ')}`); parameters.forEach(parameter => { - const requiredStatus = parameter.required ? 'Required' : 'Optional'; - const formattedDescription = padLeft(parameter.description, parameter.name); - console.log(` ${parameter.name}${formattedDescription} (${requiredStatus}, Default: ${parameter.defaultValue || 'None'})`); + console.log(` ${chalk.yellow.bold(parameter.name)} ${chalk.grey(parameter.choices ? `${parameter.choices}, Default: ${parameter.defaultValue || 'None'}` : `Default: ${parameter.defaultValue || 'None'}`)}`); + if (parameter.description) { + console.log(` ${parameter.description}`); + } + console.log(''); }); } if (options && options.length > 0) { - console.log(` ${chalk.cyan('Options: ')}`); + console.log(''); + console.log(` ${chalk.bold.cyan('Options: ')}`); options.forEach(option => { - const formattedDescription = padLeft(option.description, option.name); - console.log(` ${option.name}${formattedDescription}`); + console.log(` ${chalk.yellow.bold(option.name)} ${chalk.grey(option.choices ? `${option.choices}, Default: ${option.defaultValue || 'None'}` : `Default: ${option.defaultValue}`)}`); + if (option.description) { + console.log(` ${option.description}`); + } + console.log(''); }); } diff --git a/lib/replace-variables.js b/lib/replace-variables.js new file mode 100755 index 0000000000..07b7361add --- /dev/null +++ b/lib/replace-variables.js @@ -0,0 +1,38 @@ +const minimist = require('minimist'); +const { transformInPath } = require('../tokens/utils'); +const mapSCSStoCSS = require('../tokens/map-scss-to-css'); + +/** + * Replaces CSS or SASS variables in a file with their corresponding values. + * + * @param {string[]} commandArgs - Command line arguments for replacing variables. + * @param {string} [commandArgs.filePath] - The path to the file in which variables should be replaced. + * @param {string} [commandArgs.source] - The path to the source directory containing variable mappings. + * @param {string} [commandArgs.replacementType] - The type of replacement ('usage' or 'all'). + * @param {string} [commandArgs.direction] - The direction of replacement ('forward' or 'backward'). + */ +async function replaceVariablesCommand(commandArgs) { + const alias = { + filePath: 'p', + source: 's', + replacementType: 't', + direction: 'd', + }; + + const { + filePath, + source: sourcePath, + replacementType, + direction, + } = minimist(commandArgs, { alias }); + + const variablesMap = mapSCSStoCSS(sourcePath); + + if (replacementType === 'usage') { + await transformInPath(filePath, variablesMap, 'usage', [], direction); + } else { + await transformInPath(filePath, variablesMap); + } +} + +module.exports = replaceVariablesCommand; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000000..6855ee5b27 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,9 @@ +// eslint-disable-next-line import/prefer-default-export +function capitalize(str) { + if (typeof str !== 'string' || str.length === 0) { + return ''; + } + return str.charAt(0).toUpperCase() + str.slice(1); +} + +module.exports = { capitalize }; diff --git a/package-lock.json b/package-lock.json index f50771fc12..f49d9b1b7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,13 +22,17 @@ "child_process": "^1.0.2", "chroma-js": "^2.4.2", "classnames": "^2.3.1", + "cli-progress": "^3.12.0", "commander": "^9.4.1", "email-prop-type": "^3.0.0", "file-selector": "^0.6.0", "glob": "^8.0.3", "inquirer": "^8.2.5", "lodash.uniqby": "^4.7.0", + "log-update": "^4.0.0", "mailto-link": "^2.0.0", + "minimist": "^1.2.8", + "ora": "^5.4.1", "postcss": "^8.4.21", "postcss-combine-duplicated-selectors": "^10.0.3", "postcss-custom-media": "^9.1.2", @@ -54,10 +58,7 @@ "uuid": "^9.0.0" }, "bin": { - "build-design-tokens": "tokens/build-tokens.js", - "build-scss": "build-scss.js", - "paragon": "bin/paragon-scripts.js", - "replace-scss-with-css": "tokens/replace-variables.js" + "paragon": "bin/paragon-scripts.js" }, "devDependencies": { "@babel/cli": "^7.16.8", @@ -13286,6 +13287,17 @@ "node": ">=8" } }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cli-spinners": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", @@ -25326,6 +25338,21 @@ "node": ">=18.0.0" } }, + "node_modules/listr2/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/listr2/node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -25350,6 +25377,21 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/listr2/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/listr2/node_modules/emoji-regex": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", @@ -25362,6 +25404,99 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "dev": true }, + "node_modules/listr2/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/listr2/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/listr2/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -25689,205 +25824,63 @@ } }, "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", "dependencies": { - "environment": "^1.0.0" + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-update/node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "dev": true - }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", - "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", - "dev": true, - "dependencies": { - "get-east-asian-width": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", - "dev": true, + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=18" + "node": ">=8" }, "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, + "node_modules/log-update/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=7.0.0" } }, - "node_modules/log-update/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } + "node_modules/log-update/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", - "dev": true, + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/longest-streak": { diff --git a/package.json b/package.json index a975cb5081..7391a243e5 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,6 @@ "types": "dist/index.d.ts", "license": "Apache-2.0", "bin": { - "build-design-tokens": "./tokens/build-tokens.js", - "replace-scss-with-css": "./tokens/replace-variables.js", - "build-scss": "./build-scss.js", "paragon": "./bin/paragon-scripts.js" }, "publishConfig": { @@ -51,9 +48,9 @@ "type-check:watch": "npm run type-check -- --watch", "build-types": "tsc --emitDeclarationOnly", "playroom:start": "npm run playroom:start --workspace=www", - "build-tokens": "node tokens/build-tokens.js --build-dir ./styles/css", - "replace-variables-usage-with-css": "node tokens/replace-variables.js -p src -t usage", - "replace-variables-definition-with-css": "node tokens/replace-variables.js -p src -t definition", + "build-tokens": "./bin/paragon-scripts.js build-tokens --build-dir ../styles/css", + "replace-variables-usage-with-css": "./bin/paragon-scripts.js replace-variables -p src -t usage", + "replace-variables-definition-with-css": "./bin/paragon-scripts.js replace-variables -p src -t definition", "build-scss-to-css-map": "node tokens/map-scss-to-css.js", "playroom:build": "npm run playroom:build --workspace=www", "prepare": "husky || true" @@ -65,13 +62,17 @@ "child_process": "^1.0.2", "chroma-js": "^2.4.2", "classnames": "^2.3.1", + "cli-progress": "^3.12.0", "commander": "^9.4.1", "email-prop-type": "^3.0.0", "file-selector": "^0.6.0", "glob": "^8.0.3", "inquirer": "^8.2.5", "lodash.uniqby": "^4.7.0", + "log-update": "^4.0.0", "mailto-link": "^2.0.0", + "minimist": "^1.2.8", + "ora": "^5.4.1", "postcss": "^8.4.21", "postcss-combine-duplicated-selectors": "^10.0.3", "postcss-custom-media": "^9.1.2", diff --git a/tokens/build-tokens.js b/tokens/build-tokens.js deleted file mode 100755 index e8b022d97d..0000000000 --- a/tokens/build-tokens.js +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env node -const { program } = require('commander'); -const path = require('path'); -const { StyleDictionary, colorTransform, createCustomCSSVariables } = require('./style-dictionary'); -const { createIndexCssFile } = require('./utils'); - -program - .version('0.0.1') - .description('CLI to build design tokens for various platforms (currently only CSS is supported) from Paragon Design Tokens.') - .option('--build-dir ', 'A path to directory where to put files with built tokens, must end with a /.', './build/') - .option('--source ', 'A path where to look for additional tokens that will get merged with Paragon ones, must be a path to root directory of the token files that contains "root" and "themes" subdirectories.') - .option('--source-tokens-only', 'If provided, only tokens from --source will be included in the output; Paragon tokens will be used for references but not included in the output.') - .option('--themes ', 'A list of theme variants to build. By default, Paragon currently only supports a light theme.') - .parse(); - -const { - buildDir, - source: tokensSource, - sourceTokensOnly: hasSourceTokensOnly, - themes, -} = program.opts(); - -const coreConfig = { - include: [path.resolve(__dirname, 'src/core/**/*.json')], - source: tokensSource ? [`${tokensSource}/core/**/*.json`] : [], - platforms: { - css: { - prefix: 'pgn', - transformGroup: 'css', - // NOTE: buildPath must end with a slash - buildPath: buildDir.slice(-1) === '/' ? buildDir : `${buildDir}/`, - files: [ - { - format: 'css/custom-variables', - destination: 'core/variables.css', - filter: hasSourceTokensOnly ? 'isSource' : undefined, - options: { - outputReferences: !hasSourceTokensOnly, - }, - }, - { - format: 'css/custom-media-breakpoints', - destination: 'core/custom-media-breakpoints.css', - filter: hasSourceTokensOnly ? 'isSource' : undefined, - options: { - outputReferences: !hasSourceTokensOnly, - }, - }, - ], - transforms: StyleDictionary.transformGroup.css.filter(item => item !== 'size/rem').concat('color/sass-color-functions', 'str-replace'), - options: { - fileHeader: 'customFileHeader', - }, - }, - }, -}; - -const getStyleDictionaryConfig = (themeVariant) => ({ - ...coreConfig, - include: [...coreConfig.include, path.resolve(__dirname, `src/themes/${themeVariant}/**/*.json`)], - source: tokensSource ? [`${tokensSource}/themes/${themeVariant}/**/*.json`] : [], - transform: { - 'color/sass-color-functions': { - ...StyleDictionary.transform['color/sass-color-functions'], - transformer: (token) => colorTransform(token, themeVariant), - }, - }, - format: { - 'css/custom-variables': formatterArgs => createCustomCSSVariables({ - formatterArgs, - themeVariant, - }), - }, - platforms: { - css: { - ...coreConfig.platforms.css, - files: [ - { - format: 'css/custom-variables', - destination: `themes/${themeVariant}/variables.css`, - filter: hasSourceTokensOnly ? 'isSource' : undefined, - options: { - outputReferences: !hasSourceTokensOnly, - }, - }, - { - format: 'css/utility-classes', - destination: `themes/${themeVariant}/utility-classes.css`, - filter: hasSourceTokensOnly ? 'isSource' : undefined, - options: { - outputReferences: !hasSourceTokensOnly, - }, - }, - ], - }, - }, -}); - -StyleDictionary.extend(coreConfig).buildAllPlatforms(); - -// This line creates the index file for core folder, specially when buildDir is outside Paragon. -createIndexCssFile({ buildDir, isTheme: false }); - -const THEME_VARIANTS = themes || ['light']; - -THEME_VARIANTS.forEach((themeVariant) => { - const config = getStyleDictionaryConfig(themeVariant); - StyleDictionary.extend(config).buildAllPlatforms(); - createIndexCssFile({ buildDir, isTheme: true, themeVariant }); -});