@terrazzo/cli 2.0.0-beta.3 → 2.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -1
- package/bin/cli.js +25 -4
- package/dist/index.d.ts +49 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +578 -13
- package/dist/index.js.map +1 -1
- package/dist/lab/assets/index-Bnka3dzO.js.map +1 -1
- package/lab.tsx +1 -0
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -9,10 +9,12 @@ import { ViteNodeRunner } from "vite-node/client";
|
|
|
9
9
|
import { ViteNodeServer } from "vite-node/server";
|
|
10
10
|
import chokidar from "chokidar";
|
|
11
11
|
import yamlToMomoa from "yaml-to-momoa";
|
|
12
|
-
import { spawn } from "node:child_process";
|
|
13
12
|
import fs$1 from "node:fs/promises";
|
|
14
|
-
import { confirm, intro, multiselect, outro, select, spinner } from "@clack/prompts";
|
|
15
13
|
import { isAlias, pluralize } from "@terrazzo/token-tools";
|
|
14
|
+
import { merge } from "merge-anything";
|
|
15
|
+
import { camelCase } from "scule";
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { confirm, intro, multiselect, outro, select, spinner } from "@clack/prompts";
|
|
16
18
|
import { detect } from "detect-package-manager";
|
|
17
19
|
import { generate } from "escodegen";
|
|
18
20
|
import { parseModule } from "meriyah";
|
|
@@ -377,21 +379,584 @@ async function checkCmd({ config, logger, positionals }) {
|
|
|
377
379
|
function helpCmd() {
|
|
378
380
|
console.log(`tz
|
|
379
381
|
[commands]
|
|
380
|
-
build
|
|
381
|
-
--watch, -w
|
|
382
|
-
--no-lint
|
|
383
|
-
check [path]
|
|
384
|
-
lint [path]
|
|
385
|
-
init
|
|
386
|
-
lab
|
|
382
|
+
build Build token artifacts from tokens.json
|
|
383
|
+
--watch, -w Watch tokens.json for changes and recompile
|
|
384
|
+
--no-lint Disable linters running on build
|
|
385
|
+
check [path] Check tokens.json for errors and run linters
|
|
386
|
+
lint [path] (alias of check)
|
|
387
|
+
init Create a starter tokens.json file
|
|
388
|
+
lab Manage your tokens with a web interface
|
|
389
|
+
import [path] Import from a Figma Design file
|
|
390
|
+
--o [file] Save imported JSON
|
|
391
|
+
--unpublished Include unpublished Variables
|
|
392
|
+
--skip-styles Don’t import styles
|
|
393
|
+
--skip-variables
|
|
394
|
+
Don’t import variables
|
|
387
395
|
|
|
388
396
|
[options]
|
|
389
|
-
--help
|
|
390
|
-
--config, -c
|
|
391
|
-
--quiet
|
|
397
|
+
--help Show this message
|
|
398
|
+
--config, -c Path to config (default: ./terrazzo.config.ts)
|
|
399
|
+
--quiet Suppress warnings
|
|
392
400
|
`);
|
|
393
401
|
}
|
|
394
402
|
|
|
403
|
+
//#endregion
|
|
404
|
+
//#region src/import/figma/lib.ts
|
|
405
|
+
const KEY = ":key";
|
|
406
|
+
const FILE_KEY = ":file_key";
|
|
407
|
+
const API = {
|
|
408
|
+
file: `https://api.figma.com/v1/files/${FILE_KEY}`,
|
|
409
|
+
fileNodes: `https://api.figma.com/v1/files/${FILE_KEY}/nodes`,
|
|
410
|
+
fileStyles: `https://api.figma.com/v1/files/${FILE_KEY}/styles`,
|
|
411
|
+
localVariables: `https://api.figma.com/v1/files/${FILE_KEY}/variables/local`,
|
|
412
|
+
publishedVariables: `https://api.figma.com/v1/files/${FILE_KEY}/variables/published`,
|
|
413
|
+
styles: `https://api.figma.com/v1/styles/${KEY}`
|
|
414
|
+
};
|
|
415
|
+
/** Wrapper around camelCase to handle more cases */
|
|
416
|
+
function formatName(name) {
|
|
417
|
+
return camelCase(name.replace(/\s+/g, "-"));
|
|
418
|
+
}
|
|
419
|
+
const nf = new Intl.NumberFormat("en-us");
|
|
420
|
+
/** Wrapper around camelCase to handle more cases */
|
|
421
|
+
function formatNumber(number) {
|
|
422
|
+
return nf.format(number);
|
|
423
|
+
}
|
|
424
|
+
/** Get File ID from design URL */
|
|
425
|
+
function getFileID(url) {
|
|
426
|
+
return url.match(/^https:\/\/(www\.)?figma\.com\/design\/([^/]+)/)?.[2];
|
|
427
|
+
}
|
|
428
|
+
/** /v1/files/:file_key */
|
|
429
|
+
async function getFile(fileKey, { logger }) {
|
|
430
|
+
const res = await fetch(API.file.replace(FILE_KEY, fileKey), {
|
|
431
|
+
method: "GET",
|
|
432
|
+
headers: { "X-Figma-Token": process.env.FIGMA_ACCESS_TOKEN }
|
|
433
|
+
});
|
|
434
|
+
if (!res.ok) logger.error({
|
|
435
|
+
group: "import",
|
|
436
|
+
message: `${res.status} ${await res.text()}`
|
|
437
|
+
});
|
|
438
|
+
return await res.json();
|
|
439
|
+
}
|
|
440
|
+
/** /v1/files/:file_key/nodes */
|
|
441
|
+
async function getFileNodes(fileKey, { ids, logger }) {
|
|
442
|
+
let url = API.fileNodes.replace(FILE_KEY, fileKey);
|
|
443
|
+
if (ids?.length) url += `?ids=${ids.join(",")}`;
|
|
444
|
+
const res = await fetch(url, {
|
|
445
|
+
method: "GET",
|
|
446
|
+
headers: { "X-Figma-Token": process.env.FIGMA_ACCESS_TOKEN }
|
|
447
|
+
});
|
|
448
|
+
if (!res.ok) logger.error({
|
|
449
|
+
group: "import",
|
|
450
|
+
message: `${res.status} ${await res.text()}`
|
|
451
|
+
});
|
|
452
|
+
return await res.json();
|
|
453
|
+
}
|
|
454
|
+
/** /v1/files/:file_key/styles */
|
|
455
|
+
async function getFileStyles(fileKey, { logger }) {
|
|
456
|
+
const res = await fetch(API.fileStyles.replace(FILE_KEY, fileKey), {
|
|
457
|
+
method: "GET",
|
|
458
|
+
headers: { "X-Figma-Token": process.env.FIGMA_ACCESS_TOKEN }
|
|
459
|
+
});
|
|
460
|
+
if (!res.ok) logger.error({
|
|
461
|
+
group: "import",
|
|
462
|
+
message: `${res.status} ${await res.text()}`
|
|
463
|
+
});
|
|
464
|
+
return await res.json();
|
|
465
|
+
}
|
|
466
|
+
/** /v1/files/:file_key/variables/local */
|
|
467
|
+
async function getFileLocalVariables(fileKey, { logger }) {
|
|
468
|
+
const res = await fetch(API.localVariables.replace(FILE_KEY, fileKey), {
|
|
469
|
+
method: "GET",
|
|
470
|
+
headers: { "X-Figma-Token": process.env.FIGMA_ACCESS_TOKEN }
|
|
471
|
+
});
|
|
472
|
+
if (!res.ok) logger.error({
|
|
473
|
+
group: "import",
|
|
474
|
+
message: `${res.status} ${await res.text}`
|
|
475
|
+
});
|
|
476
|
+
return await res.json();
|
|
477
|
+
}
|
|
478
|
+
/** /v1/files/:file_key/variables/published */
|
|
479
|
+
async function getFilePublishedVariables(fileKey, { logger }) {
|
|
480
|
+
const res = await fetch(API.publishedVariables.replace(FILE_KEY, fileKey), {
|
|
481
|
+
method: "GET",
|
|
482
|
+
headers: { "X-Figma-Token": process.env.FIGMA_ACCESS_TOKEN }
|
|
483
|
+
});
|
|
484
|
+
if (!res.ok) logger.error({
|
|
485
|
+
group: "import",
|
|
486
|
+
message: `${res.status} ${await res.text}`
|
|
487
|
+
});
|
|
488
|
+
return await res.json();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/import/figma/styles.ts
|
|
493
|
+
/** /v1/files/:file_key/styles */
|
|
494
|
+
async function getStyles(fileKey, { logger, unpublished }) {
|
|
495
|
+
const result = {
|
|
496
|
+
count: 0,
|
|
497
|
+
code: { sets: { styles: { sources: [{}] } } }
|
|
498
|
+
};
|
|
499
|
+
const styleNodeIDs = /* @__PURE__ */ new Set();
|
|
500
|
+
const stylesByID = /* @__PURE__ */ new Map();
|
|
501
|
+
if (unpublished) {
|
|
502
|
+
const styles = await getFile(fileKey, { logger });
|
|
503
|
+
for (const [id, style] of Object.entries(styles.styles)) {
|
|
504
|
+
styleNodeIDs.add(id);
|
|
505
|
+
stylesByID.set(id, style);
|
|
506
|
+
}
|
|
507
|
+
} else {
|
|
508
|
+
const styles = await getFileStyles(fileKey, { logger });
|
|
509
|
+
for (const style of styles.meta.styles) {
|
|
510
|
+
styleNodeIDs.add(style.node_id);
|
|
511
|
+
stylesByID.set(style.node_id, style);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const fileNodes = await getFileNodes(fileKey, {
|
|
515
|
+
ids: [...styleNodeIDs],
|
|
516
|
+
logger
|
|
517
|
+
});
|
|
518
|
+
result.count += styleNodeIDs.size;
|
|
519
|
+
for (const [id, s] of stylesByID) {
|
|
520
|
+
const styleNode = fileNodes.nodes[id];
|
|
521
|
+
if (!styleNode) {
|
|
522
|
+
logger.warn({
|
|
523
|
+
group: "import",
|
|
524
|
+
message: `Style ${s.name} not found in file nodes. Does it need to be published?`
|
|
525
|
+
});
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const styleType = "style_type" in s ? s.style_type : s.styleType;
|
|
529
|
+
const tokenBase = {
|
|
530
|
+
$type: void 0,
|
|
531
|
+
$description: s.description || void 0,
|
|
532
|
+
$value: void 0,
|
|
533
|
+
$extensions: { "figma.com": {
|
|
534
|
+
name: s.name,
|
|
535
|
+
node_id: id,
|
|
536
|
+
created_at: "created_at" in s ? s.created_at : void 0,
|
|
537
|
+
updated_at: "updated_at" in s ? s.updated_at : void 0
|
|
538
|
+
} }
|
|
539
|
+
};
|
|
540
|
+
switch (styleType) {
|
|
541
|
+
case "FILL": {
|
|
542
|
+
const $value = fillStyle(styleNode.document);
|
|
543
|
+
if (!$value) logger.error({
|
|
544
|
+
group: "import",
|
|
545
|
+
message: `Could not parse fill for ${s.name}`,
|
|
546
|
+
continueOnError: true
|
|
547
|
+
});
|
|
548
|
+
if (Array.isArray($value)) tokenBase.$type = "gradient";
|
|
549
|
+
else tokenBase.$type = "color";
|
|
550
|
+
tokenBase.$value = $value;
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
case "TEXT": {
|
|
554
|
+
const $value = textStyle(styleNode.document);
|
|
555
|
+
if (!$value) logger.error({
|
|
556
|
+
group: "import",
|
|
557
|
+
message: `Could not parse text for ${s.name}`,
|
|
558
|
+
continueOnError: true
|
|
559
|
+
});
|
|
560
|
+
tokenBase.$type = "typography";
|
|
561
|
+
tokenBase.$value = $value;
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
case "EFFECT": {
|
|
565
|
+
const $value = effectStyle(styleNode.document);
|
|
566
|
+
if (!$value) logger.error({
|
|
567
|
+
group: "import",
|
|
568
|
+
message: `Could not parse effect for ${s.name}`,
|
|
569
|
+
continueOnError: true
|
|
570
|
+
});
|
|
571
|
+
tokenBase.$type = "shadow";
|
|
572
|
+
tokenBase.$value = $value;
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
case "GRID": {
|
|
576
|
+
const layoutGrids = gridStyles(styleNode.document);
|
|
577
|
+
if (!layoutGrids) logger.error({
|
|
578
|
+
group: "import",
|
|
579
|
+
message: `Could not parse grid for ${s.name}`,
|
|
580
|
+
continueOnError: true
|
|
581
|
+
});
|
|
582
|
+
let node = result.code.sets.styles.sources[0];
|
|
583
|
+
const path = s.name.split("/").map(formatName);
|
|
584
|
+
const name = path.pop();
|
|
585
|
+
for (const key of path) {
|
|
586
|
+
if (!(key in node)) node[key] = {};
|
|
587
|
+
node = node[key];
|
|
588
|
+
}
|
|
589
|
+
node[name] = layoutGrids;
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (tokenBase.$type !== void 0) {
|
|
594
|
+
let node = result.code.sets.styles.sources[0];
|
|
595
|
+
const path = s.name.split("/").map(formatName);
|
|
596
|
+
const name = path.pop();
|
|
597
|
+
for (const key of path) {
|
|
598
|
+
if (!(key in node)) node[key] = {};
|
|
599
|
+
node = node[key];
|
|
600
|
+
}
|
|
601
|
+
node[name] = tokenBase;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return result;
|
|
605
|
+
}
|
|
606
|
+
/** Return a shadow token from an effect */
|
|
607
|
+
function effectStyle(node) {
|
|
608
|
+
if ("effects" in node) {
|
|
609
|
+
const shadows = node.effects.filter((e) => e.type === "DROP_SHADOW" || e.type === "INNER_SHADOW");
|
|
610
|
+
if (shadows.length) return shadows.map((s) => ({
|
|
611
|
+
inset: s.type === "INNER_SHADOW",
|
|
612
|
+
offsetX: {
|
|
613
|
+
value: s.offset.x,
|
|
614
|
+
unit: "px"
|
|
615
|
+
},
|
|
616
|
+
offsetY: {
|
|
617
|
+
value: s.offset.y,
|
|
618
|
+
unit: "px"
|
|
619
|
+
},
|
|
620
|
+
blur: {
|
|
621
|
+
value: s.radius,
|
|
622
|
+
unit: "px"
|
|
623
|
+
},
|
|
624
|
+
spread: {
|
|
625
|
+
value: s.spread ?? 0,
|
|
626
|
+
unit: "px"
|
|
627
|
+
},
|
|
628
|
+
color: {
|
|
629
|
+
colorSpace: "srgb",
|
|
630
|
+
components: [
|
|
631
|
+
s.color.r,
|
|
632
|
+
s.color.g,
|
|
633
|
+
s.color.b
|
|
634
|
+
],
|
|
635
|
+
alpha: s.color.a
|
|
636
|
+
}
|
|
637
|
+
}));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/** Return a color or gradient token from a fill */
|
|
641
|
+
function fillStyle(node) {
|
|
642
|
+
if ("fills" in node) for (const fill of node.fills) switch (fill.type) {
|
|
643
|
+
case "SOLID": return {
|
|
644
|
+
colorSpace: "srgb",
|
|
645
|
+
components: [
|
|
646
|
+
fill.color.r,
|
|
647
|
+
fill.color.g,
|
|
648
|
+
fill.color.b
|
|
649
|
+
],
|
|
650
|
+
alpha: fill.color.a
|
|
651
|
+
};
|
|
652
|
+
case "GRADIENT_LINEAR":
|
|
653
|
+
case "GRADIENT_RADIAL":
|
|
654
|
+
case "GRADIENT_ANGULAR":
|
|
655
|
+
case "GRADIENT_DIAMOND": return fill.gradientStops.map((stop) => ({
|
|
656
|
+
position: stop.position,
|
|
657
|
+
color: {
|
|
658
|
+
colorSpace: "srgb",
|
|
659
|
+
components: [
|
|
660
|
+
stop.color.r,
|
|
661
|
+
stop.color.g,
|
|
662
|
+
stop.color.b
|
|
663
|
+
],
|
|
664
|
+
alpha: stop.color.a
|
|
665
|
+
}
|
|
666
|
+
}));
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/** Return a dimension token from grid */
|
|
670
|
+
function gridStyles(node) {
|
|
671
|
+
if (!("layoutGrids" in node) || !node.layoutGrids?.length) return;
|
|
672
|
+
const values = {};
|
|
673
|
+
for (const grid of node.layoutGrids) {
|
|
674
|
+
const pattern = grid.pattern.toLowerCase();
|
|
675
|
+
if (values[pattern]) continue;
|
|
676
|
+
values[pattern] = {
|
|
677
|
+
sectionSize: {
|
|
678
|
+
$type: "dimension",
|
|
679
|
+
$value: {
|
|
680
|
+
value: grid.sectionSize,
|
|
681
|
+
unit: "px"
|
|
682
|
+
}
|
|
683
|
+
},
|
|
684
|
+
gutterSize: {
|
|
685
|
+
$type: "dimension",
|
|
686
|
+
$value: {
|
|
687
|
+
value: grid.sectionSize,
|
|
688
|
+
unit: "px"
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
if (grid.count > 0) values[pattern].count = {
|
|
693
|
+
$type: "number",
|
|
694
|
+
$value: grid.count
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
return values;
|
|
698
|
+
}
|
|
699
|
+
/** Return a typography token from text */
|
|
700
|
+
function textStyle(node) {
|
|
701
|
+
if (!("style" in node)) return;
|
|
702
|
+
return {
|
|
703
|
+
fontFamily: [node.style.fontFamily],
|
|
704
|
+
fontWeight: node.style.fontWeight,
|
|
705
|
+
fontStyle: node.style.fontStyle,
|
|
706
|
+
fontSize: node.style.fontSize ? {
|
|
707
|
+
value: node.style.fontSize,
|
|
708
|
+
unit: "px"
|
|
709
|
+
} : {
|
|
710
|
+
value: 1,
|
|
711
|
+
unit: "em"
|
|
712
|
+
},
|
|
713
|
+
letterSpacing: {
|
|
714
|
+
value: node.style.letterSpacing ?? 0,
|
|
715
|
+
unit: "px"
|
|
716
|
+
},
|
|
717
|
+
lineHeight: "lineHeightPercentFontSize" in node.style ? node.style.lineHeightPercentFontSize : "lineHeightPx" in node.style ? {
|
|
718
|
+
value: node.style.lineHeightPx,
|
|
719
|
+
unit: "px"
|
|
720
|
+
} : 1
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
//#endregion
|
|
725
|
+
//#region src/import/figma/variables.ts
|
|
726
|
+
/** /v1/files/:file_key/variables/published | /v1/files/:file_key/variables/local */
|
|
727
|
+
async function getVariables(fileKey, { logger, unpublished, matchers }) {
|
|
728
|
+
const result = {
|
|
729
|
+
count: 0,
|
|
730
|
+
remoteCount: 0,
|
|
731
|
+
code: {
|
|
732
|
+
sets: {},
|
|
733
|
+
modifiers: {}
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
const allVariables = {};
|
|
737
|
+
const variableCollections = {};
|
|
738
|
+
let finalVariables = {};
|
|
739
|
+
const modeIDToName = {};
|
|
740
|
+
const local = await getFileLocalVariables(fileKey, { logger });
|
|
741
|
+
for (const id of Object.keys(local.meta.variables)) {
|
|
742
|
+
if (local.meta.variables[id].hiddenFromPublishing) continue;
|
|
743
|
+
allVariables[id] = local.meta.variables[id];
|
|
744
|
+
}
|
|
745
|
+
for (const id of Object.keys(local.meta.variableCollections)) {
|
|
746
|
+
variableCollections[id] = local.meta.variableCollections[id];
|
|
747
|
+
for (const mode of local.meta.variableCollections[id].modes) modeIDToName[mode.modeId] = formatName(mode.name);
|
|
748
|
+
}
|
|
749
|
+
if (unpublished) finalVariables = allVariables;
|
|
750
|
+
else {
|
|
751
|
+
const published = await getFilePublishedVariables(fileKey, { logger });
|
|
752
|
+
for (const id of Object.keys(published.meta.variables)) finalVariables[id] = allVariables[id];
|
|
753
|
+
}
|
|
754
|
+
const remoteIDs = /* @__PURE__ */ new Set();
|
|
755
|
+
for (const id of Object.keys(finalVariables)) {
|
|
756
|
+
const variable = finalVariables[id];
|
|
757
|
+
const collection = variableCollections[variable.variableCollectionId];
|
|
758
|
+
const collectionName = formatName(collection.name);
|
|
759
|
+
const hasMultipleModes = collection.modes.length > 1;
|
|
760
|
+
if (hasMultipleModes) {
|
|
761
|
+
if (!(collectionName in result.code.modifiers)) result.code.modifiers[collectionName] = {
|
|
762
|
+
contexts: Object.fromEntries(collection.modes.map((m) => [formatName(m.name), [{}]])),
|
|
763
|
+
default: modeIDToName[collection.defaultModeId]
|
|
764
|
+
};
|
|
765
|
+
} else if (!(collectionName in result.code.sets)) result.code.sets[collectionName] = { sources: [{}] };
|
|
766
|
+
const matches = matchers.fontFamily?.test(variable.name) && "fontFamily" || matchers.fontWeight?.test(variable.name) && "fontWeight" || matchers.number?.test(variable.name) && "number" || void 0;
|
|
767
|
+
for (const [modeID, value] of Object.entries(variable.valuesByMode)) {
|
|
768
|
+
const modeName = modeIDToName[modeID];
|
|
769
|
+
let node = result.code;
|
|
770
|
+
if (hasMultipleModes) {
|
|
771
|
+
if (!(modeName in result.code.modifiers[collectionName].contexts)) result.code.modifiers[collectionName].contexts[modeName] = [{}];
|
|
772
|
+
node = result.code.modifiers[collectionName].contexts[modeName][0];
|
|
773
|
+
} else node = result.code.sets[collectionName].sources[0];
|
|
774
|
+
const tokenBase = {
|
|
775
|
+
$type: void 0,
|
|
776
|
+
$description: variable.description || void 0,
|
|
777
|
+
$value: void 0,
|
|
778
|
+
$extensions: { "figma.com": {
|
|
779
|
+
name: variable.name,
|
|
780
|
+
id: variable.id,
|
|
781
|
+
variableCollectionId: variable.variableCollectionId,
|
|
782
|
+
codeSyntax: Object.keys(variable.codeSyntax).length ? variable.codeSyntax : void 0
|
|
783
|
+
} }
|
|
784
|
+
};
|
|
785
|
+
const isAliasOfID = typeof value === "object" && "type" in value && value.type === "VARIABLE_ALIAS" && value.id || void 0;
|
|
786
|
+
if (isAliasOfID) if (allVariables[isAliasOfID]) {
|
|
787
|
+
tokenBase.$type = matches || {
|
|
788
|
+
COLOR: "color",
|
|
789
|
+
BOOLEAN: "boolean",
|
|
790
|
+
STRING: "string",
|
|
791
|
+
FLOAT: "dimension"
|
|
792
|
+
}[variable.resolvedType];
|
|
793
|
+
tokenBase.$value = `{${allVariables[isAliasOfID].name.split("/").map(formatName).join(".")}}`;
|
|
794
|
+
} else {
|
|
795
|
+
remoteIDs.add(isAliasOfID);
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
else if (matches === "fontFamily") {
|
|
799
|
+
tokenBase.$type = "fontFamily";
|
|
800
|
+
tokenBase.$value = String(value).split(",");
|
|
801
|
+
} else if (matches === "fontWeight") {
|
|
802
|
+
tokenBase.$type = "fontWeight";
|
|
803
|
+
tokenBase.$value = value;
|
|
804
|
+
} else if (matches === "number") {
|
|
805
|
+
if (typeof value === "object") throw new Error(`Can’t coerce ${variable.name} into number type.`);
|
|
806
|
+
tokenBase.$type = "number";
|
|
807
|
+
tokenBase.$value = Number(value);
|
|
808
|
+
} else switch (variable.resolvedType) {
|
|
809
|
+
case "BOOLEAN":
|
|
810
|
+
case "STRING":
|
|
811
|
+
tokenBase.$type = variable.resolvedType.toLowerCase();
|
|
812
|
+
tokenBase.$value = value;
|
|
813
|
+
break;
|
|
814
|
+
case "FLOAT":
|
|
815
|
+
tokenBase.$type = "dimension";
|
|
816
|
+
tokenBase.$value = {
|
|
817
|
+
value,
|
|
818
|
+
unit: "px"
|
|
819
|
+
};
|
|
820
|
+
break;
|
|
821
|
+
case "COLOR": {
|
|
822
|
+
const { r, g, b, a } = value;
|
|
823
|
+
tokenBase.$type = "color";
|
|
824
|
+
tokenBase.$value = {
|
|
825
|
+
colorSpace: "srgb",
|
|
826
|
+
components: [
|
|
827
|
+
r,
|
|
828
|
+
g,
|
|
829
|
+
b
|
|
830
|
+
],
|
|
831
|
+
alpha: a
|
|
832
|
+
};
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (tokenBase.$value !== void 0) {
|
|
837
|
+
const path = variable.name.split("/").map(formatName);
|
|
838
|
+
const name = path.pop();
|
|
839
|
+
for (const key of path) {
|
|
840
|
+
if (!(key in node)) node[key] = {};
|
|
841
|
+
node = node[key];
|
|
842
|
+
}
|
|
843
|
+
node[name] = tokenBase;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
result.count = Object.keys(finalVariables).length;
|
|
848
|
+
result.remoteCount = remoteIDs.size;
|
|
849
|
+
return result;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
//#endregion
|
|
853
|
+
//#region src/import/figma/index.ts
|
|
854
|
+
async function importFromFigma({ url, logger, unpublished, skipStyles, skipVariables, fontFamilyNames, fontWeightNames, numberNames }) {
|
|
855
|
+
const fileKey = getFileID(url);
|
|
856
|
+
if (!fileKey) logger.error({
|
|
857
|
+
group: "import",
|
|
858
|
+
message: `Invalid Figma URL: ${url}`
|
|
859
|
+
});
|
|
860
|
+
const result = {
|
|
861
|
+
variableCount: 0,
|
|
862
|
+
styleCount: 0,
|
|
863
|
+
code: {
|
|
864
|
+
$schema: "https://www.designtokens.org/schemas/2025.10/resolver.json",
|
|
865
|
+
version: "2025.10",
|
|
866
|
+
resolutionOrder: [],
|
|
867
|
+
sets: {},
|
|
868
|
+
modifiers: {}
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
try {
|
|
872
|
+
const [styles, vars] = await Promise.all([!skipStyles ? getStyles(fileKey, { logger }) : null, !skipVariables ? getVariables(fileKey, {
|
|
873
|
+
logger,
|
|
874
|
+
unpublished,
|
|
875
|
+
matchers: {
|
|
876
|
+
fontFamily: new RegExp(fontFamilyNames || "/fontFamily$"),
|
|
877
|
+
fontWeight: new RegExp(fontWeightNames || "/fontWeight$"),
|
|
878
|
+
number: numberNames ? new RegExp(numberNames) : void 0
|
|
879
|
+
}
|
|
880
|
+
}) : null]);
|
|
881
|
+
if (styles) {
|
|
882
|
+
result.styleCount += styles.count;
|
|
883
|
+
result.code = merge(result.code, styles.code);
|
|
884
|
+
}
|
|
885
|
+
if (vars) {
|
|
886
|
+
result.variableCount += vars.count;
|
|
887
|
+
result.code = merge(result.code, vars.code);
|
|
888
|
+
if (vars.remoteCount) logger.warn({
|
|
889
|
+
group: "import",
|
|
890
|
+
message: `${formatNumber(vars.remoteCount)} ${pluralize(vars.remoteCount, "Variable", "Variables")} were remote and could not be accessed. Try importing from other files to grab them.`
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
} catch (err) {
|
|
894
|
+
logger.error({
|
|
895
|
+
group: "import",
|
|
896
|
+
message: err.message
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
for (const group of ["sets", "modifiers"]) for (const name of Object.keys(result.code[group])) result.code.resolutionOrder.push({ $ref: `#/${group}/${name}` });
|
|
900
|
+
return result;
|
|
901
|
+
}
|
|
902
|
+
/** Is this a valid URL, and one belonging to a Figma file? */
|
|
903
|
+
function isFigmaPath(url) {
|
|
904
|
+
try {
|
|
905
|
+
new URL(url);
|
|
906
|
+
return /^https:\/\/(www\.)?figma\.com\/design\/[A-Za-z0-9]+/.test(url);
|
|
907
|
+
} catch {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
//#endregion
|
|
913
|
+
//#region src/import/index.ts
|
|
914
|
+
async function importCmd({ flags, positionals, logger }) {
|
|
915
|
+
const [_cmd, url] = positionals;
|
|
916
|
+
if (!url) logger.error({
|
|
917
|
+
group: "import",
|
|
918
|
+
message: "Missing import path. Expected `tz import [file]`."
|
|
919
|
+
});
|
|
920
|
+
if (isFigmaPath(url)) {
|
|
921
|
+
const { FIGMA_ACCESS_TOKEN } = process.env;
|
|
922
|
+
if (!FIGMA_ACCESS_TOKEN) logger.error({
|
|
923
|
+
group: "import",
|
|
924
|
+
message: `FIGMA_ACCESS_TOKEN not set! See https://terrazzo.app/docs/guides/import-from-figma`
|
|
925
|
+
});
|
|
926
|
+
const start = performance.now();
|
|
927
|
+
const result = await importFromFigma({
|
|
928
|
+
url,
|
|
929
|
+
logger,
|
|
930
|
+
unpublished: flags.unpublished,
|
|
931
|
+
skipStyles: flags["skip-styles"],
|
|
932
|
+
skipVariables: flags["skip-variables"],
|
|
933
|
+
fontFamilyNames: flags["font-family-names"],
|
|
934
|
+
fontWeightNames: flags["font-weight-names"],
|
|
935
|
+
numberNames: flags["number-names"]
|
|
936
|
+
});
|
|
937
|
+
const end = performance.now() - start;
|
|
938
|
+
if (flags.output) {
|
|
939
|
+
const oldFile = fs.existsSync(flags.output) ? JSON.parse(await fs$1.readFile(flags.output, "utf8")) : {};
|
|
940
|
+
const code = {
|
|
941
|
+
$schema: result.code.$schema,
|
|
942
|
+
version: result.code.version,
|
|
943
|
+
resolutionOrder: oldFile.resolutionOrder?.length ? oldFile.resolutionOrder : result.code.resolutionOrder,
|
|
944
|
+
sets: result.code.sets,
|
|
945
|
+
modifiers: result.code.modifiers,
|
|
946
|
+
$defs: oldFile.$defs,
|
|
947
|
+
$extensions: oldFile.$extensions
|
|
948
|
+
};
|
|
949
|
+
await fs$1.writeFile(flags.output, `${JSON.stringify(code, void 0, 2)}\n`);
|
|
950
|
+
logger.info({
|
|
951
|
+
group: "import",
|
|
952
|
+
message: `Imported ${formatNumber(result.variableCount)} ${pluralize(result.variableCount, "Variable", "Variables")}, ${formatNumber(result.styleCount)} ${pluralize(result.styleCount, "Style", "Styles")} → ${flags.output}`,
|
|
953
|
+
timing: end
|
|
954
|
+
});
|
|
955
|
+
} else process.stdout.write(JSON.stringify(result.code));
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
395
960
|
//#endregion
|
|
396
961
|
//#region src/init.ts
|
|
397
962
|
const INSTALL_COMMAND = {
|
|
@@ -1035,5 +1600,5 @@ function defineConfig(config) {
|
|
|
1035
1600
|
}
|
|
1036
1601
|
|
|
1037
1602
|
//#endregion
|
|
1038
|
-
export { DEFAULT_CONFIG_PATH, DEFAULT_TOKENS_PATH, GREEN_CHECK, buildCmd, checkCmd, cwd, defineConfig, helpCmd, initCmd, labCmd, loadConfig, loadTokens, normalizeCmd, printError, printSuccess, resolveConfig, resolveTokenPath, time, versionCmd, writeFiles };
|
|
1603
|
+
export { DEFAULT_CONFIG_PATH, DEFAULT_TOKENS_PATH, GREEN_CHECK, buildCmd, checkCmd, cwd, defineConfig, helpCmd, importCmd, importFromFigma, initCmd, isFigmaPath, labCmd, loadConfig, loadTokens, normalizeCmd, printError, printSuccess, resolveConfig, resolveTokenPath, time, versionCmd, writeFiles };
|
|
1039
1604
|
//# sourceMappingURL=index.js.map
|