From 8850cae698fc0f1a2d389cd6174942f5abc93a8b Mon Sep 17 00:00:00 2001 From: Karsten Schmidt Date: Fri, 15 Dec 2023 12:42:59 +0100 Subject: [PATCH] feat(meta-css): switch generated framework output to JSON, simplify --- packages/meta-css/specs/base-specs.json | 79 +++---- packages/meta-css/src/api.ts | 34 ++- packages/meta-css/src/cli.ts | 3 +- packages/meta-css/src/convert.ts | 90 ++++---- packages/meta-css/src/generate.ts | 261 ++++++++++-------------- packages/meta-css/src/utils.ts | 5 +- 6 files changed, 207 insertions(+), 265 deletions(-) diff --git a/packages/meta-css/specs/base-specs.json b/packages/meta-css/specs/base-specs.json index 30da4b95ab..977620257f 100644 --- a/packages/meta-css/specs/base-specs.json +++ b/packages/meta-css/specs/base-specs.json @@ -1,4 +1,8 @@ { + "info": { + "name": "MetaCSS base specs", + "version": "0.0.1" + }, "media": { "ns": { "min-width": "30rem" }, "m": { "min-width": "30rem", "max-width": "60rem" }, @@ -148,18 +152,14 @@ "index": "v", "prop": "display", "items": "display", - "unit": "display", - "file": "display.ts", - "responsive": true + "unit": "display" }, { "comment": "abs widths", "prefix": "w", "index": "i1", "prop": "width", - "items": "sizes-abs", - "file": "size.ts", - "responsive": true + "items": "sizes-abs" }, { "comment": "relative widths", @@ -167,17 +167,14 @@ "index": "v", "prop": "width", "items": "sizes-rel", - "unit": "%", - "file": "size.ts", - "responsive": true + "unit": "%" }, { "comment": "abs heights", "prefix": "h", "index": "i1", "prop": "height", - "items": "sizes-abs", - "file": "size.ts" + "items": "sizes-abs" }, { "comment": "relative heights", @@ -185,8 +182,7 @@ "index": "v", "prop": "height", "items": "sizes-rel", - "unit": "%", - "file": "size.ts" + "unit": "%" }, { "comment": "paddings", @@ -194,7 +190,6 @@ "index": "i", "prop": "padding", "items": "margins", - "file": "margin.ts", "var": "all" }, { @@ -203,7 +198,6 @@ "index": "i", "prop": "margin", "items": "margins", - "file": "margin.ts", "var": "all" }, { @@ -211,8 +205,7 @@ "prefix": "br", "index": "i", "prop": "border-radius", - "items": "borders-r", - "file": "border.ts" + "items": "borders-r" }, { "prefix": "br*", @@ -222,7 +215,6 @@ "border-bottom*-radius": "*" }, "items": "borders-r", - "file": "border.ts", "var": "h" }, { @@ -233,7 +225,6 @@ "border*-right-radius": "*" }, "items": "borders-r", - "file": "border.ts", "var": "v" }, { @@ -245,33 +236,29 @@ "border*-width": "*" }, "items": "borders-w", - "file": "border.ts", "var": "atrbl" }, { "comment": "border colors", - "prefix": "border-", + "prefix": "b--", "index": "v", "prop": "border-color", "items": "var-palette", - "unit": "var-palette", - "file": "border.ts" + "unit": "var-palette" }, { - "prefix": "border-", + "prefix": "b--", "index": "v", "prop": "border-color", "items": "palette", - "unit": "palette", - "file": "border.ts" + "unit": "palette" }, { - "prefix": "border-", + "prefix": "b--", "index": "v", "prop": "border-color", "items": "grays", - "unit": "grays", - "file": "border.ts" + "unit": "grays" }, { "comment": "text colors", @@ -279,24 +266,21 @@ "index": "v", "prop": "color", "items": "var-palette", - "unit": "var-palette", - "file": "palette.ts" + "unit": "var-palette" }, { "prefix": "", "index": "v", "prop": "color", "items": "palette", - "unit": "palette", - "file": "palette.ts" + "unit": "palette" }, { "prefix": "", "index": "v", "prop": "color", "items": "grays", - "unit": "grays", - "file": "palette.ts" + "unit": "grays" }, { "comment": "background colors", @@ -304,31 +288,27 @@ "index": "v", "prop": "background-color", "items": "var-palette", - "unit": "var-palette", - "file": "palette.ts" + "unit": "var-palette" }, { "prefix": "bg-", "index": "v", "prop": "background-color", "items": "palette", - "unit": "palette", - "file": "palette.ts" + "unit": "palette" }, { "prefix": "bg-", "index": "v", "prop": "background-color", "items": "grays", - "unit": "grays", - "file": "palette.ts" + "unit": "grays" }, { "prefix": "o", "index": "v", "prop": "opacity", "items": "tens", - "file": "palette.ts", "unit": "norm", "comment": "opacities" }, @@ -338,16 +318,14 @@ "index": "i1", "def": { "transition": "*s background-color ease-in-out" }, "items": [0.1, 0.2, 0.5], - "unit": null, - "file": "background.ts" + "unit": null }, { "prefix": "cursor-", "index": "v", "prop": "cursor", "items": "cursors", - "unit": "cursors", - "file": "cursor.ts" + "unit": "cursors" }, { "comment": "grid column layout", @@ -355,8 +333,7 @@ "index": "v", "prop": "grid-template-columns", "items": "cols-rows", - "unit": "cols-rows", - "file": "grid.ts" + "unit": "cols-rows" }, { "comment": "grid row layout", @@ -364,16 +341,14 @@ "index": "v", "prop": "grid-template-rows", "items": "cols-rows", - "unit": "cols-rows", - "file": "grid.ts" + "unit": "cols-rows" }, { "comment": "grid gaps", "prefix": "gap", "index": "i", "prop": "gap", - "items": "borders-w", - "file": "grid.ts" + "items": "borders-w" } ] } diff --git a/packages/meta-css/src/api.ts b/packages/meta-css/src/api.ts index 5f2714d577..6123e661a8 100644 --- a/packages/meta-css/src/api.ts +++ b/packages/meta-css/src/api.ts @@ -1,8 +1,9 @@ +import type { IObjectOf } from "@thi.ng/api"; import type { CommandCtx } from "@thi.ng/args"; import type { FormatPresets } from "@thi.ng/text-format"; export interface CommonOpts { - out: string; + out?: string; verbose: boolean; } @@ -10,3 +11,34 @@ export interface AppCtx extends CommandCtx { format: FormatPresets; } + +export interface CompiledSpecs { + defs: IObjectOf; + media: IObjectOf; + info: SpecInfo; +} + +export interface GeneratorConfig { + info: SpecInfo; + media: IObjectOf; + indexed: IObjectOf; + specs: Spec[]; +} + +export interface SpecInfo { + name: string; + version: string; +} + +export interface Spec { + prefix: string; + prop: string | string[]; + def?: Record; + items: string | any[]; + index?: Index; + unit?: string | null; + comment?: string; + var?: string | string[]; +} + +export type Index = "i" | "i1" | "v"; diff --git a/packages/meta-css/src/cli.ts b/packages/meta-css/src/cli.ts index 8ec02d2302..ee40716b22 100644 --- a/packages/meta-css/src/cli.ts +++ b/packages/meta-css/src/cli.ts @@ -16,8 +16,7 @@ cliApp>({ opts: { out: string({ alias: "o", - default: "", - desc: "Output dir (or stdout)", + desc: "Output file (or stdout)", }), verbose: flag({ alias: "v", diff --git a/packages/meta-css/src/convert.ts b/packages/meta-css/src/convert.ts index 8bd8462134..e6373a1094 100644 --- a/packages/meta-css/src/convert.ts +++ b/packages/meta-css/src/convert.ts @@ -2,7 +2,7 @@ import type { IObjectOf } from "@thi.ng/api"; import { flag, string, strings, type Command } from "@thi.ng/args"; import { peek } from "@thi.ng/arrays"; import { illegalArgs, illegalState } from "@thi.ng/errors"; -import { readText } from "@thi.ng/file-io"; +import { readJSON, readText } from "@thi.ng/file-io"; import { COMPACT, PRETTY, @@ -11,9 +11,8 @@ import { type Format, } from "@thi.ng/hiccup-css"; import { type ILogger } from "@thi.ng/logger"; -import { camel } from "@thi.ng/strings"; import { basename, resolve } from "path"; -import type { AppCtx, CommonOpts } from "./api.js"; +import type { AppCtx, CommonOpts, CompiledSpecs } from "./api.js"; import { maybeWriteText } from "./utils.js"; type State = "sel" | "class" | "nest"; @@ -43,57 +42,50 @@ interface ProcessCtx { interface ProcessOpts { logger: ILogger; format: Format; - outDir: string; - include?: string[]; noHeader: boolean; specs: CompiledSpecs; } -interface CompiledSpecs { - __MEDIA_QUERIES__: IObjectOf; - [id: string]: IObjectOf; -} - export const CONVERT: Command> = { - desc: "Convert meta declarations to CSS", + desc: "Convert & bundle meta declarations to CSS", opts: { - specs: string({ alias: "s", optional: false, desc: "Specs dir" }), + specs: string({ + alias: "s", + optional: false, + desc: "Path to generated JSON defs", + }), include: strings({ alias: "I", desc: "Include CSS files (only in 1st input)", }), - pretty: flag({ alias: "p", default: false, desc: "Pretty print CSS" }), - noHeader: flag({ - default: false, - desc: "Don't emit generated header comment", - }), + pretty: flag({ alias: "p", desc: "Pretty print CSS" }), + noHeader: flag({ desc: "Don't emit generated header comment" }), }, fn: async ({ logger, - opts: { specs, out, pretty, noHeader, include }, + opts: { specs: $specs, out, pretty, noHeader, include }, inputs, }) => { - const specDir = resolve(specs); + const specs = readJSON(resolve($specs), logger); const format = pretty ? PRETTY : COMPACT; - logger.debug("importing specs:", specDir); - const module = await import(specDir); - await Promise.all( - inputs.map((file, i) => + const bundle: string[] = include + ? include.map((x) => readText(resolve(x), logger).trim()) + : []; + bundle.push( + ...inputs.map((file) => processFile(resolve(file), { logger, - specs: module, - outDir: out, + specs, format, noHeader, - include: i === 0 ? include : undefined, - }) + }).join("\n") ) ); + maybeWriteText(out, bundle, logger); }, }; const QUERY_SEP = ":"; - const PATH_SEP = "///"; const defScope = (parent?: Scope): Scope => ({ @@ -126,14 +118,18 @@ const endScope = (ctx: ProcessCtx) => { const buildScopePath = (scopes: Scope[]) => scopes.map((x) => x.sel.join(",")).join(PATH_SEP); -const buildDecls = (selectorPath: string, ids: string[], specs: any) => { +const buildDecls = ( + selectorPath: string, + ids: string[], + specs: CompiledSpecs +) => { const root: any[] = []; let parent = root; const levels = selectorPath.split(PATH_SEP); for (let i = 0; i < levels.length; i++) { const curr = levels[i].split(","); if (i == levels.length - 1) { - curr.push(Object.assign({}, ...ids.map((x) => specs[x]))); + curr.push(Object.assign({}, ...ids.map((x) => specs.defs[x]))); } parent.push(curr); parent = curr; @@ -174,7 +170,7 @@ const mergeMediaQueries = (mediaQueryDefs: IObjectOf, query: string) => const processFile = ( path: string, - { logger, format, specs, outDir, noHeader, include }: ProcessOpts + { logger, format, specs, noHeader }: ProcessOpts ) => { const root = defScope(); const initial = defScope(root); @@ -184,7 +180,7 @@ const processFile = ( scopes: [initial], }; - const mediaQueryIDs = new Set(Object.keys(specs.__MEDIA_QUERIES__)); + const mediaQueryIDs = new Set(Object.keys(specs.media)); let mediaQueryRules: IObjectOf> = {}; for (let token of readText(path, logger).split(/\s+/)) { @@ -217,13 +213,12 @@ const processFile = ( } else if (token === "}") { endScope(ctx); } else { - let { token: $token, query } = parseMediaQueryToken( + let { token: id, query } = parseMediaQueryToken( token, mediaQueryIDs ); - const id = camel($token); - const spec = specs[id]; - if (!spec) illegalArgs(`unknown rule: ${id}`); + const spec = specs.defs[id]; + if (!spec) illegalArgs(`unknown rule ID: ${id}`); if (query) { if (!mediaQueryRules[query]) mediaQueryRules[query] = {}; @@ -244,9 +239,7 @@ const processFile = ( logger.debug("root", root); logger.debug("responsives", mediaQueryRules); - const serialized: string[] = include - ? include.map((x) => readText(resolve(x), logger).trim()) - : []; + const serialized: string[] = []; if (!noHeader) { serialized.push( `/*! generated by thi.ng/meta-css from ${basename( @@ -262,21 +255,10 @@ const processFile = ( ); logger.debug("responsive rules", queryID, rules); serialized.push( - css( - at_media( - mergeMediaQueries(specs.__MEDIA_QUERIES__, queryID), - rules - ), - { - format, - } - ) + css(at_media(mergeMediaQueries(specs.media, queryID), rules), { + format, + }) ); } - maybeWriteText( - outDir, - `/${basename(path).replace(".meta", ".css")}`, - serialized, - logger - ); + return serialized; }; diff --git a/packages/meta-css/src/generate.ts b/packages/meta-css/src/generate.ts index 34b61bafce..17b9b62fdc 100644 --- a/packages/meta-css/src/generate.ts +++ b/packages/meta-css/src/generate.ts @@ -1,78 +1,24 @@ import type { Fn, IObjectOf, Nullable } from "@thi.ng/api"; -import { int, type Command, flag } from "@thi.ng/args"; -import { isArray, isNumber, isPlainObject, isString } from "@thi.ng/checks"; -import { illegalArgs } from "@thi.ng/errors"; +import { int, type Command } from "@thi.ng/args"; +import { isArray, isPlainObject, isString } from "@thi.ng/checks"; +import { assert, illegalArgs } from "@thi.ng/errors"; import { readJSON } from "@thi.ng/file-io"; import { PRECISION, percent, px, rem, setPrecision } from "@thi.ng/hiccup-css"; -import { getIn, mutIn } from "@thi.ng/paths"; -import { camel } from "@thi.ng/strings"; import { permutations } from "@thi.ng/transducers"; -import type { AppCtx, CommonOpts } from "./api.js"; +import type { + AppCtx, + CommonOpts, + CompiledSpecs, + GeneratorConfig, + Index, + Spec, +} from "./api.js"; import { maybeWriteText } from "./utils.js"; interface GenerateOpts extends CommonOpts { prec: number; - ts: boolean; -} - -interface Specs { - media: IObjectOf; - indexed: IObjectOf; - specs: Spec[]; -} - -interface Spec { - prefix: string; - prop: string | string[]; - def?: Record; - items: string | any[]; - file: string; - index?: string; - unit?: string | null; - comment?: string; - var?: string | string[]; } -interface Output { - src: string[]; - meta: string[]; -} - -export const GENERATE: Command< - GenerateOpts, - CommonOpts, - AppCtx -> = { - desc: "Generate MetaCSS specs", - opts: { - prec: int({ default: 3, desc: "Number of fractional digits" }), - ts: flag({ desc: "Emit as TypeScript" }), - }, - inputs: 1, - fn: async ({ logger, opts: { out, prec, ts }, inputs }) => { - const allSpecs: Specs = readJSON(inputs[0], logger); - const outputs = generateRules(allSpecs, prec); - for (let [file, { src }] of Object.entries(outputs)) { - maybeWriteText(out, `/${ensureExt(file, ts)}`, src, logger); - } - - const declModules = [...Object.keys(outputs)] - .sort() - .map((x) => `export * from "./${ensureExt(x, false)}";`); - - const body = [ - ...declModules, - "", - `export const __MEDIA_QUERIES__ = ${JSON.stringify( - allSpecs.media, - null, - 4 - )};`, - ]; - maybeWriteText(out, ensureExt("/index.ts", ts), body, logger); - }, -}; - const UNITS: Record> = { px, rem, @@ -99,18 +45,41 @@ const VAR_IDS = { v: ["t", "b"], }; -export const generateRules = (allSpecs: Specs, prec: number) => { +export const GENERATE: Command< + GenerateOpts, + CommonOpts, + AppCtx +> = { + desc: "Generate MetaCSS specs", + opts: { + prec: int({ default: 3, desc: "Number of fractional digits" }), + }, + inputs: 1, + fn: async (ctx) => { + const { + logger, + opts: { prec, out }, + inputs, + } = ctx; + const config: GeneratorConfig = readJSON(inputs[0], logger); + const result: CompiledSpecs = { + info: config.info!, + media: config.media || {}, + defs: generateAll(config, prec), + }; + + maybeWriteText(out, JSON.stringify(result), logger); + }, +}; + +const generateAll = (config: GeneratorConfig, prec: number) => { setPrecision(prec); - const outputs: Record = {}; - for (let $spec of allSpecs.specs) { - const spec = $spec; - let out = getIn(outputs, [spec.file]); - if (!out) mutIn(outputs, [spec.file], (out = { src: [], meta: [] })); + const defs: IObjectOf = {}; + for (let spec of config.specs) { const vars = resolveVariations(spec.var); - if (spec.comment) out.src.push(`// ${spec.comment}`); if (spec.def) { for (let vid of vars) { - genVariationDef(out, allSpecs, spec, vid); + genVariationDef(defs, config, spec, vid); } } else { const baseProps = isString(spec.prop) ? [spec.prop] : spec.prop; @@ -119,8 +88,8 @@ export const generateRules = (allSpecs: Specs, prec: number) => { ...permutations(baseProps, (VARIATIONS)[vid]), ].map((x) => x.join("")); genVariation( - out, - allSpecs, + defs, + config, { ...spec, prefix: spec.prefix + vid, @@ -131,15 +100,71 @@ export const generateRules = (allSpecs: Specs, prec: number) => { } } } - return outputs; + return defs; +}; + +const genVariation = ( + defs: IObjectOf, + config: GeneratorConfig, + spec: Spec, + varID: string +) => { + const prefix = spec.prefix.replace("*", varID); + const props = __props(spec.prop, varID); + const unit = __unit(config, spec.unit); + const items = __items(config, spec.items); + items.forEach((x, i) => { + const id = __name(prefix, spec.index, x, i); + assert(!defs[id], `duplicate rule ID: ${id}`); + const val = __value(x, unit); + defs[id] = props.reduce((acc, p) => ((acc[p] = val), acc), {}); + }); }; -const resolveValue = (x: any, unit?: Fn) => { - let val = unit ? unit(x) : x; - return isNumber(val) ? val : `"${val}"`; +const genVariationDef = ( + defs: IObjectOf, + config: GeneratorConfig, + spec: Pick, + varID: string +) => { + const prefix = spec.prefix.replace("*", varID); + const unit = __unit(config, spec.unit); + const items = __items(config, spec.items); + const $var = (VARIATIONS)[varID]; + items.forEach((x, i) => { + const id = __name(prefix, spec.index, x, i); + assert(!defs[id], `duplicate rule ID: ${id}`); + const val = __value(x, unit); + const props = Object.entries(spec.def!).reduce( + (acc, [p, v]) => ( + (acc[p.replace("*", $var)] = v.replace("*", val)), acc + ), + {} + ); + defs[id] = props; + }); }; -const resolveUnit = (specs: Specs, id: Nullable) => { +const __name = (prefix: string, index: Index | undefined, x: any, i: number) => + prefix + __index(index, x, i); + +const __index = (index: Index | undefined, x: any, i: number) => { + if (index === undefined) return ""; + switch (index) { + case "i": + return i; + case "i1": + return i + 1; + case "v": + return x; + default: + illegalArgs(`invalid index type: ${index}`); + } +}; + +const __value = (x: any, unit?: Fn) => (unit ? unit(x) : x); + +const __unit = (specs: GeneratorConfig, id: Nullable) => { if (id === undefined) return rem; if (id === null) return (x: any) => String(x); if (UNITS[id]) return UNITS[id]; @@ -148,20 +173,15 @@ const resolveUnit = (specs: Specs, id: Nullable) => { : illegalArgs(`invalid unit: ${id}`); }; -const resolveItems = (specs: Specs, $items: any) => { +const __items = (specs: GeneratorConfig, $items: any) => { let items = $items; if (isString(items)) items = specs.indexed[items]; if (isPlainObject(items)) return Object.keys(items); return isArray(items) ? items : illegalArgs($items); }; -const resolveProps = (props: string | string[], varID: string) => { - props = isString(props) ? [props] : props; - return props.map((x) => { - x = x.replace("*", varID); - return x.indexOf("-") > 0 ? `"${x}"` : x; - }); -}; +const __props = (props: string | string[], varID: string) => + (isString(props) ? [props] : props).map((x) => x.replace("*", varID)); const resolveVariations = (vars?: string | string[]) => { if (!vars) return [""]; @@ -172,68 +192,3 @@ const resolveVariations = (vars?: string | string[]) => { } return vars; }; - -const genVariation = ( - out: Output, - specs: Specs, - spec: Pick, - varID: string -) => { - const id = spec.prefix.replace("*", varID); - const props = resolveProps(spec.prop, varID); - const unit = resolveUnit(specs, spec.unit); - const items = resolveItems(specs, spec.items); - items.forEach((x, i) => { - const idx = - spec.index === "i" - ? i - : spec.index === "i1" - ? i + 1 - : spec.index === "v" - ? x - : ""; - const val = resolveValue(x, unit); - const name = camel(id + idx); - out.src.push( - `export const ${name} = { ${props - .map((p) => `${p}: ${val}`) - .join(", ")} };` - ); - }); - out.src.push(""); -}; - -const genVariationDef = ( - out: Output, - specs: Specs, - spec: Pick, - varID: string -) => { - const id = spec.prefix.replace("*", varID); - const unit = resolveUnit(specs, spec.unit); - const items = resolveItems(specs, spec.items); - const $var = (VARIATIONS)[varID]; - items.forEach((x, i) => { - const idx = - spec.index === "i" - ? i - : spec.index === "i1" - ? i + 1 - : spec.index === "v" - ? x - : ""; - const val = unit ? unit(x) : x; - const props = Object.entries(spec.def!) - .map( - ([p, v]) => - `"${p.replace("*", $var)}": "${v.replace("*", val)}"` - ) - .join(", "); - const name = camel(id + idx); - out.src.push(`export const ${name} = { ${props} };`); - }); - out.src.push(""); -}; - -const ensureExt = (file: string, isTS: boolean) => - file.replace(/\.m?[jt]s$/, isTS ? ".mts" : ".mjs"); diff --git a/packages/meta-css/src/utils.ts b/packages/meta-css/src/utils.ts index d059c14e68..76774e79ba 100644 --- a/packages/meta-css/src/utils.ts +++ b/packages/meta-css/src/utils.ts @@ -3,11 +3,10 @@ import { writeText } from "@thi.ng/file-io"; import type { ILogger } from "@thi.ng/logger"; export const maybeWriteText = ( - outDir: string, - file: string, + out: string | undefined, body: string | string[], logger: ILogger ) => { body = isString(body) ? body : body.join("\n"); - outDir ? writeText(outDir + file, body, logger) : console.log(body); + out ? writeText(out, body, logger) : console.log(body); };