UNPKG

@terrazzo/plugin-js

Version:

Use DTCG tokens in Node.js for server-rendered applications.

195 lines (194 loc) 6.78 kB
import { calculatePermutations } from "@terrazzo/parser"; import { pascalCase } from "scule"; //#region src/lib.ts const FORMAT_ID = "js"; const DEFAULT_PROPERTIES = new Set([ "$type", "$value", "$description", "$extensions", "$deprecated" ]); const FILE_HEADER = `/** ------------------------------------------ * Autogenerated by ⛋ Terrazzo. DO NOT EDIT! * ------------------------------------------- */`; const TYPE_MAP = { boolean: "BooleanTokenNormalized", border: "BorderTokenNormalized", color: "ColorTokenNormalized", cubicBezier: "CubicBezierTokenNormalized", dimension: "DimensionTokenNormalized", duration: "DurationTokenNormalized", fontFamily: "FontFamilyTokenNormalized", fontWeight: "FontWeightTokenNormalized", gradient: "GradientTokenNormalized", link: "LinkTokenNormalized", number: "NumberTokenNormalized", shadow: "ShadowTokenNormalized", string: "StringTokenNormalized", strokeStyle: "StrokeStyleTokenNormalized", typography: "TypographyTokenNormalized", transition: "TransitionTokenNormalized" }; //#endregion //#region src/build.ts const RESOLVER_JSDOC_COMMENT = "/** Produce a token set from a given input. */"; function buildJS({ resolver, properties, logger, contexts }) { const entry = { group: "plugin", label: "@terrazzo/plugin-js" }; const typeMap = {}; let output = FILE_HEADER; output += "\n"; const permutations = (contexts ? calculatePermutations(Object.entries(contexts)) : resolver.listPermutations?.() ?? []).map((value) => ({ value, id: JSON.stringify(Object.fromEntries(Object.entries(value).sort((a, b) => a[0].localeCompare(b[0], "en-us", { numeric: true })))) })); output += "export const PERMUTATIONS = {\n"; let permutationI = 1; for (const { value, id } of permutations) { const start = performance.now(); try { const tokens = resolver.apply(value); output += ` ${JSON.stringify(id)}: {\n`; for (const id of Object.keys(tokens)) { output += ` ${serializeToken(tokens[id], properties)},\n`; if (!(id in typeMap)) typeMap[id] = tokens[id].$type; } output += " },\n"; } catch (err) { logger.error({ ...entry, message: err.message }); } const timing = performance.now() - start; logger.info({ ...entry, message: `Permutation ${permutationI}/${permutations.length} done (${id})`, timing }); permutationI++; } output += "};\n"; const inputDefaults = Object.fromEntries(resolver.source.resolutionOrder.filter((i) => i.type === "modifier" && "default" in i).map((m) => [m.name, m.default])); output += `const INPUT_DEFAULTS = ${JSON.stringify(inputDefaults)};\n`; output += RESOLVER_JSDOC_COMMENT; output += "\n"; output += `export const resolver = { apply(userInput) { if (!userInput || typeof userInput !== "object") { throw new Error(\`invalid input: \${userInput}\`); } const input = { ...INPUT_DEFAULTS, ...userInput }; const inputKey = JSON.stringify(Object.fromEntries(Object.entries(input).sort((a, b) => a[0].localeCompare(b[0], "en-us", { numeric: true })))); return PERMUTATIONS[inputKey]; }, listPermutations() { return [${permutations.map((p) => p.id).join(", ")}]; }, };\n`; return { code: output, typeMap }; } function buildDTS({ resolver, contexts, properties, typeMap }) { const unique$type = new Set(Object.values(typeMap)); let output = FILE_HEADER; output += "\n"; output += "import type {\n"; output += " TokenNormalizedSet,\n"; for (const $type of unique$type) if ($type in TYPE_MAP) output += ` ${TYPE_MAP[$type]},\n`; output += "} from \"@terrazzo/token-types\";"; output += "\n\n"; output += "export const PERMUTATIONS: Record<string, TokenNormalizedSet>;\n\n"; output += `type InputType = ${buildInputType(resolver, contexts)}\n\n`; for (const $type of unique$type) { if (!($type in TYPE_MAP)) continue; output += `export type ${localTypeName($type)} = Pick<${TYPE_MAP[$type]}, ${[...properties].map((p) => JSON.stringify(p)).join(" | ")}>;\n`; } output += "\nexport interface Tokens {\n"; for (const [id, $type] of Object.entries(typeMap)) output += ` ${JSON.stringify(id)}: ${$type in TYPE_MAP ? localTypeName($type) : "any"};\n`; output += "}\n"; output += "\n"; output += RESOLVER_JSDOC_COMMENT; output += "\n"; output += `export const resolver: { apply(input: InputType): Tokens; listPermutations(): InputType[]; }; `; return output; } /** Generate TypeScript definition of valid inputs given a resolver */ function buildInputType(resolver, contexts) { const validContexts = contexts ?? {}; if (!Object.keys(validContexts).length) { for (const [name, m] of Object.entries(resolver.source.modifiers ?? {})) validContexts[name] = Object.keys(m.contexts); for (const m of resolver.source.resolutionOrder) { if (m?.type !== "modifier") continue; validContexts[m.name] = Object.keys(m.contexts); } } let output = "{"; for (const [name, values] of Object.entries(validContexts)) output += `\n ${JSON.stringify(name)}: ${values.length ? values.map((v) => JSON.stringify(v)).join(" | ") : "never"};`; output += "\n};"; return output; } function localTypeName($type) { return pascalCase($type); } /** Serialize normalized Tokens set into a string */ function serializeToken(token, includeProperties) { return `${JSON.stringify(token.id)}:${JSON.stringify(Object.fromEntries(Object.entries(token).filter(([k]) => includeProperties.has(k))))}`; } //#endregion //#region src/index.ts function pluginJS({ filename = "index.js", properties: userProperties, contexts } = {}) { const entry = { group: "plugin", label: "@terrazzo/plugin-js" }; return { name: "@terrazzo/plugin-js", config(_, context) { if (Array.isArray(userProperties) && !userProperties.length) context.logger.error({ ...entry, message: "properties option can’t be empty" }); }, async transform({ tokens, setTransform }) { for (const id of Object.keys(tokens)) { const accessor = `tokens[${JSON.stringify(id)}]`; setTransform(id, { format: "js", localID: id, value: accessor, meta: { "token-listing": { name: accessor } } }); } }, async build({ resolver, outputFile, context }) { const properties = userProperties ? new Set(userProperties) : DEFAULT_PROPERTIES; const { code: js, typeMap } = buildJS({ resolver, contexts, properties, logger: context.logger }); outputFile(filename, js); const dts = buildDTS({ resolver, contexts, properties, typeMap }); outputFile(typeof filename === "string" ? filename.replace(/\.(c|m)?js$/, ".d.$1ts") : "index.d.ts", dts); } }; } //#endregion export { DEFAULT_PROPERTIES, FILE_HEADER, FORMAT_ID, TYPE_MAP, buildDTS, buildJS, pluginJS as default }; //# sourceMappingURL=index.js.map