From 799328497f5b72709d2d35d1f4e4234bef6ab3c0 Mon Sep 17 00:00:00 2001 From: Matteo Bertamini <61736059+brtbrt@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:16:03 +0200 Subject: [PATCH 1/3] feat(generate-design-tokens): support community skins generation Enable generate-design-tokens to discover and generate community skins from JSON tokens in the mistica-design repo. Community skin tokens are expected in tokens/community/ folder, with generated files output to src/community/skins/ and css/community/ directories. - Add community skins folder paths (src/community/skins, css/community) - Modify generateSkinSrc to handle community skins with correct import paths and type usage - Add generateCommunitySkins function to discover and process community token files - Community skins use generic Skin type instead of KnownSkin - Constants are defined inline in community skin files --- packages/generate-design-tokens/index.js | 68 +++++++++++++++++++++--- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/packages/generate-design-tokens/index.js b/packages/generate-design-tokens/index.js index 66cea4bc6f..8c669bc0d5 100644 --- a/packages/generate-design-tokens/index.js +++ b/packages/generate-design-tokens/index.js @@ -24,6 +24,8 @@ 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']; @@ -91,11 +93,13 @@ const buildColor = (colorDescription) => { throw new Error(`Unknown color format: ${colorDescription.value}`); }; -const generateSkinSrc = (skinName) => { +const generateSkinSrc = (skinName, isCommunitySkin = false) => { const designTokensFile = fs.readFileSync(path.join(DESIGN_TOKENS_FOLDER, `${skinName}.json`), 'utf8'); const needsApplyAlphaImport = designTokensFile.includes('rgba'); const designTokens = JSON.parse(designTokensFile); const skinConstantName = `${skinName.toUpperCase().replace(/-/g, '_')}_SKIN`; + const importPrefix = isCommunitySkin ? '../..' : '..'; + const typeImportPath = isCommunitySkin ? '../../skins/types' : './types'; const textTokens = {}; Object.entries(designTokens.text).forEach(([textAttribute, textAttributeConfig]) => { @@ -107,20 +111,31 @@ const generateSkinSrc = (skinName) => { }); }); - return ` -import {${skinConstantName}} from './constants'; -${needsApplyAlphaImport ? `import {applyAlpha} from '../utils/color';` : ''} + const constantsImport = isCommunitySkin ? '' : `import {${skinConstantName}} from './constants';\n`; + const skinTypeImport = isCommunitySkin + ? `import type {Skin} from '${typeImportPath}';` + : `import type {GetKnownSkin, KnownSkin} from './types';`; + const skinName_const = isCommunitySkin + ? `export const ${skinConstantName} = '${toPascalCase(skinName)}';\n\n` + : ''; + const skinType = isCommunitySkin ? 'Skin' : 'KnownSkin'; + const functionSignature = isCommunitySkin + ? `export const get${toPascalCase(skinName)}Skin = (): ${skinType} => {` + : `export const get${toPascalCase(skinName)}Skin: GetKnownSkin = () => {`; + const applyAlphaImportPath = isCommunitySkin ? '../../utils/color' : '../utils/color'; -import type {GetKnownSkin, KnownSkin} from './types'; + return ` +${constantsImport}${needsApplyAlphaImport ? `import {applyAlpha} from '${applyAlphaImportPath}';\n` : ''} +${skinTypeImport} -export const palette = { +${skinName_const}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) @@ -182,6 +197,41 @@ const formatTs = async (source) => parser: 'typescript', }); +const generateCommunitySkins = async () => { + const communityTokensFolder = path.join(DESIGN_TOKENS_FOLDER, 'community'); + + if (!fs.existsSync(communityTokensFolder)) { + console.log('No community skins folder found in design tokens'); + return; + } + + const communitySkinFiles = fs.readdirSync(communityTokensFolder).filter((file) => file.endsWith('.json')); + + if (communitySkinFiles.length === 0) { + console.log('No community skin token files found'); + return; + } + + if (!fs.existsSync(COMMUNITY_SKINS_FOLDER)) { + fs.mkdirSync(COMMUNITY_SKINS_FOLDER, {recursive: true}); + } + + if (!fs.existsSync(COMMUNITY_CSS_FOLDER)) { + fs.mkdirSync(COMMUNITY_CSS_FOLDER, {recursive: true}); + } + + for (const file of communitySkinFiles) { + const skinName = file.replace('.json', ''); + console.log('Generating community skin tokens for', skinName); + + const skinSrc = await formatTs(generateSkinSrc(skinName, true)); + fs.writeFileSync(path.join(COMMUNITY_SKINS_FOLDER, `${skinName}-skin.tsx`), skinSrc); + + const skinCssSrc = await formatCss(generateSkinCssSrc(skinName, communityTokensFolder)); + fs.writeFileSync(path.join(COMMUNITY_CSS_FOLDER, `${skinName}.css`), skinCssSrc); + } +}; + const generateSkinFiles = async () => { let anyGeneratedSkin; @@ -210,6 +260,8 @@ const generateSkinFiles = async () => { const commonCssSrc = await formatCss(generateCommonCssSrc(DESIGN_TOKENS_FOLDER)); fs.writeFileSync(path.join(CSS_FOLDER, `mistica-common.css`), commonCssSrc); } + + await generateCommunitySkins(); }; generateSkinFiles(); From 0b75aa71feeee672a3dd38e7c307bfe9f60589af Mon Sep 17 00:00:00 2001 From: Matteo Bertamini <61736059+brtbrt@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:16:26 +0200 Subject: [PATCH 2/3] docs(generate-design-tokens): document community skins generation --- packages/generate-design-tokens/README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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 From 4d9e253cc55c6a1d4ea63bf3a3e1d9d2a05a1a4f Mon Sep 17 00:00:00 2001 From: Matteo Bertamini <61736059+brtbrt@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:54:10 +0200 Subject: [PATCH 3/3] simplify release workflow into multi-step in place of multi-jobs --- packages/generate-design-tokens/index.js | 203 +++++++++++++++-------- 1 file changed, 131 insertions(+), 72 deletions(-) diff --git a/packages/generate-design-tokens/index.js b/packages/generate-design-tokens/index.js index 8c669bc0d5..f9e2148f93 100644 --- a/packages/generate-design-tokens/index.js +++ b/packages/generate-design-tokens/index.js @@ -27,7 +27,15 @@ 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()); @@ -93,13 +101,15 @@ const buildColor = (colorDescription) => { throw new Error(`Unknown color format: ${colorDescription.value}`); }; -const generateSkinSrc = (skinName, isCommunitySkin = false) => { - 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`; - const importPrefix = isCommunitySkin ? '../..' : '..'; - const typeImportPath = isCommunitySkin ? '../../skins/types' : './types'; const textTokens = {}; Object.entries(designTokens.text).forEach(([textAttribute, textAttributeConfig]) => { @@ -111,24 +121,21 @@ const generateSkinSrc = (skinName, isCommunitySkin = false) => { }); }); - const constantsImport = isCommunitySkin ? '' : `import {${skinConstantName}} from './constants';\n`; - const skinTypeImport = isCommunitySkin - ? `import type {Skin} from '${typeImportPath}';` - : `import type {GetKnownSkin, KnownSkin} from './types';`; - const skinName_const = isCommunitySkin - ? `export const ${skinConstantName} = '${toPascalCase(skinName)}';\n\n` - : ''; - const skinType = isCommunitySkin ? 'Skin' : 'KnownSkin'; - const functionSignature = isCommunitySkin - ? `export const get${toPascalCase(skinName)}Skin = (): ${skinType} => {` - : `export const get${toPascalCase(skinName)}Skin: GetKnownSkin = () => {`; - const applyAlphaImportPath = isCommunitySkin ? '../../utils/color' : '../utils/color'; + const { + constantsImport, + skinTypeImport, + skinConstantDeclaration, + skinType, + functionSignature, + applyAlphaImportPath, + } = family.skinModule(skinName, skinConstantName); return ` + ${constantsImport}${needsApplyAlphaImport ? `import {applyAlpha} from '${applyAlphaImportPath}';\n` : ''} ${skinTypeImport} -${skinName_const}export const palette = { +${skinConstantDeclaration}export const palette = { ${Object.entries(designTokens.global.palette) .map(([colorName, colorDescription]) => `'${colorName}':'${colorDescription.value}'`) .join(',')} @@ -173,8 +180,8 @@ ${functionSignature} `; }; -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 ` @@ -186,82 +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 generateCommunitySkins = async () => { - const communityTokensFolder = path.join(DESIGN_TOKENS_FOLDER, 'community'); - - if (!fs.existsSync(communityTokensFolder)) { - console.log('No community skins folder found in design tokens'); - return; - } +/** + * 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 + */ - const communitySkinFiles = fs.readdirSync(communityTokensFolder).filter((file) => file.endsWith('.json')); +/** + * 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 (communitySkinFiles.length === 0) { - console.log('No community skin token files found'); - return; - } +/** + * 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; + }); - if (!fs.existsSync(COMMUNITY_SKINS_FOLDER)) { - fs.mkdirSync(COMMUNITY_SKINS_FOLDER, {recursive: true}); - } +/** @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; + } - if (!fs.existsSync(COMMUNITY_CSS_FOLDER)) { - fs.mkdirSync(COMMUNITY_CSS_FOLDER, {recursive: true}); - } + const typesSrc = await formatTs(generateColorTypesSrc(referenceSkin, this.tokensFolder)); + fs.writeFileSync(path.join(this.skinsFolder, 'types', 'colors.tsx'), typesSrc); - for (const file of communitySkinFiles) { - const skinName = file.replace('.json', ''); - console.log('Generating community skin tokens for', skinName); + const commonCssSrc = await formatCss(generateCommonCssSrc(this.tokensFolder)); + fs.writeFileSync(path.join(this.cssFolder, 'mistica-common.css'), commonCssSrc); + }, +}; - const skinSrc = await formatTs(generateSkinSrc(skinName, true)); - fs.writeFileSync(path.join(COMMUNITY_SKINS_FOLDER, `${skinName}-skin.tsx`), skinSrc); +/** @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', + }), +}; - const skinCssSrc = await formatCss(generateSkinCssSrc(skinName, communityTokensFolder)); - fs.writeFileSync(path.join(COMMUNITY_CSS_FOLDER, `${skinName}.css`), skinCssSrc); +/** @param {SkinFamily} family */ +const generateSkinFamily = async (family) => { + const skinNames = listExistingSkins(family); + if (skinNames.length === 0) { + return; } -}; -const generateSkinFiles = async () => { - let anyGeneratedSkin; + fs.mkdirSync(family.skinsFolder, {recursive: true}); + fs.mkdirSync(family.cssFolder, {recursive: true}); - for (const skinName of KNOWN_SKINS) { + for (const skinName of skinNames) { console.log('Generating tokens for skin', skinName); - if (!fs.existsSync(path.join(DESIGN_TOKENS_FOLDER, `${skinName}.json`))) { - console.error(`Missing ${skinName}.json file`); - 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 skinSrc = await formatTs(generateSkinSrc(skinName, family)); + fs.writeFileSync(path.join(family.skinsFolder, family.skinFileName(skinName)), skinSrc); - const skinCssSrc = await formatCss(generateSkinCssSrc(skinName, DESIGN_TOKENS_FOLDER)); - fs.writeFileSync(path.join(CSS_FOLDER, `${skinName}.css`), skinCssSrc); - - anyGeneratedSkin = skinName; + const skinCssSrc = await formatCss(generateSkinCssSrc(skinName, family.tokensFolder)); + fs.writeFileSync(path.join(family.cssFolder, `${skinName}.css`), skinCssSrc); } - if (anyGeneratedSkin) { - const typesSrc = await formatTs(generateColorTypesSrc(anyGeneratedSkin)); - fs.writeFileSync(path.join(SKINS_FOLDER, 'types', 'colors.tsx'), typesSrc); - - const commonCssSrc = await formatCss(generateCommonCssSrc(DESIGN_TOKENS_FOLDER)); - fs.writeFileSync(path.join(CSS_FOLDER, `mistica-common.css`), commonCssSrc); - } + await family.generateSharedFiles?.(skinNames); +}; - await generateCommunitySkins(); +const main = async () => { + await generateSkinFamily(OFFICIAL_SKINS_FAMILY); + await generateSkinFamily(COMMUNITY_SKINS_FAMILY); }; -generateSkinFiles(); +main();