@kanso-labs/unplugin-style-dictionary 0.7.0 → 0.9.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/dist/index.js CHANGED
@@ -11,6 +11,14 @@ import { createUnplugin } from "unplugin";
11
11
  function asError(error) {
12
12
  return error instanceof Error ? error : new Error(errorMessage(error));
13
13
  }
14
+ function colourAllowed(stream) {
15
+ if (process.env.NO_COLOR) return false;
16
+ const forced = process.env.FORCE_COLOR;
17
+ if (forced === "0") return false;
18
+ if (forced !== void 0 && forced !== "") return true;
19
+ if (process.env.TERM === "dumb") return false;
20
+ return stream.isTTY === true;
21
+ }
14
22
  function discardTemporaryFile(temporary) {
15
23
  try {
16
24
  fs.rmSync(temporary, { force: true });
@@ -22,6 +30,45 @@ function errorMessage(error) {
22
30
  function isConfig(value) {
23
31
  return typeof value === "object" && value !== null;
24
32
  }
33
+ function isMessageChannel(value) {
34
+ return typeof value === "function";
35
+ }
36
+ function isThenable(value) {
37
+ return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
38
+ }
39
+ function looksLikeConfig(value) {
40
+ if (typeof value !== "object" || value === null) return false;
41
+ return [
42
+ "include",
43
+ "platforms",
44
+ "source",
45
+ "tokens"
46
+ ].some((key) => key in value);
47
+ }
48
+ function nodeModulesNegations(paths) {
49
+ const negations = /* @__PURE__ */ new Set();
50
+ for (const file of paths) {
51
+ const normalised = file.replace(/\\/g, "/");
52
+ if (normalised.includes("/node_modules/")) negations.add(`!${normalised}`);
53
+ }
54
+ return Array.from(negations);
55
+ }
56
+ function paint(code, value, allowed) {
57
+ return allowed ? `\u001B[${code}m${value}\u001B[0m` : value;
58
+ }
59
+ async function patternsMatchingNothing(patterns) {
60
+ const barren = [];
61
+ for (const pattern of patterns) {
62
+ if (!GLOB_CHARACTERS.test(pattern)) {
63
+ if (!fs.existsSync(pattern)) barren.push(pattern);
64
+ continue;
65
+ }
66
+ try {
67
+ if ((await glob([pattern], { absolute: true })).length === 0) barren.push(pattern);
68
+ } catch {}
69
+ }
70
+ return barren;
71
+ }
25
72
  function unwrapDefault(value) {
26
73
  return typeof value === "object" && value !== null && "default" in value ? value.default ?? value : value;
27
74
  }
@@ -88,6 +135,9 @@ const IMPORTED_CONFIG_EXTENSIONS = [
88
135
  ".mjs",
89
136
  ".ts"
90
137
  ];
138
+ function describeConfig(item, index) {
139
+ return item.file ? `The configuration ${item.file}` : `The configuration at position ${index + 1}`;
140
+ }
91
141
  function isImportedConfig(file) {
92
142
  return IMPORTED_CONFIG_EXTENSIONS.some((extension) => file.endsWith(extension));
93
143
  }
@@ -123,16 +173,36 @@ function statOrNull(file) {
123
173
  }
124
174
  const unpluginFactory = (options = {}, meta) => {
125
175
  const isWebpack = meta.framework === "webpack";
126
- const { cache = true, failOnError = "build", logLevel, report = true, root: rootOption, silent = false } = options;
176
+ const { cache = true, errorOverlay = true, failOnError = "build", logLevel, onBuildEnd, onBuildError, onBuildStart, platforms: platformsOption, report = true, root: rootOption, silent = false } = options;
127
177
  const level = logLevel ?? (silent ? "silent" : void 0);
128
178
  const quiet = level === "silent" || level === "warn";
129
179
  const verbosity = level === void 0 ? void 0 : level === "verbose" ? "verbose" : level === "silent" ? "silent" : "default";
180
+ const platformsFor = (context) => {
181
+ if (platformsOption === void 0) return void 0;
182
+ if (Array.isArray(platformsOption)) return platformsOption;
183
+ return context === void 0 ? platformsOption.build : platformsOption.watch;
184
+ };
130
185
  const failsTheBuild = (context) => failOnError === true || (context === void 0 ? failOnError === "build" : failOnError === "serve");
186
+ let hostCommand = "build";
187
+ let hostMode;
188
+ let isWatching = false;
189
+ const configContext = () => ({
190
+ command: hostCommand,
191
+ mode: hostMode ?? (hostCommand === "serve" ? "development" : "production"),
192
+ watch: isWatching
193
+ });
194
+ const adoptWatchMode = (context) => {
195
+ const hookMeta = "meta" in context ? context.meta : void 0;
196
+ if (typeof hookMeta !== "object" || hookMeta === null) return;
197
+ const watching = "watchMode" in hookMeta ? hookMeta.watchMode : void 0;
198
+ if (typeof watching === "boolean") isWatching = watching;
199
+ };
131
200
  let root = rootOption ? path.resolve(process.cwd(), rootOption) : process.cwd();
132
201
  const generatedDestinations = /* @__PURE__ */ new Set();
133
202
  let cachedPatterns;
134
203
  let watchRebuild = false;
135
204
  let hasCompiled = false;
205
+ let hostClosed = false;
136
206
  const expandPatterns = async (patterns) => {
137
207
  const paths = /* @__PURE__ */ new Set();
138
208
  const globs = [];
@@ -149,35 +219,88 @@ const unpluginFactory = (options = {}, meta) => {
149
219
  return Array.from(paths);
150
220
  };
151
221
  const isWatchedSource = (file, patterns) => !generatedDestinations.has(file.replace(/\\/g, "/")) && matchesWatchedFile(file, patterns);
222
+ const stdoutColour = colourAllowed(process.stdout);
223
+ const stderrColour = colourAllowed(process.stderr);
224
+ let host;
225
+ const adoptHost = (context) => {
226
+ if (host) return;
227
+ const warn = "warn" in context ? context.warn : void 0;
228
+ if (!isMessageChannel(warn)) return;
229
+ const info = "info" in context ? context.info : void 0;
230
+ host = {
231
+ error: (message) => {
232
+ warn.call(context, message);
233
+ },
234
+ info: isMessageChannel(info) ? (message) => {
235
+ info.call(context, message);
236
+ } : void 0
237
+ };
238
+ };
152
239
  const log = (message, type = "info") => {
153
240
  const prefix = "[unplugin-style-dictionary]";
154
241
  if (type === "error") {
155
- console.error(`\x1b[31m${prefix} ${message}\x1b[0m`);
242
+ if (host) {
243
+ host.error(`${prefix} ${message}`);
244
+ return;
245
+ }
246
+ console.error(paint("31", `${prefix} ${message}`, stderrColour));
156
247
  return;
157
248
  }
158
249
  if (quiet) return;
159
- if (type === "success") console.log(`\x1b[32m${prefix} ${message}\x1b[0m`);
160
- else console.log(`\x1b[36m${prefix} ${message}\x1b[0m`);
250
+ if (host?.info) {
251
+ host.info(`${prefix} ${message}`);
252
+ return;
253
+ }
254
+ console.log(paint(type === "success" ? "32" : "36", `${prefix} ${message}`, stdoutColour));
255
+ };
256
+ const callHook = (name, hook, ...args) => {
257
+ let result;
258
+ try {
259
+ result = hook(...args);
260
+ } catch (err) {
261
+ log(`The ${name} hook threw: ${errorMessage(err)}`, "error");
262
+ return;
263
+ }
264
+ if (!isThenable(result)) return;
265
+ Promise.resolve(result).catch((err) => {
266
+ log(`The ${name} hook rejected: ${errorMessage(err)}`, "error");
267
+ });
161
268
  };
162
269
  const resolveConfigs = async () => {
163
270
  let rawConfig = options.config;
164
- if (!rawConfig) for (const file of [
165
- "sd.config.json",
166
- "config.json",
167
- "sd.config.js",
168
- "sd.config.mjs"
169
- ]) {
170
- const fullPath = path.resolve(root, file);
171
- if (fs.existsSync(fullPath)) {
271
+ if (rawConfig === false) return [];
272
+ if (!rawConfig) {
273
+ const defaults = [
274
+ "sd.config.json",
275
+ "config.json",
276
+ "sd.config.js",
277
+ "sd.config.mjs"
278
+ ];
279
+ const rejected = [];
280
+ for (const file of defaults) {
281
+ const fullPath = path.resolve(root, file);
282
+ if (!fs.existsSync(fullPath)) continue;
283
+ if (!looksLikeConfig(await readConfigObject({
284
+ config: fullPath,
285
+ file: fullPath
286
+ }, false))) {
287
+ rejected.push(file);
288
+ continue;
289
+ }
290
+ if (!announcedDiscovery) {
291
+ announcedDiscovery = true;
292
+ log(`Using the configuration it found at ${fullPath}`, "info");
293
+ }
172
294
  rawConfig = file;
173
295
  break;
174
296
  }
297
+ if (rejected.length > 0) log(`Ignored ${rejected.join(", ")} in ${root}: nothing there declares platforms, source, include or tokens, so it does not look like a Style Dictionary configuration. Name it with the config option if it is one, or set config to false to stop looking.`, "error");
175
298
  }
176
299
  if (!rawConfig) {
177
300
  log("No configuration specified and no default config file found. Style Dictionary will not compile.", "error");
178
301
  return [];
179
302
  }
180
- if (typeof rawConfig === "function") rawConfig = await rawConfig();
303
+ if (typeof rawConfig === "function") rawConfig = await rawConfig(configContext());
181
304
  return (Array.isArray(rawConfig) ? rawConfig : [rawConfig]).map((conf) => {
182
305
  if (typeof conf === "string") {
183
306
  const fullPath = path.resolve(root, conf);
@@ -256,9 +379,11 @@ const unpluginFactory = (options = {}, meta) => {
256
379
  return loaded;
257
380
  }
258
381
  };
259
- const declaredDestinations = (configObj) => {
382
+ const declaredDestinations = (configObj, only) => {
260
383
  const destinations = [];
261
- for (const platform of Object.values(configObj.platforms ?? {})) {
384
+ const entries = Object.entries(configObj.platforms ?? {});
385
+ const selected = only ? entries.filter(([name]) => only.includes(name)) : entries;
386
+ for (const [, platform] of selected) {
262
387
  const buildPath = platform.buildPath ?? "";
263
388
  const absoluteBuildPath = path.isAbsolute(buildPath) ? buildPath : path.resolve(root, buildPath);
264
389
  for (const file of platform.files ?? []) if (file.destination) destinations.push(path.isAbsolute(file.destination) ? file.destination : path.resolve(absoluteBuildPath, file.destination));
@@ -272,9 +397,9 @@ const unpluginFactory = (options = {}, meta) => {
272
397
  return null;
273
398
  }
274
399
  };
275
- const isUpToDate = async (item, configObj) => {
400
+ const isUpToDate = async (item, configObj, only) => {
276
401
  if (Object.values(configObj.platforms ?? {}).some((platform) => (platform.actions?.length ?? 0) > 0)) return false;
277
- const destinations = declaredDestinations(configObj);
402
+ const destinations = declaredDestinations(configObj, only);
278
403
  if (destinations.length === 0) return false;
279
404
  const extraWatches = options.watch ? Array.isArray(options.watch) ? options.watch : [options.watch] : [];
280
405
  const sources = await expandPatterns([...sourcePatternsOf(configObj), ...extraWatches.map((pattern) => (path.isAbsolute(pattern) ? pattern : path.resolve(root, pattern)).replace(/\\/g, "/"))]);
@@ -307,7 +432,7 @@ const unpluginFactory = (options = {}, meta) => {
307
432
  const displayPath = path.relative(root, filePath).replace(/\\/g, "/");
308
433
  const dir = path.dirname(displayPath);
309
434
  const base = path.basename(displayPath);
310
- const coloredPath = dir === "." ? `\x1b[32m${base}\x1b[0m` : `\x1b[90m${dir}/\x1b[0m\x1b[32m${base}\x1b[0m`;
435
+ const coloredPath = dir === "." ? paint("32", base, stdoutColour) : paint("90", `${dir}/`, stdoutColour) + paint("32", base, stdoutColour);
311
436
  try {
312
437
  const sizeStr = `${(fs.statSync(filePath).size / 1024).toFixed(2)} kB`;
313
438
  const content = fs.readFileSync(filePath);
@@ -326,7 +451,7 @@ const unpluginFactory = (options = {}, meta) => {
326
451
  for (const info of fileInfos) {
327
452
  const pathPadding = " ".repeat(Math.max(2, longestPathLength - info.relativeDisplayPath.length + 2));
328
453
  const sizePadded = info.sizeStr.padStart(longestSizeLength);
329
- console.log(`${info.coloredPath}${pathPadding}\x1b[90m${sizePadded} │ gzip: ${info.gzipSizeStr}\x1b[0m`);
454
+ console.log(info.coloredPath + pathPadding + paint("90", `${sizePadded} │ gzip: ${info.gzipSizeStr}`, stdoutColour));
330
455
  }
331
456
  }
332
457
  };
@@ -336,9 +461,11 @@ const unpluginFactory = (options = {}, meta) => {
336
461
  let skipped = 0;
337
462
  try {
338
463
  if (!context) log("Compiling design tokens...", "info");
339
- for (const item of resolvedConfigs) {
464
+ if (onBuildStart) callHook("onBuildStart", onBuildStart);
465
+ for (const [index, item] of resolvedConfigs.entries()) {
340
466
  const declared = cache ? await readConfigObject(item, false) : null;
341
- if (declared && await isUpToDate(item, declared)) {
467
+ const selectedPlatforms = platformsFor(context);
468
+ if (declared && await isUpToDate(item, declared, selectedPlatforms)) {
342
469
  for (const destination of declaredDestinations(declared)) generatedFiles.add(destination);
343
470
  skipped++;
344
471
  continue;
@@ -348,8 +475,23 @@ const unpluginFactory = (options = {}, meta) => {
348
475
  mutateOriginal: true,
349
476
  verbosity
350
477
  });
478
+ if (sd.allTokens.length === 0) {
479
+ const asObject = await readConfigObject(item, false);
480
+ const barren = asObject ? await patternsMatchingNothing(sourcePatternsOf(asObject)) : [];
481
+ throw new Error([
482
+ `${describeConfig(item, index)} resolved no tokens, so its output would be emptied.`,
483
+ barren.length > 0 ? `These patterns matched no files: ${barren.join(", ")}` : `It declares no source or include patterns that matched anything.`,
484
+ `Nothing was written. Set failOnError to false to build anyway.`
485
+ ].join(" "));
486
+ }
351
487
  sd.volume = atomicVolume;
352
- await sd.buildAllPlatforms();
488
+ if (selectedPlatforms === void 0) await sd.buildAllPlatforms();
489
+ else {
490
+ const defined = Object.keys(sd.platforms);
491
+ const unknown = selectedPlatforms.filter((name) => !defined.includes(name));
492
+ if (unknown.length > 0) throw new Error(`${describeConfig(item, index)} does not define the platform(s) ${unknown.join(", ")}. It defines ${defined.join(", ")}.`);
493
+ for (const name of selectedPlatforms) await sd.buildPlatform(name);
494
+ }
353
495
  for (const platform of Object.values(sd.platforms)) {
354
496
  const buildPath = platform.buildPath ?? "";
355
497
  for (const file of platform.files ?? []) if (file.destination) {
@@ -366,10 +508,17 @@ const unpluginFactory = (options = {}, meta) => {
366
508
  } catch (err) {
367
509
  const duration = Date.now() - startTime;
368
510
  log(`Compilation failed after ${duration}ms: ${errorMessage(err)}`, "error");
511
+ notifyBuildOutcome?.(asError(err));
512
+ if (onBuildError) callHook("onBuildError", onBuildError, err);
369
513
  if (failsTheBuild(context)) throw err;
370
514
  return;
371
515
  }
516
+ notifyBuildOutcome?.(null);
372
517
  const duration = Date.now() - startTime;
518
+ if (onBuildEnd) {
519
+ const files = Array.from(generatedFiles).sort((left, right) => left.localeCompare(right));
520
+ callHook("onBuildEnd", onBuildEnd, files, duration);
521
+ }
373
522
  const everythingSkipped = skipped === resolvedConfigs.length;
374
523
  if (context) {
375
524
  log(everythingSkipped ? `Design tokens already up to date after change in ${context} (${duration}ms)` : `Rebuilt design tokens due to change in ${context} (${duration}ms)`, "success");
@@ -411,6 +560,9 @@ const unpluginFactory = (options = {}, meta) => {
411
560
  let inFlight;
412
561
  let waiting = [];
413
562
  let refreshServerWatchList;
563
+ let announcedDiscovery = false;
564
+ let startupResolved;
565
+ let notifyBuildOutcome;
414
566
  const drain = async () => {
415
567
  while (pendingReason !== void 0) {
416
568
  const reason = pendingReason;
@@ -430,12 +582,16 @@ const unpluginFactory = (options = {}, meta) => {
430
582
  }
431
583
  } catch (err) {
432
584
  failure = { error: err };
433
- if (!compiling) log(`Rebuild failed: ${errorMessage(err)}`, "error");
585
+ if (!compiling) {
586
+ log(`Rebuild failed: ${errorMessage(err)}`, "error");
587
+ notifyBuildOutcome?.(asError(err));
588
+ }
434
589
  }
435
590
  for (const settle of resolvers) settle(failure);
436
591
  }
437
592
  };
438
593
  const schedule = async (reason) => {
594
+ if (hostClosed) return;
439
595
  pendingReason = reason;
440
596
  const covered = new Promise((resolve, reject) => {
441
597
  waiting.push((failure) => {
@@ -451,12 +607,19 @@ const unpluginFactory = (options = {}, meta) => {
451
607
  debounceTimer.unref();
452
608
  return covered;
453
609
  };
610
+ const closeWatcher = () => {
611
+ hostClosed = true;
612
+ };
454
613
  return {
455
614
  async buildStart() {
615
+ adoptHost(this);
616
+ adoptWatchMode(this);
456
617
  const resolved = await resolveConfigs();
457
618
  if (resolved.length === 0) return;
458
- const { paths } = await getWatchTargets(resolved);
459
- for (const file of paths) this.addWatchFile(file);
619
+ if (!hostClosed) {
620
+ const { paths } = await getWatchTargets(resolved);
621
+ for (const file of paths) this.addWatchFile(file);
622
+ }
460
623
  if (isWebpack) return;
461
624
  if (watchRebuild && hasCompiled) {
462
625
  watchRebuild = false;
@@ -466,12 +629,44 @@ const unpluginFactory = (options = {}, meta) => {
466
629
  hasCompiled = true;
467
630
  },
468
631
  name: "unplugin-style-dictionary",
632
+ rolldown: { closeWatcher },
633
+ rollup: { closeWatcher },
469
634
  vite: {
470
- configResolved(config) {
635
+ closeWatcher,
636
+ async configResolved(config) {
471
637
  if (rootOption === void 0) root = config.root || process.cwd();
638
+ hostCommand = config.command;
639
+ hostMode = config.mode;
640
+ host = {
641
+ error: (message) => {
642
+ config.logger.error(message);
643
+ },
644
+ info: (message) => {
645
+ config.logger.info(message);
646
+ }
647
+ };
648
+ if (config.command !== "serve") return;
649
+ isWatching = true;
650
+ try {
651
+ startupResolved = await resolveConfigs();
652
+ if (startupResolved.length === 0) return;
653
+ const { paths } = await getWatchTargets(startupResolved);
654
+ const negations = nodeModulesNegations(paths);
655
+ if (negations.length === 0) return;
656
+ const existing = config.server.watch?.ignored;
657
+ config.server.watch = {
658
+ ...config.server.watch,
659
+ ignored: [...Array.isArray(existing) ? existing : existing === void 0 ? [] : [existing], ...negations]
660
+ };
661
+ } catch (err) {
662
+ log(`Could not read the configuration while preparing the watch list: ${errorMessage(err)}`, "error");
663
+ startupResolved = void 0;
664
+ }
472
665
  },
473
666
  async configureServer(server) {
474
- const resolved = await resolveConfigs();
667
+ isWatching = true;
668
+ const resolved = startupResolved ?? await resolveConfigs();
669
+ startupResolved = void 0;
475
670
  if (resolved.length === 0) return;
476
671
  let targets = await getWatchTargets(resolved);
477
672
  server.watcher.add(targets.paths);
@@ -479,6 +674,29 @@ const unpluginFactory = (options = {}, meta) => {
479
674
  targets = await getWatchTargets(rebuilt);
480
675
  server.watcher.add(targets.paths);
481
676
  };
677
+ if (errorOverlay) {
678
+ let overlayShowing = false;
679
+ notifyBuildOutcome = (error) => {
680
+ if (error) {
681
+ overlayShowing = true;
682
+ server.hot.send({
683
+ err: {
684
+ message: error.message,
685
+ plugin: "unplugin-style-dictionary",
686
+ stack: error.stack ?? ""
687
+ },
688
+ type: "error"
689
+ });
690
+ return;
691
+ }
692
+ if (!overlayShowing) return;
693
+ overlayShowing = false;
694
+ server.hot.send({
695
+ type: "update",
696
+ updates: []
697
+ });
698
+ };
699
+ }
482
700
  server.watcher.on("all", (_event, file) => {
483
701
  if (!isWatchedSource(file, targets.patterns)) return;
484
702
  schedule(path.basename(file)).catch(() => {});
@@ -486,6 +704,9 @@ const unpluginFactory = (options = {}, meta) => {
486
704
  }
487
705
  },
488
706
  async watchChange(id) {
707
+ adoptHost(this);
708
+ adoptWatchMode(this);
709
+ if (hostClosed) return;
489
710
  watchRebuild = true;
490
711
  if (cachedPatterns && !isWatchedSource(id, cachedPatterns)) return;
491
712
  const resolved = await resolveConfigs();
@@ -497,7 +718,25 @@ const unpluginFactory = (options = {}, meta) => {
497
718
  },
498
719
  webpack(compiler) {
499
720
  if (rootOption === void 0) root = compiler.options.context ?? process.cwd();
721
+ hostMode = compiler.options.mode;
722
+ const pending = [];
723
+ host = { error: (message) => {
724
+ pending.push(message);
725
+ } };
726
+ compiler.hooks.compilation.tap("unplugin-style-dictionary", (compilation) => {
727
+ for (const message of pending.splice(0)) {
728
+ const reported = new Error(message);
729
+ reported.name = "UnpluginStyleDictionaryWarning";
730
+ compilation.warnings.push(reported);
731
+ }
732
+ });
733
+ const drainToConsole = () => {
734
+ for (const message of pending.splice(0)) console.error(paint("31", message, stderrColour));
735
+ };
736
+ compiler.hooks.failed.tap("unplugin-style-dictionary", drainToConsole);
737
+ compiler.hooks.done.tap("unplugin-style-dictionary", drainToConsole);
500
738
  compiler.hooks.beforeCompile.tapPromise("unplugin-style-dictionary", async () => {
739
+ isWatching = compiler.watchMode;
501
740
  const resolved = await resolveConfigs();
502
741
  if (resolved.length === 0) return;
503
742
  await compileOnceAcrossInstances(resolved);