spec-layer 0.3.0 → 0.4.0
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/README.md +47 -11
- package/dist/cli.js +1376 -28
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -588,6 +588,71 @@ var STATE_ORDER = [
|
|
|
588
588
|
];
|
|
589
589
|
var STATE_VOCAB = new Set(STATE_ORDER);
|
|
590
590
|
|
|
591
|
+
// ../extractor/src/v5/precision.ts
|
|
592
|
+
var SIGNIFICANT_DIGITS = 7;
|
|
593
|
+
var FLOAT32_EXACT_LIMIT = 16777216;
|
|
594
|
+
function canonicalNumber(n) {
|
|
595
|
+
if (!Number.isFinite(n)) return n;
|
|
596
|
+
if (Number.isInteger(n)) return n + 0;
|
|
597
|
+
if (Math.abs(n) >= FLOAT32_EXACT_LIMIT) return n + 0;
|
|
598
|
+
return Number(n.toPrecision(SIGNIFICANT_DIGITS)) + 0;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ../extractor/src/v5/diagnostics.ts
|
|
602
|
+
var DEFAULT_SEVERITY = {
|
|
603
|
+
UNRESOLVED_ALIAS: "error",
|
|
604
|
+
UNRESOLVED_EXTERNAL_ALIAS: "error",
|
|
605
|
+
ALIAS_CYCLE: "error",
|
|
606
|
+
ALIAS_TYPE_MISMATCH: "error",
|
|
607
|
+
MISSING_MODE_VALUE: "error",
|
|
608
|
+
DUPLICATE_SOURCE_ID: "error",
|
|
609
|
+
PATH_COLLISION: "error",
|
|
610
|
+
UNSUPPORTED_VALUE_TYPE: "error",
|
|
611
|
+
INCONSISTENT_VALUE_SHAPE: "error",
|
|
612
|
+
AMBIGUOUS_ALIAS_TARGET: "error",
|
|
613
|
+
INVALID_SOURCE_COLOR: "error",
|
|
614
|
+
// Error, and the same severity UNRESOLVED_ALIAS carries: §18 Level 2 makes
|
|
615
|
+
// no distinction between reference classes, and an artifact whose token
|
|
616
|
+
// points at a collection that is not there is broken in exactly the way a
|
|
617
|
+
// dangling alias is -- a consumer joining on the id gets nothing.
|
|
618
|
+
UNRESOLVED_REFERENCE: "error",
|
|
619
|
+
SOURCE_PARTIALLY_UNAVAILABLE: "error",
|
|
620
|
+
STYLE_BINDING_DRIFT: "warning",
|
|
621
|
+
CONFUSABLE_NAME: "warning",
|
|
622
|
+
INFERRED_LIFECYCLE: "warning",
|
|
623
|
+
DEPRECATED_REFERENCE: "warning",
|
|
624
|
+
GENERATED_NAME_COLLISION: "warning",
|
|
625
|
+
// Warning, not error: a migrated artifact with synthetic ids is still usable
|
|
626
|
+
// for generation -- what it cannot do is survive a rename, which is a fact
|
|
627
|
+
// about future diffs rather than about this artifact's correctness.
|
|
628
|
+
SYNTHETIC_IDENTITY: "warning",
|
|
629
|
+
// Warning, not error: the number is retained and usable, and Level 4
|
|
630
|
+
// readiness is a consumer's judgment. What the consumer must decide is the
|
|
631
|
+
// unit; the message says so and `units` overrides in the CLI are the
|
|
632
|
+
// remedy.
|
|
633
|
+
UNIT_METADATA_UNAVAILABLE: "warning",
|
|
634
|
+
MODE_VALUES_IDENTICAL: "info",
|
|
635
|
+
MISSING_DESCRIPTION: "info",
|
|
636
|
+
// Info, not error: no value depends on metadata the Plugin API never
|
|
637
|
+
// exposes in the first place -- there is nothing for a consumer to decide
|
|
638
|
+
// and nothing missing that this export could have captured.
|
|
639
|
+
METADATA_UNAVAILABLE: "info",
|
|
640
|
+
// Info, not error: a deliberately scoped export states its own scope in
|
|
641
|
+
// `completeness`; that is a fact about the request, not a defect in it.
|
|
642
|
+
EXPORT_SCOPED: "info"
|
|
643
|
+
};
|
|
644
|
+
var compareCodeUnits = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
645
|
+
function diagnostic(code, fields) {
|
|
646
|
+
return {
|
|
647
|
+
code,
|
|
648
|
+
severity: DEFAULT_SEVERITY[code],
|
|
649
|
+
entity_id: fields.entity_id,
|
|
650
|
+
...fields.mode_id !== void 0 ? { mode_id: fields.mode_id } : {},
|
|
651
|
+
message: fields.message,
|
|
652
|
+
...fields.details !== void 0 ? { details: fields.details } : {}
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
591
656
|
// ../extractor/src/hash.ts
|
|
592
657
|
var import_js_sha256 = __toESM(require_sha256(), 1);
|
|
593
658
|
|
|
@@ -733,9 +798,1241 @@ var FOUNDATION_SYSTEM_PROMPT = [
|
|
|
733
798
|
"No prose outside the JSON, no code fence."
|
|
734
799
|
].join("\n");
|
|
735
800
|
|
|
801
|
+
// ../extractor/src/v5/value.ts
|
|
802
|
+
var SUPPORTED_UNITS = ["px", "rem", "em", "%", "deg", "ms", "s"];
|
|
803
|
+
var SUPPORTED_TOKEN_TYPES = [
|
|
804
|
+
"color",
|
|
805
|
+
"dimension",
|
|
806
|
+
"number",
|
|
807
|
+
"string",
|
|
808
|
+
"boolean",
|
|
809
|
+
"duration",
|
|
810
|
+
"cubic_bezier",
|
|
811
|
+
"font_family"
|
|
812
|
+
];
|
|
813
|
+
var SUPPORTED_VALUE_KINDS = ["literal", "alias", "missing"];
|
|
814
|
+
var SUPPORTED_DURATION_UNITS = ["ms", "s"];
|
|
815
|
+
|
|
736
816
|
// ../extractor/src/v5/canonical.ts
|
|
737
817
|
var import_js_sha2562 = __toESM(require_sha256(), 1);
|
|
738
818
|
|
|
819
|
+
// ../extractor/src/v5/validate.ts
|
|
820
|
+
var ROOT = "<artifact>";
|
|
821
|
+
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
822
|
+
var isNonEmptyString = (v) => typeof v === "string" && v.length > 0;
|
|
823
|
+
var isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
824
|
+
var isStringArray = (v) => Array.isArray(v) && v.every((item) => typeof item === "string");
|
|
825
|
+
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
826
|
+
function shape(entityId, message, modeId) {
|
|
827
|
+
return diagnostic("INCONSISTENT_VALUE_SHAPE", {
|
|
828
|
+
entity_id: entityId,
|
|
829
|
+
message,
|
|
830
|
+
...modeId !== void 0 ? { mode_id: modeId } : {}
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
function unsupported(entityId, message, modeId) {
|
|
834
|
+
return diagnostic("UNSUPPORTED_VALUE_TYPE", {
|
|
835
|
+
entity_id: entityId,
|
|
836
|
+
message,
|
|
837
|
+
...modeId !== void 0 ? { mode_id: modeId } : {}
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
function validateTypedValue(type, tv, entityId, out, modeId) {
|
|
841
|
+
switch (type) {
|
|
842
|
+
case "color": {
|
|
843
|
+
if (tv.color_space !== "srgb") {
|
|
844
|
+
out.push(unsupported(entityId, 'color.color_space must be "srgb".', modeId));
|
|
845
|
+
}
|
|
846
|
+
if (typeof tv.hex !== "string" || !HEX_RE.test(tv.hex)) {
|
|
847
|
+
out.push(unsupported(entityId, 'color.hex must be six lowercase hex digits with a leading "#".', modeId));
|
|
848
|
+
}
|
|
849
|
+
if (!isFiniteNumber(tv.alpha) || tv.alpha < 0 || tv.alpha > 1) {
|
|
850
|
+
out.push(unsupported(entityId, "color.alpha must be a finite number between 0 and 1.", modeId));
|
|
851
|
+
}
|
|
852
|
+
if (tv.channels !== void 0) {
|
|
853
|
+
const channels = tv.channels;
|
|
854
|
+
if (!Array.isArray(channels) || channels.length !== 3 || !channels.every(isFiniteNumber)) {
|
|
855
|
+
out.push(unsupported(entityId, "color.channels must be three finite numbers when present.", modeId));
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
case "dimension": {
|
|
861
|
+
if (!isFiniteNumber(tv.number)) {
|
|
862
|
+
out.push(unsupported(entityId, "dimension.number must be a finite number.", modeId));
|
|
863
|
+
}
|
|
864
|
+
if (typeof tv.unit !== "string" || !SUPPORTED_UNITS.includes(tv.unit)) {
|
|
865
|
+
out.push(unsupported(entityId, "dimension.unit is missing or outside the supported unit vocabulary.", modeId));
|
|
866
|
+
}
|
|
867
|
+
break;
|
|
868
|
+
}
|
|
869
|
+
case "number": {
|
|
870
|
+
if (!isFiniteNumber(tv.value)) {
|
|
871
|
+
out.push(unsupported(entityId, "number.value must be a finite number.", modeId));
|
|
872
|
+
}
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
case "string": {
|
|
876
|
+
if (typeof tv.value !== "string") {
|
|
877
|
+
out.push(unsupported(entityId, "string.value must be a string.", modeId));
|
|
878
|
+
}
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
case "boolean": {
|
|
882
|
+
if (typeof tv.value !== "boolean") {
|
|
883
|
+
out.push(unsupported(entityId, "boolean.value must be a boolean.", modeId));
|
|
884
|
+
}
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
case "duration": {
|
|
888
|
+
if (!isFiniteNumber(tv.number)) {
|
|
889
|
+
out.push(unsupported(entityId, "duration.number must be a finite number.", modeId));
|
|
890
|
+
}
|
|
891
|
+
if (typeof tv.unit !== "string" || !SUPPORTED_DURATION_UNITS.includes(tv.unit)) {
|
|
892
|
+
out.push(unsupported(entityId, 'duration.unit must be "ms" or "s".', modeId));
|
|
893
|
+
}
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
case "cubic_bezier": {
|
|
897
|
+
const value = tv.value;
|
|
898
|
+
if (!Array.isArray(value) || value.length !== 4 || !value.every(isFiniteNumber)) {
|
|
899
|
+
out.push(unsupported(entityId, "cubic_bezier.value must be four finite numbers.", modeId));
|
|
900
|
+
}
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
case "font_family": {
|
|
904
|
+
if (typeof tv.value !== "string") {
|
|
905
|
+
out.push(unsupported(entityId, "font_family.value must be a string.", modeId));
|
|
906
|
+
}
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
default:
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function validateTypedValueEnvelope(value, entityId, out, modeId, expectedType) {
|
|
914
|
+
if (!isRecord(value)) {
|
|
915
|
+
out.push(shape(entityId, "A typed value must be an object.", modeId));
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const { type } = value;
|
|
919
|
+
if (typeof type !== "string" || !SUPPORTED_TOKEN_TYPES.includes(type)) {
|
|
920
|
+
out.push(shape(entityId, `Typed value has an unrecognized or missing "type" discriminant: ${JSON.stringify(type)}.`, modeId));
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (expectedType !== void 0 && type !== expectedType) {
|
|
924
|
+
out.push(shape(
|
|
925
|
+
entityId,
|
|
926
|
+
`Expected a "${expectedType}" typed value, but the value declares "${type}".`,
|
|
927
|
+
modeId
|
|
928
|
+
));
|
|
929
|
+
}
|
|
930
|
+
validateTypedValue(type, value, entityId, out, modeId);
|
|
931
|
+
}
|
|
932
|
+
function validateChainStep(step, entityId, out, modeId) {
|
|
933
|
+
if (!isRecord(step) || typeof step.token_id !== "string" || typeof step.mode_id !== "string") {
|
|
934
|
+
out.push(shape(entityId, "A resolution chain step must carry both token_id and mode_id.", modeId));
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
function validateAliasReference(ref, entityId, out, modeId) {
|
|
938
|
+
if (!(ref.target_id === null || typeof ref.target_id === "string")) {
|
|
939
|
+
out.push(shape(entityId, "alias.reference.target_id must be a string or null.", modeId));
|
|
940
|
+
}
|
|
941
|
+
if (!(ref.target_collection_id === null || typeof ref.target_collection_id === "string")) {
|
|
942
|
+
out.push(shape(entityId, "alias.reference.target_collection_id must be a string or null.", modeId));
|
|
943
|
+
}
|
|
944
|
+
if (!isStringArray(ref.target_path)) {
|
|
945
|
+
out.push(shape(entityId, "alias.reference.target_path must be an array of strings.", modeId));
|
|
946
|
+
}
|
|
947
|
+
if (typeof ref.external !== "boolean") {
|
|
948
|
+
out.push(shape(entityId, "alias.reference.external must be a boolean.", modeId));
|
|
949
|
+
}
|
|
950
|
+
if (ref.source_library_name !== void 0 && typeof ref.source_library_name !== "string") {
|
|
951
|
+
out.push(shape(entityId, "alias.reference.source_library_name must be a string when present.", modeId));
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
function validateAliasResolution(resolved2, entityId, out, modeId, expectedType) {
|
|
955
|
+
const { status } = resolved2;
|
|
956
|
+
if (status === "resolved") {
|
|
957
|
+
validateTypedValueEnvelope(resolved2.value, entityId, out, modeId, expectedType);
|
|
958
|
+
} else if (status === "unresolved") {
|
|
959
|
+
if (!isNonEmptyString(resolved2.reason)) {
|
|
960
|
+
out.push(shape(entityId, "An unresolved alias must carry a non-empty reason.", modeId));
|
|
961
|
+
}
|
|
962
|
+
if (resolved2.value !== null) {
|
|
963
|
+
out.push(shape(entityId, "An unresolved alias must carry a null value.", modeId));
|
|
964
|
+
}
|
|
965
|
+
} else {
|
|
966
|
+
out.push(shape(entityId, `alias.resolved.status has an unrecognized value: ${JSON.stringify(status)}.`, modeId));
|
|
967
|
+
}
|
|
968
|
+
if (!Array.isArray(resolved2.chain)) {
|
|
969
|
+
out.push(shape(entityId, "alias.resolved.chain must be an array.", modeId));
|
|
970
|
+
} else {
|
|
971
|
+
for (const step of resolved2.chain) validateChainStep(step, entityId, out, modeId);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function validateValue(value, entityId, modeId, out, expectedType) {
|
|
975
|
+
if (!isRecord(value)) {
|
|
976
|
+
out.push(shape(entityId, "A token value must be an object, not a bare primitive.", modeId));
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const { kind } = value;
|
|
980
|
+
if (typeof kind !== "string" || !SUPPORTED_VALUE_KINDS.includes(kind)) {
|
|
981
|
+
out.push(shape(entityId, `Value has an unrecognized or missing "kind" discriminant: ${JSON.stringify(kind)}.`, modeId));
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (kind === "literal") {
|
|
985
|
+
validateTypedValueEnvelope(value.value, entityId, out, modeId, expectedType);
|
|
986
|
+
} else if (kind === "alias") {
|
|
987
|
+
if (!isRecord(value.reference)) {
|
|
988
|
+
out.push(shape(entityId, "An alias value must carry a reference object.", modeId));
|
|
989
|
+
} else {
|
|
990
|
+
validateAliasReference(value.reference, entityId, out, modeId);
|
|
991
|
+
}
|
|
992
|
+
if (!isRecord(value.resolved)) {
|
|
993
|
+
out.push(shape(entityId, "An alias value must carry a resolved object.", modeId));
|
|
994
|
+
} else {
|
|
995
|
+
validateAliasResolution(value.resolved, entityId, out, modeId, expectedType);
|
|
996
|
+
}
|
|
997
|
+
} else if (kind === "missing") {
|
|
998
|
+
if (!isNonEmptyString(value.reason)) {
|
|
999
|
+
out.push(shape(entityId, "A missing value must carry a non-empty reason.", modeId));
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
function validateIdentity(entity, entityId, kind, out) {
|
|
1004
|
+
if (!isNonEmptyString(entity.id)) {
|
|
1005
|
+
out.push(shape(entityId, `${kind}.id must be a non-empty string.`));
|
|
1006
|
+
}
|
|
1007
|
+
if (typeof entity.name !== "string") {
|
|
1008
|
+
out.push(shape(entityId, `${kind}.name must be a string.`));
|
|
1009
|
+
}
|
|
1010
|
+
if (!isStringArray(entity.path) || entity.path.length === 0) {
|
|
1011
|
+
out.push(shape(entityId, `${kind}.path must be a non-empty array of strings.`));
|
|
1012
|
+
}
|
|
1013
|
+
if (entity.suggested_code_name !== void 0 && typeof entity.suggested_code_name !== "string") {
|
|
1014
|
+
out.push(shape(entityId, `${kind}.suggested_code_name must be a string when present.`));
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function validatePublication(value, entityId, out) {
|
|
1018
|
+
if (!isRecord(value) || typeof value.published !== "boolean" || typeof value.hidden_from_publishing !== "boolean") {
|
|
1019
|
+
out.push(shape(
|
|
1020
|
+
entityId,
|
|
1021
|
+
"publication must state boolean published and hidden_from_publishing fields."
|
|
1022
|
+
));
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
function validateLifecycle(value, entityId, out) {
|
|
1026
|
+
if (!isRecord(value)) {
|
|
1027
|
+
out.push(shape(entityId, "lifecycle must be an object when present."));
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (!["active", "deprecated", "archived"].includes(value.status)) {
|
|
1031
|
+
out.push(shape(entityId, "lifecycle.status must be active, deprecated, or archived."));
|
|
1032
|
+
}
|
|
1033
|
+
if (!(value.replacement_id === null || typeof value.replacement_id === "string")) {
|
|
1034
|
+
out.push(shape(entityId, "lifecycle.replacement_id must be a string or null."));
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function validateSource(value, entityId, out) {
|
|
1038
|
+
if (!isRecord(value)) {
|
|
1039
|
+
out.push(shape(entityId, "source must be an object when present."));
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
if (typeof value.remote !== "boolean") {
|
|
1043
|
+
out.push(shape(entityId, "source.remote must be a boolean."));
|
|
1044
|
+
}
|
|
1045
|
+
for (const field of ["library_file_id", "library_name", "modified_at"]) {
|
|
1046
|
+
if (!(value[field] === null || typeof value[field] === "string")) {
|
|
1047
|
+
out.push(shape(entityId, `source.${field} must be a string or null.`));
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
function validateOptionalEntityMetadata(entity, entityId, out, includeSource = false, includeLifecycle = true) {
|
|
1052
|
+
if (entity.publication !== void 0) validatePublication(entity.publication, entityId, out);
|
|
1053
|
+
if (includeLifecycle && entity.lifecycle !== void 0) validateLifecycle(entity.lifecycle, entityId, out);
|
|
1054
|
+
if (includeSource && entity.source !== void 0) validateSource(entity.source, entityId, out);
|
|
1055
|
+
}
|
|
1056
|
+
function validateCollection(collection, index, out) {
|
|
1057
|
+
if (!isRecord(collection)) {
|
|
1058
|
+
out.push(shape(`collections[${index}]`, "A collection must be an object."));
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const entityId = isNonEmptyString(collection.id) ? collection.id : `collections[${index}]`;
|
|
1062
|
+
validateIdentity(collection, entityId, "collection", out);
|
|
1063
|
+
if (!isNonEmptyString(collection.default_mode_id)) {
|
|
1064
|
+
out.push(shape(entityId, "collection.default_mode_id must be a non-empty string."));
|
|
1065
|
+
}
|
|
1066
|
+
if (!Array.isArray(collection.modes)) {
|
|
1067
|
+
out.push(shape(entityId, "collection.modes must be an array."));
|
|
1068
|
+
} else {
|
|
1069
|
+
collection.modes.forEach((mode, modeIndex) => {
|
|
1070
|
+
if (!isRecord(mode)) {
|
|
1071
|
+
out.push(shape(entityId, `collection.modes[${modeIndex}] must be an object.`));
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
if (!isNonEmptyString(mode.id)) {
|
|
1075
|
+
out.push(shape(entityId, `collection.modes[${modeIndex}].id must be a non-empty string.`));
|
|
1076
|
+
}
|
|
1077
|
+
if (typeof mode.name !== "string") {
|
|
1078
|
+
out.push(shape(entityId, `collection.modes[${modeIndex}].name must be a string.`));
|
|
1079
|
+
}
|
|
1080
|
+
if (!isFiniteNumber(mode.order)) {
|
|
1081
|
+
out.push(shape(entityId, `collection.modes[${modeIndex}].order must be a finite number.`));
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
validateOptionalEntityMetadata(collection, entityId, out, true, false);
|
|
1086
|
+
}
|
|
1087
|
+
function validateToken(token, index, out) {
|
|
1088
|
+
if (!isRecord(token)) {
|
|
1089
|
+
out.push(shape(`tokens[${index}]`, "A token must be an object."));
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
const entityId = isNonEmptyString(token.id) ? token.id : `tokens[${index}]`;
|
|
1093
|
+
if (!isNonEmptyString(token.id)) {
|
|
1094
|
+
out.push(shape(entityId, "token.id must be a non-empty string."));
|
|
1095
|
+
}
|
|
1096
|
+
if (typeof token.collection_id !== "string" || token.collection_id.length === 0) {
|
|
1097
|
+
out.push(shape(entityId, "token.collection_id must be a non-empty string."));
|
|
1098
|
+
}
|
|
1099
|
+
if (typeof token.name !== "string") {
|
|
1100
|
+
out.push(shape(entityId, "token.name must be a string."));
|
|
1101
|
+
}
|
|
1102
|
+
if (!isStringArray(token.path) || token.path.length === 0) {
|
|
1103
|
+
out.push(shape(entityId, "token.path must be a non-empty array of strings."));
|
|
1104
|
+
}
|
|
1105
|
+
if (token.suggested_code_name !== void 0 && typeof token.suggested_code_name !== "string") {
|
|
1106
|
+
out.push(shape(entityId, "token.suggested_code_name must be a string when present."));
|
|
1107
|
+
}
|
|
1108
|
+
if (token.code_syntax !== void 0) {
|
|
1109
|
+
if (!isRecord(token.code_syntax) || Object.values(token.code_syntax).some((v) => typeof v !== "string")) {
|
|
1110
|
+
out.push(shape(entityId, "token.code_syntax must be an object of strings when present."));
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
let tokenType;
|
|
1114
|
+
if (typeof token.type !== "string") {
|
|
1115
|
+
out.push(shape(entityId, "token.type must be a string."));
|
|
1116
|
+
} else if (!SUPPORTED_TOKEN_TYPES.includes(token.type)) {
|
|
1117
|
+
out.push(shape(entityId, `token.type is not a recognized token type: ${JSON.stringify(token.type)}.`));
|
|
1118
|
+
} else {
|
|
1119
|
+
tokenType = token.type;
|
|
1120
|
+
}
|
|
1121
|
+
if (typeof token.description !== "string") {
|
|
1122
|
+
out.push(shape(entityId, "token.description must be present and a string (an empty string is allowed)."));
|
|
1123
|
+
}
|
|
1124
|
+
if (!isStringArray(token.scopes)) {
|
|
1125
|
+
out.push(shape(entityId, "token.scopes must be an array of strings."));
|
|
1126
|
+
}
|
|
1127
|
+
validateOptionalEntityMetadata(token, entityId, out);
|
|
1128
|
+
if (!isRecord(token.values)) {
|
|
1129
|
+
out.push(shape(entityId, "token.values must be an object keyed by mode id."));
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
for (const [modeId, value] of Object.entries(token.values)) {
|
|
1133
|
+
validateValue(value, entityId, modeId, out, tokenType);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
function validateStyleProperty(property, entityId, propertyName, out) {
|
|
1137
|
+
if (!isRecord(property)) {
|
|
1138
|
+
out.push(shape(entityId, `typography.properties.${propertyName} must be an object.`));
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
const { source } = property;
|
|
1142
|
+
if (!isRecord(source)) {
|
|
1143
|
+
out.push(shape(entityId, `typography.properties.${propertyName}.source must be an object.`));
|
|
1144
|
+
} else if (source.kind === "alias") {
|
|
1145
|
+
if (!(source.target_id === null || typeof source.target_id === "string")) {
|
|
1146
|
+
out.push(shape(entityId, `typography.properties.${propertyName}.source.target_id must be a string or null.`));
|
|
1147
|
+
}
|
|
1148
|
+
if (!isStringArray(source.target_path)) {
|
|
1149
|
+
out.push(shape(entityId, `typography.properties.${propertyName}.source.target_path must be an array of strings.`));
|
|
1150
|
+
}
|
|
1151
|
+
} else if (source.kind !== "literal") {
|
|
1152
|
+
out.push(shape(entityId, `typography.properties.${propertyName}.source.kind must be literal or alias.`));
|
|
1153
|
+
}
|
|
1154
|
+
if (property.resolved !== null) {
|
|
1155
|
+
validateTypedValueEnvelope(property.resolved, entityId, out);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
var TYPOGRAPHY_STYLE_PROPERTIES = [
|
|
1159
|
+
"font_family",
|
|
1160
|
+
"font_weight",
|
|
1161
|
+
"font_size",
|
|
1162
|
+
"line_height",
|
|
1163
|
+
"letter_spacing",
|
|
1164
|
+
"paragraph_spacing",
|
|
1165
|
+
"paragraph_indent"
|
|
1166
|
+
];
|
|
1167
|
+
function validateTypographyStyle(style, index, out) {
|
|
1168
|
+
if (!isRecord(style)) {
|
|
1169
|
+
out.push(shape(`styles.typography[${index}]`, "A typography style must be an object."));
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const entityId = isNonEmptyString(style.id) ? style.id : `styles.typography[${index}]`;
|
|
1173
|
+
validateIdentity(style, entityId, "typography style", out);
|
|
1174
|
+
if (typeof style.description !== "string") {
|
|
1175
|
+
out.push(shape(entityId, "typography.description must be a string."));
|
|
1176
|
+
}
|
|
1177
|
+
if (!isRecord(style.properties)) {
|
|
1178
|
+
out.push(shape(entityId, "typography.properties must be an object."));
|
|
1179
|
+
} else {
|
|
1180
|
+
for (const propertyName of TYPOGRAPHY_STYLE_PROPERTIES) {
|
|
1181
|
+
validateStyleProperty(style.properties[propertyName], entityId, propertyName, out);
|
|
1182
|
+
}
|
|
1183
|
+
if (typeof style.properties.text_case !== "string") {
|
|
1184
|
+
out.push(shape(entityId, "typography.properties.text_case must be a string."));
|
|
1185
|
+
}
|
|
1186
|
+
if (typeof style.properties.text_decoration !== "string") {
|
|
1187
|
+
out.push(shape(entityId, "typography.properties.text_decoration must be a string."));
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
validateOptionalEntityMetadata(style, entityId, out, true);
|
|
1191
|
+
}
|
|
1192
|
+
var EFFECT_KINDS = ["drop_shadow", "inner_shadow", "layer_blur", "background_blur"];
|
|
1193
|
+
function validateEffect(effect, entityId, index, out) {
|
|
1194
|
+
if (!isRecord(effect)) {
|
|
1195
|
+
out.push(shape(entityId, `effects[${index}] must be an object.`));
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
if (!EFFECT_KINDS.includes(effect.type)) {
|
|
1199
|
+
out.push(shape(entityId, `effects[${index}].type is not a recognized effect kind.`));
|
|
1200
|
+
}
|
|
1201
|
+
if (typeof effect.visible !== "boolean") {
|
|
1202
|
+
out.push(shape(entityId, `effects[${index}].visible must be a boolean.`));
|
|
1203
|
+
}
|
|
1204
|
+
if (effect.blend_mode !== void 0 && typeof effect.blend_mode !== "string") {
|
|
1205
|
+
out.push(shape(entityId, `effects[${index}].blend_mode must be a string when present.`));
|
|
1206
|
+
}
|
|
1207
|
+
if (effect.color !== void 0) {
|
|
1208
|
+
validateTypedValueEnvelope(effect.color, entityId, out, void 0, "color");
|
|
1209
|
+
}
|
|
1210
|
+
for (const field of ["offset_x", "offset_y", "blur", "spread"]) {
|
|
1211
|
+
if (effect[field] !== void 0) {
|
|
1212
|
+
validateTypedValueEnvelope(effect[field], entityId, out, void 0, "dimension");
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (effect.show_behind_node !== void 0 && typeof effect.show_behind_node !== "boolean") {
|
|
1216
|
+
out.push(shape(entityId, `effects[${index}].show_behind_node must be a boolean when present.`));
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
function validateEffectStyle(style, index, out) {
|
|
1220
|
+
if (!isRecord(style)) {
|
|
1221
|
+
out.push(shape(`styles.effects[${index}]`, "An effect style must be an object."));
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const entityId = isNonEmptyString(style.id) ? style.id : `styles.effects[${index}]`;
|
|
1225
|
+
validateIdentity(style, entityId, "effect style", out);
|
|
1226
|
+
if (!(style.mode_id === null || typeof style.mode_id === "string")) {
|
|
1227
|
+
out.push(shape(entityId, "effect style.mode_id must be a string or null."));
|
|
1228
|
+
}
|
|
1229
|
+
if (!Array.isArray(style.effects)) {
|
|
1230
|
+
out.push(shape(entityId, "effect style.effects must be an array."));
|
|
1231
|
+
} else {
|
|
1232
|
+
style.effects.forEach((effect, effectIndex) => validateEffect(effect, entityId, effectIndex, out));
|
|
1233
|
+
}
|
|
1234
|
+
if (style.bindings !== void 0) {
|
|
1235
|
+
if (!Array.isArray(style.bindings)) {
|
|
1236
|
+
out.push(shape(entityId, "effect style.bindings must be an array when present."));
|
|
1237
|
+
} else {
|
|
1238
|
+
style.bindings.forEach((binding, bindingIndex) => {
|
|
1239
|
+
if (!isRecord(binding) || !isNonEmptyString(binding.property) || !isNonEmptyString(binding.token_id)) {
|
|
1240
|
+
out.push(shape(
|
|
1241
|
+
entityId,
|
|
1242
|
+
`effect style.bindings[${bindingIndex}] must carry non-empty property and token_id strings.`
|
|
1243
|
+
));
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
validateOptionalEntityMetadata(style, entityId, out, true);
|
|
1249
|
+
}
|
|
1250
|
+
var COMPLETENESS_VALUES = ["complete", "partial", "unavailable"];
|
|
1251
|
+
function validateRootSections(artifact, out) {
|
|
1252
|
+
if (!isRecord(artifact.spec_layer)) {
|
|
1253
|
+
out.push(shape(ROOT, "`spec_layer` must be an object."));
|
|
1254
|
+
}
|
|
1255
|
+
const completeness = artifact.completeness;
|
|
1256
|
+
if (!isRecord(completeness) || !COMPLETENESS_VALUES.includes(completeness.collections) || !COMPLETENESS_VALUES.includes(completeness.styles) || !isStringArray(completeness.unavailable_sources)) {
|
|
1257
|
+
out.push(shape(ROOT, "`completeness` must state collections/styles completeness and list unavailable sources."));
|
|
1258
|
+
}
|
|
1259
|
+
if (!Array.isArray(artifact.collections)) {
|
|
1260
|
+
out.push(shape(ROOT, "`collections` must be an array."));
|
|
1261
|
+
} else {
|
|
1262
|
+
artifact.collections.forEach((collection, index) => validateCollection(collection, index, out));
|
|
1263
|
+
}
|
|
1264
|
+
if (!isRecord(artifact.styles) || !Array.isArray(artifact.styles.typography) || !Array.isArray(artifact.styles.effects)) {
|
|
1265
|
+
out.push(shape(ROOT, "`styles` must be an object with `typography` and `effects` arrays."));
|
|
1266
|
+
} else {
|
|
1267
|
+
artifact.styles.typography.forEach((style, index) => validateTypographyStyle(style, index, out));
|
|
1268
|
+
artifact.styles.effects.forEach((style, index) => validateEffectStyle(style, index, out));
|
|
1269
|
+
}
|
|
1270
|
+
if (!Array.isArray(artifact.diagnostics)) {
|
|
1271
|
+
out.push(shape(ROOT, "`diagnostics` must be an array."));
|
|
1272
|
+
}
|
|
1273
|
+
if (!isRecord(artifact.statistics)) {
|
|
1274
|
+
out.push(shape(ROOT, "`statistics` must be an object."));
|
|
1275
|
+
}
|
|
1276
|
+
if (artifact.guidelines !== void 0) {
|
|
1277
|
+
const guidelines = artifact.guidelines;
|
|
1278
|
+
if (!isRecord(guidelines) || guidelines.origin !== "generated" || !isRecord(guidelines.group_descriptions)) {
|
|
1279
|
+
out.push(shape(
|
|
1280
|
+
ROOT,
|
|
1281
|
+
'`guidelines` must state origin "generated" and a group_descriptions object.'
|
|
1282
|
+
));
|
|
1283
|
+
} else {
|
|
1284
|
+
for (const [collectionName, folders] of Object.entries(guidelines.group_descriptions)) {
|
|
1285
|
+
if (!isRecord(folders) || Object.values(folders).some((description) => typeof description !== "string")) {
|
|
1286
|
+
out.push(shape(
|
|
1287
|
+
ROOT,
|
|
1288
|
+
`guidelines.group_descriptions[${JSON.stringify(collectionName)}] must map folder names to strings.`
|
|
1289
|
+
));
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
function validateLevel1(artifact) {
|
|
1296
|
+
try {
|
|
1297
|
+
const out = [];
|
|
1298
|
+
if (!isRecord(artifact)) {
|
|
1299
|
+
out.push(shape(ROOT, "The artifact root must be an object."));
|
|
1300
|
+
return out;
|
|
1301
|
+
}
|
|
1302
|
+
validateRootSections(artifact, out);
|
|
1303
|
+
if (!Array.isArray(artifact.tokens)) {
|
|
1304
|
+
out.push(shape(ROOT, "`tokens` must be an array."));
|
|
1305
|
+
} else {
|
|
1306
|
+
artifact.tokens.forEach((token, index) => validateToken(token, index, out));
|
|
1307
|
+
}
|
|
1308
|
+
return out;
|
|
1309
|
+
} catch (err) {
|
|
1310
|
+
const message = err instanceof Error && err.message ? `The artifact could not be read; accessing its properties threw: ${err.message}` : "The artifact could not be read; accessing its properties threw.";
|
|
1311
|
+
return [diagnostic("INCONSISTENT_VALUE_SHAPE", {
|
|
1312
|
+
entity_id: "artifact",
|
|
1313
|
+
message
|
|
1314
|
+
})];
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// ../extractor/src/v5/dtcg.ts
|
|
1319
|
+
function dtcgSegments(name) {
|
|
1320
|
+
const segments = [];
|
|
1321
|
+
const notes = [];
|
|
1322
|
+
for (const raw of name.split("/")) {
|
|
1323
|
+
const parts = raw.includes(".") ? raw.split(".") : [raw];
|
|
1324
|
+
if (parts.length > 1) notes.push({ code: "segment_split", original: raw });
|
|
1325
|
+
for (const part of parts) {
|
|
1326
|
+
let out = part;
|
|
1327
|
+
if (out === "") out = "_";
|
|
1328
|
+
if (/[{}]/.test(out)) out = out.replace(/[{}]/g, "_");
|
|
1329
|
+
if (out.startsWith("$")) out = `_${out}`;
|
|
1330
|
+
if (out !== part) notes.push({ code: "name_escaped", original: part });
|
|
1331
|
+
segments.push(out);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
return { segments, notes };
|
|
1335
|
+
}
|
|
1336
|
+
var slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed";
|
|
1337
|
+
var RESERVED_FILE_NAMES = [
|
|
1338
|
+
"styles.typography.json",
|
|
1339
|
+
"styles.effects.json",
|
|
1340
|
+
"resolver.json",
|
|
1341
|
+
"spec-layer.meta.json",
|
|
1342
|
+
"report.json"
|
|
1343
|
+
];
|
|
1344
|
+
function fileNameFor(collection, mode, taken) {
|
|
1345
|
+
const base = `${slug(collection.name)}.${slug(mode.name)}`;
|
|
1346
|
+
let candidate = `${base}.json`;
|
|
1347
|
+
let n = 1;
|
|
1348
|
+
while (taken.has(candidate)) {
|
|
1349
|
+
n += 1;
|
|
1350
|
+
candidate = `${base}-${n}.json`;
|
|
1351
|
+
}
|
|
1352
|
+
taken.add(candidate);
|
|
1353
|
+
return candidate;
|
|
1354
|
+
}
|
|
1355
|
+
function hexByte(n) {
|
|
1356
|
+
return Math.round(n * 255).toString(16).padStart(2, "0");
|
|
1357
|
+
}
|
|
1358
|
+
function colorComponents(color) {
|
|
1359
|
+
if (color.channels) return [color.channels[0], color.channels[1], color.channels[2]];
|
|
1360
|
+
const at = (i) => canonicalNumber(parseInt(color.hex.slice(i, i + 2), 16) / 255);
|
|
1361
|
+
return [at(1), at(3), at(5)];
|
|
1362
|
+
}
|
|
1363
|
+
function dtcgLiteral(value, scopes, style) {
|
|
1364
|
+
switch (value.type) {
|
|
1365
|
+
case "color": {
|
|
1366
|
+
if (style === "legacy") {
|
|
1367
|
+
const alpha = value.alpha === 1 ? "" : hexByte(value.alpha);
|
|
1368
|
+
return { $type: "color", $value: `${value.hex}${alpha}` };
|
|
1369
|
+
}
|
|
1370
|
+
return {
|
|
1371
|
+
$type: "color",
|
|
1372
|
+
$value: {
|
|
1373
|
+
colorSpace: "srgb",
|
|
1374
|
+
components: colorComponents(value),
|
|
1375
|
+
alpha: value.alpha,
|
|
1376
|
+
hex: value.hex
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
case "dimension": {
|
|
1381
|
+
if (value.unit !== "px" && value.unit !== "rem") {
|
|
1382
|
+
return { omit: "unit_not_expressible", details: { unit: value.unit, number: value.number } };
|
|
1383
|
+
}
|
|
1384
|
+
return {
|
|
1385
|
+
$type: "dimension",
|
|
1386
|
+
$value: style === "legacy" ? `${value.number}${value.unit}` : { value: value.number, unit: value.unit }
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
case "duration":
|
|
1390
|
+
return {
|
|
1391
|
+
$type: "duration",
|
|
1392
|
+
$value: style === "legacy" ? `${value.number}${value.unit}` : { value: value.number, unit: value.unit }
|
|
1393
|
+
};
|
|
1394
|
+
case "number":
|
|
1395
|
+
return { $type: scopes.includes("FONT_WEIGHT") ? "fontWeight" : "number", $value: value.value };
|
|
1396
|
+
case "cubic_bezier":
|
|
1397
|
+
return { $type: "cubicBezier", $value: [...value.value] };
|
|
1398
|
+
case "font_family":
|
|
1399
|
+
return { $type: "fontFamily", $value: value.value };
|
|
1400
|
+
case "string":
|
|
1401
|
+
case "boolean":
|
|
1402
|
+
return { omit: "type_not_expressible", details: { type: value.type } };
|
|
1403
|
+
default: {
|
|
1404
|
+
const exhaustive = value;
|
|
1405
|
+
return exhaustive;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
function setLeaf(tree, segments, leaf) {
|
|
1410
|
+
let node = tree;
|
|
1411
|
+
for (const seg of segments.slice(0, -1)) {
|
|
1412
|
+
const next = node[seg];
|
|
1413
|
+
if (typeof next !== "object" || next === null || Array.isArray(next)) node[seg] = {};
|
|
1414
|
+
node = node[seg];
|
|
1415
|
+
}
|
|
1416
|
+
node[segments[segments.length - 1]] = leaf;
|
|
1417
|
+
}
|
|
1418
|
+
var KEY_ORDER = ["$type", "$value", "$description", "$deprecated", "$extensions"];
|
|
1419
|
+
function sortTree(value) {
|
|
1420
|
+
if (Array.isArray(value)) return value.map(sortTree);
|
|
1421
|
+
if (typeof value !== "object" || value === null) return value;
|
|
1422
|
+
const rank = (k) => {
|
|
1423
|
+
const i = KEY_ORDER.indexOf(k);
|
|
1424
|
+
return i === -1 ? KEY_ORDER.length : i;
|
|
1425
|
+
};
|
|
1426
|
+
const keys = Object.keys(value).sort((a, b) => rank(a) - rank(b) || compareCodeUnits(a, b));
|
|
1427
|
+
return Object.fromEntries(keys.map((k) => [k, sortTree(value[k])]));
|
|
1428
|
+
}
|
|
1429
|
+
function reportOnce(p, entry2) {
|
|
1430
|
+
const key = JSON.stringify([entry2.code, entry2.path, entry2.mode ?? null, entry2.details]);
|
|
1431
|
+
if (p.reportKeys.has(key)) return;
|
|
1432
|
+
p.reportKeys.add(key);
|
|
1433
|
+
p.report.push(entry2);
|
|
1434
|
+
}
|
|
1435
|
+
function indexPaths(p) {
|
|
1436
|
+
const owners = /* @__PURE__ */ new Map();
|
|
1437
|
+
for (const token of p.artifact.tokens) {
|
|
1438
|
+
const collection = p.collectionById.get(token.collection_id);
|
|
1439
|
+
if (!collection) continue;
|
|
1440
|
+
const head = dtcgSegments(collection.name);
|
|
1441
|
+
const tail = dtcgSegments(token.name);
|
|
1442
|
+
const segments = [...head.segments, ...tail.segments];
|
|
1443
|
+
const path = segments.join(".");
|
|
1444
|
+
p.segmentsById.set(token.id, segments);
|
|
1445
|
+
for (const note of [...head.notes, ...tail.notes]) {
|
|
1446
|
+
reportOnce(p, {
|
|
1447
|
+
code: note.code,
|
|
1448
|
+
severity: note.code === "segment_split" ? "info" : "warning",
|
|
1449
|
+
path,
|
|
1450
|
+
message: note.code === "segment_split" ? `The segment "${note.original}" contains "." and was split into nested groups.` : `The segment "${note.original}" contains a character DTCG forbids and was escaped.`,
|
|
1451
|
+
details: { id: token.id, original: note.original }
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
owners.set(path, [...owners.get(path) ?? [], token]);
|
|
1455
|
+
}
|
|
1456
|
+
for (const [path, tokens] of owners) {
|
|
1457
|
+
if (tokens.length === 1) {
|
|
1458
|
+
p.pathById.set(tokens[0].id, path);
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
for (const token of tokens) {
|
|
1462
|
+
p.omittedIds.add(token.id);
|
|
1463
|
+
p.collidedIds.add(token.id);
|
|
1464
|
+
reportOnce(p, {
|
|
1465
|
+
code: "path_collision",
|
|
1466
|
+
severity: "error",
|
|
1467
|
+
path,
|
|
1468
|
+
message: `${tokens.length} tokens share this DTCG path after escaping; all were omitted.`,
|
|
1469
|
+
details: { id: token.id, ids: tokens.map((t) => t.id) }
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
function modeName(collection, modeId) {
|
|
1475
|
+
return collection.modes.find((m) => m.id === modeId)?.name ?? modeId;
|
|
1476
|
+
}
|
|
1477
|
+
function unitOverrideFor(p, token, collection) {
|
|
1478
|
+
const units = p.options.units;
|
|
1479
|
+
if (!units) return void 0;
|
|
1480
|
+
for (const key of Object.keys(units).sort(compareCodeUnits)) {
|
|
1481
|
+
const slash = key.indexOf("/");
|
|
1482
|
+
if (slash === -1 || key.slice(0, slash) !== collection.name) continue;
|
|
1483
|
+
const glob = key.slice(slash + 1);
|
|
1484
|
+
const escaped = glob.split("*").map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
|
|
1485
|
+
if (new RegExp(`^${escaped}$`).test(token.name)) return units[key];
|
|
1486
|
+
}
|
|
1487
|
+
return void 0;
|
|
1488
|
+
}
|
|
1489
|
+
var STATED_NUMBER_SCOPES = ["FONT_WEIGHT", "OPACITY"];
|
|
1490
|
+
function projectedLiteral(p, token, resolved2, onOverrideConflict) {
|
|
1491
|
+
const collection = p.collectionById.get(token.collection_id);
|
|
1492
|
+
const override = collection ? unitOverrideFor(p, token, collection) : void 0;
|
|
1493
|
+
let literal = resolved2;
|
|
1494
|
+
if (override !== void 0 && literal.type === "number") {
|
|
1495
|
+
if (token.scopes.some((s) => STATED_NUMBER_SCOPES.includes(s))) onOverrideConflict?.(override);
|
|
1496
|
+
else literal = { type: "dimension", number: literal.value, unit: override };
|
|
1497
|
+
}
|
|
1498
|
+
return dtcgLiteral(literal, token.scopes, p.options.values);
|
|
1499
|
+
}
|
|
1500
|
+
function aliasLeafType(p, token, chain, resolved2) {
|
|
1501
|
+
const terminal = chain.length > 0 ? p.tokenById.get(chain[chain.length - 1].token_id) : void 0;
|
|
1502
|
+
return projectedLiteral(p, terminal ?? token, resolved2);
|
|
1503
|
+
}
|
|
1504
|
+
function modeLabels(collection) {
|
|
1505
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1506
|
+
for (const m of collection.modes) counts.set(m.name, (counts.get(m.name) ?? 0) + 1);
|
|
1507
|
+
return new Map(collection.modes.map((m) => [m.id, counts.get(m.name) === 1 ? m.name : `${m.name} [${m.id}]`]));
|
|
1508
|
+
}
|
|
1509
|
+
function collectionLabels(collections) {
|
|
1510
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1511
|
+
for (const c of collections) counts.set(c.name, (counts.get(c.name) ?? 0) + 1);
|
|
1512
|
+
return new Map(collections.map(
|
|
1513
|
+
(c) => [c.id, counts.get(c.name) === 1 ? c.name : `${c.name} [${c.id}]`]
|
|
1514
|
+
));
|
|
1515
|
+
}
|
|
1516
|
+
function modeLabelOf(p, collection, modeId) {
|
|
1517
|
+
return p.modeLabelsById.get(collection.id)?.get(modeId) ?? modeId;
|
|
1518
|
+
}
|
|
1519
|
+
var collectionLabelOf = (p, collection) => p.collectionLabelById.get(collection.id) ?? collection.name;
|
|
1520
|
+
function reportCollectionNameCollisions(p) {
|
|
1521
|
+
const owners = /* @__PURE__ */ new Map();
|
|
1522
|
+
for (const collection of p.artifact.collections) {
|
|
1523
|
+
owners.set(collection.name, [...owners.get(collection.name) ?? [], collection]);
|
|
1524
|
+
}
|
|
1525
|
+
for (const [name, collections] of owners) {
|
|
1526
|
+
if (collections.length < 2) continue;
|
|
1527
|
+
for (const collection of collections) {
|
|
1528
|
+
reportOnce(p, {
|
|
1529
|
+
code: "collection_name_collision",
|
|
1530
|
+
severity: "warning",
|
|
1531
|
+
path: collectionLabelOf(p, collection),
|
|
1532
|
+
message: `${collections.length} collections are named "${name}"; the resolver labels each one by its id.`,
|
|
1533
|
+
details: { id: collection.id, ids: collections.map((c) => c.id) }
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
function asJson(value) {
|
|
1539
|
+
return JSON.parse(JSON.stringify(value));
|
|
1540
|
+
}
|
|
1541
|
+
function metaEntry(p, token, collection) {
|
|
1542
|
+
const labels = p.modeLabelsById.get(collection.id) ?? modeLabels(collection);
|
|
1543
|
+
const omitted = p.omittedIds.has(token.id);
|
|
1544
|
+
const plain = (v) => {
|
|
1545
|
+
if (v.kind === "literal" && (v.value.type === "boolean" || v.value.type === "string" || v.value.type === "number" || v.value.type === "font_family")) return v.value.value;
|
|
1546
|
+
return asJson(v);
|
|
1547
|
+
};
|
|
1548
|
+
return {
|
|
1549
|
+
id: token.id,
|
|
1550
|
+
collection_id: token.collection_id,
|
|
1551
|
+
type: token.type,
|
|
1552
|
+
scopes: [...token.scopes],
|
|
1553
|
+
...token.code_syntax ? { code_syntax: token.code_syntax } : {},
|
|
1554
|
+
...token.publication ? { publication: token.publication } : {},
|
|
1555
|
+
...omitted ? {
|
|
1556
|
+
omitted: true,
|
|
1557
|
+
values: Object.fromEntries(Object.entries(token.values).map(([modeId, v]) => [labels.get(modeId) ?? modeId, plain(v)]))
|
|
1558
|
+
} : {}
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
function reportDuplicateCodeSyntax(p) {
|
|
1562
|
+
const owners = /* @__PURE__ */ new Map();
|
|
1563
|
+
for (const token of p.artifact.tokens) {
|
|
1564
|
+
for (const [platform, identifier] of Object.entries(token.code_syntax ?? {})) {
|
|
1565
|
+
const key = JSON.stringify([platform, identifier]);
|
|
1566
|
+
owners.set(key, [...owners.get(key) ?? [], token]);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
for (const [key, tokens] of owners) {
|
|
1570
|
+
if (tokens.length < 2) continue;
|
|
1571
|
+
const [platform, identifier] = JSON.parse(key);
|
|
1572
|
+
for (const token of tokens) {
|
|
1573
|
+
reportOnce(p, {
|
|
1574
|
+
code: "duplicate_code_syntax",
|
|
1575
|
+
severity: "warning",
|
|
1576
|
+
path: p.pathById.get(token.id) ?? p.segmentsById.get(token.id)?.join(".") ?? token.name,
|
|
1577
|
+
message: `${tokens.length} tokens declare the ${platform} identifier "${identifier}".`,
|
|
1578
|
+
details: { id: token.id, platform, identifier, ids: tokens.map((t) => t.id) }
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
var SPEC_LAYER_EXT = "com.spec-layer";
|
|
1584
|
+
function reportBindingDropped(p, path, property, targetId) {
|
|
1585
|
+
reportOnce(p, {
|
|
1586
|
+
code: "binding_dropped",
|
|
1587
|
+
severity: "warning",
|
|
1588
|
+
path,
|
|
1589
|
+
message: `The ${property} property is bound to a token this export does not carry; the resolved literal is written instead.`,
|
|
1590
|
+
details: {
|
|
1591
|
+
property,
|
|
1592
|
+
target_id: targetId,
|
|
1593
|
+
reason: p.tokenIds.has(targetId) ? "target_omitted" : "target_unavailable"
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
function styleMember(p, property, scopes, path, name) {
|
|
1598
|
+
if (property.source.kind === "alias" && property.source.target_id !== null) {
|
|
1599
|
+
const targetId = property.source.target_id;
|
|
1600
|
+
const target = p.omittedIds.has(targetId) ? void 0 : p.pathById.get(targetId);
|
|
1601
|
+
if (target !== void 0) return { value: `{${target}}` };
|
|
1602
|
+
reportBindingDropped(p, path, name, targetId);
|
|
1603
|
+
}
|
|
1604
|
+
if (property.resolved === null) {
|
|
1605
|
+
reportOnce(p, {
|
|
1606
|
+
code: "value_omitted",
|
|
1607
|
+
severity: "warning",
|
|
1608
|
+
path,
|
|
1609
|
+
message: `The ${name} property has no resolved value and was omitted.`,
|
|
1610
|
+
details: { property: name, reason: "source_unavailable" }
|
|
1611
|
+
});
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
const converted = dtcgLiteral(property.resolved, scopes, p.options.values);
|
|
1615
|
+
if ("omit" in converted) {
|
|
1616
|
+
if (converted.omit === "unit_not_expressible") {
|
|
1617
|
+
reportOnce(p, {
|
|
1618
|
+
code: "unit_not_expressible",
|
|
1619
|
+
severity: "info",
|
|
1620
|
+
path,
|
|
1621
|
+
message: `The ${name} unit is not a DTCG dimension unit; the value is kept under $extensions.`,
|
|
1622
|
+
details: { property: name, ...converted.details }
|
|
1623
|
+
});
|
|
1624
|
+
const d = property.resolved;
|
|
1625
|
+
return { extension: { value: d.number, unit: d.unit } };
|
|
1626
|
+
}
|
|
1627
|
+
reportOnce(p, {
|
|
1628
|
+
code: "type_not_expressible",
|
|
1629
|
+
severity: "warning",
|
|
1630
|
+
path,
|
|
1631
|
+
message: `The ${name} property has a type DTCG cannot state and was omitted.`,
|
|
1632
|
+
details: { property: name, ...converted.details }
|
|
1633
|
+
});
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1636
|
+
return { value: converted.$value };
|
|
1637
|
+
}
|
|
1638
|
+
var TYPOGRAPHY_MEMBERS = [
|
|
1639
|
+
["font_family", "fontFamily", []],
|
|
1640
|
+
["font_size", "fontSize", []],
|
|
1641
|
+
["font_weight", "fontWeight", ["FONT_WEIGHT"]],
|
|
1642
|
+
["line_height", "lineHeight", []],
|
|
1643
|
+
["letter_spacing", "letterSpacing", []]
|
|
1644
|
+
];
|
|
1645
|
+
var TYPOGRAPHY_EXTENSION_MEMBERS = [
|
|
1646
|
+
["paragraph_spacing", "paragraphSpacing"],
|
|
1647
|
+
["paragraph_indent", "paragraphIndent"]
|
|
1648
|
+
];
|
|
1649
|
+
function typographyLeaf(p, style, path) {
|
|
1650
|
+
const value = {};
|
|
1651
|
+
const ext = {};
|
|
1652
|
+
for (const [key, name, scopes] of TYPOGRAPHY_MEMBERS) {
|
|
1653
|
+
const property = style.properties[key];
|
|
1654
|
+
if (key === "line_height" && property.resolved?.type === "dimension") {
|
|
1655
|
+
const boundTo = property.source.kind === "alias" ? property.source.target_id : null;
|
|
1656
|
+
let targetExported = false;
|
|
1657
|
+
if (property.resolved.unit === "%" && boundTo !== null) {
|
|
1658
|
+
targetExported = !p.omittedIds.has(boundTo) && p.pathById.get(boundTo) !== void 0;
|
|
1659
|
+
if (!targetExported) reportBindingDropped(p, path, name, boundTo);
|
|
1660
|
+
}
|
|
1661
|
+
if (property.resolved.unit === "%" && !targetExported) {
|
|
1662
|
+
value[name] = canonicalNumber(property.resolved.number / 100);
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
reportOnce(p, {
|
|
1666
|
+
code: "unit_not_expressible",
|
|
1667
|
+
severity: "info",
|
|
1668
|
+
path,
|
|
1669
|
+
message: "DTCG line height is a unitless multiplier of the font size; the measured value is kept under $extensions.",
|
|
1670
|
+
// The binding is replaced by a literal here, so the entry names the
|
|
1671
|
+
// target it stood for; without it a consumer cannot tell this value
|
|
1672
|
+
// was bound at all.
|
|
1673
|
+
details: {
|
|
1674
|
+
property: name,
|
|
1675
|
+
unit: property.resolved.unit,
|
|
1676
|
+
number: property.resolved.number,
|
|
1677
|
+
...boundTo !== null ? { target_id: boundTo } : {}
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
ext[name] = { value: property.resolved.number, unit: property.resolved.unit };
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
const member = styleMember(p, property, scopes, path, name);
|
|
1684
|
+
if (member === null) continue;
|
|
1685
|
+
if ("value" in member) value[name] = member.value;
|
|
1686
|
+
else ext[name] = member.extension;
|
|
1687
|
+
}
|
|
1688
|
+
for (const [key, name] of TYPOGRAPHY_EXTENSION_MEMBERS) {
|
|
1689
|
+
const member = styleMember(p, style.properties[key], [], path, name);
|
|
1690
|
+
if (member === null) continue;
|
|
1691
|
+
ext[name] = "value" in member ? member.value : member.extension;
|
|
1692
|
+
}
|
|
1693
|
+
ext.textCase = style.properties.text_case;
|
|
1694
|
+
ext.textDecoration = style.properties.text_decoration;
|
|
1695
|
+
return {
|
|
1696
|
+
$type: "typography",
|
|
1697
|
+
$value: value,
|
|
1698
|
+
...style.description.length > 0 ? { $description: style.description } : {},
|
|
1699
|
+
$extensions: { [SPEC_LAYER_EXT]: ext }
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
var SHADOW_FIELDS = [
|
|
1703
|
+
["color", "color"],
|
|
1704
|
+
["offset_x", "offsetX"],
|
|
1705
|
+
["offset_y", "offsetY"],
|
|
1706
|
+
["blur", "blur"],
|
|
1707
|
+
["spread", "spread"]
|
|
1708
|
+
];
|
|
1709
|
+
function effectLeaf(p, style, path) {
|
|
1710
|
+
const bindings = new Map((style.bindings ?? []).map((b) => [b.property, b.token_id]));
|
|
1711
|
+
const shadows = [];
|
|
1712
|
+
const layers = [];
|
|
1713
|
+
style.effects.forEach((effect, index) => {
|
|
1714
|
+
const isShadow = effect.type === "drop_shadow" || effect.type === "inner_shadow";
|
|
1715
|
+
const layer = { index, type: effect.type, visible: effect.visible };
|
|
1716
|
+
if (effect.blend_mode !== void 0) layer.blend_mode = effect.blend_mode;
|
|
1717
|
+
if (!isShadow && effect.blur) {
|
|
1718
|
+
const b = dtcgLiteral(effect.blur, [], p.options.values);
|
|
1719
|
+
if (!("omit" in b)) layer.blur = b.$value;
|
|
1720
|
+
}
|
|
1721
|
+
layers.push(layer);
|
|
1722
|
+
if (!isShadow || !effect.visible) return;
|
|
1723
|
+
const shadow = {};
|
|
1724
|
+
for (const [field, name] of SHADOW_FIELDS) {
|
|
1725
|
+
const boundId = bindings.get(`effects[${index}].${field}`);
|
|
1726
|
+
const boundPath = boundId !== void 0 && !p.omittedIds.has(boundId) ? p.pathById.get(boundId) : void 0;
|
|
1727
|
+
if (boundPath !== void 0) {
|
|
1728
|
+
shadow[name] = `{${boundPath}}`;
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
if (boundId !== void 0) {
|
|
1732
|
+
reportBindingDropped(p, path, `effects[${index}].${field}`, boundId);
|
|
1733
|
+
}
|
|
1734
|
+
const raw = effect[field];
|
|
1735
|
+
if (raw === void 0) continue;
|
|
1736
|
+
const converted = dtcgLiteral(raw, [], p.options.values);
|
|
1737
|
+
if (!("omit" in converted)) shadow[name] = converted.$value;
|
|
1738
|
+
}
|
|
1739
|
+
shadow.inset = effect.type === "inner_shadow";
|
|
1740
|
+
shadows.push(shadow);
|
|
1741
|
+
});
|
|
1742
|
+
if (shadows.length === 0) {
|
|
1743
|
+
reportOnce(p, {
|
|
1744
|
+
code: "effect_not_expressible",
|
|
1745
|
+
severity: "warning",
|
|
1746
|
+
path,
|
|
1747
|
+
message: "The style has no visible shadow; DTCG has no blur type, so it is kept only under $extensions.",
|
|
1748
|
+
details: { id: style.id }
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
return {
|
|
1752
|
+
$type: "shadow",
|
|
1753
|
+
$value: shadows,
|
|
1754
|
+
$extensions: { [SPEC_LAYER_EXT]: { layers } }
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
function styleFiles(p) {
|
|
1758
|
+
const files = {};
|
|
1759
|
+
const build = (styles, root, file, leafOf) => {
|
|
1760
|
+
if (styles.length === 0) return;
|
|
1761
|
+
const tree = {};
|
|
1762
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1763
|
+
for (const style of styles) {
|
|
1764
|
+
const segments = [root, ...dtcgSegments(style.name).segments];
|
|
1765
|
+
const path = segments.join(".");
|
|
1766
|
+
const other = seen.get(path);
|
|
1767
|
+
if (other !== void 0) {
|
|
1768
|
+
reportOnce(p, {
|
|
1769
|
+
code: "path_collision",
|
|
1770
|
+
severity: "error",
|
|
1771
|
+
path,
|
|
1772
|
+
message: "Two styles share this DTCG path after escaping; the later one was omitted.",
|
|
1773
|
+
details: { id: style.id, ids: [other, style.id] }
|
|
1774
|
+
});
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
seen.set(path, style.id);
|
|
1778
|
+
setLeaf(tree, segments, leafOf(style, path));
|
|
1779
|
+
}
|
|
1780
|
+
files[file] = sortTree(tree);
|
|
1781
|
+
};
|
|
1782
|
+
build(
|
|
1783
|
+
p.artifact.styles.typography,
|
|
1784
|
+
"Typography styles",
|
|
1785
|
+
"styles.typography.json",
|
|
1786
|
+
(s, path) => typographyLeaf(p, s, path)
|
|
1787
|
+
);
|
|
1788
|
+
build(
|
|
1789
|
+
p.artifact.styles.effects,
|
|
1790
|
+
"Effect styles",
|
|
1791
|
+
"styles.effects.json",
|
|
1792
|
+
(s, path) => effectLeaf(p, s, path)
|
|
1793
|
+
);
|
|
1794
|
+
return files;
|
|
1795
|
+
}
|
|
1796
|
+
var pointer = (s) => s.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
1797
|
+
var STYLE_ROOTS = {
|
|
1798
|
+
"styles.typography.json": "Typography styles",
|
|
1799
|
+
"styles.effects.json": "Effect styles"
|
|
1800
|
+
};
|
|
1801
|
+
function buildResolver(p, plans, styleFileNames) {
|
|
1802
|
+
const sets = {};
|
|
1803
|
+
const modifiers = {};
|
|
1804
|
+
const order = [];
|
|
1805
|
+
for (const collection of p.artifact.collections) {
|
|
1806
|
+
const own = plans.filter((f) => f.collection.id === collection.id);
|
|
1807
|
+
if (own.length === 0) continue;
|
|
1808
|
+
const labels = p.modeLabelsById.get(collection.id) ?? modeLabels(collection);
|
|
1809
|
+
const label = collectionLabelOf(p, collection);
|
|
1810
|
+
if (own.length === 1) {
|
|
1811
|
+
sets[label] = { sources: [{ $ref: own[0].file }] };
|
|
1812
|
+
order.push({ $ref: `#/sets/${pointer(label)}` });
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
const contexts = {};
|
|
1816
|
+
for (const plan of own) contexts[labels.get(plan.modeId) ?? plan.modeId] = [{ $ref: plan.file }];
|
|
1817
|
+
const def = labels.get(collection.default_mode_id);
|
|
1818
|
+
modifiers[label] = { contexts, ...def !== void 0 ? { default: def } : {} };
|
|
1819
|
+
order.push({ $ref: `#/modifiers/${pointer(label)}` });
|
|
1820
|
+
}
|
|
1821
|
+
for (const file of styleFileNames) {
|
|
1822
|
+
const root = STYLE_ROOTS[file];
|
|
1823
|
+
sets[root] = { sources: [{ $ref: file }] };
|
|
1824
|
+
order.push({ $ref: `#/sets/${pointer(root)}` });
|
|
1825
|
+
}
|
|
1826
|
+
const fileName = p.artifact.spec_layer.source.file_name;
|
|
1827
|
+
return {
|
|
1828
|
+
version: "2025.10",
|
|
1829
|
+
...typeof fileName === "string" && fileName.length > 0 ? { name: fileName } : {},
|
|
1830
|
+
sets,
|
|
1831
|
+
modifiers,
|
|
1832
|
+
resolutionOrder: order
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
function annotateGroups(p, tree, collection) {
|
|
1836
|
+
const groups = p.artifact.guidelines?.group_descriptions[collection.name];
|
|
1837
|
+
if (!groups) return;
|
|
1838
|
+
const head = dtcgSegments(collection.name).segments;
|
|
1839
|
+
folders: for (const [folder, text] of Object.entries(groups)) {
|
|
1840
|
+
if (text.length === 0) continue;
|
|
1841
|
+
let node = tree;
|
|
1842
|
+
for (const seg of [...head, ...dtcgSegments(folder).segments]) {
|
|
1843
|
+
if (typeof node !== "object" || node === null || Array.isArray(node)) continue folders;
|
|
1844
|
+
node = node[seg];
|
|
1845
|
+
}
|
|
1846
|
+
if (typeof node === "object" && node !== null && !Array.isArray(node) && !("$value" in node)) {
|
|
1847
|
+
node.$description = text;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
function foundationDtcg(artifact, options = {}) {
|
|
1852
|
+
const p = {
|
|
1853
|
+
artifact,
|
|
1854
|
+
options: { values: options.values ?? "standard", ...options.units ? { units: options.units } : {} },
|
|
1855
|
+
tokenById: new Map(artifact.tokens.map((t) => [t.id, t])),
|
|
1856
|
+
tokenIds: new Set(artifact.tokens.map((t) => t.id)),
|
|
1857
|
+
collectionById: new Map(artifact.collections.map((c) => [c.id, c])),
|
|
1858
|
+
collectionLabelById: collectionLabels(artifact.collections),
|
|
1859
|
+
modeLabelsById: new Map(artifact.collections.map((c) => [c.id, modeLabels(c)])),
|
|
1860
|
+
pathById: /* @__PURE__ */ new Map(),
|
|
1861
|
+
segmentsById: /* @__PURE__ */ new Map(),
|
|
1862
|
+
omittedIds: /* @__PURE__ */ new Set(),
|
|
1863
|
+
collidedIds: /* @__PURE__ */ new Set(),
|
|
1864
|
+
report: [],
|
|
1865
|
+
reportKeys: /* @__PURE__ */ new Set()
|
|
1866
|
+
};
|
|
1867
|
+
indexPaths(p);
|
|
1868
|
+
omitInexpressibleTypes(p);
|
|
1869
|
+
reportDuplicateCodeSyntax(p);
|
|
1870
|
+
reportCollectionNameCollisions(p);
|
|
1871
|
+
const files = {};
|
|
1872
|
+
const plans = [];
|
|
1873
|
+
const taken = new Set(RESERVED_FILE_NAMES);
|
|
1874
|
+
for (const collection of artifact.collections) {
|
|
1875
|
+
for (const mode of collection.modes) {
|
|
1876
|
+
const tree = {};
|
|
1877
|
+
for (const token of artifact.tokens) {
|
|
1878
|
+
if (token.collection_id !== collection.id || p.omittedIds.has(token.id)) continue;
|
|
1879
|
+
const leaf = tokenLeaf(p, token, collection, mode.id);
|
|
1880
|
+
if (leaf) setLeaf(tree, p.segmentsById.get(token.id) ?? [], leaf);
|
|
1881
|
+
}
|
|
1882
|
+
annotateGroups(p, tree, collection);
|
|
1883
|
+
const file = fileNameFor(collection, mode, taken);
|
|
1884
|
+
plans.push({ collection, modeId: mode.id, file });
|
|
1885
|
+
files[file] = sortTree(tree);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
const styles = styleFiles(p);
|
|
1889
|
+
Object.assign(files, styles);
|
|
1890
|
+
const resolver = buildResolver(p, plans, Object.keys(styles).sort(compareCodeUnits));
|
|
1891
|
+
p.report.sort((a, b) => compareCodeUnits(a.path, b.path) || compareCodeUnits(a.code, b.code) || compareCodeUnits(a.mode ?? "", b.mode ?? ""));
|
|
1892
|
+
const meta = {};
|
|
1893
|
+
for (const token of artifact.tokens) {
|
|
1894
|
+
const collection = p.collectionById.get(token.collection_id);
|
|
1895
|
+
if (!collection) continue;
|
|
1896
|
+
const path = p.pathById.get(token.id) ?? p.segmentsById.get(token.id)?.join(".") ?? token.name;
|
|
1897
|
+
meta[p.collidedIds.has(token.id) ? `${path} [${token.id}]` : path] = metaEntry(p, token, collection);
|
|
1898
|
+
}
|
|
1899
|
+
const sortedMeta = Object.fromEntries(Object.entries(meta).sort(([a], [b]) => compareCodeUnits(a, b)));
|
|
1900
|
+
return { files, resolver, meta: sortedMeta, report: p.report };
|
|
1901
|
+
}
|
|
1902
|
+
function omitInexpressibleTypes(p) {
|
|
1903
|
+
for (const token of p.artifact.tokens) {
|
|
1904
|
+
if (token.type !== "string" && token.type !== "boolean") continue;
|
|
1905
|
+
p.omittedIds.add(token.id);
|
|
1906
|
+
reportOnce(p, {
|
|
1907
|
+
code: "type_not_expressible",
|
|
1908
|
+
severity: "warning",
|
|
1909
|
+
path: p.pathById.get(token.id) ?? p.segmentsById.get(token.id)?.join(".") ?? token.name,
|
|
1910
|
+
message: `DTCG has no ${token.type} type; the token was omitted.`,
|
|
1911
|
+
details: { id: token.id, type: token.type }
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
function tokenLeaf(p, token, collection, modeId) {
|
|
1916
|
+
const value = token.values[modeId];
|
|
1917
|
+
const path = p.pathById.get(token.id) ?? "";
|
|
1918
|
+
const mode = modeLabelOf(p, collection, modeId);
|
|
1919
|
+
const description = token.description.length > 0 ? { $description: token.description } : {};
|
|
1920
|
+
if (value === void 0 || value.kind === "missing") {
|
|
1921
|
+
reportOnce(p, {
|
|
1922
|
+
code: "value_omitted",
|
|
1923
|
+
severity: "warning",
|
|
1924
|
+
path,
|
|
1925
|
+
mode,
|
|
1926
|
+
message: "The token has no value for this mode.",
|
|
1927
|
+
details: { id: token.id, reason: value?.reason ?? "no_value_for_mode" }
|
|
1928
|
+
});
|
|
1929
|
+
return null;
|
|
1930
|
+
}
|
|
1931
|
+
if (value.kind === "alias") {
|
|
1932
|
+
if (value.resolved.status === "unresolved") {
|
|
1933
|
+
reportOnce(p, {
|
|
1934
|
+
code: "value_omitted",
|
|
1935
|
+
severity: "warning",
|
|
1936
|
+
path,
|
|
1937
|
+
mode,
|
|
1938
|
+
message: `The alias could not be resolved (${value.resolved.reason}); no value was written.`,
|
|
1939
|
+
details: {
|
|
1940
|
+
id: token.id,
|
|
1941
|
+
reason: value.resolved.reason,
|
|
1942
|
+
target_path: value.reference.target_path.join("/"),
|
|
1943
|
+
...value.reference.target_id !== null ? { target_id: value.reference.target_id } : {},
|
|
1944
|
+
...value.reference.source_library_name ? { source_library_name: value.reference.source_library_name } : {}
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
return null;
|
|
1948
|
+
}
|
|
1949
|
+
const targetId = value.reference.target_id;
|
|
1950
|
+
const targetPath = targetId !== null && !p.omittedIds.has(targetId) ? p.pathById.get(targetId) : void 0;
|
|
1951
|
+
if (targetPath === void 0) {
|
|
1952
|
+
reportOnce(p, {
|
|
1953
|
+
code: "value_omitted",
|
|
1954
|
+
severity: "warning",
|
|
1955
|
+
path,
|
|
1956
|
+
mode,
|
|
1957
|
+
message: "The alias target was itself omitted from the DTCG output.",
|
|
1958
|
+
details: {
|
|
1959
|
+
id: token.id,
|
|
1960
|
+
reason: "target_omitted",
|
|
1961
|
+
target_path: value.reference.target_path.join("/"),
|
|
1962
|
+
...targetId !== null ? { target_id: targetId } : {},
|
|
1963
|
+
...value.reference.source_library_name ? { source_library_name: value.reference.source_library_name } : {}
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
return null;
|
|
1967
|
+
}
|
|
1968
|
+
const target = targetId !== null ? p.tokenById.get(targetId) : void 0;
|
|
1969
|
+
const hop = value.resolved.chain[0];
|
|
1970
|
+
if (target && hop && target.collection_id !== token.collection_id) {
|
|
1971
|
+
const targetCollection = p.collectionById.get(target.collection_id);
|
|
1972
|
+
if (targetCollection !== void 0 && targetCollection.modes.length > 1 && modeName(targetCollection, hop.mode_id) !== modeName(collection, modeId)) {
|
|
1973
|
+
const hopMode = modeLabelOf(p, targetCollection, hop.mode_id);
|
|
1974
|
+
reportOnce(p, {
|
|
1975
|
+
code: "mode_selection_not_expressible",
|
|
1976
|
+
severity: "info",
|
|
1977
|
+
path,
|
|
1978
|
+
mode,
|
|
1979
|
+
message: `Figma resolved this alias through the target's "${hopMode}" mode; DTCG resolves it by the consumer's context.`,
|
|
1980
|
+
details: {
|
|
1981
|
+
id: token.id,
|
|
1982
|
+
target_id: targetId ?? "",
|
|
1983
|
+
target_mode: hopMode,
|
|
1984
|
+
resolved: asJson(value.resolved.value)
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
const typed = aliasLeafType(p, token, value.resolved.chain, value.resolved.value);
|
|
1990
|
+
if ("omit" in typed) {
|
|
1991
|
+
reportOnce(p, {
|
|
1992
|
+
code: typed.omit,
|
|
1993
|
+
severity: "warning",
|
|
1994
|
+
path,
|
|
1995
|
+
mode,
|
|
1996
|
+
message: "The alias resolves to a value DTCG cannot state; the value was omitted.",
|
|
1997
|
+
details: { id: token.id, ...typed.details }
|
|
1998
|
+
});
|
|
1999
|
+
return null;
|
|
2000
|
+
}
|
|
2001
|
+
return { $type: typed.$type, $value: `{${targetPath}}`, ...description };
|
|
2002
|
+
}
|
|
2003
|
+
const converted = projectedLiteral(p, token, value.value, (override) => {
|
|
2004
|
+
reportOnce(p, {
|
|
2005
|
+
code: "unit_override_conflicts_with_scope",
|
|
2006
|
+
severity: "warning",
|
|
2007
|
+
path,
|
|
2008
|
+
message: "A unit override names this token but its scopes state a unitless number; the override was ignored.",
|
|
2009
|
+
details: { id: token.id, override, scopes: [...token.scopes] }
|
|
2010
|
+
});
|
|
2011
|
+
});
|
|
2012
|
+
if ("omit" in converted) {
|
|
2013
|
+
reportOnce(p, {
|
|
2014
|
+
code: converted.omit,
|
|
2015
|
+
severity: "warning",
|
|
2016
|
+
path,
|
|
2017
|
+
mode,
|
|
2018
|
+
message: converted.omit === "type_not_expressible" ? `DTCG has no ${String(converted.details.type)} type; the value was omitted.` : `DTCG dimensions take px or rem; a ${String(converted.details.unit)} value was omitted.`,
|
|
2019
|
+
details: { id: token.id, ...converted.details }
|
|
2020
|
+
});
|
|
2021
|
+
return null;
|
|
2022
|
+
}
|
|
2023
|
+
return { $type: converted.$type, $value: converted.$value, ...description };
|
|
2024
|
+
}
|
|
2025
|
+
function dtcgExportFiles(out) {
|
|
2026
|
+
const text = (v) => `${JSON.stringify(v, null, 2)}
|
|
2027
|
+
`;
|
|
2028
|
+
const files = {};
|
|
2029
|
+
for (const name of Object.keys(out.files).sort(compareCodeUnits)) files[name] = text(out.files[name]);
|
|
2030
|
+
files["resolver.json"] = text(out.resolver);
|
|
2031
|
+
files["spec-layer.meta.json"] = text(out.meta);
|
|
2032
|
+
files["report.json"] = text(out.report);
|
|
2033
|
+
return files;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
739
2036
|
// ../extractor/src/v5/componentContext.ts
|
|
740
2037
|
var import_js_sha2563 = __toESM(require_sha256(), 1);
|
|
741
2038
|
|
|
@@ -748,14 +2045,14 @@ var LibraryBundleError = class extends Error {
|
|
|
748
2045
|
this.code = code;
|
|
749
2046
|
}
|
|
750
2047
|
};
|
|
751
|
-
var
|
|
2048
|
+
var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
752
2049
|
function hasContentHash(artifact) {
|
|
753
|
-
if (!
|
|
2050
|
+
if (!isRecord2(artifact) || !isRecord2(artifact.spec_layer)) return false;
|
|
754
2051
|
const exp = artifact.spec_layer.export;
|
|
755
|
-
return
|
|
2052
|
+
return isRecord2(exp) && typeof exp.content_hash === "string";
|
|
756
2053
|
}
|
|
757
2054
|
function entry(v, where) {
|
|
758
|
-
if (!
|
|
2055
|
+
if (!isRecord2(v) || typeof v.name !== "string" || typeof v.ai !== "string" || !hasContentHash(v.artifact)) {
|
|
759
2056
|
throw new LibraryBundleError("malformed", `The ${where} entry in this bundle is malformed.`);
|
|
760
2057
|
}
|
|
761
2058
|
return { name: v.name, ai: v.ai, artifact: v.artifact };
|
|
@@ -772,7 +2069,7 @@ function parseLibraryBundle(input) {
|
|
|
772
2069
|
throw new LibraryBundleError("not_json", "This is not valid JSON.");
|
|
773
2070
|
}
|
|
774
2071
|
}
|
|
775
|
-
if (!
|
|
2072
|
+
if (!isRecord2(parsed) || parsed.schema !== LIBRARY_BUNDLE_SCHEMA) {
|
|
776
2073
|
throw new LibraryBundleError("not_bundle", "This is not a Spec Layer library bundle.");
|
|
777
2074
|
}
|
|
778
2075
|
if (!supportedVersion(parsed.version)) {
|
|
@@ -788,7 +2085,7 @@ function parseLibraryBundle(input) {
|
|
|
788
2085
|
const foundationRaw = parsed.foundation ?? null;
|
|
789
2086
|
let foundation = null;
|
|
790
2087
|
if (foundationRaw !== null) {
|
|
791
|
-
if (!
|
|
2088
|
+
if (!isRecord2(foundationRaw) || typeof foundationRaw.ai !== "string" || !hasContentHash(foundationRaw.artifact)) {
|
|
792
2089
|
throw new LibraryBundleError("malformed", "The foundation entry in this bundle is malformed.");
|
|
793
2090
|
}
|
|
794
2091
|
foundation = { ai: foundationRaw.ai, artifact: foundationRaw.artifact };
|
|
@@ -875,6 +2172,23 @@ function parseInclude(value) {
|
|
|
875
2172
|
components: record.components === void 0 ? null : record.components
|
|
876
2173
|
};
|
|
877
2174
|
}
|
|
2175
|
+
function parseDtcg(value) {
|
|
2176
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalidConfig();
|
|
2177
|
+
const record = value;
|
|
2178
|
+
const out = {};
|
|
2179
|
+
if (record.values !== void 0) {
|
|
2180
|
+
if (record.values !== "standard" && record.values !== "legacy") throw invalidConfig();
|
|
2181
|
+
out.values = record.values;
|
|
2182
|
+
}
|
|
2183
|
+
if (record.units !== void 0) {
|
|
2184
|
+
if (typeof record.units !== "object" || record.units === null || Array.isArray(record.units)) throw invalidConfig();
|
|
2185
|
+
for (const unit of Object.values(record.units)) {
|
|
2186
|
+
if (unit !== "px" && unit !== "rem") throw invalidConfig();
|
|
2187
|
+
}
|
|
2188
|
+
out.units = record.units;
|
|
2189
|
+
}
|
|
2190
|
+
return out;
|
|
2191
|
+
}
|
|
878
2192
|
function readConfig(cwd) {
|
|
879
2193
|
const path = join2(cwd, CONFIG_NAME);
|
|
880
2194
|
if (!existsSync2(path)) return null;
|
|
@@ -889,14 +2203,16 @@ function readConfig(cwd) {
|
|
|
889
2203
|
return {
|
|
890
2204
|
...typeof record.libraryId === "string" ? { libraryId: record.libraryId } : {},
|
|
891
2205
|
...typeof record.outDir === "string" ? { outDir: record.outDir } : {},
|
|
892
|
-
...record.include !== void 0 ? { include: parseInclude(record.include) } : {}
|
|
2206
|
+
...record.include !== void 0 ? { include: parseInclude(record.include) } : {},
|
|
2207
|
+
...record.dtcg !== void 0 ? { dtcg: parseDtcg(record.dtcg) } : {}
|
|
893
2208
|
};
|
|
894
2209
|
}
|
|
895
2210
|
function writeConfig(cwd, config) {
|
|
896
2211
|
const body = {
|
|
897
2212
|
libraryId: config.libraryId,
|
|
898
2213
|
outDir: config.outDir,
|
|
899
|
-
...config.include ? { include: config.include } : {}
|
|
2214
|
+
...config.include ? { include: config.include } : {},
|
|
2215
|
+
...config.dtcg ? { dtcg: config.dtcg } : {}
|
|
900
2216
|
};
|
|
901
2217
|
writeFileSync2(join2(cwd, CONFIG_NAME), `${JSON.stringify(body, null, 2)}
|
|
902
2218
|
`);
|
|
@@ -922,6 +2238,7 @@ function resolveOptions(cwd, flags, env, manifestLibraryId) {
|
|
|
922
2238
|
api: (flags.api ?? env.SPEC_LAYER_API ?? DEFAULT_API).replace(/\/+$/, ""),
|
|
923
2239
|
key: supplied ?? storedKey,
|
|
924
2240
|
...config?.include ? { include: config.include } : {},
|
|
2241
|
+
...config?.dtcg ? { dtcg: config.dtcg } : {},
|
|
925
2242
|
...storedKeyFor ? { storedKeyFor } : {}
|
|
926
2243
|
};
|
|
927
2244
|
}
|
|
@@ -996,8 +2313,8 @@ Available: ${available || "none"}.`
|
|
|
996
2313
|
|
|
997
2314
|
// src/files.ts
|
|
998
2315
|
function slugify(name) {
|
|
999
|
-
const
|
|
1000
|
-
return
|
|
2316
|
+
const slug2 = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2317
|
+
return slug2 || "component";
|
|
1001
2318
|
}
|
|
1002
2319
|
function readManifest(outDir) {
|
|
1003
2320
|
const path = join3(outDir, "manifest.json");
|
|
@@ -1022,20 +2339,20 @@ function componentSlugs(bundle) {
|
|
|
1022
2339
|
const nextSuffix = /* @__PURE__ */ new Map();
|
|
1023
2340
|
return bundle.components.map((component) => {
|
|
1024
2341
|
const base = slugify(component.name);
|
|
1025
|
-
let
|
|
1026
|
-
if (usedSlugs.has(
|
|
2342
|
+
let slug2 = base;
|
|
2343
|
+
if (usedSlugs.has(slug2)) {
|
|
1027
2344
|
let n = (nextSuffix.get(base) ?? 1) + 1;
|
|
1028
|
-
|
|
1029
|
-
while (usedSlugs.has(
|
|
2345
|
+
slug2 = `${base}-${n}`;
|
|
2346
|
+
while (usedSlugs.has(slug2)) {
|
|
1030
2347
|
n += 1;
|
|
1031
|
-
|
|
2348
|
+
slug2 = `${base}-${n}`;
|
|
1032
2349
|
}
|
|
1033
2350
|
nextSuffix.set(base, n);
|
|
1034
2351
|
} else {
|
|
1035
2352
|
nextSuffix.set(base, 1);
|
|
1036
2353
|
}
|
|
1037
|
-
usedSlugs.add(
|
|
1038
|
-
return
|
|
2354
|
+
usedSlugs.add(slug2);
|
|
2355
|
+
return slug2;
|
|
1039
2356
|
});
|
|
1040
2357
|
}
|
|
1041
2358
|
function assertReplaceable(outDir, cwd) {
|
|
@@ -1065,8 +2382,16 @@ function writeBundleFiles(opts) {
|
|
|
1065
2382
|
put("bundle.json", opts.raw);
|
|
1066
2383
|
const artifacts = [];
|
|
1067
2384
|
if (opts.bundle.foundation) {
|
|
1068
|
-
|
|
1069
|
-
if (
|
|
2385
|
+
let aiPath = null;
|
|
2386
|
+
if (selection.foundation) {
|
|
2387
|
+
const artifact = opts.bundle.foundation.artifact;
|
|
2388
|
+
if (validateLevel1(artifact).some((d) => d.severity === "error")) {
|
|
2389
|
+
throw new Error("The published Foundation context did not pass schema validation. Republish from the plugin, then pull again.");
|
|
2390
|
+
}
|
|
2391
|
+
const files = dtcgExportFiles(foundationDtcg(artifact, opts.dtcg ?? {}));
|
|
2392
|
+
for (const [name, text] of Object.entries(files)) put(`tokens/${name}`, text);
|
|
2393
|
+
aiPath = "tokens/resolver.json";
|
|
2394
|
+
}
|
|
1070
2395
|
artifacts.push({
|
|
1071
2396
|
kind: "foundation",
|
|
1072
2397
|
name: "foundation",
|
|
@@ -1091,7 +2416,8 @@ function writeBundleFiles(opts) {
|
|
|
1091
2416
|
pluginVersion: opts.bundle.pluginVersion,
|
|
1092
2417
|
extractorVersion: opts.bundle.extractorVersion,
|
|
1093
2418
|
selection,
|
|
1094
|
-
artifacts
|
|
2419
|
+
artifacts,
|
|
2420
|
+
...opts.dtcg && Object.keys(opts.dtcg).length > 0 ? { dtcg: opts.dtcg } : {}
|
|
1095
2421
|
};
|
|
1096
2422
|
put("manifest.json", `${JSON.stringify(manifest, null, 2)}
|
|
1097
2423
|
`);
|
|
@@ -1167,9 +2493,17 @@ function manifestReader() {
|
|
|
1167
2493
|
return cache.get(outDir) ?? null;
|
|
1168
2494
|
};
|
|
1169
2495
|
}
|
|
1170
|
-
function
|
|
1171
|
-
const
|
|
1172
|
-
|
|
2496
|
+
function sameOutput(a, b) {
|
|
2497
|
+
const selectionKey = (s) => JSON.stringify([s.foundation, s.components === null ? null : [...new Set(s.components.map(slugify))].sort()]);
|
|
2498
|
+
const dtcgKey = (d) => JSON.stringify(sortKeys(d ?? {}));
|
|
2499
|
+
return selectionKey(a.selection) === selectionKey(b.selection) && dtcgKey(a.dtcg) === dtcgKey(b.dtcg);
|
|
2500
|
+
}
|
|
2501
|
+
function sortKeys(value) {
|
|
2502
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
2503
|
+
if (value && typeof value === "object") {
|
|
2504
|
+
return Object.fromEntries(Object.keys(value).sort().map((k) => [k, sortKeys(value[k])]));
|
|
2505
|
+
}
|
|
2506
|
+
return value;
|
|
1173
2507
|
}
|
|
1174
2508
|
var errorText = (err) => err instanceof Error ? err.message : String(err);
|
|
1175
2509
|
function runInit(cwd, flags, io2) {
|
|
@@ -1248,7 +2582,13 @@ async function runSetup(cwd, flags, env, io2, fetcher) {
|
|
|
1248
2582
|
}
|
|
1249
2583
|
const outDir = flags.out ?? existing?.outDir ?? DEFAULT_OUT_DIR;
|
|
1250
2584
|
const keptInclude = include ?? existing?.include ?? null;
|
|
1251
|
-
|
|
2585
|
+
const keptDtcg = existing?.dtcg ?? null;
|
|
2586
|
+
writeConfig(cwd, {
|
|
2587
|
+
libraryId: flags.id,
|
|
2588
|
+
outDir,
|
|
2589
|
+
...keptInclude ? { include: keptInclude } : {},
|
|
2590
|
+
...keptDtcg ? { dtcg: keptDtcg } : {}
|
|
2591
|
+
});
|
|
1252
2592
|
io2.out(`Wrote speclayer.json (library ${flags.id}, output ${outDir}).`);
|
|
1253
2593
|
const ignored = ensureIgnored(cwd, CREDENTIALS_NAME);
|
|
1254
2594
|
switch (ignored.kind) {
|
|
@@ -1301,7 +2641,10 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
1301
2641
|
return 1;
|
|
1302
2642
|
}
|
|
1303
2643
|
const manifest = manifestAt(join5(cwd, opts.outDir));
|
|
1304
|
-
const etag = manifest &&
|
|
2644
|
+
const etag = manifest && sameOutput(
|
|
2645
|
+
{ selection: manifest.selection ?? DEFAULT_SELECTION, dtcg: manifest.dtcg },
|
|
2646
|
+
{ selection, dtcg: opts.dtcg }
|
|
2647
|
+
) ? manifest.bundleHash : void 0;
|
|
1305
2648
|
const result = await fetchBundle({
|
|
1306
2649
|
api: opts.api,
|
|
1307
2650
|
libraryId: opts.libraryId,
|
|
@@ -1329,7 +2672,8 @@ async function runPull(cwd, flags, env, io2, fetcher) {
|
|
|
1329
2672
|
selection,
|
|
1330
2673
|
libraryId: opts.libraryId,
|
|
1331
2674
|
publishedAt: result.publishedAt,
|
|
1332
|
-
bundleHash: result.bundleHash
|
|
2675
|
+
bundleHash: result.bundleHash,
|
|
2676
|
+
dtcg: opts.dtcg
|
|
1333
2677
|
});
|
|
1334
2678
|
io2.out(
|
|
1335
2679
|
`Pulled ${bundle.fileName ?? opts.libraryId}: ${describePull(bundle, selection, selected)} (published ${result.publishedAt}).`
|
|
@@ -1440,11 +2784,11 @@ Commands:
|
|
|
1440
2784
|
store the key, then pull
|
|
1441
2785
|
init --id lib_... [--out DIR] [selection] write speclayer.json
|
|
1442
2786
|
pull [--id lib_...] [--key sl_...] [selection]
|
|
1443
|
-
fetch the library into DIR (default .speclayer)
|
|
2787
|
+
fetch the library into DIR (default .speclayer); the foundation lands as DTCG under DIR/tokens/
|
|
1444
2788
|
status [--id lib_...] [--key sl_...] check freshness; exits 2 when behind
|
|
1445
2789
|
list list every artifact in the last pull
|
|
1446
2790
|
show foundation | component NAME [--canonical]
|
|
1447
|
-
print one artifact
|
|
2791
|
+
print one artifact (foundation: the DTCG document; component: its AI YAML; --canonical for JSON)
|
|
1448
2792
|
|
|
1449
2793
|
Selection (setup, pull and init; flags replace the include block in speclayer.json):
|
|
1450
2794
|
--only foundation | components write just the foundation, or just components
|
|
@@ -1496,6 +2840,10 @@ async function main() {
|
|
|
1496
2840
|
return 1;
|
|
1497
2841
|
}
|
|
1498
2842
|
}
|
|
2843
|
+
process.stdout.on("error", (err) => {
|
|
2844
|
+
if (err.code === "EPIPE") process.exit(process.exitCode ?? 0);
|
|
2845
|
+
throw err;
|
|
2846
|
+
});
|
|
1499
2847
|
process.exitCode = await main();
|
|
1500
2848
|
/*! Bundled license information:
|
|
1501
2849
|
|