@terrazzo/parser 0.0.9 → 0.0.11

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 CHANGED
@@ -1,4 +1,4 @@
1
- # 💠 @terrazzo/parser
1
+ # @terrazzo/parser
2
2
 
3
3
  JS API for parsing / validating / transforming DTCG tokens.
4
4
 
package/config.js CHANGED
@@ -11,12 +11,16 @@ const TRAILING_SLASH_RE = /\/*$/;
11
11
  * @param {Logger} options.logger
12
12
  * @param {URL} options.cwd
13
13
  */
14
- export default function defineConfig(rawConfig, { logger = new Logger(), cwd = import.meta.url } = {}) {
14
+ export default function defineConfig(rawConfig, { logger = new Logger(), cwd } = {}) {
15
15
  const configStart = performance.now();
16
16
 
17
+ if (!cwd) {
18
+ logger.error({ label: 'core', message: 'defineConfig() missing `cwd` for JS API' });
19
+ }
20
+
17
21
  logger.debug({ group: 'parser', task: 'config', message: 'Start config validation' });
18
22
 
19
- const config = { ...rawConfig };
23
+ const config = merge({}, rawConfig);
20
24
 
21
25
  // config.tokens
22
26
  if (rawConfig.tokens === undefined) {
@@ -26,7 +30,7 @@ export default function defineConfig(rawConfig, { logger = new Logger(), cwd = i
26
30
  } else if (Array.isArray(rawConfig.tokens)) {
27
31
  config.tokens = [];
28
32
  for (const file of rawConfig.tokens) {
29
- if (typeof file === 'string') {
33
+ if (typeof file === 'string' || file instanceof URL) {
30
34
  config.tokens.push(file); // will be normalized in next step
31
35
  } else {
32
36
  logger.error({
@@ -43,15 +47,20 @@ export default function defineConfig(rawConfig, { logger = new Logger(), cwd = i
43
47
  }
44
48
  for (let i = 0; i < config.tokens.length; i++) {
45
49
  const filepath = config.tokens[i];
50
+ if (filepath instanceof URL) {
51
+ continue; // skip if already resolved
52
+ }
46
53
  try {
47
54
  config.tokens[i] = new URL(filepath, cwd);
48
- } catch {
55
+ } catch (err) {
49
56
  logger.error({ label: 'config.tokens', message: `Invalid URL ${filepath}` });
50
57
  }
51
58
  }
52
59
 
53
60
  // config.outDir
54
- if (typeof config.outDir === 'undefined') {
61
+ if (config.outDir instanceof URL) {
62
+ // noop
63
+ } else if (typeof config.outDir === 'undefined') {
55
64
  config.outDir = new URL('./tokens/', cwd);
56
65
  } else if (typeof config.outDir !== 'string') {
57
66
  logger.error({ label: 'config.outDir', message: `Expected string, received ${JSON.stringify(config.outDir)}` });
@@ -95,7 +104,6 @@ export default function defineConfig(rawConfig, { logger = new Logger(), cwd = i
95
104
  logger.error({ label: 'config.lint', message: 'Must be an object' });
96
105
  return config;
97
106
  }
98
-
99
107
  if (!config.lint.build) {
100
108
  config.lint.build = { enabled: true };
101
109
  }
@@ -109,15 +117,15 @@ export default function defineConfig(rawConfig, { logger = new Logger(), cwd = i
109
117
  } else {
110
118
  config.lint.build.enabled = true;
111
119
  }
112
-
113
- if (config.lint.rules !== undefined) {
120
+ if (config.lint.rules === undefined) {
121
+ config.lint.rules = {};
122
+ } else {
114
123
  if (config.lint.rules === null || typeof config.lint.rules !== 'object' || Array.isArray(config.lint.rules)) {
115
124
  logger.error({
116
125
  label: 'config.lint.rules',
117
126
  message: `Expected object, received ${JSON.stringify(config.lint.rules)}`,
118
127
  });
119
128
  }
120
-
121
129
  for (const id in config.lint.rules) {
122
130
  if (!Object.hasOwn(config.lint.rules, id)) {
123
131
  continue;
package/logger.d.ts CHANGED
@@ -36,14 +36,6 @@ export interface DebugEntry {
36
36
 
37
37
  export default class Logger {
38
38
  constructor(options?: { level?: LogLevel; debugScope: string });
39
- }
40
-
41
- export class TokensJSONError extends Error {
42
- level: LogLevel;
43
- debugScope: string;
44
- node?: AnyNode;
45
-
46
- constructor(message: string, node: AnyNode);
47
39
 
48
40
  setLevel(level: LogLevel): void;
49
41
 
@@ -58,4 +50,20 @@ export class TokensJSONError extends Error {
58
50
 
59
51
  /** Log a diagnostics message (if logging level permits) */
60
52
  debug(entry: DebugEntry): void;
53
+
54
+ /** Get current stats for logger instance */
55
+ stats(): {
56
+ errorCount: number;
57
+ infoCount: number;
58
+ warnCount: number;
59
+ debugCount: number;
60
+ };
61
+ }
62
+
63
+ export class TokensJSONError extends Error {
64
+ level: LogLevel;
65
+ debugScope: string;
66
+ node?: AnyNode;
67
+
68
+ constructor(message: string, node: AnyNode);
61
69
  }
package/logger.js CHANGED
@@ -32,6 +32,10 @@ export function formatMessage(entry, severity) {
32
32
  export default class Logger {
33
33
  level = 'info';
34
34
  debugScope = '*';
35
+ errorCount = 0;
36
+ warnCount = 0;
37
+ infoCount = 0;
38
+ debugCount = 0;
35
39
 
36
40
  constructor(options) {
37
41
  if (options?.level) {
@@ -48,6 +52,7 @@ export default class Logger {
48
52
 
49
53
  /** Log an error message (always; can’t be silenced) */
50
54
  error(entry) {
55
+ this.errorCount++;
51
56
  const message = formatMessage(entry, 'error');
52
57
  if (entry.continueOnError) {
53
58
  console.error(message);
@@ -64,6 +69,7 @@ export default class Logger {
64
69
 
65
70
  /** Log an info message (if logging level permits) */
66
71
  info(entry) {
72
+ this.infoCount++;
67
73
  if (this.level === 'silent' || LOG_ORDER.indexOf(this.level) < LOG_ORDER.indexOf('info')) {
68
74
  return;
69
75
  }
@@ -73,6 +79,7 @@ export default class Logger {
73
79
 
74
80
  /** Log a warning message (if logging level permits) */
75
81
  warn(entry) {
82
+ this.warnCount++;
76
83
  if (this.level === 'silent' || LOG_ORDER.indexOf(this.level) < LOG_ORDER.indexOf('warn')) {
77
84
  return;
78
85
  }
@@ -81,6 +88,7 @@ export default class Logger {
81
88
 
82
89
  /** Log a diagnostics message (if logging level permits) */
83
90
  debug(entry) {
91
+ this.debugCount++;
84
92
  if (this.level === 'silent' || LOG_ORDER.indexOf(this.level) < LOG_ORDER.indexOf('debug')) {
85
93
  return;
86
94
  }
@@ -107,6 +115,16 @@ export default class Logger {
107
115
  // biome-ignore lint/suspicious/noConsoleLog: this is its job
108
116
  console.log(message);
109
117
  }
118
+
119
+ /** Get stats for current logger instance */
120
+ stats() {
121
+ return {
122
+ errorCount: this.errorCount,
123
+ warnCount: this.warnCount,
124
+ infoCount: this.infoCount,
125
+ debugCount: this.debugCount,
126
+ };
127
+ }
110
128
  }
111
129
 
112
130
  export class TokensJSONError extends Error {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrazzo/parser",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "Parser/validator for the Design Tokens Community Group (DTCG) standard.",
5
5
  "type": "module",
6
6
  "author": {
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "scripts": {
48
48
  "lint": "biome check .",
49
- "test": "pnpm run \"/^test:.*/\"",
49
+ "test": "pnpm --filter @terrazzo/parser run \"/^test:.*/\"",
50
50
  "test:js": "vitest run",
51
51
  "test:ts": "tsc --noEmit"
52
52
  }
package/parse/index.d.ts CHANGED
@@ -10,6 +10,8 @@ export interface ParseOptions {
10
10
  /** Skip lint step (default: false) */
11
11
  skipLint?: boolean;
12
12
  config: ConfigInit;
13
+ /** Continue on error? (Useful for `tz check`) (default: false) */
14
+ continueOnError?: boolean;
13
15
  }
14
16
 
15
17
  export interface ParseResult {
package/parse/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { evaluate, parse as parseJSON, print } from '@humanwhocodes/momoa';
2
- import { isAlias, parseAlias, splitID } from '@terrazzo/token-tools';
2
+ import { isAlias, parseAlias, pluralize, splitID } from '@terrazzo/token-tools';
3
3
  import lintRunner from '../lint/index.js';
4
4
  import Logger from '../logger.js';
5
5
  import normalize from './normalize.js';
@@ -28,7 +28,10 @@ export * from './validate.js';
28
28
  * @param {ParseOptions} options
29
29
  * @return {Promise<ParseResult>}
30
30
  */
31
- export default async function parse(input, { logger = new Logger(), skipLint = false, config } = {}) {
31
+ export default async function parse(
32
+ input,
33
+ { logger = new Logger(), skipLint = false, config, continueOnError = false } = {},
34
+ ) {
32
35
  const { plugins } = config;
33
36
 
34
37
  const totalStart = performance.now();
@@ -193,7 +196,7 @@ export default async function parse(input, { logger = new Logger(), skipLint = f
193
196
  if (members.$value) {
194
197
  node = members.$value;
195
198
  }
196
- logger.error({ message: err.message, source, node, continueOnError: true });
199
+ logger.error({ message: err.message, source, node, continueOnError });
197
200
  }
198
201
  for (const mode in tokens[id].mode) {
199
202
  if (mode === '.') {
@@ -207,7 +210,7 @@ export default async function parse(input, { logger = new Logger(), skipLint = f
207
210
  if (members.$value) {
208
211
  node = members.$value;
209
212
  }
210
- logger.error({ message: err.message, source, node: tokens[id].mode[mode].sourceNode, continueOnError: true });
213
+ logger.error({ message: err.message, source, node: tokens[id].mode[mode].sourceNode, continueOnError });
211
214
  }
212
215
  }
213
216
  }
@@ -273,6 +276,15 @@ export default async function parse(input, { logger = new Logger(), skipLint = f
273
276
  timing: performance.now() - totalStart,
274
277
  });
275
278
 
279
+ if (continueOnError) {
280
+ const { errorCount } = logger.stats();
281
+ if (errorCount > 0) {
282
+ logger.error({
283
+ message: `Parser encountered ${errorCount} ${pluralize(errorCount, 'error', 'errors')}. Exiting.`,
284
+ });
285
+ }
286
+ }
287
+
276
288
  return {
277
289
  tokens,
278
290
  ast,
@@ -302,10 +314,10 @@ export function maybeJSONString(input) {
302
314
  export function resolveAlias(alias, { tokens, logger, source, node, scanned = [] }) {
303
315
  const { id } = parseAlias(alias);
304
316
  if (!tokens[id]) {
305
- logger.error({ message: `Alias "${alias}" not found`, source, node, continueOnError: true });
317
+ logger.error({ message: `Alias "${alias}" not found`, source, node });
306
318
  }
307
319
  if (scanned.includes(id)) {
308
- logger.error({ message: `Circular alias detected from "${alias}"`, source, node, continueOnError: true });
320
+ logger.error({ message: `Circular alias detected from "${alias}"`, source, node });
309
321
  }
310
322
  const token = tokens[id];
311
323
  if (!isAlias(token.$value)) {
@@ -49,7 +49,7 @@ export default function normalizeValue(token) {
49
49
  }
50
50
  case 'dimension': {
51
51
  if (token.$value === 0) {
52
- return 0;
52
+ return '0';
53
53
  }
54
54
  return typeof token.$value === 'number' ? `${token.$value}px` : token.$value;
55
55
  }
@@ -84,7 +84,13 @@ export default function normalizeValue(token) {
84
84
  return typeof token.$value === 'number' ? token.$value : Number.parseFloat(token.$value);
85
85
  }
86
86
  case 'shadow': {
87
- return Array.isArray(token.$value) ? token.$value : [token.$value];
87
+ return (Array.isArray(token.$value) ? token.$value : [token.$value]).map((layer) => ({
88
+ color: normalizeValue({ $type: 'color', $value: layer.color }),
89
+ offsetX: normalizeValue({ $type: 'dimension', $value: layer.offsetX ?? 0 }),
90
+ offsetY: normalizeValue({ $type: 'dimension', $value: layer.offsetY ?? 0 }),
91
+ blur: normalizeValue({ $type: 'dimension', $value: layer.blur ?? 0 }),
92
+ spread: normalizeValue({ $type: 'dimension', $value: layer.spread ?? 0 }),
93
+ }));
88
94
  }
89
95
  case 'strokeStyle': {
90
96
  return token.$value;
@@ -1,4 +1,4 @@
1
- import type { AnyNode, DocumentNode, MemberNode, ValueNode } from '@humanwhocodes/momoa';
1
+ import type { AnyNode, MemberNode, ValueNode } from '@humanwhocodes/momoa';
2
2
  import type Logger from '../logger.js';
3
3
 
4
4
  declare const FONT_WEIGHT_VALUES: Set<string>;
@@ -17,7 +17,7 @@ export function validateBorder($value: ValueNode, node: AnyNode, options: Valida
17
17
 
18
18
  export function validateColor($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
19
19
 
20
- export function validateCubicBézier($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
20
+ export function validateCubicBezier($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
21
21
 
22
22
  export function validateDimension($value: ValueNode, node: AnyNode, options: ValidateOptions): void;
23
23
 
package/parse/validate.js CHANGED
@@ -220,7 +220,7 @@ export function validateColor($value, node, { source, logger }) {
220
220
  * @param {ValidateOptions} options
221
221
  * @return {void}
222
222
  */
223
- export function validateCubicBézier($value, node, { source, logger }) {
223
+ export function validateCubicBezier($value, node, { source, logger }) {
224
224
  if ($value.type !== 'Array') {
225
225
  logger.error({ message: `Expected array of strings, received ${print($value)}`, node: $value, source });
226
226
  } else if (
@@ -494,7 +494,7 @@ export function validateTransition($value, node, { source, logger }) {
494
494
  {
495
495
  duration: { validator: validateDuration, required: true },
496
496
  delay: { validator: validateDuration, required: false }, // note: spec says delay is required, but Terrazzo makes delay optional
497
- timingFunction: { validator: validateCubicBézier, required: true },
497
+ timingFunction: { validator: validateCubicBezier, required: true },
498
498
  },
499
499
  node,
500
500
  { source, logger },
@@ -544,7 +544,7 @@ export default function validate(node, { source, logger }) {
544
544
  break;
545
545
  }
546
546
  case 'cubicBezier': {
547
- validateCubicBézier($value, node, { logger, source });
547
+ validateCubicBezier($value, node, { logger, source });
548
548
  break;
549
549
  }
550
550
  case 'dimension': {