diff --git a/packages/generate-design-tokens/README.md b/packages/generate-design-tokens/README.md index 7ed65faaeb..64433062c8 100644 --- a/packages/generate-design-tokens/README.md +++ b/packages/generate-design-tokens/README.md @@ -1,8 +1,16 @@ # generate-design-tokens -Script to generate mistica skins (in src/skins folder) from design tokens imported from mistica-design repo. +Script to generate mistica skins from design tokens imported from the mistica-design repo. -Run: +## Generated Files + +- **Main skins** → `src/skins/` and `css/` +- **Community skins** → `src/community/skins/` and `css/community/` + +The script discovers community skin tokens in the `tokens/community/` folder of the mistica-design repository +and generates corresponding TypeScript and CSS files. + +## Run ```bash node index.js diff --git a/packages/generate-design-tokens/index.js b/packages/generate-design-tokens/index.js index 66cea4bc6f..f9e2148f93 100644 --- a/packages/generate-design-tokens/index.js +++ b/packages/generate-design-tokens/index.js @@ -24,8 +24,18 @@ console.log('Using design tokens from:', DESIGN_TOKENS_FOLDER); const SKINS_FOLDER = path.join(import.meta.dirname, '..', '..', 'src', 'skins'); const CSS_FOLDER = path.join(import.meta.dirname, '..', '..', 'css'); +const COMMUNITY_SKINS_FOLDER = path.join(import.meta.dirname, '..', '..', 'src', 'community', 'skins'); +const COMMUNITY_CSS_FOLDER = path.join(import.meta.dirname, '..', '..', 'css', 'community'); -const KNOWN_SKINS = ['blau', 'movistar', 'o2', 'telefonica', 'vivo', 'vivo-evolution', 'esimflag']; +const OFFICIAL_SKINS = ['blau', 'movistar', 'o2', 'telefonica', 'vivo', 'vivo-evolution', 'esimflag']; +const COMMUNITY_SKINS = ['cyber']; + +const GENERATED_BANNER = `/** + * @generated + * This file was automatically generated by the generate-design-tokens script. + * Do not edit it by hand: your changes will be overwritten the next time the script runs. + */ +`; const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1); const toCamelCase = (str) => str.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); @@ -91,8 +101,12 @@ const buildColor = (colorDescription) => { throw new Error(`Unknown color format: ${colorDescription.value}`); }; -const generateSkinSrc = (skinName) => { - const designTokensFile = fs.readFileSync(path.join(DESIGN_TOKENS_FOLDER, `${skinName}.json`), 'utf8'); +/** + * @param {string} skinName + * @param {SkinFamily} family + */ +const generateSkinSrc = (skinName, family) => { + const designTokensFile = fs.readFileSync(path.join(family.tokensFolder, `${skinName}.json`), 'utf8'); const needsApplyAlphaImport = designTokensFile.includes('rgba'); const designTokens = JSON.parse(designTokensFile); const skinConstantName = `${skinName.toUpperCase().replace(/-/g, '_')}_SKIN`; @@ -107,20 +121,28 @@ const generateSkinSrc = (skinName) => { }); }); + const { + constantsImport, + skinTypeImport, + skinConstantDeclaration, + skinType, + functionSignature, + applyAlphaImportPath, + } = family.skinModule(skinName, skinConstantName); + return ` -import {${skinConstantName}} from './constants'; -${needsApplyAlphaImport ? `import {applyAlpha} from '../utils/color';` : ''} -import type {GetKnownSkin, KnownSkin} from './types'; +${constantsImport}${needsApplyAlphaImport ? `import {applyAlpha} from '${applyAlphaImportPath}';\n` : ''} +${skinTypeImport} -export const palette = { +${skinConstantDeclaration}export const palette = { ${Object.entries(designTokens.global.palette) .map(([colorName, colorDescription]) => `'${colorName}':'${colorDescription.value}'`) .join(',')} }; -export const get${toPascalCase(skinName)}Skin: GetKnownSkin = () => { - const skin: KnownSkin = { +${functionSignature} + const skin: ${skinType} = { name: ${skinConstantName}, colors: { ${Object.entries(designTokens.light) @@ -158,8 +180,8 @@ export const get${toPascalCase(skinName)}Skin: GetKnownSkin = () => { `; }; -const generateColorTypesSrc = (skinName) => { - const designTokensFile = fs.readFileSync(path.join(DESIGN_TOKENS_FOLDER, `${skinName}.json`), 'utf8'); +const generateColorTypesSrc = (skinName, tokensFolder) => { + const designTokensFile = fs.readFileSync(path.join(tokensFolder, `${skinName}.json`), 'utf8'); const designTokens = JSON.parse(designTokensFile); return ` @@ -171,45 +193,134 @@ export type Colors = { }; const formatCss = async (source) => - prettier.format(source, { + prettier.format(`${GENERATED_BANNER}\n${source}`, { ...(await prettier.resolveConfig('.')), parser: 'css', }); const formatTs = async (source) => - prettier.format(source, { + prettier.format(`${GENERATED_BANNER}\n${source}`, { ...(await prettier.resolveConfig('.')), parser: 'typescript', }); -const generateSkinFiles = async () => { - let anyGeneratedSkin; +/** + * The variant-specific parts of a generated skin module: its imports, the exported skin type, + * the optional skin-name constant declaration, and the generated function signature. + * + * @typedef {{ + * constantsImport: string, + * skinTypeImport: string, + * skinConstantDeclaration: string, + * skinType: string, + * functionSignature: string, + * applyAlphaImportPath: string, + * }} SkinModule + */ - for (const skinName of KNOWN_SKINS) { - console.log('Generating tokens for skin', skinName); +/** + * A skin family describes where its tokens live, where the generated files go, how each skin + * module is shaped, and which skins it contains. Known and community skins share the same + * generation pipeline and differ only in this descriptor. + * + * @typedef {{ + * tokensFolder: string, + * skinsFolder: string, + * cssFolder: string, + * skins: Array, + * skinFileName: (skinName: string) => string, + * skinModule: (skinName: string, skinConstantName: string) => SkinModule, + * generateSharedFiles?: (skinNames: Array) => Promise, + * }} SkinFamily + */ - if (!fs.existsSync(path.join(DESIGN_TOKENS_FOLDER, `${skinName}.json`))) { +/** + * Returns the family's skins whose token file exists, logging an error for any that are missing. + * + * @param {SkinFamily} family + * @returns {Array} + */ +const listExistingSkins = (family) => + family.skins.filter((skinName) => { + const exists = fs.existsSync(path.join(family.tokensFolder, `${skinName}.json`)); + if (!exists) { console.error(`Missing ${skinName}.json file`); + } + return exists; + }); + +/** @type {SkinFamily} */ +const OFFICIAL_SKINS_FAMILY = { + tokensFolder: DESIGN_TOKENS_FOLDER, + skinsFolder: SKINS_FOLDER, + cssFolder: CSS_FOLDER, + skins: OFFICIAL_SKINS, + skinFileName: (skinName) => `${skinName}.tsx`, + skinModule: (skinName, skinConstantName) => ({ + constantsImport: `import {${skinConstantName}} from './constants';\n`, + skinTypeImport: `import type {GetKnownSkin, KnownSkin} from './types';`, + skinConstantDeclaration: '', + skinType: 'KnownSkin', + functionSignature: `export const get${toPascalCase(skinName)}Skin: GetKnownSkin = () => {`, + applyAlphaImportPath: '../utils/color', + }), + async generateSharedFiles(skinNames) { + const [referenceSkin] = skinNames; + if (!referenceSkin) { return; } - // todo https://github.com/Telefonica/mistica-web/issues/1633 add @generated banner to generated skin files so they are not mistakenly edited by hand - const skinSrc = await formatTs(generateSkinSrc(skinName)); - fs.writeFileSync(path.join(SKINS_FOLDER, `${skinName}.tsx`), skinSrc); + const typesSrc = await formatTs(generateColorTypesSrc(referenceSkin, this.tokensFolder)); + fs.writeFileSync(path.join(this.skinsFolder, 'types', 'colors.tsx'), typesSrc); - const skinCssSrc = await formatCss(generateSkinCssSrc(skinName, DESIGN_TOKENS_FOLDER)); - fs.writeFileSync(path.join(CSS_FOLDER, `${skinName}.css`), skinCssSrc); + const commonCssSrc = await formatCss(generateCommonCssSrc(this.tokensFolder)); + fs.writeFileSync(path.join(this.cssFolder, 'mistica-common.css'), commonCssSrc); + }, +}; + +/** @type {SkinFamily} */ +const COMMUNITY_SKINS_FAMILY = { + tokensFolder: path.join(DESIGN_TOKENS_FOLDER, 'community'), + skinsFolder: COMMUNITY_SKINS_FOLDER, + cssFolder: COMMUNITY_CSS_FOLDER, + skins: COMMUNITY_SKINS, + skinFileName: (skinName) => `${skinName}-skin.tsx`, + skinModule: (skinName, skinConstantName) => ({ + constantsImport: '', + skinTypeImport: `import type {Skin} from '../../skins/types';`, + skinConstantDeclaration: `export const ${skinConstantName} = '${toPascalCase(skinName)}';\n\n`, + skinType: 'Skin', + functionSignature: `export const get${toPascalCase(skinName)}Skin = (): Skin => {`, + applyAlphaImportPath: '../../utils/color', + }), +}; - anyGeneratedSkin = skinName; +/** @param {SkinFamily} family */ +const generateSkinFamily = async (family) => { + const skinNames = listExistingSkins(family); + if (skinNames.length === 0) { + return; } - if (anyGeneratedSkin) { - const typesSrc = await formatTs(generateColorTypesSrc(anyGeneratedSkin)); - fs.writeFileSync(path.join(SKINS_FOLDER, 'types', 'colors.tsx'), typesSrc); + fs.mkdirSync(family.skinsFolder, {recursive: true}); + fs.mkdirSync(family.cssFolder, {recursive: true}); - const commonCssSrc = await formatCss(generateCommonCssSrc(DESIGN_TOKENS_FOLDER)); - fs.writeFileSync(path.join(CSS_FOLDER, `mistica-common.css`), commonCssSrc); + for (const skinName of skinNames) { + console.log('Generating tokens for skin', skinName); + + const skinSrc = await formatTs(generateSkinSrc(skinName, family)); + fs.writeFileSync(path.join(family.skinsFolder, family.skinFileName(skinName)), skinSrc); + + const skinCssSrc = await formatCss(generateSkinCssSrc(skinName, family.tokensFolder)); + fs.writeFileSync(path.join(family.cssFolder, `${skinName}.css`), skinCssSrc); } + + await family.generateSharedFiles?.(skinNames); +}; + +const main = async () => { + await generateSkinFamily(OFFICIAL_SKINS_FAMILY); + await generateSkinFamily(COMMUNITY_SKINS_FAMILY); }; -generateSkinFiles(); +main();