@terrazzo/parser 0.10.1 → 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.1",
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.1"
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",
@@ -18,7 +18,11 @@ function validateTransformParams({
18
18
  params,
19
19
  logger,
20
20
  pluginName,
21
- }: { params: TokenTransformed; logger: Logger; pluginName: string }): void {
21
+ }: {
22
+ params: TokenTransformed;
23
+ logger: Logger;
24
+ pluginName: string;
25
+ }): void {
22
26
  const baseMessage: LogEntry = { group: 'plugin', label: pluginName, message: '' };
23
27
 
24
28
  // validate value is valid for SINGLE_VALUE or MULTI_VALUE
package/src/config.ts CHANGED
@@ -48,7 +48,12 @@ function normalizeTokens({
48
48
  config,
49
49
  logger,
50
50
  cwd,
51
- }: { rawConfig: Config; config: ConfigInit; logger: Logger; cwd: URL }) {
51
+ }: {
52
+ rawConfig: Config;
53
+ config: ConfigInit;
54
+ logger: Logger;
55
+ cwd: URL;
56
+ }) {
52
57
  if (rawConfig.tokens === undefined) {
53
58
  config.tokens = [
54
59
  // @ts-ignore we’ll normalize in next step
@@ -89,7 +94,7 @@ function normalizeTokens({
89
94
  }
90
95
  try {
91
96
  config.tokens[i] = new URL(filepath, cwd);
92
- } catch (err) {
97
+ } catch {
93
98
  logger.error({ group: 'config', label: 'tokens', message: `Invalid URL ${filepath}` });
94
99
  }
95
100
  }
package/src/index.ts CHANGED
@@ -1,20 +1,3 @@
1
- export { default as build } from './build/index.js';
2
- export * from './build/index.js';
3
-
4
- export { default as defineConfig } from './config.js';
5
- export * from './config.js';
6
-
7
- export { default as lintRunner } from './lint/index.js';
8
- export * from './lint/index.js';
9
-
10
- export { default as Logger } from './logger.js';
11
- export * from './logger.js';
12
-
13
- export { default as parse } from './parse/index.js';
14
- export * from './parse/index.js';
15
-
16
- export * from './types.js';
17
-
18
1
  export type {
19
2
  AliasToken,
20
3
  AliasValue,
@@ -91,3 +74,20 @@ export type {
91
74
  TypographyValue,
92
75
  TypographyValueNormalized,
93
76
  } from '@terrazzo/token-tools';
77
+
78
+ export * from './build/index.js';
79
+ export { default as build } from './build/index.js';
80
+
81
+ export * from './config.js';
82
+ export { default as defineConfig } from './config.js';
83
+
84
+ export * from './lint/index.js';
85
+ export { default as lintRunner } from './lint/index.js';
86
+
87
+ export * from './logger.js';
88
+ export { default as Logger } from './logger.js';
89
+
90
+ export * from './parse/index.js';
91
+ export { default as parse } from './parse/index.js';
92
+
93
+ export * from './types.js';
package/src/lint/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type TokenNormalized, pluralize } from '@terrazzo/token-tools';
1
+ import { pluralize, type TokenNormalized } from '@terrazzo/token-tools';
2
2
  import { merge } from 'merge-anything';
3
3
  import type { LogEntry, default as Logger } from '../logger.js';
4
4
  import type { ConfigInit } from '../types.js';
@@ -2,14 +2,14 @@ import type { AnyNode, ArrayNode, ObjectNode } from '@humanwhocodes/momoa';
2
2
  import {
3
3
  type BorderTokenNormalized,
4
4
  type GradientTokenNormalized,
5
+ isAlias,
6
+ parseAlias,
5
7
  type ShadowTokenNormalized,
6
8
  type StrokeStyleTokenNormalized,
7
9
  type TokenNormalized,
8
10
  type TokenNormalizedSet,
9
11
  type TransitionTokenNormalized,
10
12
  type TypographyTokenNormalized,
11
- isAlias,
12
- parseAlias,
13
13
  } from '@terrazzo/token-tools';
14
14
  import type Logger from '../logger.js';
15
15
  import { getObjMembers } from './json.js';
@@ -1,5 +1,5 @@
1
- import { type DocumentNode, type MemberNode, type ObjectNode, evaluate } from '@humanwhocodes/momoa';
2
- import { type Token, type TokenNormalized, pluralize, splitID } from '@terrazzo/token-tools';
1
+ import { type DocumentNode, evaluate, type MemberNode, type ObjectNode } from '@humanwhocodes/momoa';
2
+ import { pluralize, splitID, type Token, type TokenNormalized } from '@terrazzo/token-tools';
3
3
  import type ytm from 'yaml-to-momoa';
4
4
  import lintRunner from '../lint/index.js';
5
5
  import Logger from '../logger.js';
@@ -7,11 +7,11 @@ 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, { type Visitors, getInheritedType } from './validate.js';
10
+ import validateTokenNode, { computeInheritedProperty, isGroupNode, type Visitors } from './validate.js';
11
11
 
12
12
  export * from './alias.js';
13
- export * from './normalize.js';
14
13
  export * from './json.js';
14
+ export * from './normalize.js';
15
15
  export * from './validate.js';
16
16
  export { normalize, validateTokenNode };
17
17
 
@@ -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) {
package/src/parse/json.ts CHANGED
@@ -2,11 +2,11 @@ import {
2
2
  type AnyNode,
3
3
  type DocumentNode,
4
4
  type MemberNode,
5
+ parse as momoaParse,
5
6
  type ObjectNode,
6
7
  type ParseOptions,
7
- type ValueNode,
8
- parse as momoaParse,
9
8
  print,
9
+ type ValueNode,
10
10
  } from '@humanwhocodes/momoa';
11
11
  import type yamlToMomoa from 'yaml-to-momoa';
12
12
  import type Logger from '../logger.js';
@@ -5,12 +5,12 @@ import {
5
5
  type FontFamilyValue,
6
6
  type GradientStopNormalized,
7
7
  type GradientValueNormalized,
8
+ isAlias,
9
+ parseColor,
8
10
  type ShadowValueNormalized,
9
11
  type Token,
10
12
  type TransitionValue,
11
13
  type TypographyValueNormalized,
12
- isAlias,
13
- parseColor,
14
14
  } from '@terrazzo/token-tools';
15
15
 
16
16
  export const FONT_WEIGHT_MAP = {
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  type AnyNode,
3
+ type BooleanNode,
4
+ evaluate,
3
5
  type MemberNode,
4
6
  type ObjectNode,
7
+ print,
5
8
  type StringNode,
6
9
  type ValueNode,
7
- evaluate,
8
- print,
9
10
  } from '@humanwhocodes/momoa';
10
- import { type Token, type TokenNormalized, isAlias, splitID } from '@terrazzo/token-tools';
11
+ import { isAlias, splitID, type Token, type TokenNormalized } from '@terrazzo/token-tools';
11
12
  import wcmatch from 'wildcard-match';
12
13
  import type Logger from '../logger.js';
13
14
  import type { ConfigInit } from '../types.js';
@@ -559,10 +560,12 @@ export function validateStrokeStyle($value: ValueNode, node: AnyNode, { filename
559
560
  } else {
560
561
  validateDimension(element.value, node, { logger, src });
561
562
  }
563
+ } else if (element.value.type === 'Object') {
564
+ validateDimension(element.value, node, { logger, src });
562
565
  } else {
563
566
  logger.error({
564
567
  ...baseMessage,
565
- message: 'Expected array of strings, recieved some non-strings or empty strings.',
568
+ message: `Expected array of dimensions, received ${element.value.type}.`,
566
569
  node: element,
567
570
  });
568
571
  }
@@ -769,28 +772,47 @@ export function validateTokenMemberNode(node: MemberNode, { filename, src, logge
769
772
  }
770
773
  }
771
774
 
772
- /** Return any token node with its inherited $type */
773
- 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(
774
793
  node: MemberNode,
775
- { subpath, $typeInheritance }: { subpath: string[]; $typeInheritance?: Record<string, MemberNode> },
794
+ propertyName: string,
795
+ { subpath, inherited }: { subpath: string[]; inherited?: Record<string, MemberNode> },
776
796
  ): MemberNode | undefined {
777
797
  if (node.value.type !== 'Object') {
778
798
  return;
779
799
  }
780
800
 
781
- // keep track of $types
782
- const $type = node.value.members.find((m) => m.name.type === 'String' && m.name.value === '$type');
783
- const $value = node.value.members.find((m) => m.name.type === 'String' && m.name.value === '$value');
784
- if ($typeInheritance && $type && !$value) {
785
- $typeInheritance[subpath.join('.') || '.'] = $type;
786
- }
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;
787
806
 
788
- const id = subpath.join('.');
807
+ // We know this is the closest property, so return early
808
+ return property;
809
+ }
789
810
 
790
811
  // get parent type by taking the closest-scoped $type (length === closer)
812
+ const id = subpath.join('.');
791
813
  let parent$type: MemberNode | undefined;
792
814
  let longestPath = '';
793
- for (const [k, v] of Object.entries($typeInheritance ?? {})) {
815
+ for (const [k, v] of Object.entries(inherited ?? {})) {
794
816
  if (k === '.' || id.startsWith(k)) {
795
817
  if (k.length > longestPath.length) {
796
818
  parent$type = v;
@@ -810,6 +832,7 @@ export interface ValidateTokenNodeOptions {
810
832
  logger: Logger;
811
833
  parent: AnyNode | undefined;
812
834
  transform?: Visitors;
835
+ inheritedDeprecatedNode?: MemberNode;
813
836
  inheritedTypeNode?: MemberNode;
814
837
  }
815
838
 
@@ -820,7 +843,16 @@ export interface ValidateTokenNodeOptions {
820
843
  */
821
844
  export default function validateTokenNode(
822
845
  node: MemberNode,
823
- { 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,
824
856
  ): TokenNormalized | undefined {
825
857
  // don’t validate $value
826
858
  if (subpath.includes('$value') || node.value.type !== 'Object') {
@@ -848,17 +880,33 @@ export default function validateTokenNode(
848
880
  }
849
881
 
850
882
  const nodeWithType = structuredClone(node);
883
+ // inject $type that can be inherited in the DTCG format
851
884
  let $type = (members.$type?.type === 'String' && members.$type.value) || undefined;
852
885
  if (inheritedTypeNode && !members.$type) {
853
886
  injectObjMembers(nodeWithType.value as ObjectNode, [inheritedTypeNode]);
854
887
  $type = (inheritedTypeNode.value as StringNode).value;
855
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
856
905
  validateTokenMemberNode(nodeWithType, { filename, src, logger });
857
906
 
858
907
  // All tokens must be valid, so we want to validate it up till this
859
908
  // point. However, if we are ignoring this token (or respecting
860
909
  // $deprecated, we can omit it from the output.
861
- const $deprecated = members.$deprecated && (evaluate(members.$deprecated) as string | boolean | undefined);
862
910
  if ((config.ignore.deprecated && $deprecated) || (config.ignore.tokens && wcmatch(config.ignore.tokens)(id))) {
863
911
  return;
864
912
  }
@@ -879,6 +927,7 @@ export default function validateTokenNode(
879
927
  const token = {
880
928
  $type,
881
929
  $value,
930
+ $deprecated,
882
931
  id,
883
932
  mode: {},
884
933
  originalValue: evaluate(node.value),