diff --git a/packages/tokens/package.json b/packages/tokens/package.json index a01d822..613c30a 100644 --- a/packages/tokens/package.json +++ b/packages/tokens/package.json @@ -16,8 +16,8 @@ "lint": "eslint . 'src/**/*.{ts,tsx}'", "dev": "nodemon --watch 'src/bin' --ext '*' --exec 'yarn build'", "build": "tsc -p tsconfig.json && tsc-alias && 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" + "build-default-theme": "./dist/bin/Main.js -o dist -s html -g css,js", + "test": "FORCE_COLOR=1 jest --verbose src/bin" }, "dependencies": { "chalk": "4.1.2", diff --git a/packages/tokens/src/bin/Config.ts b/packages/tokens/src/bin/Config.ts index 087bd9b..f89455a 100644 --- a/packages/tokens/src/bin/Config.ts +++ b/packages/tokens/src/bin/Config.ts @@ -1,8 +1,11 @@ export default { configurationFilenames: ["cunningham.js", "cunningham.cjs"], + tokenFilenames: { + css: "cunningham-tokens.css", + js: "cunningham-tokens.js", + }, sass: { varSeparator: "--", varPrefix: "c--", - tokenFilenameCss: "cunningham-tokens.css", }, }; diff --git a/packages/tokens/src/bin/Generators/CssGenerator.spec.ts b/packages/tokens/src/bin/Generators/CssGenerator.spec.ts new file mode 100644 index 0000000..0f0b46d --- /dev/null +++ b/packages/tokens/src/bin/Generators/CssGenerator.spec.ts @@ -0,0 +1,38 @@ +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("CssGenerator", () => { + beforeAll(() => { + jest.spyOn(console, "log").mockImplementation(() => {}); + cleanup(__dirname); + }); + + afterEach(() => { + cleanup(__dirname); + }); + + const testSelector = async (opt: string) => { + const cssTokensFile = path.join(__dirname, Config.tokenFilenames.css); + expect(fs.existsSync(cssTokensFile)).toEqual(false); + await run(["", "", "-g", "css", 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"); + }); +}); diff --git a/packages/tokens/src/bin/Generators/CssGenerator.ts b/packages/tokens/src/bin/Generators/CssGenerator.ts index 3cb5b75..35d821d 100644 --- a/packages/tokens/src/bin/Generators/CssGenerator.ts +++ b/packages/tokens/src/bin/Generators/CssGenerator.ts @@ -1,13 +1,10 @@ import * as path from "path"; -import * as fs from "fs"; -import chalk from "chalk"; import { flatify } from "Utils/Flatify"; import Config from "Config"; +import { Generator } from "Generators/index"; +import { put } from "Utils/Files"; -export const cssGenerator = async ( - tokens: any, - opts: { path: string; selector: string } -) => { +export const cssGenerator: Generator = async (tokens, opts) => { const flatTokens = flatify(tokens, Config.sass.varSeparator); const cssVars = Object.keys(flatTokens).reduce((acc, token) => { return ( @@ -15,12 +12,7 @@ export const cssGenerator = async ( ); }, ""); const cssContent = `${opts.selector} {\n${cssVars}}`; + const dest = path.join(opts.path, Config.tokenFilenames.css); - 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."))); + put(dest, cssContent); }; diff --git a/packages/tokens/src/bin/Generators/JsGenerator.spec.ts b/packages/tokens/src/bin/Generators/JsGenerator.spec.ts new file mode 100644 index 0000000..0d6eeec --- /dev/null +++ b/packages/tokens/src/bin/Generators/JsGenerator.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("JsGenerator", () => { + beforeAll(() => { + jest.spyOn(console, "log").mockImplementation(() => {}); + cleanup(__dirname); + }); + + afterEach(() => { + cleanup(__dirname); + }); + + it("generates valid JS file.", async () => { + const tokensFile = path.join(__dirname, Config.tokenFilenames.js); + expect(fs.existsSync(tokensFile)).toEqual(false); + await run(["", "", "-g", "js"]); + 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/JsGenerator.ts b/packages/tokens/src/bin/Generators/JsGenerator.ts new file mode 100644 index 0000000..a03e56f --- /dev/null +++ b/packages/tokens/src/bin/Generators/JsGenerator.ts @@ -0,0 +1,15 @@ +import path from "path"; +import { Generator } from "Generators/index"; +import Config from "Config"; +import { put } from "Utils/Files"; +import { Tokens } from "TokensGenerator"; + +export const jsGenerator: Generator = async (tokens, opts) => { + const dest = path.join(opts.path, Config.tokenFilename + ".js"); + put(dest, await jsGeneratorContent(tokens)); +}; + +export const jsGeneratorContent = async (tokens: Tokens) => { + const variable = JSON.stringify(tokens); + return `export const tokens = ${variable};`; +}; diff --git a/packages/tokens/src/bin/Generators/index.ts b/packages/tokens/src/bin/Generators/index.ts new file mode 100644 index 0000000..5caefc8 --- /dev/null +++ b/packages/tokens/src/bin/Generators/index.ts @@ -0,0 +1,13 @@ +import { cssGenerator } from "Generators/CssGenerator"; +import { jsGenerator } from "Generators/JsGenerator"; +import { Tokens } from "TokensGenerator"; + +export type Generator = ( + tokens: Tokens, + opts: { path: string; selector: string } +) => Promise; + +export const Generators: Record = { + css: cssGenerator, + js: jsGenerator, +}; diff --git a/packages/tokens/src/bin/ThemeGenerator.ts b/packages/tokens/src/bin/ThemeGenerator.ts index e2b315a..3f00757 100644 --- a/packages/tokens/src/bin/ThemeGenerator.ts +++ b/packages/tokens/src/bin/ThemeGenerator.ts @@ -1,19 +1,27 @@ import { program } from "commander"; import chalk from "chalk"; import figlet from "figlet"; -import { cssGenerator } from "Generators/CssGenerator"; import { getConfig } from "ConfigLoader"; import { tokensGenerator } from "TokensGenerator"; import { workPath } from "Paths"; +import { Generators } from "Generators"; 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, - }); + const { generators } = options; + await Promise.allSettled( + generators.map((generator: string) => { + if (!Generators[generator]) { + throw new Error('The generator "' + generator + '" does not exist.'); + } + return Generators[generator](tokens, { + path: options.output, + selector: options.selector, + }); + }) + ); }; export const run = async (args: string[]) => { @@ -21,6 +29,10 @@ export const run = async (args: string[]) => { chalk.red(figlet.textSync("Cunningham", { horizontalLayout: "full" })) ); + const commaSeparatedList = (value: string) => { + return value.split(","); + }; + program .description("Cunningham's CLI tool.") .option( @@ -33,6 +45,11 @@ export const run = async (args: string[]) => { "Specify the css root selector element.", ":root" ) + .requiredOption( + "-g, --generators ", + "Specify the generators to use.", + commaSeparatedList + ) .parse(args); await buildTheme(); diff --git a/packages/tokens/src/bin/TokensGenerator.ts b/packages/tokens/src/bin/TokensGenerator.ts index 44b44d0..dc66c12 100644 --- a/packages/tokens/src/bin/TokensGenerator.ts +++ b/packages/tokens/src/bin/TokensGenerator.ts @@ -8,7 +8,9 @@ export interface ConfigShape { theme: ThemeShape; } -export const tokensGenerator = (config: ConfigShape) => { +export type Tokens = Record; + +export const tokensGenerator = (config: ConfigShape): Tokens => { return { colors: { ...config.theme.colors }, }; diff --git a/packages/tokens/src/bin/Utils/Files.ts b/packages/tokens/src/bin/Utils/Files.ts new file mode 100644 index 0000000..f784bb2 --- /dev/null +++ b/packages/tokens/src/bin/Utils/Files.ts @@ -0,0 +1,14 @@ +import fs from "fs"; +import chalk from "chalk"; + +export const put = (path: string, content: string) => { + console.log("Generating tokens file to " + path + " ..."); + const dir = path.substring(0, path.lastIndexOf("/")); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + fs.writeFileSync(path, content); + console.log( + chalk.bgGreen(chalk.white("File " + path + " generated successfully.")) + ); +}; diff --git a/packages/tokens/src/bin/tests/Cunningham.spec.ts b/packages/tokens/src/bin/tests/Cunningham.spec.ts index e38c0ba..bcbab24 100644 --- a/packages/tokens/src/bin/tests/Cunningham.spec.ts +++ b/packages/tokens/src/bin/tests/Cunningham.spec.ts @@ -1,6 +1,7 @@ import * as fs from "fs"; import * as path from "path"; import { run } from "ThemeGenerator"; +import { cleanup } from "tests/Utils"; import Config from "../Config"; jest.mock("../Paths", () => ({ @@ -8,43 +9,25 @@ jest.mock("../Paths", () => ({ })); /** - * Empty the current directory from generated tokens file and local - * config to start with an predictable environment. + * Test written here are supposed to be general ones and not specific to any generator. + * + * But as we need at least one generator to execute the bin we need to choose one to use by default, + * that's why we use the css generator. */ -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(); + cleanup(__dirname); }); afterEach(() => { - cleanup(); + cleanup(__dirname); }); it("Runs without existing config file with default values.", async () => { - const cssTokensFile = path.join(__dirname, Config.sass.tokenFilenameCss); + const cssTokensFile = path.join(__dirname, Config.tokenFilenames.css); expect(fs.existsSync(cssTokensFile)).toEqual(false); - await run([]); + await run(["", "", "-g", "css"]); expect(fs.existsSync(cssTokensFile)).toEqual(true); expect(fs.readFileSync(cssTokensFile).toString()).toEqual(`:root { \t--c--colors--primary: #055FD2; @@ -59,7 +42,7 @@ describe("Cunningham Bin", () => { ); expect(fs.existsSync(localConfigurationFile)).toEqual(false); - const cssTokensFile = path.join(__dirname, Config.sass.tokenFilenameCss); + const cssTokensFile = path.join(__dirname, Config.tokenFilenames.css); expect(fs.existsSync(cssTokensFile)).toEqual(false); fs.copyFileSync( @@ -68,7 +51,7 @@ describe("Cunningham Bin", () => { ); expect(fs.existsSync(localConfigurationFile)).toEqual(true); - await run([]); + await run(["", "", "-g", "css"]); expect(fs.existsSync(cssTokensFile)).toEqual(true); expect(fs.readFileSync(cssTokensFile).toString()).toEqual(`:root { \t--c--colors--primary: AntiqueWhite; @@ -78,9 +61,9 @@ describe("Cunningham Bin", () => { const testOutput = async (opt: string) => { const outputDir = path.join(__dirname, "output"); - const cssTokensFile = path.join(outputDir, Config.sass.tokenFilenameCss); + const cssTokensFile = path.join(outputDir, Config.tokenFilenames.css); expect(fs.existsSync(cssTokensFile)).toEqual(false); - await run(["", "", opt, outputDir]); + await run(["", "", "-g", "css", opt, outputDir]); expect(fs.existsSync(cssTokensFile)).toEqual(true); expect(fs.readFileSync(cssTokensFile).toString()).toEqual(`:root { \t--c--colors--primary: #055FD2; @@ -95,22 +78,4 @@ describe("Cunningham Bin", () => { 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"); - }); }); diff --git a/packages/tokens/src/bin/tests/Utils.ts b/packages/tokens/src/bin/tests/Utils.ts new file mode 100644 index 0000000..e1346ec --- /dev/null +++ b/packages/tokens/src/bin/tests/Utils.ts @@ -0,0 +1,29 @@ +import path from "path"; +import fs from "fs"; +import Config from "Config"; + +/** + * 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 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); + } + + const outputPath = path.join(dir, "output"); + if (fs.existsSync(outputPath)) { + fs.rmSync(outputPath, { recursive: true }); + } +};