Skip to content

Commit

Permalink
feat: move design tokens CLI commands to paragon CLI (#2609)
Browse files Browse the repository at this point in the history
* feat: move cli design tokens to paragon cli

* feat: update paths for build-tokens.js

* feat: added descriptions for CLI commands

* refactor: removed commander implementation

* refactor: added build-scss process status

* feat: added ora compilation status

* refactor: code refactoring

* feat: added help description for single command

* refactor: after review

* refactor: refactoring after review

* chore: update docs and cli params parsing

---------

Co-authored-by: monteri <lansevermore>
Co-authored-by: PKulkoRaccoonGang <[email protected]>
Co-authored-by: Viktor Rusakov <[email protected]>
  • Loading branch information
3 people committed Aug 4, 2024
1 parent 3ec0c07 commit 23a9519
Show file tree
Hide file tree
Showing 9 changed files with 342 additions and 218 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
127 changes: 52 additions & 75 deletions build-scss.js → lib/build-scss.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env node
const fs = require('fs');
const sass = require('sass');
const postCSS = require('postcss');
Expand All @@ -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';

Expand Down Expand Up @@ -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) => {
Expand All @@ -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 <corePath>',
'Path to the theme\'s core SCSS file, defaults to Paragon\'s core.scss.',
),
)
.addOption(
new Option(
'--themesPath <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 <outDir>',
'Specifies directory where to out resulting CSS files.',
),
)
.addOption(
new Option(
'--defaultThemeVariants <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<string>} 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;
119 changes: 119 additions & 0 deletions lib/build-tokens.js
Original file line number Diff line number Diff line change
@@ -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;
59 changes: 36 additions & 23 deletions lib/help.js
Original file line number Diff line number Diff line change
@@ -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 <command>.'));
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('');
});
}

Expand Down
Loading

0 comments on commit 23a9519

Please sign in to comment.