@terrazzo/cli
Version:
CLI for managing design tokens using the Design Tokens Community Group (DTCG) standard and generating code for any platform via plugins.
1,620 lines (1,618 loc) • 50.5 kB
JavaScript
import { createRequire } from "node:module";
import fsSync from "node:fs";
import { fileURLToPath, pathToFileURL } from "node:url";
import { build, defineConfig as defineConfig$1, parse } from "@terrazzo/parser";
import path from "node:path";
import pc from "picocolors";
import { createServer } from "vite";
import { ViteNodeRunner } from "vite-node/client";
import { ViteNodeServer } from "vite-node/server";
import { camelCase } from "scule";
import chokidar from "chokidar";
import yamlToMomoa from "yaml-to-momoa";
import * as momoa from "@humanwhocodes/momoa";
import { getObjMember, getObjMembers, traverse } from "@terrazzo/json-schema-tools";
import { isAlias, pluralize } from "@terrazzo/token-tools";
import yaml from "yaml";
import fs from "node:fs/promises";
import { merge } from "merge-anything";
import { spawn } from "node:child_process";
import { confirm, intro, multiselect, outro, select, spinner } from "@clack/prompts";
import { detect } from "detect-package-manager";
import { generate } from "escodegen";
import { parseModule } from "meriyah";
import { Readable, Writable } from "node:stream";
import mime from "mime";
//#region src/import/figma/lib.ts
const KEY = ":key";
const FILE_KEY = ":file_key";
const API = {
file: `https://api.figma.com/v1/files/${FILE_KEY}`,
fileNodes: `https://api.figma.com/v1/files/${FILE_KEY}/nodes`,
fileStyles: `https://api.figma.com/v1/files/${FILE_KEY}/styles`,
localVariables: `https://api.figma.com/v1/files/${FILE_KEY}/variables/local`,
publishedVariables: `https://api.figma.com/v1/files/${FILE_KEY}/variables/published`,
styles: `https://api.figma.com/v1/styles/${KEY}`
};
/** Wrapper around camelCase to handle more cases */
function formatName(name) {
return camelCase(name.replace(/\s+/g, "-"));
}
const nf = new Intl.NumberFormat("en-us");
/** Wrapper around camelCase to handle more cases */
function formatNumber(number) {
return nf.format(number);
}
/** Get File ID from design URL */
function getFileID(url) {
return url.match(/^https:\/\/(www\.)?figma\.com\/design\/([^/]+)/)?.[2];
}
/**
* Returns Figma API auth headers based on environment variables.
* Prefers OAuth (FIGMA_OAUTH_TOKEN) over PAT (FIGMA_ACCESS_TOKEN) if both are set.
* Warns via logger and returns empty headers if neither is set.
*/
function getFigmaAuthHeaders(logger) {
if (process.env.FIGMA_OAUTH_TOKEN) return { Authorization: `Bearer ${process.env.FIGMA_OAUTH_TOKEN}` };
if (process.env.FIGMA_ACCESS_TOKEN) return { "X-Figma-Token": process.env.FIGMA_ACCESS_TOKEN };
logger.warn({
group: "config",
message: "Figma auth not configured! Set FIGMA_OAUTH_TOKEN or FIGMA_ACCESS_TOKEN"
});
return {};
}
/** /v1/files/:file_key */
async function getFile(fileKey, { logger }) {
const res = await fetch(API.file.replace(FILE_KEY, fileKey), {
method: "GET",
headers: getFigmaAuthHeaders(logger)
});
if (!res.ok) logger.error({
group: "import",
message: `${res.status} ${await res.text()}`
});
return await res.json();
}
/** /v1/files/:file_key/nodes */
async function getFileNodes(fileKey, { ids, logger }) {
let url = API.fileNodes.replace(FILE_KEY, fileKey);
if (ids?.length) url += `?ids=${ids.join(",")}`;
const res = await fetch(url, {
method: "GET",
headers: getFigmaAuthHeaders(logger)
});
if (!res.ok) logger.error({
group: "import",
message: `${res.status} ${await res.text()}`
});
return await res.json();
}
/** /v1/files/:file_key/styles */
async function getFileStyles(fileKey, { logger }) {
const res = await fetch(API.fileStyles.replace(FILE_KEY, fileKey), {
method: "GET",
headers: getFigmaAuthHeaders(logger)
});
if (!res.ok) logger.error({
group: "import",
message: `${res.status} ${await res.text()}`
});
return await res.json();
}
/** /v1/files/:file_key/variables/local */
async function getFileLocalVariables(fileKey, { logger }) {
const res = await fetch(API.localVariables.replace(FILE_KEY, fileKey), {
method: "GET",
headers: getFigmaAuthHeaders(logger)
});
if (!res.ok) logger.error({
group: "import",
message: `${res.status} ${await res.text}`
});
return await res.json();
}
/** /v1/files/:file_key/variables/published */
async function getFilePublishedVariables(fileKey, { logger }) {
const res = await fetch(API.publishedVariables.replace(FILE_KEY, fileKey), {
method: "GET",
headers: getFigmaAuthHeaders(logger)
});
if (!res.ok) logger.error({
group: "import",
message: `${res.status} ${await res.text}`
});
return await res.json();
}
//#endregion
//#region src/shared.ts
const cwd = new URL(`${pathToFileURL(process.cwd())}/`);
const DEFAULT_CONFIG_PATH = new URL("./terrazzo.config.ts", cwd);
const DEFAULT_TOKENS_PATH = new URL("./tokens.json", cwd);
const GREEN_CHECK = pc.green("✔");
/** Load config */
async function loadConfig({ cmd, flags, logger }) {
/**
* Vite server for loading .ts config files
* TODO: remove me when Node 24 is the oldest-supported Node version
*/
let viteServer;
try {
let config = {
tokens: [DEFAULT_TOKENS_PATH],
outDir: new URL("./tokens/", cwd),
plugins: [],
lint: {
build: { enabled: true },
rules: {}
},
alphabetize: false,
ignore: {
tokens: [],
deprecated: false
},
permutationLimit: 1e3
};
let configPath;
if (typeof flags.config === "string") {
if (flags.config === "") {
logger.error({
group: "config",
message: "Missing path after --config flag"
});
process.exit(1);
}
configPath = resolveConfig(flags.config);
if (!configPath) logger.error({
group: "config",
message: `Could not locate ${flags.config}`
});
}
const resolvedConfigPath = resolveConfig(configPath);
if (resolvedConfigPath) try {
viteServer = await createServer({ mode: "development" });
const viteNodeServer = new ViteNodeServer(viteServer);
const mod = await new ViteNodeRunner({
root: viteServer.config.root,
base: viteServer.config.base,
fetchModule(...args) {
return viteNodeServer.fetchModule(...args);
},
resolveId(...args) {
return viteNodeServer.resolveId(...args);
}
}).executeFile(resolvedConfigPath);
if (!mod.default) throw new Error(`No default export found in ${resolvedConfigPath.replace(fileURLToPath(cwd), "")}. See https://terrazzo.dev/docs for instructions.`);
config = defineConfig$1(mod.default, {
cwd,
logger
});
if (flags["no-lint"]) config.lint.build.enabled = false;
} catch (err) {
logger.error({
group: "config",
message: err.message || err
});
}
else if (cmd !== "init" && cmd !== "check") logger.error({
group: "config",
message: "No config file found. Create one with `npx terrazzo init`."
});
if (viteServer) await viteServer?.close();
return {
config,
configPath: resolvedConfigPath
};
} catch (err) {
printError(err.message);
if (viteServer) await viteServer.close();
process.exit(1);
}
}
/** load tokens */
async function loadTokens(tokenPaths, { logger }) {
try {
const allTokens = [];
if (!Array.isArray(tokenPaths)) logger.error({
group: "config",
message: `loadTokens: Expected array, received ${typeof tokenPaths}`
});
if (tokenPaths.length === 1 && tokenPaths[0].href === DEFAULT_TOKENS_PATH.href) {
if (!fsSync.existsSync(tokenPaths[0])) {
const yamlPath = new URL("./tokens.yaml", cwd);
if (fsSync.existsSync(yamlPath)) tokenPaths[0] = yamlPath;
else {
logger.error({
group: "config",
message: `Could not locate ${path.relative(cwd.href, tokenPaths[0].href)}. To create one, run \`npx tz init\`.`
});
return;
}
}
}
for (let i = 0; i < tokenPaths.length; i++) {
const filename = tokenPaths[i];
if (!(filename instanceof URL)) {
logger.error({
group: "config",
message: `Expected URL, received ${filename}`,
label: `loadTokens[${i}]`
});
return;
} else if (filename.protocol === "http:" || filename.protocol === "https:") try {
if (filename.host === "figma.com" || filename.host === "www.figma.com") {
const [_, fileKeyword, fileKey] = filename.pathname.split("/");
if (fileKeyword !== "file" || !fileKey) logger.error({
group: "config",
message: `Unexpected Figma URL. Expected "https://www.figma.com/file/:file_key/:file_name?…", received "${filename.href}"`
});
const headers = new Headers({
Accept: "*/*",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0",
...getFigmaAuthHeaders(logger)
});
const res = await fetch(`https://api.figma.com/v1/files/${fileKey}/variables/local`, {
method: "GET",
headers
});
if (res.ok) allTokens.push({
filename,
src: await res.text()
});
const message = res.status !== 404 ? JSON.stringify(await res.json(), void 0, 2) : "";
logger.error({
group: "config",
message: `Figma responded with ${res.status}${message ? `:\n${message}` : ""}`
});
break;
}
const res = await fetch(filename, {
method: "GET",
headers: {
Accept: "*/*",
"User-Agent": "Mozilla/5.0 Gecko/20100101 Firefox/123.0"
}
});
allTokens.push({
filename,
src: await res.text()
});
} catch (err) {
logger.error({
group: "config",
message: `${filename.href}: ${err}`
});
return;
}
else if (fsSync.existsSync(filename)) allTokens.push({
filename,
src: fsSync.readFileSync(filename, "utf8")
});
else {
logger.error({
group: "config",
message: `Could not locate ${path.relative(cwd.href, filename.href)}. To create one, run \`npx tz init\`.`
});
return;
}
}
return allTokens;
} catch (err) {
printError(err.message);
process.exit(1);
}
}
/** Print error */
function printError(message) {
console.error(pc.red(`✗ ${message}`));
}
/** Print success */
function printSuccess(message, startTime) {
console.log(`${GREEN_CHECK} ${message}${startTime ? ` ${time(startTime)}` : ""}`);
}
/** Resolve config */
function resolveConfig(filename) {
if (filename && fsSync.existsSync(new URL(filename, cwd))) return filename;
for (const ext of [
".ts",
".js",
".mts",
".cts",
".mjs",
".cjs"
]) {
const maybeFilename = `terrazzo.config${ext}`;
if (fsSync.existsSync(new URL(maybeFilename, cwd))) return fileURLToPath(new URL(maybeFilename, cwd));
}
}
/** Resolve tokens.json path (for lint command) */
function resolveTokenPath(filename, { logger }) {
const tokensPath = new URL(filename, cwd);
if (!fsSync.existsSync(tokensPath)) logger.error({
group: "config",
message: `Could not locate ${filename}. Does the file exist?`
});
else if (!fsSync.statSync(tokensPath).isFile()) logger.error({
group: "config",
message: `Expected JSON or YAML file, received ${filename}.`
});
return tokensPath;
}
/** Print time elapsed */
function time(start) {
const diff = performance.now() - start;
return pc.dim(diff < 750 ? `${Math.round(diff)}ms` : `${(diff / 1e3).toFixed(1)}s`);
}
//#endregion
//#region src/build.ts
/** tz build */
async function buildCmd({ config, configPath, flags, logger }) {
try {
const startTime = performance.now();
if (!Array.isArray(config.plugins) || !config.plugins.length) logger.error({
group: "config",
message: `No plugins defined! Add some in ${configPath || "terrazzo.config.ts"}`
});
let rawSchemas = await loadTokens(config.tokens, { logger });
if (!rawSchemas) {
logger.error({
group: "config",
message: `Error loading ${path.relative(fileURLToPath(cwd), fileURLToPath(config.tokens[0] || DEFAULT_TOKENS_PATH))}`
});
return;
}
const skipLint = !config.lint.build.enabled;
let { tokens, resolver, sources } = await parse(rawSchemas, {
config,
logger,
skipLint,
yamlToMomoa
});
let result = await build(tokens, {
resolver,
sources,
config,
logger
});
writeFiles(result, {
config,
logger
});
if (flags.watch) {
const dt = new Intl.DateTimeFormat("en-us", {
hour: "2-digit",
minute: "2-digit"
});
async function rebuild({ messageBefore, messageAfter } = {}) {
try {
if (messageBefore) logger.info({
group: "plugin",
label: "watch",
message: messageBefore
});
rawSchemas = await loadTokens(config.tokens, { logger });
if (!rawSchemas) throw new Error(`Error loading ${path.relative(fileURLToPath(cwd), fileURLToPath(config.tokens[0] || DEFAULT_TOKENS_PATH))}`);
const parseResult = await parse(rawSchemas, {
config,
logger,
skipLint,
yamlToMomoa
});
tokens = parseResult.tokens;
sources = parseResult.sources;
resolver = parseResult.resolver;
result = await build(tokens, {
resolver,
sources,
config,
logger
});
if (messageAfter) logger.info({
group: "plugin",
label: "watch",
message: messageAfter
});
writeFiles(result, {
config,
logger
});
} catch (err) {
console.error(pc.red(`✗ ${err.message || err}`));
}
}
chokidar.watch(config.tokens.map((filename) => fileURLToPath(filename))).on("change", async (filename) => {
await rebuild({ messageBefore: `${pc.dim(dt.format(/* @__PURE__ */ new Date()))} ${pc.green("tz")}} ${pc.yellow(filename)} updated ${GREEN_CHECK}` });
});
chokidar.watch(resolveConfig(configPath)).on("change", async () => {
await rebuild({ messageBefore: `${pc.dim(dt.format(/* @__PURE__ */ new Date()))} ${pc.green("tz")} ${pc.yellow("Config updated. Reloading…")}` });
});
await new Promise(() => {});
} else printSuccess(`${Object.keys(tokens).length} token${Object.keys(tokens).length !== 1 ? "s" : ""} built`, startTime);
} catch (err) {
printError(err.message);
process.exit(1);
}
}
/** Write files */
function writeFiles(result, { config, logger }) {
for (const { filename, contents } of result.outputFiles) {
const output = new URL(filename, config.outDir);
fsSync.mkdirSync(new URL(".", output), { recursive: true });
fsSync.writeFileSync(output, contents);
logger.debug({
group: "parser",
label: "buildEnd",
message: `Wrote file ${fileURLToPath(output)}`
});
}
}
//#endregion
//#region src/check.ts
/** tz check */
async function checkCmd({ config, logger, positionals }) {
try {
const startTime = performance.now();
const sources = await loadTokens(positionals.slice(1).length ? positionals.slice(1).map((tokenPath) => resolveTokenPath(tokenPath, { logger })) : config.tokens, { logger });
if (!sources?.length) {
logger.error({
group: "config",
message: "Couldn’t find any tokens. Run `npx tz init` to create some."
});
return;
}
await parse(sources, {
config,
continueOnError: true,
logger,
yamlToMomoa
});
printSuccess("No errors", startTime);
} catch (err) {
printError(err.message);
process.exit(1);
}
}
//#endregion
//#region src/format.ts
function findMember(name) {
return function(member) {
return member.name.type === "String" && member.name.value === name;
};
}
async function formatCmd(filename, { logger, output }) {
try {
if (!filename) {
logger.error({
group: "config",
message: "Expected input: `tz format <tokens.json> -o output.json`"
});
return;
}
const sourceLoc = new URL(filename, cwd);
if (!fsSync.existsSync(sourceLoc)) logger.error({
group: "config",
message: `Couldn’t find ${path.relative(cwd.href, sourceLoc.href)}. Does it exist?`
});
const sourceData = fsSync.readFileSync(sourceLoc, "utf8");
const isYaml = filename.endsWith(".yml") || filename.endsWith(".yaml") || !sourceData.startsWith("{");
const document = isYaml ? yamlToMomoa(sourceData) : momoa.parse(sourceData, { mode: "jsonc" });
const { tokens } = await parse([{
src: sourceData,
filename: sourceLoc
}], {
config: defineConfig$1({ lint: { rules: {
"core/consistent-naming": "off",
"core/valid-color": "off",
"core/valid-dimension": "off",
"core/valid-duration": "off",
"core/valid-typography": "off"
} } }, { cwd }),
logger,
resolveAliases: false,
yamlToMomoa
});
traverse(document, { enter(node, _parent, nodePath) {
const token = tokens[nodePath.join(".")];
if (!token || token.aliasOf || node.type !== "Member" || node.value.type !== "Object") return;
const $valueI = node.value.members.findIndex(findMember("$value"));
switch (token.$type) {
case "color":
if (node.value.members[$valueI].value.type === "String") {
if (isAlias(node.value.members[$valueI].value.value)) return;
const hex = node.value.members[$valueI].value.value;
node.value.members[$valueI].value = momoa.parse(JSON.stringify({
...token.$value,
hex: hex.startsWith("#") ? hex.slice(0, 7) : void 0
})).body;
const $extensions = getObjMember(node.value, "$extensions");
if ($extensions?.type === "Object") {
const mode = getObjMember($extensions, "mode");
if (mode?.type === "Object") for (let i = 0; i < mode.members.length; i++) {
const modeName = mode.members[i].name.value;
const hex = mode.members[i].value.value;
mode.members[i].value = momoa.parse(JSON.stringify({
...token.mode[modeName].$value,
hex: hex.startsWith("#") ? hex.slice(0, 7) : void 0
})).body;
}
}
}
break;
case "dimension":
case "duration":
if (node.value.members[$valueI].value.type === "String") {
if (isAlias(node.value.members[$valueI].value.value)) return;
node.value.members[$valueI].value = formatDurationDimension(node.value.members[$valueI].value);
const $extensions = getObjMember(node.value, "$extensions");
if ($extensions?.type === "Object") {
const mode = getObjMember($extensions, "mode");
if (mode?.type === "Object") for (let i = 0; i < mode.members.length; i++) mode.members[i].value = formatDurationDimension(node.value.members[$valueI].value);
}
}
break;
case "typography": {
if (node.value.members[$valueI]?.value.type !== "Object") return;
node.value.members[$valueI].value = formatTypography(node.value.members[$valueI].value);
const $extensions = getObjMember(node.value, "$extensions");
if ($extensions?.type === "Object") {
const mode = getObjMember($extensions, "mode");
if (mode?.type === "Object") for (let i = 0; i < mode.members.length; i++) mode.members[i].value = formatTypography(mode.members[i].value);
}
}
}
} });
const outputLoc = new URL(output, cwd);
const contents = isYaml ? yaml.stringify(JSON.parse(momoa.print(document))) : momoa.print(document, { indent: 2 });
fsSync.mkdirSync(new URL(".", outputLoc), { recursive: true });
fsSync.writeFileSync(outputLoc, contents);
} catch (err) {
printError(err.message);
process.exit(1);
}
}
function formatDurationDimension(node) {
const value = Number.parseFloat(node.value);
if (!Number.isFinite(value)) return node;
node.type = "Object";
node.members = momoa.parse(JSON.stringify({
value,
unit: node.value.replace(String(value), "")
})).body.members;
delete node.value;
return node;
}
function formatTypography(node) {
const { fontFamily, fontSize, fontWeight, letterSpacing, lineHeight } = getObjMembers(node);
if (!fontFamily) node.members.push(momoa.parse("{\"fontFamily\":[\"inherit\"]}").body.members[0]);
if (!fontSize) node.members.push(momoa.parse("{\"fontSize\":{\"value\":1,\"unit\":\"rem\"}}").body.members[0]);
if (!fontWeight) node.members.push(momoa.parse("{\"fontWeight\":400}").body.members[0]);
if (!letterSpacing) node.members.push(momoa.parse("{\"letterSpacing\":{\"value\":0,\"unit\":\"rem\"}}").body.members[0]);
if (!lineHeight) node.members.push(momoa.parse("{\"lineHeight\":1}").body.members[0]);
return node;
}
//#endregion
//#region src/help.ts
/** Show help */
function helpCmd() {
console.log(`tz
[commands]
build Build token artifacts from tokens.json
--watch, -w Watch tokens.json for changes and recompile
--no-lint Disable linters running on build
check [path] Check tokens.json for errors and run linters
lint [path] (alias of check)
format [path] Format your tokens
--o [file] Output file
normalize (alias of format)
init Create a starter tokens.json file
lab Manage your tokens with a web interface
import [path] Import from a Figma Design file
--o [file] Save imported JSON
--unpublished Include unpublished Variables
--skip-styles Don’t import styles
--skip-variables
Don’t import variables
[options]
--help Show this message
--config, -c Path to config (default: ./terrazzo.config.ts)
--quiet Suppress warnings
`);
}
//#endregion
//#region src/import/figma/styles.ts
/** /v1/files/:file_key/styles */
async function getStyles(fileKey, { logger, unpublished }) {
const result = {
count: 0,
code: { sets: { styles: { sources: [{}] } } }
};
const styleNodeIDs = /* @__PURE__ */ new Set();
const stylesByID = /* @__PURE__ */ new Map();
if (unpublished) {
const styles = await getFile(fileKey, { logger });
for (const [id, style] of Object.entries(styles.styles)) {
styleNodeIDs.add(id);
stylesByID.set(id, style);
}
} else {
const styles = await getFileStyles(fileKey, { logger });
for (const style of styles.meta.styles) {
styleNodeIDs.add(style.node_id);
stylesByID.set(style.node_id, style);
}
}
const fileNodes = await getFileNodes(fileKey, {
ids: [...styleNodeIDs],
logger
});
result.count += styleNodeIDs.size;
for (const [id, s] of stylesByID) {
const styleNode = fileNodes.nodes[id];
if (!styleNode) {
logger.warn({
group: "import",
message: `Style ${s.name} not found in file nodes. Does it need to be published?`
});
continue;
}
const styleType = "style_type" in s ? s.style_type : s.styleType;
const tokenBase = {
$type: void 0,
$description: s.description || void 0,
$value: void 0,
$extensions: { "figma.com": {
name: s.name,
node_id: id,
created_at: "created_at" in s ? s.created_at : void 0,
updated_at: "updated_at" in s ? s.updated_at : void 0
} }
};
switch (styleType) {
case "FILL": {
const $value = fillStyle(styleNode.document);
if (!$value) logger.error({
group: "import",
message: `Could not parse fill for ${s.name}`,
continueOnError: true
});
if (Array.isArray($value)) tokenBase.$type = "gradient";
else tokenBase.$type = "color";
tokenBase.$value = $value;
break;
}
case "TEXT": {
const $value = textStyle(styleNode.document);
if (!$value) logger.error({
group: "import",
message: `Could not parse text for ${s.name}`,
continueOnError: true
});
tokenBase.$type = "typography";
tokenBase.$value = $value;
break;
}
case "EFFECT": {
const $value = effectStyle(styleNode.document);
if (!$value) logger.error({
group: "import",
message: `Could not parse effect for ${s.name}`,
continueOnError: true
});
tokenBase.$type = "shadow";
tokenBase.$value = $value;
break;
}
case "GRID": {
const layoutGrids = gridStyles(styleNode.document);
if (!layoutGrids) logger.error({
group: "import",
message: `Could not parse grid for ${s.name}`,
continueOnError: true
});
let node = result.code.sets.styles.sources[0];
const path = s.name.split("/").map(formatName);
const name = path.pop();
for (const key of path) {
if (!(key in node)) node[key] = {};
node = node[key];
}
node[name] = layoutGrids;
break;
}
}
if (tokenBase.$type !== void 0) {
let node = result.code.sets.styles.sources[0];
const path = s.name.split("/").map(formatName);
const name = path.pop();
for (const key of path) {
if (!(key in node)) node[key] = {};
node = node[key];
}
node[name] = tokenBase;
}
}
return result;
}
/** Return a shadow token from an effect */
function effectStyle(node) {
if ("effects" in node) {
const shadows = node.effects.filter((e) => e.type === "DROP_SHADOW" || e.type === "INNER_SHADOW");
if (shadows.length) return shadows.map((s) => ({
inset: s.type === "INNER_SHADOW",
offsetX: {
value: s.offset.x,
unit: "px"
},
offsetY: {
value: s.offset.y,
unit: "px"
},
blur: {
value: s.radius,
unit: "px"
},
spread: {
value: s.spread ?? 0,
unit: "px"
},
color: {
colorSpace: "srgb",
components: [
s.color.r,
s.color.g,
s.color.b
],
alpha: s.color.a
}
}));
}
}
/** Return a color or gradient token from a fill */
function fillStyle(node) {
if ("fills" in node) for (const fill of node.fills) switch (fill.type) {
case "SOLID": return {
colorSpace: "srgb",
components: [
fill.color.r,
fill.color.g,
fill.color.b
],
alpha: fill.color.a
};
case "GRADIENT_LINEAR":
case "GRADIENT_RADIAL":
case "GRADIENT_ANGULAR":
case "GRADIENT_DIAMOND": return fill.gradientStops.map((stop) => ({
position: stop.position,
color: {
colorSpace: "srgb",
components: [
stop.color.r,
stop.color.g,
stop.color.b
],
alpha: stop.color.a
}
}));
}
}
/** Return a dimension token from grid */
function gridStyles(node) {
if (!("layoutGrids" in node) || !node.layoutGrids?.length) return;
const values = {};
for (const grid of node.layoutGrids) {
const pattern = grid.pattern.toLowerCase();
if (values[pattern]) continue;
values[pattern] = {
sectionSize: {
$type: "dimension",
$value: {
value: grid.sectionSize,
unit: "px"
}
},
gutterSize: {
$type: "dimension",
$value: {
value: grid.sectionSize,
unit: "px"
}
}
};
if (grid.count > 0) values[pattern].count = {
$type: "number",
$value: grid.count
};
}
return values;
}
/** Return a typography token from text */
function textStyle(node) {
if (!("style" in node)) return;
return {
fontFamily: [node.style.fontFamily],
fontWeight: node.style.fontWeight,
fontStyle: node.style.fontStyle,
fontSize: node.style.fontSize ? {
value: node.style.fontSize,
unit: "px"
} : {
value: 1,
unit: "em"
},
letterSpacing: {
value: node.style.letterSpacing ?? 0,
unit: "px"
},
lineHeight: "lineHeightPercentFontSize" in node.style ? node.style.lineHeightPercentFontSize : "lineHeightPx" in node.style ? {
value: node.style.lineHeightPx,
unit: "px"
} : 1
};
}
//#endregion
//#region src/import/figma/variables.ts
function getAliasID(value) {
if (typeof value === "object" && value && "type" in value && value.type === "VARIABLE_ALIAS" && "id" in value && typeof value.id === "string") return value.id;
}
/** /v1/files/:file_key/variables/published | /v1/files/:file_key/variables/local */
async function getVariables(fileKey, { logger, unpublished, matchers }) {
const result = {
count: 0,
remoteCount: 0,
code: {
sets: {},
modifiers: {}
}
};
const allVariables = {};
const variableCollections = {};
let finalVariables = {};
const modeIDToName = {};
const local = await getFileLocalVariables(fileKey, { logger });
for (const id of Object.keys(local.meta.variables)) allVariables[id] = local.meta.variables[id];
for (const id of Object.keys(local.meta.variableCollections)) {
variableCollections[id] = local.meta.variableCollections[id];
for (const mode of local.meta.variableCollections[id].modes) modeIDToName[mode.modeId] = formatName(mode.name);
}
if (unpublished) finalVariables = Object.fromEntries(Object.entries(allVariables).filter(([, variable]) => !variable.hiddenFromPublishing));
else {
const published = await getFilePublishedVariables(fileKey, { logger });
for (const id of Object.keys(published.meta.variables)) finalVariables[id] = allVariables[id];
}
const pendingIDs = Object.keys(finalVariables);
for (let i = 0; i < pendingIDs.length; i++) {
const variable = finalVariables[pendingIDs[i]];
if (!variable) continue;
for (const value of Object.values(variable.valuesByMode)) {
const aliasID = getAliasID(value);
if (!aliasID || !allVariables[aliasID] || aliasID in finalVariables) continue;
finalVariables[aliasID] = allVariables[aliasID];
pendingIDs.push(aliasID);
}
}
const remoteIDs = /* @__PURE__ */ new Set();
for (const id of Object.keys(finalVariables)) {
const variable = finalVariables[id];
const collection = variableCollections[variable.variableCollectionId];
const collectionName = formatName(collection.name);
const hasMultipleModes = collection.modes.length > 1;
if (hasMultipleModes) {
if (!(collectionName in result.code.modifiers)) result.code.modifiers[collectionName] = {
contexts: Object.fromEntries(collection.modes.map((m) => [formatName(m.name), [{}]])),
default: modeIDToName[collection.defaultModeId]
};
} else if (!(collectionName in result.code.sets)) result.code.sets[collectionName] = { sources: [{}] };
const matches = matchers.fontFamily?.test(variable.name) && "fontFamily" || matchers.fontWeight?.test(variable.name) && "fontWeight" || matchers.number?.test(variable.name) && "number" || void 0;
for (const [modeID, value] of Object.entries(variable.valuesByMode)) {
const modeName = modeIDToName[modeID];
let node = result.code;
if (hasMultipleModes) {
if (!(modeName in result.code.modifiers[collectionName].contexts)) result.code.modifiers[collectionName].contexts[modeName] = [{}];
node = result.code.modifiers[collectionName].contexts[modeName][0];
} else node = result.code.sets[collectionName].sources[0];
const tokenBase = {
$type: void 0,
$description: variable.description || void 0,
$value: void 0,
$extensions: { "figma.com": {
name: variable.name,
id: variable.id,
variableCollectionId: variable.variableCollectionId,
codeSyntax: Object.keys(variable.codeSyntax).length ? variable.codeSyntax : void 0
} }
};
const isAliasOfID = getAliasID(value);
if (isAliasOfID) if (allVariables[isAliasOfID]) {
tokenBase.$type = matches || {
COLOR: "color",
BOOLEAN: "boolean",
STRING: "string",
FLOAT: "dimension"
}[variable.resolvedType];
tokenBase.$value = `{${allVariables[isAliasOfID].name.split("/").map(formatName).join(".")}}`;
} else {
remoteIDs.add(isAliasOfID);
continue;
}
else if (matches === "fontFamily") {
tokenBase.$type = "fontFamily";
tokenBase.$value = String(value).split(",");
} else if (matches === "fontWeight") {
tokenBase.$type = "fontWeight";
tokenBase.$value = value;
} else if (matches === "number") {
if (typeof value === "object") throw new Error(`Can’t coerce ${variable.name} into number type.`);
tokenBase.$type = "number";
tokenBase.$value = Number(value);
} else switch (variable.resolvedType) {
case "BOOLEAN":
case "STRING":
tokenBase.$type = variable.resolvedType.toLowerCase();
tokenBase.$value = value;
break;
case "FLOAT":
tokenBase.$type = "dimension";
tokenBase.$value = {
value,
unit: "px"
};
break;
case "COLOR": {
const { r, g, b, a } = value;
tokenBase.$type = "color";
tokenBase.$value = {
colorSpace: "srgb",
components: [
r,
g,
b
],
alpha: a
};
break;
}
}
if (tokenBase.$value !== void 0) {
const path = variable.name.split("/").map(formatName);
const name = path.pop();
for (const key of path) {
if (!(key in node)) node[key] = {};
node = node[key];
}
node[name] = tokenBase;
}
}
}
result.count = Object.keys(finalVariables).length;
result.remoteCount = remoteIDs.size;
return result;
}
//#endregion
//#region src/import/figma/index.ts
async function importFromFigma({ url, logger, unpublished, skipStyles, skipVariables, fontFamilyNames = "/fontFamily$", fontWeightNames = "/fontWeight$", numberNames }) {
const fileKey = getFileID(url);
if (!fileKey) logger.error({
group: "import",
message: `Invalid Figma URL: ${url}`
});
const result = {
variableCount: 0,
styleCount: 0,
code: {
$schema: "https://www.designtokens.org/schemas/2025.10/resolver.json",
version: "2025.10",
resolutionOrder: [],
sets: {},
modifiers: {}
}
};
try {
const [styles, vars] = await Promise.all([!skipStyles ? getStyles(fileKey, { logger }) : null, !skipVariables ? getVariables(fileKey, {
logger,
unpublished,
matchers: {
fontFamily: fontFamilyNames ? new RegExp(fontFamilyNames) : void 0,
fontWeight: fontWeightNames ? new RegExp(fontWeightNames) : void 0,
number: numberNames ? new RegExp(numberNames) : void 0
}
}) : null]);
if (styles) {
result.styleCount += styles.count;
result.code = merge(result.code, styles.code);
}
if (vars) {
result.variableCount += vars.count;
result.code = merge(result.code, vars.code);
if (vars.remoteCount) logger.warn({
group: "import",
message: `${formatNumber(vars.remoteCount)} ${pluralize(vars.remoteCount, "Variable", "Variables")} were remote and could not be accessed. Try importing from other files to grab them.`
});
}
} catch (err) {
logger.error({
group: "import",
message: err.message
});
}
for (const group of ["sets", "modifiers"]) for (const name of Object.keys(result.code[group])) result.code.resolutionOrder.push({ $ref: `#/${group}/${name}` });
return result;
}
/** Is this a valid URL, and one belonging to a Figma file? */
function isFigmaPath(url) {
try {
new URL(url);
return /^https:\/\/(www\.)?figma\.com\/design\/[A-Za-z0-9]+/.test(url);
} catch {
return false;
}
}
//#endregion
//#region src/import/index.ts
async function importCmd({ flags, positionals, logger }) {
const [_cmd, url] = positionals;
if (!url) logger.error({
group: "import",
message: "Missing import path. Expected `tz import [file]`."
});
if (isFigmaPath(url)) {
const { FIGMA_ACCESS_TOKEN, FIGMA_OAUTH_TOKEN } = process.env;
if (!FIGMA_ACCESS_TOKEN && !FIGMA_OAUTH_TOKEN) logger.error({
group: "import",
message: `Figma auth not configured! Set FIGMA_OAUTH_TOKEN (OAuth access token) or FIGMA_ACCESS_TOKEN (Personal Access Token). See https://terrazzo.app/docs/guides/import-from-figma`
});
const start = performance.now();
const result = await importFromFigma({
url,
logger,
unpublished: flags.unpublished,
skipStyles: flags["skip-styles"],
skipVariables: flags["skip-variables"],
fontFamilyNames: flags["font-family-names"],
fontWeightNames: flags["font-weight-names"],
numberNames: flags["number-names"]
});
const end = performance.now() - start;
if (flags.output) {
const oldFile = fsSync.existsSync(flags.output) ? JSON.parse(await fs.readFile(flags.output, "utf8")) : {};
const code = {
$schema: result.code.$schema,
version: result.code.version,
resolutionOrder: oldFile.resolutionOrder?.length ? oldFile.resolutionOrder : result.code.resolutionOrder,
sets: result.code.sets,
modifiers: result.code.modifiers,
$defs: oldFile.$defs,
$extensions: oldFile.$extensions
};
await fs.writeFile(flags.output, `${JSON.stringify(code, void 0, 2)}\n`);
logger.info({
group: "import",
message: `Imported ${formatNumber(result.variableCount)} ${pluralize(result.variableCount, "Variable", "Variables")}, ${formatNumber(result.styleCount)} ${pluralize(result.styleCount, "Style", "Styles")} → ${flags.output}`,
timing: end
});
} else process.stdout.write(JSON.stringify(result.code));
return;
}
}
//#endregion
//#region src/init.ts
const INSTALL_COMMAND = {
npm: "install -D --silent",
yarn: "add -D --silent",
pnpm: "add -D --silent",
bun: "install -D --silent"
};
const SYNTAX_SETTINGS = { format: {
indent: { style: " " },
quotes: "single",
semicolons: true
} };
const EXAMPLE_TOKENS_PATH = "my-tokens.tokens.json";
const DESIGN_SYSTEMS = {
"adobe-spectrum": {
name: "Spectrum",
author: "Adobe",
tokens: ["dtcg-examples/adobe-spectrum.resolver.json"]
},
"apple-hig": {
name: "Human Interface Guidelines",
author: "Apple",
tokens: ["dtcg-examples/apple-hig.resolver.json"]
},
"figma-sds": {
name: "Simple Design System",
author: "Figma",
tokens: ["dtcg-examples/figma-sds.resolver.json"]
},
"github-primer": {
name: "Primer",
author: "GitHub",
tokens: ["dtcg-examples/github-primer.resolver.json"]
},
"ibm-carbon": {
name: "Carbon",
author: "IBM",
tokens: ["dtcg-examples/ibm-carbon.resolver.json"]
},
"microsoft-fluent": {
name: "Fluent",
author: "Microsoft",
tokens: ["dtcg-examples/microsoft-fluent.resolver.json"]
},
"shopify-polaris": {
name: "Polaris",
author: "Shopify",
tokens: ["dtcg-examples/shopify-polaris.resolver.json"]
}
};
async function initCmd({ logger }) {
try {
intro("⛋ Welcome to Terrazzo");
const packageManager = await detect({ cwd: fileURLToPath(cwd) });
const { config, configPath = "terrazzo.config.ts" } = await loadConfig({
cmd: "init",
flags: {},
logger
});
const tokensPath = config.tokens[0];
const hasExistingConfig = fsSync.existsSync(configPath);
let startFromDS = !(tokensPath && fsSync.existsSync(tokensPath));
if (tokensPath && fsSync.existsSync(tokensPath)) {
if (await confirm({ message: `Found tokens at ${path.relative(fileURLToPath(cwd), fileURLToPath(tokensPath))}. Overwrite with a new design system?` })) startFromDS = true;
}
if (startFromDS) {
const ds = DESIGN_SYSTEMS[await select({
message: "Start from existing design system?",
options: [...Object.entries(DESIGN_SYSTEMS).map(([id, { author, name }]) => ({
value: id,
label: `${author} ${name}`
})), {
value: "none",
label: "None"
}]
})];
if (ds) {
const s = spinner();
s.start("Downloading");
await new Promise((resolve, reject) => {
const subprocess = spawn(packageManager, [INSTALL_COMMAND[packageManager], "dtcg-examples"], { cwd });
subprocess.on("error", reject);
subprocess.on("exit", resolve);
});
s.stop("Download complete");
if (hasExistingConfig) await updateConfigTokens(configPath, ds.tokens);
else await newConfigFile(configPath, ds.tokens);
} else startFromDS = false;
}
if (!hasExistingConfig) {
await newConfigFile(configPath, [EXAMPLE_TOKENS_PATH]);
await fs.writeFile(EXAMPLE_TOKENS_PATH, JSON.stringify(EXAMPLE_TOKENS, void 0, 2));
}
const existingPlugins = config.plugins.map((p) => p.name);
const pluginSelection = await multiselect({
message: "Install plugins?",
options: [
{
value: ["@terrazzo/plugin-css"],
label: "CSS"
},
{
value: ["@terrazzo/plugin-js"],
label: "JS + TS"
},
{
value: ["@terrazzo/plugin-css", "@terrazzo/plugin-sass"],
label: "Sass"
},
{
value: ["@terrazzo/plugin-tailwind"],
label: "Tailwind"
}
],
required: false
});
const newPlugins = Array.isArray(pluginSelection) ? Array.from(new Set(pluginSelection.flat().filter((p) => !existingPlugins.includes(p)))) : [];
if (newPlugins?.length) {
const plugins = newPlugins.map((p) => ({
specifier: p.replace("@terrazzo/plugin-", ""),
path: p
}));
const pluginCount = `${newPlugins.length} ${pluralize(newPlugins.length, "plugin", "plugins")}`;
const s = spinner();
s.start(`Installing ${pluginCount}`);
await new Promise((resolve, reject) => {
const subprocess = spawn(packageManager, [INSTALL_COMMAND[packageManager], newPlugins.join(" ")], { cwd });
subprocess.on("error", reject);
subprocess.on("exit", resolve);
});
s.message("Updating config");
await updateConfigPlugins(configPath, plugins);
s.stop(`Installed ${pluginCount}`);
}
if (!startFromDS && !newPlugins?.length) {
outro("Nothing to do. Exiting.");
return;
}
outro("⛋ Done! 🎉");
} catch (err) {
printError(err.message);
process.exit(1);
}
}
async function newConfigFile(configPath, tokens, imports = []) {
await fs.writeFile(configPath, `import { defineConfig } from '@terrazzo/cli';
${imports.map((p) => `import ${p.specifier} from '${p.path}';`).join("\n")}
export default defineConfig({
tokens: ['${tokens.join("', '")}'],
plugins: [
${imports.length ? imports.map((p) => `${p.specifier}(),`).join("\n ") : "/** @see https://terrazzo.app/docs */"}
],
outDir: './dist/',
lint: {
/** @see https://terrazzo.app/docs/linting */
build: {
enabled: true,
},
rules: {
'core/valid-color': 'error',
'core/valid-dimension': 'error',
'core/valid-font-family': 'error',
'core/valid-font-weight': 'error',
'core/valid-duration': 'error',
'core/valid-cubic-bezier': 'error',
'core/valid-number': 'error',
'core/valid-link': 'error',
'core/valid-boolean': 'error',
'core/valid-string': 'error',
'core/valid-stroke-style': 'error',
'core/valid-border': 'error',
'core/valid-transition': 'error',
'core/valid-shadow': 'error',
'core/valid-gradient': 'error',
'core/valid-typography': 'error',
'core/consistent-naming': 'warn',
},
},
});`);
}
function getConfigObjFromAst(ast, configPath) {
const astExport = ast.body.find((node) => node.type === "ExportDefaultDeclaration");
if (!astExport) {
const relConfigPath = configPath ? path.relative(fileURLToPath(cwd), fileURLToPath(new URL(configPath))) : void 0;
throw new Error(`SyntaxError: ${relConfigPath} does not have default export.`);
}
const astConfig = astExport.declaration.type === "CallExpression" ? astExport.declaration.arguments[0] : astExport.declaration;
if (astConfig.type !== "ObjectExpression") throw new Error(`Config: expected object default export, received ${astConfig.type}.`);
return astConfig;
}
async function updateConfigTokens(configPath, tokens) {
const ast = parseModule(await fs.readFile(configPath, "utf8"));
const astConfig = getConfigObjFromAst(ast, configPath);
let tokensKey = astConfig.properties.find((p) => p.type === "Property" && p.key.type === "Identifier" && p.key.name === "tokens");
if (!tokensKey) {
tokensKey = {
type: "Property",
key: {
type: "Identifier",
name: "tokens"
},
method: false,
computed: false,
shorthand: false,
value: {
type: "ArrayExpression",
elements: tokens.map((value) => ({
type: "Literal",
value,
raw: `'${value}'`
}))
}
};
astConfig.properties.unshift(tokensKey);
}
await fs.writeFile(configPath, generate(ast, SYNTAX_SETTINGS));
}
/**
* Add plugin imports
* note: this has the potential to duplicate plugins, but we tried our
* best to filter already, and this may be the user’s fault if they
* selected to install a plugin already installed. But also, this is
* easily-fixable, so let’s not waste too much time here (and possibly
* introduce bugs).
*/
async function updateConfigPlugins(configPath, plugins) {
const ast = parseModule(await fs.readFile(configPath, "utf8"));
ast.body.push(...plugins.map((p) => ({
type: "ImportDeclaration",
source: {
type: "Literal",
value: p.path
},
specifiers: [{
type: "ImportDefaultSpecifier",
local: {
type: "Identifier",
name: p.specifier
}
}],
attributes: []
})));
const astConfig = getConfigObjFromAst(ast, configPath);
const pluginsArray = astConfig.properties.find((property) => property.type === "Property" && property.key.type === "Identifier" && property.key.name === "plugins")?.value;
const pluginsAst = plugins.map((p) => ({
type: "CallExpression",
callee: {
type: "Identifier",
name: p.specifier
},
arguments: [],
optional: false
}));
if (pluginsArray) pluginsArray.elements.push(...pluginsAst);
else astConfig.properties.push({
type: "Property",
key: {
type: "Identifier",
name: "plugins"
},
value: {
type: "ArrayExpression",
elements: pluginsAst
},
kind: "init",
computed: false,
method: false,
shorthand: false
});
await fs.writeFile(configPath, generate(ast, SYNTAX_SETTINGS));
}
const EXAMPLE_TOKENS = {
color: {
$description: "Color tokens",
black: {
"100": {
$type: "color",
$value: {
colorSpace: "srgb",
components: [
.047,
.047,
.047
],
alpha: .05,
hex: "#0c0c0d"
}
},
"200": {
$type: "color",
$value: {
colorSpace: "srgb",
components: [
.047,
.047,
.047
],
alpha: .1,
hex: "#0c0c0d"
}
},
"300": {
$type: "color",
$value: {
colorSpace: "srgb",
components: [
.047,
.047,
.047
],
alpha: .2,
hex: "#0c0c0d"
}
},
"400": {
$type: "color",
$value: {
colorSpace: "srgb",
components: [
.047,
.047,
.047
],
alpha: .34,
hex: "#0c0c04"
}
},
"500": {
$type: "color",
$value: {
colorSpace: "srgb",
components: [
.047,
.047,
.047
],
alpha: .7,
hex: "#0c0c0d"
}
},
"600": {
$type: "color",
$value: {
colorSpace: "srgb",
components: [
.047,
.047,
.047
],
alpha: .8,
hex: "#0c0c0d"
}
},
"700": {
$type: "color",
$value: {
colorSpace: "srgb",
components: [
.047,
.047,
.047
],
alpha: .85,
hex: "#0c0c0d"
}
},
"800": {
$type: "color",
$value: {
colorSpace: "srgb",
components: [
.047,
.047,
.047
],
alpha: .9,
hex: "#0c0c0d"
}
},
"900": {
$type: "color",
$value: {
colorSpace: "srgb",
components: [
.047,
.047,
.047
],
alpha: .95,
hex: "#0c0c0d"
}
},
"1000": {
$type: "color",
$value: {
colorSpace: "srgb",
components: [
.047,
.047,
.047
],
alpha: 0,
hex: "#0c0c0d"
}
}
}
},
border: {
$description: "Border tokens",
default: {
type: "border",
$value: {
color: "{color.black.900}",
style: "solid",
width: {
value: 1,
unit: "px"
}
}
}
},
radius: {
$description: "Corner radius tokens",
"100": { $value: {
value: .25,
unit: "rem"
} }
},
space: {
$description: "Dimension tokens",
"100": { $value: {
value: .25,
unit: "rem"
} }
},
typography: {
$description: "Typography tokens",
body: {
$type: "typography",
$value: {
fontFamily: "{typography.family.sans}",
fontSize: "{typography.scale.03}",
fontWeight: "{typography.weight.regular}",
letterSpacing: {
value: 0,
unit: "em"
},
lineHeight: 1
}
}
}
};
//#endregion
//#region src/lab.ts
function serve(_options, _ready) {
throw new Error(`tz lab not available yet.`);
}
async function labCmd({ config, logger }) {
/** TODO: handle multiple files */
const [tokenFileUrl] = config.tokens;
const staticFiles = /* @__PURE__ */ new Set();
const dirEntries = await fs.readdir(fileURLToPath(import.meta.resolve("./lab")), {
withFileTypes: true,
recursive: true
});
for (const entry of dirEntries) {
if (entry.isFile() === false) continue;
const absolutePath = `${entry.parentPath.replaceAll("\\", "/")}/${entry.name}`;
staticFiles.add(absolutePath.replace(fileURLToPath(import.meta.resolve("./lab")).replaceAll("\\", "/"), ""));
}
const server = serve({
port: 9e3,
overrideGlobalObjects: false,
async fetch(request) {
const pathname = new URL(request.url).pathname;
if (pathname === "/") return new Response(Readable.toWeb(fsSync.createReadStream(fileURLToPath(import.meta.resolve("./lab/index.html")))), { headers: { "Content-Type": "text/html" } });
if (pathname === "/api/tokens") {
if (request.method === "GET") return new Response(Readable.toWeb(fsSync.createReadStream(tokenFileUrl)), { headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache"
} });
else if (request.method === "POST" && request.body) {
await request.body.pipeTo(Writable.toWeb(fsSync.createWriteStream(tokenFileUrl)));
return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" } });
}
}
if (staticFiles.has(pathname)) return new Response(Readable.toWeb(fsSync.createReadStream(fileURLToPath(import.meta.resolve(`./lab${pathname}`)))), { headers: { "Content-Type": mime.getType(pathname) ?? "application/octet-stream" } });
return new Response("Not found", { status: 404 });
}
}, (info) => {
logger.info({
group: "server",
message: `Token Lab running at http://${info.address === "::" ? "localhost" : info.address}:${info.port}`
});
});
/**
* The cli entrypoint is going to manually exit the process after labCmd returns.
*/
await new Promise((resolve, reject) => {
server.on("close", resolve);
server.on("error", reject);
});
}
//#endregion
//#region src/version.ts
function versionCmd() {
const { version } = JSON.parse(fsSync.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
console.log(version);
}
//#endregion
//#region src/index.ts
const require = createRequire(cwd);
function defineConfig(config) {
const normalizedConfig = { ...config };
if (typeof normalizedConfig.tokens === "string" || Array.isArray(normalizedConfig.tokens)) normalizedConfig.tokens = (Array.isArray(normalizedConfig.tokens) ? normalizedConfig.tokens : [normalizedConfig.tokens]).map((tokenPath) => {
if (tokenPath.startsWith(".") || /^(https?|file):\/\//i.test(tokenPath)) return tokenPath;
else if (fsSync.existsSync(new URL(tokenPath, cwd))) return new URL(tokenPath, cwd);
try {
return pathToFileURL(require.resolve(tokenPath));
} catch (err) {
console.error(err);
retu