@terrazzo/plugin-js
Version:
Use DTCG tokens in Node.js for server-rendered applications.
195 lines (194 loc) • 6.78 kB
JavaScript
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