(react) add tokens.ts files handling

These files will be used to define the custom design tokens per components.
They are automatically aggregated by the packages/react/cunningham.ts file,
this is why handling typescript config file was important.
This commit is contained in:
Nathan Vasse
2023-01-04 15:52:24 +01:00
committed by NathanVss
parent 67dd0048d0
commit be1c9d000b
19 changed files with 147 additions and 29 deletions

View File

@@ -1,2 +1,3 @@
node_modules
dist
dist
src/cunningham-tokens.ts

View File

@@ -4,12 +4,9 @@
"custom"
],
"parserOptions": {
"project": [
"./tsconfig.json",
"./tsconfig.node.json"
]
"project": "./tsconfig.eslint.json"
},
"rules": {
"import/no-extraneous-dependencies": ["error", {"devDependencies": ["vite.config.ts", "**/*.stories.tsx", "**/*.spec.tsx"]}]
"import/no-extraneous-dependencies": ["error", {"devDependencies": ["vite.config.ts", "cunningham.ts","**/*.stories.tsx", "**/*.spec.tsx"]}]
}
}

View File

@@ -1,3 +1,8 @@
const viteTsconfig = require('vite-tsconfig-paths');
const tsconfigPaths = viteTsconfig.default;
const { mergeConfig } = require('vite');
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
@@ -18,5 +23,10 @@ module.exports = {
],
"features": {
"storyStoreV7": true
}
},
async viteFinal(config) {
return mergeConfig(config, {
plugins: [tsconfigPaths()],
});
},
}

View File

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

View File

@@ -0,0 +1,32 @@
import { glob } from "glob";
import { defaultTokens } from "@openfun/cunningham-tokens";
/**
* This scripts dynamically imports all tokens.ts files from components and load them in a single object that will be
* exported as the local configuration of cunningham under the `components` key.
*
* Hence, any consumers of this package will be able to customize the tokens of the components they use by overriding
* them in their own local configuration file. ( cunningham.ts|js )
*/
const components: any = {};
const files = glob.sync("src/components/**/tokens.ts");
files.forEach((file) => {
const importPath = "./" + file.replace(/\.ts$/, "");
const matches = /^.+components\/(.+)\/tokens$/gm.exec(importPath);
let componentName = matches && matches[1];
if (!componentName) {
throw new Error("Could not find component name from file path " + file);
}
componentName = componentName.toLowerCase();
const res = require(importPath);
if (!res.tokens) {
throw new Error("Tokens file does not export tokens " + file);
}
components[componentName] = res.tokens(defaultTokens);
});
export default {
components,
};

View File

@@ -20,8 +20,9 @@
],
"scripts": {
"lint": "eslint . 'src/**/*.{ts,tsx}'",
"dev": "yarn storybook & nodemon --watch src --ext '*' --exec npm run build",
"build": "tsc && vite build",
"dev": "yarn storybook & nodemon --watch src --ext '*' --ignore src/cunningham-tokens.ts --ignore src/cunningham-tokens.css --exec npm run build",
"build": "tsc && yarn build-theme && vite build",
"build-theme": "cunningham -o src -s html -g css,ts",
"preview": "vite preview",
"test": "FORCE_COLOR=1 vitest run",
"test-watch": "vitest",
@@ -39,6 +40,7 @@
},
"devDependencies": {
"@babel/core": "7.20.7",
"@openfun/cunningham-tokens": "*",
"@openfun/typescript-configs": "*",
"@storybook/addon-actions": "6.5.15",
"@storybook/addon-essentials": "6.5.15",
@@ -53,9 +55,11 @@
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"@vitejs/plugin-react": "3.0.0",
"@vitest/coverage-c8": "0.26.3",
"@vitest/ui": "0.26.2",
"babel-loader": "9.1.0",
"css-loader": "6.7.3",
"glob": "8.0.3",
"jsdom": "20.0.3",
"sass": "1.57.1",
"sass-loader": "13.2.0",
@@ -63,6 +67,7 @@
"typescript": "4.9.4",
"vite": "4.0.3",
"vite-plugin-dts": "1.7.1",
"vite-tsconfig-paths": "4.0.3",
"vitest": "0.26.2"
}
}

View File

@@ -1,8 +1,10 @@
.c__button {
background-color: var(--c--colors--primary);
background-image: var(--c--theme--colors--primary-gradient);
padding: 8px 30px;
border-radius: 6px;
border-radius: var(--c--components--button--border-radius);
border: none;
box-shadow: var(--c--components--button--shadow);
color: white;
font-weight: bold;
cursor: pointer;
}

View File

@@ -1,12 +1,20 @@
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import React from "react";
import { buildTheme, loadTokens } from "tests/Theme";
import { Button } from "./index";
describe("<Button/>", () => {
it("renders", () => {
render(<Button />);
const button = screen.getByRole("button", { name: "<Button/>" });
render(<Button>Test button</Button>);
const button = screen.getByRole("button", { name: "Test button" });
expect(button.classList.contains("c__button")).toBe(true);
});
it("uses custom token", async () => {
await buildTheme();
const tokens = await loadTokens();
expect(tokens.components.button["border-radius"]).toBeDefined();
expect(tokens.components.button.shadow).toBeDefined();
});
});

View File

@@ -7,7 +7,9 @@ export default {
component: Button,
} as ComponentMeta<typeof Button>;
const Template: ComponentStory<typeof Button> = () => <Button />;
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
export const Default = Template.bind({});
Default.args = {};
Default.args = {
children: "Amazing button",
};

View File

@@ -1,5 +1,7 @@
import React from "react";
import React, { PropsWithChildren } from "react";
export const Button = () => {
return <button className="c__button">{"<Button/>"}</button>;
interface Props extends PropsWithChildren {}
export const Button = ({ children }: Props) => {
return <button className="c__button">{children}</button>;
};

View File

@@ -0,0 +1,8 @@
import { DefaultTokens } from "@openfun/cunningham-tokens";
export const tokens = (defaults: DefaultTokens) => {
return {
"border-radius": "5px",
shadow: "0px 0px 10px 1px " + defaults.theme.colors.primary + ";",
};
};

View File

@@ -0,0 +1,7 @@
html {
--c--theme--colors--primary: #002d7f;
--c--theme--colors--primary-gradient: linear-gradient(90deg,#002d7f,#0069b3);
--c--theme--colors--secondary: #DA0000;
--c--components--button--border-radius: 5px;
--c--components--button--shadow: 0px 0px 10px 1px #002d7f;;
}

View File

@@ -0,0 +1 @@
export const tokens = {"theme":{"colors":{"primary":"#002d7f","primary-gradient":"linear-gradient(90deg,#002d7f,#0069b3)","secondary":"#DA0000"}},"components":{"button":{"border-radius":"5px","shadow":"0px 0px 10px 1px #002d7f;"}}};

View File

@@ -2,5 +2,6 @@
@import "@fontsource/roboto/400";
@import "@fontsource/roboto/700";
@import "@fontsource/roboto/900";
@import "cunningham-tokens";
@import '@openfun/cunningham-tokens/default-tokens';
@import './components/Button';

View File

@@ -0,0 +1,33 @@
import child_process from "child_process";
import path from "path";
/**
* Run the NPM script 'build-theme' in order to generate the tokens files ( cunningham-token.ts|css ).
* ( The purpose is mainly to generate the tokens files and then verify in tests that custom tokens defined in
* tokens.ts files are correctly taken into account. )
*/
export const buildTheme = (debug?: boolean) => {
const child = child_process.exec(
"cd " + path.join(__dirname, "..", "..") + " && yarn build-theme"
);
return new Promise<void>((resolve) => {
child.stdout?.on("data", (data) => {
// eslint-disable-next-line no-console
if (debug) console.log("stdout: " + data);
});
child.stderr?.on("data", (data) => {
// eslint-disable-next-line no-console
if (debug) console.log("stderr: " + data);
});
child.on("close", (code) => {
// eslint-disable-next-line no-console
if (debug) console.log("closing code: " + code);
resolve();
});
});
};
export const loadTokens = async () => {
const module = await import("../cunningham-tokens");
return module.tokens;
};

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"exclude": [],
"include": [
"**/*.ts",
"**/*.tsx"
]
}

View File

@@ -2,8 +2,9 @@
"extends": "@openfun/typescript-configs/react.json",
"compilerOptions": {
"noEmit": true,
"baseUrl": "./src"
},
"include": ["src"],
"exclude": ["node_modules","dist"],
"include": ["src", "cunningham.ts"],
"exclude": ["node_modules","dist", "**/tokens.ts"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -1,6 +1,7 @@
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import dts from "vite-plugin-dts";
import tsconfigPaths from "vite-tsconfig-paths";
// https://vitejs.dev/config/
export default defineConfig({
@@ -24,9 +25,15 @@ export default defineConfig({
},
},
},
plugins: [dts(), react()],
plugins: [tsconfigPaths(), dts(), react()],
test: {
environment: "jsdom",
reporters: "verbose",
globals: true,
coverage: {
all: true,
include: ["src/**/*.{ts,tsx}"],
exclude: ["**/*.stories.tsx", "**/*.spec.tsx"],
},
},
});

View File

@@ -12,6 +12,6 @@ export type Tokens = Record<PropertyKey, unknown>;
export const tokensGenerator = (config: ConfigShape): Tokens => {
return {
colors: { ...config.theme.colors },
...config,
};
};