Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions packages/generate-design-tokens/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
171 changes: 141 additions & 30 deletions packages/generate-design-tokens/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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`;
Expand All @@ -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)
Expand Down Expand Up @@ -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 `
Expand All @@ -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<string>,
* skinFileName: (skinName: string) => string,
* skinModule: (skinName: string, skinConstantName: string) => SkinModule,
* generateSharedFiles?: (skinNames: Array<string>) => Promise<void>,
* }} 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<string>}
*/
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();
Loading