✨(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:
2
packages/tokens/.eslintignore
Normal file
2
packages/tokens/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
16
packages/tokens/.eslintrc.json
Normal file
16
packages/tokens/.eslintrc.json
Normal 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
88
packages/tokens/README.md
Normal 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);
|
||||||
|
}
|
||||||
|
```
|
||||||
4
packages/tokens/jest.config.ts
Normal file
4
packages/tokens/jest.config.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export default {
|
||||||
|
preset: "ts-jest",
|
||||||
|
testEnvironment: "node",
|
||||||
|
};
|
||||||
43
packages/tokens/package.json
Normal file
43
packages/tokens/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
packages/tokens/src/bin/Config.ts
Normal file
8
packages/tokens/src/bin/Config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
configurationFilenames: ["cunningham.js", "cunningham.cjs"],
|
||||||
|
sass: {
|
||||||
|
varSeparator: "--",
|
||||||
|
varPrefix: "c--",
|
||||||
|
tokenFilenameCss: "cunningham-tokens.css",
|
||||||
|
},
|
||||||
|
};
|
||||||
32
packages/tokens/src/bin/ConfigLoader.ts
Normal file
32
packages/tokens/src/bin/ConfigLoader.ts
Normal 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;
|
||||||
|
};
|
||||||
26
packages/tokens/src/bin/CssGenerator.ts
Normal file
26
packages/tokens/src/bin/CssGenerator.ts
Normal 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.")));
|
||||||
|
};
|
||||||
5
packages/tokens/src/bin/Main.ts
Normal file
5
packages/tokens/src/bin/Main.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { run } from "./ThemeGenerator";
|
||||||
|
|
||||||
|
run(process.argv);
|
||||||
3
packages/tokens/src/bin/Paths.ts
Normal file
3
packages/tokens/src/bin/Paths.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const workPath = () => {
|
||||||
|
return process.cwd();
|
||||||
|
};
|
||||||
39
packages/tokens/src/bin/ThemeGenerator.ts
Normal file
39
packages/tokens/src/bin/ThemeGenerator.ts
Normal 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();
|
||||||
|
};
|
||||||
15
packages/tokens/src/bin/TokensGenerator.ts
Normal file
15
packages/tokens/src/bin/TokensGenerator.ts
Normal 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 },
|
||||||
|
};
|
||||||
|
};
|
||||||
15
packages/tokens/src/bin/Utils/Flatify.ts
Normal file
15
packages/tokens/src/bin/Utils/Flatify.ts
Normal 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;
|
||||||
|
};
|
||||||
8
packages/tokens/src/bin/cunningham.dist.js
Normal file
8
packages/tokens/src/bin/cunningham.dist.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
module.exports = {
|
||||||
|
theme: {
|
||||||
|
colors: {
|
||||||
|
primary: "#055FD2",
|
||||||
|
secondary: "#DA0000",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
116
packages/tokens/src/bin/tests/Cunningham.spec.ts
Normal file
116
packages/tokens/src/bin/tests/Cunningham.spec.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
7
packages/tokens/src/bin/tests/assets/cunningham.js
Normal file
7
packages/tokens/src/bin/tests/assets/cunningham.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
theme: {
|
||||||
|
colors: {
|
||||||
|
primary: "AntiqueWhite",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
10
packages/tokens/tsconfig.json
Normal file
10
packages/tokens/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "@openfun/typescript-configs/node.json",
|
||||||
|
"include": [
|
||||||
|
"./src/**/*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./src/bin",
|
||||||
|
"outDir": "./dist/bin",
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/tokens/tsconfig.node.json
Normal file
3
packages/tokens/tsconfig.node.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"include": ["jest.config.ts"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user