diff --git a/apps/demo/package.json b/apps/demo/package.json index e34c0a3..44d48f1 100644 --- a/apps/demo/package.json +++ b/apps/demo/package.json @@ -6,7 +6,7 @@ "scripts": { "lint": "eslint . 'src/**/*.{ts,tsx}'", "dev": "vite", - "build-theme": "cunningham -o src", + "build-theme": "cunningham -o src -g css,ts", "build": "tsc && vite build", "preview": "vite preview" }, diff --git a/apps/demo/src/cunningham-tokens.css b/apps/demo/src/cunningham-tokens.css index ffc8101..a561176 100644 --- a/apps/demo/src/cunningham-tokens.css +++ b/apps/demo/src/cunningham-tokens.css @@ -1,6 +1,4 @@ :root { --c--colors--primary: purple; --c--colors--secondary: #DA0000; - --c--text--primary: purple; - --c--text--secondary: #DA0000; } \ No newline at end of file diff --git a/apps/demo/src/cunningham-tokens.ts b/apps/demo/src/cunningham-tokens.ts new file mode 100644 index 0000000..049983b --- /dev/null +++ b/apps/demo/src/cunningham-tokens.ts @@ -0,0 +1 @@ +export const tokens = {"colors":{"primary":"purple","secondary":"#DA0000"}}; \ No newline at end of file diff --git a/apps/demo/src/index.scss b/apps/demo/src/index.scss index 0035f57..da02991 100644 --- a/apps/demo/src/index.scss +++ b/apps/demo/src/index.scss @@ -20,7 +20,7 @@ flex-direction: column; align-items: center; - h1 { + h1, h3 { color: white; margin-bottom: 40px; } diff --git a/apps/demo/src/main.tsx b/apps/demo/src/main.tsx index 81fd1fb..9d6c78e 100644 --- a/apps/demo/src/main.tsx +++ b/apps/demo/src/main.tsx @@ -2,12 +2,14 @@ import React from "react"; import ReactDOM from "react-dom/client"; import "./index.scss"; import { Button } from "@openfun/cunningham-react"; +import { tokens } from "./cunningham-tokens"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(

Cunningham Demo.

); diff --git a/packages/react/cunningham.cjs b/packages/react/cunningham.cjs new file mode 100644 index 0000000..80d7028 --- /dev/null +++ b/packages/react/cunningham.cjs @@ -0,0 +1,7 @@ +module.exports = { + theme: { + colors: { + primary: 'grey' + }, + }, +}; diff --git a/packages/tokens/src/bin/Config.ts b/packages/tokens/src/bin/Config.ts index f89455a..8137e0e 100644 --- a/packages/tokens/src/bin/Config.ts +++ b/packages/tokens/src/bin/Config.ts @@ -1,9 +1,6 @@ export default { - configurationFilenames: ["cunningham.js", "cunningham.cjs"], - tokenFilenames: { - css: "cunningham-tokens.css", - js: "cunningham-tokens.js", - }, + configurationFilenames: ["cunningham.js", "cunningham.cjs", "cunningham.ts"], + tokenFilename: "cunningham-tokens", sass: { varSeparator: "--", varPrefix: "c--", diff --git a/packages/tokens/src/bin/ConfigLoader.ts b/packages/tokens/src/bin/ConfigLoader.ts index 03358b5..496f8d6 100644 --- a/packages/tokens/src/bin/ConfigLoader.ts +++ b/packages/tokens/src/bin/ConfigLoader.ts @@ -3,6 +3,7 @@ import * as fs from "fs"; import deepmerge from "deepmerge"; import { ConfigShape } from "TokensGenerator"; import { workPath } from "Paths"; +import { register } from "ts-node"; import Config from "./Config"; const getLocalConfig = async () => { @@ -14,11 +15,28 @@ const getLocalConfig = async () => { console.log("No local config found, using default config."); return {}; } + console.log("Found local config file: " + filename); + + const ext = path.extname(filename); + if (ext === ".ts") { + registerTypescriptLoader(); + } const config = await import(filename); return config.default; }; +const registerTypescriptLoader = () => { + register({ + moduleTypes: { + "**/*.ts": "cjs", + }, + compilerOptions: { + module: "commonjs", + }, + }); +}; + const getDistConfig = async () => { const config = await import("./cunningham.dist.js"); return config.default; diff --git a/packages/tokens/src/bin/Generators/CssGenerator.spec.ts b/packages/tokens/src/bin/Generators/CssGenerator.spec.ts index 0f0b46d..d9a7a00 100644 --- a/packages/tokens/src/bin/Generators/CssGenerator.spec.ts +++ b/packages/tokens/src/bin/Generators/CssGenerator.spec.ts @@ -19,7 +19,7 @@ describe("CssGenerator", () => { }); const testSelector = async (opt: string) => { - const cssTokensFile = path.join(__dirname, Config.tokenFilenames.css); + const cssTokensFile = path.join(__dirname, Config.tokenFilename + ".css"); expect(fs.existsSync(cssTokensFile)).toEqual(false); await run(["", "", "-g", "css", opt, "html"]); expect(fs.existsSync(cssTokensFile)).toEqual(true); diff --git a/packages/tokens/src/bin/Generators/CssGenerator.ts b/packages/tokens/src/bin/Generators/CssGenerator.ts index 35d821d..5ae1601 100644 --- a/packages/tokens/src/bin/Generators/CssGenerator.ts +++ b/packages/tokens/src/bin/Generators/CssGenerator.ts @@ -12,7 +12,7 @@ export const cssGenerator: Generator = async (tokens, opts) => { ); }, ""); const cssContent = `${opts.selector} {\n${cssVars}}`; - const dest = path.join(opts.path, Config.tokenFilenames.css); + const dest = path.join(opts.path, Config.tokenFilename + ".css"); put(dest, cssContent); }; diff --git a/packages/tokens/src/bin/Generators/JsGenerator.spec.ts b/packages/tokens/src/bin/Generators/JsGenerator.spec.ts index 0d6eeec..f6712ac 100644 --- a/packages/tokens/src/bin/Generators/JsGenerator.spec.ts +++ b/packages/tokens/src/bin/Generators/JsGenerator.spec.ts @@ -19,7 +19,7 @@ describe("JsGenerator", () => { }); it("generates valid JS file.", async () => { - const tokensFile = path.join(__dirname, Config.tokenFilenames.js); + const tokensFile = path.join(__dirname, Config.tokenFilename + ".js"); expect(fs.existsSync(tokensFile)).toEqual(false); await run(["", "", "-g", "js"]); expect(fs.existsSync(tokensFile)).toEqual(true); diff --git a/packages/tokens/src/bin/Generators/TsGenerator.spec.ts b/packages/tokens/src/bin/Generators/TsGenerator.spec.ts new file mode 100644 index 0000000..7463772 --- /dev/null +++ b/packages/tokens/src/bin/Generators/TsGenerator.spec.ts @@ -0,0 +1,30 @@ +import path from "path"; +import fs from "fs"; +import Config from "Config"; +import { run } from "ThemeGenerator"; +import { cleanup } from "tests/Utils"; + +jest.mock("../Paths", () => ({ + workPath: () => __dirname, +})); + +describe("TsGenerator", () => { + beforeAll(() => { + jest.spyOn(console, "log").mockImplementation(() => {}); + cleanup(__dirname); + }); + + afterEach(() => { + cleanup(__dirname); + }); + + it("generates valid TS file.", async () => { + const tokensFile = path.join(__dirname, Config.tokenFilename + ".ts"); + expect(fs.existsSync(tokensFile)).toEqual(false); + await run(["", "", "-g", "ts"]); + expect(fs.existsSync(tokensFile)).toEqual(true); + expect(fs.readFileSync(tokensFile).toString()).toEqual( + `export const tokens = {"colors":{"primary":"#055FD2","secondary":"#DA0000"}};` + ); + }); +}); diff --git a/packages/tokens/src/bin/Generators/TsGenerator.ts b/packages/tokens/src/bin/Generators/TsGenerator.ts new file mode 100644 index 0000000..40ac46b --- /dev/null +++ b/packages/tokens/src/bin/Generators/TsGenerator.ts @@ -0,0 +1,10 @@ +import path from "path"; +import { Generator } from "Generators/index"; +import Config from "Config"; +import { put } from "Utils/Files"; +import { jsGeneratorContent } from "Generators/JsGenerator"; + +export const tsGenerator: Generator = async (tokens, opts) => { + const dest = path.join(opts.path, Config.tokenFilename + ".ts"); + put(dest, await jsGeneratorContent(tokens)); +}; diff --git a/packages/tokens/src/bin/Generators/index.ts b/packages/tokens/src/bin/Generators/index.ts index 5caefc8..c5172a2 100644 --- a/packages/tokens/src/bin/Generators/index.ts +++ b/packages/tokens/src/bin/Generators/index.ts @@ -1,6 +1,7 @@ import { cssGenerator } from "Generators/CssGenerator"; import { jsGenerator } from "Generators/JsGenerator"; import { Tokens } from "TokensGenerator"; +import { tsGenerator } from "Generators/TsGenerator"; export type Generator = ( tokens: Tokens, @@ -10,4 +11,5 @@ export type Generator = ( export const Generators: Record = { css: cssGenerator, js: jsGenerator, + ts: tsGenerator, }; diff --git a/packages/tokens/src/bin/tests/Cunningham.spec.ts b/packages/tokens/src/bin/tests/Cunningham.spec.ts index bcbab24..cc049b4 100644 --- a/packages/tokens/src/bin/tests/Cunningham.spec.ts +++ b/packages/tokens/src/bin/tests/Cunningham.spec.ts @@ -1,13 +1,48 @@ import * as fs from "fs"; import * as path from "path"; +import * as child_process from "child_process"; import { run } from "ThemeGenerator"; import { cleanup } from "tests/Utils"; +// eslint-disable-next-line import/no-extraneous-dependencies +import * as util from "util"; import Config from "../Config"; jest.mock("../Paths", () => ({ workPath: () => __dirname, })); +jest.mock("../cunningham.dist.js", () => ({ + theme: { + colors: { + primary: "#055FD2", + secondary: "#DA0000", + }, + }, +})); + +const runBin = async (args: string) => { + const exec = util.promisify(child_process.exec); + const promise = exec( + path.join(__dirname, "..", "..", "..", "dist", "bin", "Main.js") + + " " + + args + ); + + promise.child.stdout?.on("data", (data) => { + console.log("stdout: " + data); + }); + promise.child.stderr?.on("data", (data) => { + console.log("stderr: " + data); + }); + promise.child.on("close", (code) => { + console.log("closing code: " + code); + }); + + const { stdout, stderr } = await promise; + console.log("stdout", stdout); + console.log("stderr", stderr); +}; + /** * Test written here are supposed to be general ones and not specific to any generator. * @@ -25,28 +60,25 @@ describe("Cunningham Bin", () => { }); it("Runs without existing config file with default values.", async () => { - const cssTokensFile = path.join(__dirname, Config.tokenFilenames.css); + const cssTokensFile = path.join(__dirname, Config.tokenFilename + ".css"); expect(fs.existsSync(cssTokensFile)).toEqual(false); await run(["", "", "-g", "css"]); expect(fs.existsSync(cssTokensFile)).toEqual(true); expect(fs.readFileSync(cssTokensFile).toString()).toEqual(`:root { -\t--c--colors--primary: #055FD2; -\t--c--colors--secondary: #DA0000; +\t--c--theme--colors--primary: #055FD2; +\t--c--theme--colors--secondary: #DA0000; }`); }); - it("Runs with existing config file using local values.", async () => { - const localConfigurationFile = path.join( - __dirname, - Config.configurationFilenames[0] - ); + it("Runs with existing JS config file using local values.", async () => { + const localConfigurationFile = path.join(__dirname, "cunningham.js"); expect(fs.existsSync(localConfigurationFile)).toEqual(false); - const cssTokensFile = path.join(__dirname, Config.tokenFilenames.css); + const cssTokensFile = path.join(__dirname, Config.tokenFilename + ".css"); expect(fs.existsSync(cssTokensFile)).toEqual(false); fs.copyFileSync( - path.join(__dirname, "assets", Config.configurationFilenames[0]), + path.join(__dirname, "assets", "cunningham.js"), localConfigurationFile ); expect(fs.existsSync(localConfigurationFile)).toEqual(true); @@ -54,20 +86,43 @@ describe("Cunningham Bin", () => { await run(["", "", "-g", "css"]); expect(fs.existsSync(cssTokensFile)).toEqual(true); expect(fs.readFileSync(cssTokensFile).toString()).toEqual(`:root { -\t--c--colors--primary: AntiqueWhite; -\t--c--colors--secondary: #DA0000; +\t--c--theme--colors--primary: AntiqueWhite; +\t--c--theme--colors--secondary: #DA0000; }`); }); + it("Runs with existing TS config file using local values.", async () => { + const localConfigurationFile = path.join(__dirname, "cunningham.ts"); + expect(fs.existsSync(localConfigurationFile)).toEqual(false); + + const cssTokensFile = path.join(__dirname, Config.tokenFilename + ".css"); + expect(fs.existsSync(cssTokensFile)).toEqual(false); + + fs.copyFileSync( + path.join(__dirname, "assets", "cunningham.ts"), + localConfigurationFile + ); + expect(fs.existsSync(localConfigurationFile)).toEqual(true); + + // We must run the bin directly to be sure that it compiles the TS file. ( Importing TS from + // TS will always work ) + await runBin(`-g css -cwd ${__dirname}`); + + // await run(["", "", "-g", "css"]); + expect(fs.existsSync(cssTokensFile)).toEqual(true); + expect(fs.readFileSync(cssTokensFile).toString()).toContain(` +\t--c--theme--colors--primary: typescript;`); + }); + const testOutput = async (opt: string) => { const outputDir = path.join(__dirname, "output"); - const cssTokensFile = path.join(outputDir, Config.tokenFilenames.css); + const cssTokensFile = path.join(outputDir, Config.tokenFilename + ".css"); expect(fs.existsSync(cssTokensFile)).toEqual(false); await run(["", "", "-g", "css", 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; +\t--c--theme--colors--primary: #055FD2; +\t--c--theme--colors--secondary: #DA0000; }`); }; diff --git a/packages/tokens/src/bin/tests/Utils.ts b/packages/tokens/src/bin/tests/Utils.ts index e1346ec..3fabecd 100644 --- a/packages/tokens/src/bin/tests/Utils.ts +++ b/packages/tokens/src/bin/tests/Utils.ts @@ -1,26 +1,30 @@ import path from "path"; import fs from "fs"; import Config from "Config"; +import { Generators } from "Generators"; /** * Empty the current directory from generated tokens file and local * config to start with an predictable environment. */ export const cleanup = (dir: string) => { - Object.entries(Config.tokenFilenames).forEach(([key, filename]) => { + const tokenFilenames = Object.keys(Generators).map( + (extension) => Config.tokenFilename + "." + extension + ); + + tokenFilenames.forEach((filename) => { const filePath = path.join(dir, filename); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } }); - const localConfigurationFile = path.join( - dir, - Config.configurationFilenames[0] - ); - if (fs.existsSync(localConfigurationFile)) { - fs.unlinkSync(localConfigurationFile); - } + Config.configurationFilenames.forEach((filename) => { + const localConfigurationFile = path.join(dir, filename); + if (fs.existsSync(localConfigurationFile)) { + fs.unlinkSync(localConfigurationFile); + } + }); const outputPath = path.join(dir, "output"); if (fs.existsSync(outputPath)) {