@terrazzo/parser 0.10.2 → 0.10.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrazzo/parser",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "description": "Parser/validator for the Design Tokens Community Group (DTCG) standard.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,14 +42,14 @@
42
42
  "picocolors": "^1.1.1",
43
43
  "scule": "^1.3.0",
44
44
  "wildcard-match": "^5.1.4",
45
- "@terrazzo/token-tools": "^0.10.2"
45
+ "@terrazzo/token-tools": "^0.10.3"
46
46
  },
47
47
  "devDependencies": {
48
48
  "yaml-to-momoa": "^0.0.3"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "rolldown -c && attw --profile esm-only --pack .",
52
- "dev": "pnpm run build --watch",
52
+ "dev": "rolldown -w -c",
53
53
  "lint": "pnpm --filter @terrazzo/parser run \"/^lint:(js|ts)/\"",
54
54
  "lint:js": "biome check .",
55
55
  "lint:ts": "tsc --noEmit",
@@ -7,7 +7,7 @@ import type { ConfigInit, InputSource } from '../types.js';
7
7
  import applyAliases from './alias.js';
8
8
  import { getObjMembers, parseJSON, replaceObjMembers, toMomoa, traverse } from './json.js';
9
9
  import normalize from './normalize.js';
10
- import validateTokenNode, { getInheritedType, type Visitors } from './validate.js';
10
+ import validateTokenNode, { computeInheritedProperty, isGroupNode, type Visitors } from './validate.js';
11
11
 
12
12
  export * from './alias.js';
13
13
  export * from './json.js';
@@ -210,16 +210,24 @@ async function parseSingle(
210
210
  // 2. Walk AST to validate tokens
211
211
  let tokenCount = 0;
212
212
  const startValidate = performance.now();
213
+ // $type and $deprecated can be applied at group level to target all child tokens,
214
+ // these two objects keep track of inherited prop values as we traverse the token tree
213
215
  const $typeInheritance: Record<string, MemberNode> = {};
216
+ const $deprecatedInheritance: Record<string, MemberNode> = {};
214
217
  traverse(document, {
215
218
  enter(node, parent, subpath) {
216
219
  // if $type appears at root of tokens.json, collect it
217
220
  if (node.type === 'Document' && node.body.type === 'Object' && node.body.members) {
218
221
  const { members: rootMembers } = node.body;
219
- const root$type = rootMembers.find((m) => m.name.type === 'String' && m.name.value === '$type');
220
- const root$value = rootMembers.find((m) => m.name.type === 'String' && m.name.value === '$value');
221
- if (root$type && !root$value) {
222
- $typeInheritance['.'] = root$type;
222
+ if (isGroupNode(node.body)) {
223
+ const root$type = rootMembers.find((m) => m.name.type === 'String' && m.name.value === '$type');
224
+ if (root$type) {
225
+ $typeInheritance['.'] = root$type;
226
+ }
227
+ const root$deprecated = rootMembers.find((m) => m.name.type === 'String' && m.name.value === '$deprecated');
228
+ if (root$deprecated) {
229
+ $deprecatedInheritance['.'] = root$deprecated;
230
+ }
223
231
  }
224
232
  }
225
233
 
@@ -241,13 +249,15 @@ async function parseSingle(
241
249
 
242
250
  // handle tokens
243
251
  if (node.type === 'Member') {
244
- const inheritedTypeNode = getInheritedType(node, { subpath, $typeInheritance });
252
+ const inheritedDeprecatedNode = computeInheritedProperty(node, '$deprecated', {
253
+ subpath,
254
+ inherited: $deprecatedInheritance,
255
+ });
256
+ const inheritedTypeNode = computeInheritedProperty(node, '$type', { subpath, inherited: $typeInheritance });
245
257
  if (node.value.type === 'Object') {
246
- const $type =
247
- node.value.members.find((m) => m.name.type === 'String' && m.name.value === '$type') || inheritedTypeNode;
248
258
  const $value = node.value.members.find((m) => m.name.type === 'String' && m.name.value === '$value');
249
- if ($value && $type?.value.type === 'String' && transform?.[$type.value.value]) {
250
- const result = transform[$type.value.value]?.(evaluate(node.value), subpath.join('.'), node);
259
+ if ($value && inheritedTypeNode?.value.type === 'String' && transform?.[inheritedTypeNode.value.value]) {
260
+ const result = transform[inheritedTypeNode.value.value]?.(evaluate(node.value), subpath.join('.'), node);
251
261
  if (result) {
252
262
  node.value = parseJSON(result).body;
253
263
  }
@@ -261,6 +271,7 @@ async function parseSingle(
261
271
  parent,
262
272
  subpath,
263
273
  transform,
274
+ inheritedDeprecatedNode,
264
275
  inheritedTypeNode,
265
276
  });
266
277
  if (token) {
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  type AnyNode,
3
+ type BooleanNode,
3
4
  evaluate,
4
5
  type MemberNode,
5
6
  type ObjectNode,
@@ -771,28 +772,47 @@ export function validateTokenMemberNode(node: MemberNode, { filename, src, logge
771
772
  }
772
773
  }
773
774
 
774
- /** Return any token node with its inherited $type */
775
- export function getInheritedType(
775
+ /** Return whether a MemberNode is a group (has no `$value`).
776
+ * Groups can have properties that their child nodes will inherit. */
777
+ export function isGroupNode(node: ObjectNode): boolean {
778
+ if (node.type !== 'Object') {
779
+ return false;
780
+ }
781
+
782
+ // check for $value
783
+ const has$value = node.members.some((m) => m.name.type === 'String' && m.name.value === '$value');
784
+ return !has$value;
785
+ }
786
+
787
+ /** Check if a token node has the specified property name, and if it does, stores
788
+ * the value in the `inherited` object as a side effect for future use. If not,
789
+ * traverses the `inherited` object to find the closest parent that has the property.
790
+ *
791
+ * Returns the property value if found locally or in a parent, otherwise undefined. */
792
+ export function computeInheritedProperty(
776
793
  node: MemberNode,
777
- { subpath, $typeInheritance }: { subpath: string[]; $typeInheritance?: Record<string, MemberNode> },
794
+ propertyName: string,
795
+ { subpath, inherited }: { subpath: string[]; inherited?: Record<string, MemberNode> },
778
796
  ): MemberNode | undefined {
779
797
  if (node.value.type !== 'Object') {
780
798
  return;
781
799
  }
782
800
 
783
- // keep track of $types
784
- const $type = node.value.members.find((m) => m.name.type === 'String' && m.name.value === '$type');
785
- const $value = node.value.members.find((m) => m.name.type === 'String' && m.name.value === '$value');
786
- if ($typeInheritance && $type && !$value) {
787
- $typeInheritance[subpath.join('.') || '.'] = $type;
788
- }
801
+ // if property exists locally in the token node, add it to the inherited tree
802
+ const property = node.value.members.find((m) => m.name.type === 'String' && m.name.value === propertyName);
803
+ if (inherited && property && isGroupNode(node.value)) {
804
+ // this is where the side effect occurs
805
+ inherited[subpath.join('.') || '.'] = property;
789
806
 
790
- const id = subpath.join('.');
807
+ // We know this is the closest property, so return early
808
+ return property;
809
+ }
791
810
 
792
811
  // get parent type by taking the closest-scoped $type (length === closer)
812
+ const id = subpath.join('.');
793
813
  let parent$type: MemberNode | undefined;
794
814
  let longestPath = '';
795
- for (const [k, v] of Object.entries($typeInheritance ?? {})) {
815
+ for (const [k, v] of Object.entries(inherited ?? {})) {
796
816
  if (k === '.' || id.startsWith(k)) {
797
817
  if (k.length > longestPath.length) {
798
818
  parent$type = v;
@@ -812,6 +832,7 @@ export interface ValidateTokenNodeOptions {
812
832
  logger: Logger;
813
833
  parent: AnyNode | undefined;
814
834
  transform?: Visitors;
835
+ inheritedDeprecatedNode?: MemberNode;
815
836
  inheritedTypeNode?: MemberNode;
816
837
  }
817
838
 
@@ -822,7 +843,16 @@ export interface ValidateTokenNodeOptions {
822
843
  */
823
844
  export default function validateTokenNode(
824
845
  node: MemberNode,
825
- { config, filename, logger, parent, inheritedTypeNode, src, subpath }: ValidateTokenNodeOptions,
846
+ {
847
+ config,
848
+ filename,
849
+ logger,
850
+ parent,
851
+ inheritedDeprecatedNode,
852
+ inheritedTypeNode,
853
+ src,
854
+ subpath,
855
+ }: ValidateTokenNodeOptions,
826
856
  ): TokenNormalized | undefined {
827
857
  // don’t validate $value
828
858
  if (subpath.includes('$value') || node.value.type !== 'Object') {
@@ -850,17 +880,33 @@ export default function validateTokenNode(
850
880
  }
851
881
 
852
882
  const nodeWithType = structuredClone(node);
883
+ // inject $type that can be inherited in the DTCG format
853
884
  let $type = (members.$type?.type === 'String' && members.$type.value) || undefined;
854
885
  if (inheritedTypeNode && !members.$type) {
855
886
  injectObjMembers(nodeWithType.value as ObjectNode, [inheritedTypeNode]);
856
887
  $type = (inheritedTypeNode.value as StringNode).value;
857
888
  }
889
+
890
+ // inject $deprecated that can also be inherited
891
+ let $deprecated = members.$deprecated
892
+ ? members.$deprecated?.type === 'String'
893
+ ? (members.$deprecated as StringNode).value
894
+ : (members.$deprecated as BooleanNode).value
895
+ : undefined;
896
+ if (inheritedDeprecatedNode && !members.$deprecated) {
897
+ injectObjMembers(nodeWithType.value as ObjectNode, [inheritedDeprecatedNode]);
898
+ $deprecated =
899
+ inheritedDeprecatedNode.value.type === 'String'
900
+ ? (inheritedDeprecatedNode.value as StringNode).value
901
+ : (inheritedDeprecatedNode.value as BooleanNode).value;
902
+ }
903
+
904
+ // validate once after injecting all inherited properties
858
905
  validateTokenMemberNode(nodeWithType, { filename, src, logger });
859
906
 
860
907
  // All tokens must be valid, so we want to validate it up till this
861
908
  // point. However, if we are ignoring this token (or respecting
862
909
  // $deprecated, we can omit it from the output.
863
- const $deprecated = members.$deprecated && (evaluate(members.$deprecated) as string | boolean | undefined);
864
910
  if ((config.ignore.deprecated && $deprecated) || (config.ignore.tokens && wcmatch(config.ignore.tokens)(id))) {
865
911
  return;
866
912
  }
@@ -881,6 +927,7 @@ export default function validateTokenNode(
881
927
  const token = {
882
928
  $type,
883
929
  $value,
930
+ $deprecated,
884
931
  id,
885
932
  mode: {},
886
933
  originalValue: evaluate(node.value),