(bin) add tokens repo

This repo is made for the bin script that generates the tokens files,
at the moment it only generates a css file, but it is designed to be
able to generate any other file format ( Typescript, Javascript for example )
This commit is contained in:
Nathan Vasse
2022-12-01 12:03:37 +01:00
committed by NathanVss
parent 16172e7a00
commit 32a48e9e46
18 changed files with 440 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
node_modules
dist

View File

@@ -0,0 +1,16 @@
{
"root": true,
"extends": [
"custom"
],
"parserOptions": {
"project": [
"./tsconfig.json",
"./tsconfig.node.json"
]
},
"rules": {
"no-console": "off",
"no-eval": "off"
}
}

88
packages/tokens/README.md Normal file
View File

@@ -0,0 +1,88 @@
# @openfun/cunningham-tokens
## Introduction
**What are design tokens ?**
Design tokens are the fundamental variables defining the precise behavior and rendering of ui components.
For example:
- The primary color of a text element
- The standard spacing between two elements
- The border radius of a button
- ...
## Getting started
In this section we will install the library and generate the file that contains the design tokens of your app in order to
make Cunningham's Design System yours!
Install the lib
```
yarn add @openfun/cunningham-tokens
```
Create a file named `cunningham.cjs` at the root of your project
```
module.exports = {
theme: {
colors: {
primary: 'purple'
},
},
};
```
In this configuration file you can overwrite all the default values of the design system.
You can find the default values [here](./src/bin/cunningham.dist.js).
Now add this script to your `package.json`
```
{
"scripts": {
...
"build-theme": "cunningham"
}
}
```
The cunningham CLI's main purpose is to build a ad-hoc CSS file that contains all your customized design tokens,
by taking into account your local configuration ( defined in the file that you previously created : `cunningham.cjs`,
it is worth mentioning that this file is optional, hence it will generate a file containing the default values of the
design system )
> You can run `yarn run cunningham -h` to see the available options.
And in order to generate the tokens css file, run
```
yarn build-theme
```
It will generate a file named `cunningham-tokens.css`. **Don't forget to run this command everytime you
change the content of the `cunningham.cjs` file !**
Then, add these lines at the top of your main stylesheet file:
```
@import "cunningham-tokens"; // Imports the file you just generated.
@import "@openfun/cunningham-react/style";
```
It's all done!
## Use the design tokens
Design tokens variable are all present in the `cunningham-tokens.css` file. They are all prefixed with `--c` in order to
avoid collision.
Here is an example to make the text's color renders with the value of the primary color in `.my-element` matching elements
```
.my-element {
color: var(--c--colors--primary);
}
```

View File

@@ -0,0 +1,4 @@
export default {
preset: "ts-jest",
testEnvironment: "node",
};

View File

@@ -0,0 +1,43 @@
{
"name": "@openfun/cunningham-tokens",
"private": false,
"version": "0.0.0",
"license": "MIT",
"bin": {
"cunningham": "dist/bin/Main.js"
},
"exports": {
"./default-tokens": "./dist/cunningham-tokens.css"
},
"files": [
"dist/"
],
"scripts": {
"lint": "eslint . 'src/**/*.{ts,tsx}'",
"dev": "nodemon --watch 'src/bin' --ext '*' --exec 'yarn build'",
"build": "tsc -p tsconfig.json && cp src/bin/cunningham.dist.js dist/bin && chmod +x dist/bin/Main.js && yarn build-default-theme",
"build-default-theme": "./dist/bin/Main.js -o dist -s html",
"test": "FORCE_COLOR=1 jest --verbose src/bin/tests"
},
"dependencies": {
"chalk": "4.1.2",
"commander": "9.4.1",
"deepmerge": "4.2.2",
"figlet": "1.5.2"
},
"devDependencies": {
"@types/figlet": "1.5.5",
"@types/jest": "29.2.3",
"@types/node": "18.11.9",
"eslint-config-custom": "*",
"jest": "29.3.1",
"nodemon": "2.0.20",
"prettier": "2.8.0",
"ts-jest": "29.0.3",
"ts-node": "10.9.1",
"typescript": "4.9.3"
},
"engines": {
"node": ">=16.0.0"
}
}

View File

@@ -0,0 +1,8 @@
export default {
configurationFilenames: ["cunningham.js", "cunningham.cjs"],
sass: {
varSeparator: "--",
varPrefix: "c--",
tokenFilenameCss: "cunningham-tokens.css",
},
};

View File

@@ -0,0 +1,32 @@
import path from "path";
import * as fs from "fs";
import deepmerge from "deepmerge";
import Config from "./Config";
import { ConfigShape } from "./TokensGenerator";
import { workPath } from "./Paths";
const getLocalConfig = async () => {
const filename = Config.configurationFilenames
.map((filename_) => path.join(workPath(), filename_))
.find((filename_) => fs.existsSync(filename_));
if (!filename) {
console.log("No local config found, using default config.");
return {};
}
const config = await import(filename);
return config.default;
};
const getDistConfig = async () => {
const config = await import("./cunningham.dist.js");
return config.default;
};
export const getConfig = async () => {
const localConfig = await getLocalConfig();
const distConfig = await getDistConfig();
const config: ConfigShape = deepmerge(distConfig, localConfig);
return config;
};

View File

@@ -0,0 +1,26 @@
import * as path from "path";
import * as fs from "fs";
import chalk from "chalk";
import Config from "./Config";
import { flatify } from "./Utils/Flatify";
export const cssGenerator = async (
tokens: any,
opts: { path: string; selector: string }
) => {
const flatTokens = flatify(tokens, Config.sass.varSeparator);
const cssVars = Object.keys(flatTokens).reduce((acc, token) => {
return (
acc + `\t--${Config.sass.varPrefix}${token}: ${flatTokens[token]};\n`
);
}, "");
const cssContent = `${opts.selector} {\n${cssVars}}`;
const dest = path.join(opts.path, Config.sass.tokenFilenameCss);
console.log("Generating tokens file to " + dest + " ...");
if (!fs.existsSync(opts.path)) {
fs.mkdirSync(opts.path);
}
fs.writeFileSync(dest, cssContent);
console.log(chalk.bgGreen(chalk.white("File generated successfully.")));
};

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env node
import { run } from "./ThemeGenerator";
run(process.argv);

View File

@@ -0,0 +1,3 @@
export const workPath = () => {
return process.cwd();
};

View File

@@ -0,0 +1,39 @@
import { program } from "commander";
import chalk from "chalk";
import figlet from "figlet";
import { getConfig } from "./ConfigLoader";
import { tokensGenerator } from "./TokensGenerator";
import { cssGenerator } from "./CssGenerator";
import { workPath } from "./Paths";
export const buildTheme = async () => {
const options = program.opts();
const config = await getConfig();
const tokens = tokensGenerator(config);
await cssGenerator(tokens, {
path: options.output,
selector: options.selector,
});
};
export const run = async (args: string[]) => {
console.log(
chalk.red(figlet.textSync("Cunningham", { horizontalLayout: "full" }))
);
program
.description("Cunningham's CLI tool.")
.option(
"-o, --output <directory>",
"Specify the output dir of generated files.",
workPath()
)
.option(
"-s, --selector <selector>",
"Specify the css root selector element.",
":root"
)
.parse(args);
await buildTheme();
};

View File

@@ -0,0 +1,15 @@
interface ThemeShape {
colors: {
[key: string]: string;
};
}
export interface ConfigShape {
theme: ThemeShape;
}
export const tokensGenerator = (config: ConfigShape) => {
return {
colors: { ...config.theme.colors },
};
};

View File

@@ -0,0 +1,15 @@
export const flatify = (obj: any, separator: string) => {
const flatObj: any = {};
Object.keys(obj).forEach((key) => {
const value = obj[key];
if (typeof value === "object") {
const flatChild = flatify(value, separator);
Object.keys(flatChild).forEach((subKey) => {
flatObj[key + separator + subKey] = flatChild[subKey];
});
} else {
flatObj[key] = value;
}
});
return flatObj;
};

View File

@@ -0,0 +1,8 @@
module.exports = {
theme: {
colors: {
primary: "#055FD2",
secondary: "#DA0000",
},
},
};

View File

@@ -0,0 +1,116 @@
import * as fs from "fs";
import * as path from "path";
import { run } from "../ThemeGenerator";
import Config from "../Config";
jest.mock("../Paths", () => ({
workPath: () => __dirname,
}));
/**
* Empty the current directory from generated tokens file and local
* config to start with an predictable environment.
*/
const cleanup = () => {
const filePath = path.join(__dirname, Config.sass.tokenFilenameCss);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
const localConfigurationFile = path.join(
__dirname,
Config.configurationFilenames[0]
);
if (fs.existsSync(localConfigurationFile)) {
fs.unlinkSync(localConfigurationFile);
}
const outputPath = path.join(__dirname, "output");
if (fs.existsSync(outputPath)) {
fs.rmSync(outputPath, { recursive: true });
}
};
describe("Cunningham Bin", () => {
beforeAll(() => {
jest.spyOn(console, "log").mockImplementation(() => {});
cleanup();
});
afterEach(() => {
cleanup();
});
it("Runs without existing config file with default values.", async () => {
const cssTokensFile = path.join(__dirname, Config.sass.tokenFilenameCss);
expect(fs.existsSync(cssTokensFile)).toEqual(false);
await run([]);
expect(fs.existsSync(cssTokensFile)).toEqual(true);
expect(fs.readFileSync(cssTokensFile).toString()).toEqual(`:root {
\t--c--colors--primary: #055FD2;
\t--c--colors--secondary: #DA0000;
}`);
});
it("Runs with existing config file using local values.", async () => {
const localConfigurationFile = path.join(
__dirname,
Config.configurationFilenames[0]
);
expect(fs.existsSync(localConfigurationFile)).toEqual(false);
const cssTokensFile = path.join(__dirname, Config.sass.tokenFilenameCss);
expect(fs.existsSync(cssTokensFile)).toEqual(false);
fs.copyFileSync(
path.join(__dirname, "assets", Config.configurationFilenames[0]),
localConfigurationFile
);
expect(fs.existsSync(localConfigurationFile)).toEqual(true);
await run([]);
expect(fs.existsSync(cssTokensFile)).toEqual(true);
expect(fs.readFileSync(cssTokensFile).toString()).toEqual(`:root {
\t--c--colors--primary: AntiqueWhite;
\t--c--colors--secondary: #DA0000;
}`);
});
const testOutput = async (opt: string) => {
const outputDir = path.join(__dirname, "output");
const cssTokensFile = path.join(outputDir, Config.sass.tokenFilenameCss);
expect(fs.existsSync(cssTokensFile)).toEqual(false);
await run(["", "", opt, outputDir]);
expect(fs.existsSync(cssTokensFile)).toEqual(true);
expect(fs.readFileSync(cssTokensFile).toString()).toEqual(`:root {
\t--c--colors--primary: #055FD2;
\t--c--colors--secondary: #DA0000;
}`);
};
it("Runs with -o options.", async () => {
await testOutput("-o");
});
it("Runs with --output options.", async () => {
await testOutput("--output");
});
const testSelector = async (opt: string) => {
const cssTokensFile = path.join(__dirname, Config.sass.tokenFilenameCss);
expect(fs.existsSync(cssTokensFile)).toEqual(false);
await run(["", "", opt, "html"]);
expect(fs.existsSync(cssTokensFile)).toEqual(true);
expect(fs.readFileSync(cssTokensFile).toString()).toEqual(`html {
\t--c--colors--primary: #055FD2;
\t--c--colors--secondary: #DA0000;
}`);
};
it("Runs with -s options.", async () => {
await testSelector("-s");
});
it("Runs with --selector options.", async () => {
await testSelector("--selector");
});
});

View File

@@ -0,0 +1,7 @@
module.exports = {
theme: {
colors: {
primary: "AntiqueWhite",
},
},
};

View File

@@ -0,0 +1,10 @@
{
"extends": "@openfun/typescript-configs/node.json",
"include": [
"./src/**/*"
],
"compilerOptions": {
"baseUrl": "./src/bin",
"outDir": "./dist/bin",
}
}

View File

@@ -0,0 +1,3 @@
{
"include": ["jest.config.ts"]
}