@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/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 Build token artifacts from tokens.json
381
- --watch, -w Watch tokens.json for changes and recompile
382
- --no-lint Disable linters running on build
383
- check [path] Check tokens.json for errors and run linters
384
- lint [path] (alias of check)
385
- init Create a starter tokens.json file
386
- lab Manage your tokens with a web interface
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 Show this message
390
- --config, -c Path to config (default: ./terrazzo.config.ts)
391
- --quiet Suppress warnings
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