@terrazzo/parser 0.0.19 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @terrazzo/parser
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#339](https://github.com/terrazzoapp/terrazzo/pull/339) [`9197405`](https://github.com/terrazzoapp/terrazzo/commit/9197405209d560f406494b6bd7aa1634608999c6) Thanks [@tomasfrancisco](https://github.com/tomasfrancisco)! - Fix missing letter spacing transformation as a dimension token
8
+
9
+ - Updated dependencies [[`9197405`](https://github.com/terrazzoapp/terrazzo/commit/9197405209d560f406494b6bd7aa1634608999c6), [`a637f67`](https://github.com/terrazzoapp/terrazzo/commit/a637f67e20009ce5eef1d5bc5b115cfa00b002d4)]:
10
+ - @terrazzo/token-tools@0.1.1
11
+
12
+ ## 0.1.0
13
+
14
+ ### Minor Changes
15
+
16
+ - [#319](https://github.com/terrazzoapp/terrazzo/pull/319) [`e7f272d`](https://github.com/terrazzoapp/terrazzo/commit/e7f272defcd889f5a410fdbd30497cf704671b32) Thanks [@drwpow](https://github.com/drwpow)! - ⚠️ Breaking change: dimension and duration tokens normalize to object syntax in plugins (following upcoming changes in DTCG spec; see https://github.com/design-tokens/community-group/pull/244).
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies [[`e7f272d`](https://github.com/terrazzoapp/terrazzo/commit/e7f272defcd889f5a410fdbd30497cf704671b32)]:
21
+ - @terrazzo/token-tools@0.1.0
22
+
3
23
  ## 0.0.19
4
24
 
5
25
  ### Patch Changes
@@ -0,0 +1,25 @@
1
+ # Contributing to @terrazzo/parser
2
+
3
+ This document contains developer notes and context for @terrazzo/parser. For general contribution guidelines, see [CONTRIBUTING.md](../../CONTRIBUTING.md) in the root.
4
+
5
+ ## What this package does
6
+
7
+ - Parses, validates, and normalizes tokens.json
8
+ - Executes plugins and builds output
9
+ - Executes linting
10
+
11
+ ## What this package DOES NOT do
12
+
13
+ - Write files to disk (that’s a job for Node.js; this can be run in a browser)
14
+
15
+ ## Manual JS and DTS
16
+
17
+ This package is unique in its manual handling of `.js` and `.d.ts` files (TypeScript definitions). The expected norm is to write `.ts`, and have `.js` and `.d.ts` be generated automatically from source. But in this project, we write `.js` by hand because this specific module has several unique concerns:
18
+
19
+ 1. **It’s dealing with external (possibly invalid) code.** Now, you’d think that `.ts` would be better at handling this, but the reality when dealing with user-submitted code is if you type it too strongly, TS lulls you into a false sense of security and misses necessary assertions. If you type it weakly, then TS requires _too many_ assertions and you end up with boilerplate and noise. Or if you don’t type it at all, then TS doesn’t really provide any value in the first place, and you might as well skip it.
20
+
21
+ 2. **It’s dealing with ASTs.** We’re parsing JSON ASTs using Momoa, and building ad hoc structures on-the-fly. ASTs usually trip TS up because the discrimination isn’t clear (e.g. if `.type == 'foo'`, then `[property]` exists). Most ASTs don’t have perfect types out-of-the-box, and attempting to re-type an AST yourself is overkill. Further, to the previous point, we may be dealing with invalid code, and the AST may not always be in predictable shapes! When faced with these problems, TS at best requires extra boilerplate; at worst guides you into incorrect decisions.
22
+
23
+ Given both of these, you usually end up with so many `// @ts-ignore`s or `as any`s that TS doesn’t provide benefit.
24
+
25
+ But this package is unique! Look in any other package in this monorepo, and you’ll see it’s fully written in `.ts` source files. Different concerns require different approaches.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrazzo/parser",
3
- "version": "0.0.19",
3
+ "version": "0.1.1",
4
4
  "description": "Parser/validator for the Design Tokens Community Group (DTCG) standard.",
5
5
  "type": "module",
6
6
  "author": {
@@ -30,16 +30,16 @@
30
30
  },
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "@humanwhocodes/momoa": "^3.2.2",
33
+ "@humanwhocodes/momoa": "^3.3.3",
34
34
  "@types/babel__code-frame": "^7.0.6",
35
35
  "@types/culori": "^2.1.1",
36
36
  "culori": "^4.0.1",
37
37
  "is-what": "^4.1.16",
38
38
  "merge-anything": "^5.1.7",
39
- "picocolors": "^1.1.0",
39
+ "picocolors": "^1.1.1",
40
40
  "wildcard-match": "^5.1.3",
41
41
  "yaml": "^2.6.0",
42
- "@terrazzo/token-tools": "^0.0.9"
42
+ "@terrazzo/token-tools": "^0.1.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "esbuild": "^0.23.1",
package/parse/index.d.ts CHANGED
@@ -33,7 +33,7 @@ export interface ParseResult {
33
33
  /**
34
34
  * Parse and validate Tokens JSON, and lint it
35
35
  */
36
- export default function parse(input: ParseInput[], options?: ParseOptions): Promise<ParseResult>;
36
+ export default function parse(input: ParseInput[], options: ParseOptions): Promise<ParseResult>;
37
37
 
38
38
  /** Determine if an input is likely a JSON string */
39
39
  export function maybeJSONString(input: unknown): boolean;
package/parse/index.js CHANGED
@@ -240,10 +240,11 @@ async function parseSingle(input, { filename, logger, config, skipLint, continue
240
240
  $typeInheritance[path.join('.') || '.'] = node.value.members.find((m) => m.name.value === '$type');
241
241
  }
242
242
 
243
+ const id = path.join('.');
244
+
243
245
  if (members.$value) {
244
246
  const extensions = members.$extensions ? getObjMembers(members.$extensions) : undefined;
245
247
  const sourceNode = structuredClone(node);
246
- const id = path.join('.');
247
248
 
248
249
  // get parent type by taking the closest-scoped $type (length === closer)
249
250
  let parent$type;
@@ -309,7 +310,7 @@ async function parseSingle(input, { filename, logger, config, skipLint, continue
309
310
  }
310
311
 
311
312
  tokens[id] = token;
312
- } else if (members.value) {
313
+ } else if (!id.includes('.$value') && members.value) {
313
314
  logger.warn({ message: `Group ${id} has "value". Did you mean "$value"?`, filename, node, src });
314
315
  }
315
316
  }
@@ -21,6 +21,8 @@ export const FONT_WEIGHT_MAP = {
21
21
  'ultra-black': 950,
22
22
  };
23
23
 
24
+ const NUMBER_WITH_UNIT_RE = /(-?\d*\.?\d+)(.*)/;
25
+
24
26
  export default function normalizeValue(token) {
25
27
  if (isAlias(token.$value)) {
26
28
  return token.$value;
@@ -49,15 +51,25 @@ export default function normalizeValue(token) {
49
51
  }
50
52
  case 'dimension': {
51
53
  if (token.$value === 0) {
52
- return '0';
54
+ return { value: 0, unit: 'px' };
55
+ }
56
+ // Backwards compat: handle string
57
+ if (typeof token.$value === 'string') {
58
+ const match = token.$value.match(NUMBER_WITH_UNIT_RE);
59
+ return { value: Number.parseFloat(match?.[1] || token.$value), unit: match[2] || 'px' };
53
60
  }
54
- return typeof token.$value === 'number' ? `${token.$value}px` : token.$value;
61
+ return token.$value;
55
62
  }
56
63
  case 'duration': {
57
64
  if (token.$value === 0) {
58
- return 0;
65
+ return { value: 0, unit: 'ms' };
59
66
  }
60
- return typeof token.$value === 'number' ? `${token.$value}ms` : token.$value;
67
+ // Backwards compat: handle string
68
+ if (typeof token.$value === 'string') {
69
+ const match = token.$value.match(NUMBER_WITH_UNIT_RE);
70
+ return { value: Number.parseFloat(match?.[1] || token.$value), unit: match[2] || 'ms' };
71
+ }
72
+ return token.$value;
61
73
  }
62
74
  case 'fontFamily': {
63
75
  return Array.isArray(token.$value) ? token.$value : [token.$value];
@@ -108,10 +120,13 @@ export default function normalizeValue(token) {
108
120
  case 'typography': {
109
121
  const output = {};
110
122
  for (const k in token.$value) {
111
- if (k === 'fontSize') {
112
- output[k] = normalizeValue({ $type: 'dimension', $value: token.$value[k] });
113
- } else {
114
- output[k] = token.$value[k];
123
+ switch (k) {
124
+ case 'letterSpacing':
125
+ case 'fontSize':
126
+ output[k] = normalizeValue({ $type: 'dimension', $value: token.$value[k] });
127
+ break;
128
+ default:
129
+ output[k] = token.$value[k];
115
130
  }
116
131
  }
117
132
  return output;
package/parse/validate.js CHANGED
@@ -262,13 +262,43 @@ export function validateDimension($value, node, { filename, src, logger }) {
262
262
  if ($value.type === 'Number' && $value.value === 0) {
263
263
  return; // `0` is a valid number
264
264
  }
265
+ if ($value.type === 'Object') {
266
+ const { value, unit } = getObjMembers($value);
267
+ if (!value) {
268
+ logger.error({ message: 'Missing required property "value".', filename, node: $value, src });
269
+ }
270
+ if (!unit) {
271
+ logger.error({ message: 'Missing required property "unit".', filename, node: $value, src });
272
+ }
273
+ if (value.type !== 'Number') {
274
+ logger.error({ message: `Expected number, received ${value.type}`, filename, node: value, src });
275
+ }
276
+ if (!['px', 'em', 'rem'].includes(unit.value)) {
277
+ logger.error({
278
+ message: `Expected unit "px", "em", or "rem", received ${print(unit)}`,
279
+ filename,
280
+ node: unit,
281
+ src,
282
+ });
283
+ }
284
+ return;
285
+ }
286
+ // Backwards compat: string
265
287
  if ($value.type !== 'String') {
266
288
  logger.error({ message: `Expected string, received ${$value.type}`, filename, node: $value, src });
267
- } else if ($value.value === '') {
289
+ }
290
+ const value = $value.value.match(/^-?[0-9.]+/)?.[0];
291
+ const unit = $value.value.replace(value, '');
292
+ if ($value.value === '') {
268
293
  logger.error({ message: 'Expected dimension, received empty string', filename, node: $value, src });
269
- } else if (String(Number.parseFloat($value.value)) === $value.value) {
270
- logger.error({ message: 'Missing units', filename, node: $value, src });
271
- } else if (!/^-?[0-9]+(\.[0-9]+)?/.test($value.value)) {
294
+ } else if (!['px', 'em', 'rem'].includes(unit)) {
295
+ logger.error({
296
+ message: `Expected unit "px", "em", or "rem", received ${JSON.stringify(unit || $value.value)}`,
297
+ filename,
298
+ node: $value,
299
+ src,
300
+ });
301
+ } else if (!Number.isFinite(Number.parseFloat(value))) {
272
302
  logger.error({ message: `Expected dimension with units, received ${print($value)}`, filename, node: $value, src });
273
303
  }
274
304
  }
@@ -284,19 +314,39 @@ export function validateDuration($value, node, { filename, src, logger }) {
284
314
  if ($value.type === 'Number' && $value.value === 0) {
285
315
  return; // `0` is a valid number
286
316
  }
317
+ if ($value.type === 'Object') {
318
+ const { value, unit } = getObjMembers($value);
319
+ if (!value) {
320
+ logger.error({ message: 'Missing required property "value".', filename, node: $value, src });
321
+ }
322
+ if (!unit) {
323
+ logger.error({ message: 'Missing required property "unit".', filename, node: $value, src });
324
+ }
325
+ if (value.type !== 'Number') {
326
+ logger.error({ message: `Expected number, received ${value.type}`, filename, node: value, src });
327
+ }
328
+ if (!['ms', 's'].includes(unit.value)) {
329
+ logger.error({ message: `Expected unit "ms" or "s", received ${print(unit)}`, filename, node: unit, src });
330
+ }
331
+ return;
332
+ }
333
+ // Backwards compat: string
287
334
  if ($value.type !== 'String') {
288
335
  logger.error({ message: `Expected string, received ${$value.type}`, filename, node: $value, src });
289
- } else if ($value.value === '') {
336
+ }
337
+ const value = $value.value.match(/^-?[0-9.]+/)?.[0];
338
+ const unit = $value.value.replace(value, '');
339
+ if ($value.value === '') {
290
340
  logger.error({ message: 'Expected duration, received empty string', filename, node: $value, src });
291
- } else if (!/m?s$/.test($value.value)) {
292
- logger.error({ message: 'Missing unit "ms" or "s"', filename, node: $value, src });
293
- } else if (!/^[0-9]+/.test($value.value)) {
341
+ } else if (!['ms', 's'].includes(unit)) {
294
342
  logger.error({
295
- message: `Expected duration in \`ms\` or \`s\`, received ${print($value)}`,
343
+ message: `Expected unit "ms" or "s", received ${JSON.stringify(unit || $value.value)}`,
296
344
  filename,
297
345
  node: $value,
298
346
  src,
299
347
  });
348
+ } else if (!Number.isFinite(Number.parseFloat(value))) {
349
+ logger.error({ message: `Expected duration with units, received ${print($value)}`, filename, node: $value, src });
300
350
  }
301
351
  }
302
352