@terrazzo/parser 0.0.19 → 0.1.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/CHANGELOG.md +11 -0
- package/CONTRIBUTING.md +25 -0
- package/package.json +4 -4
- package/parse/index.js +3 -2
- package/parse/normalize.js +16 -4
- package/parse/validate.js +59 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @terrazzo/parser
|
|
2
2
|
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#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).
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`e7f272d`](https://github.com/terrazzoapp/terrazzo/commit/e7f272defcd889f5a410fdbd30497cf704671b32)]:
|
|
12
|
+
- @terrazzo/token-tools@0.1.0
|
|
13
|
+
|
|
3
14
|
## 0.0.19
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
package/CONTRIBUTING.md
ADDED
|
@@ -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
|
|
3
|
+
"version": "0.1.0",
|
|
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.
|
|
33
|
+
"@humanwhocodes/momoa": "^3.3.0",
|
|
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.
|
|
39
|
+
"picocolors": "^1.1.1",
|
|
40
40
|
"wildcard-match": "^5.1.3",
|
|
41
41
|
"yaml": "^2.6.0",
|
|
42
|
-
"@terrazzo/token-tools": "^0.0
|
|
42
|
+
"@terrazzo/token-tools": "^0.1.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"esbuild": "^0.23.1",
|
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
|
}
|
package/parse/normalize.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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];
|
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
|
-
}
|
|
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 (
|
|
270
|
-
logger.error({
|
|
271
|
-
|
|
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
|
-
}
|
|
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 (
|
|
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
|
|
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
|
|