@kanso-labs/unplugin-style-dictionary 0.7.0 → 0.8.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/README.md CHANGED
@@ -27,6 +27,9 @@ build on Rolldown/tsdown) that both need tokens compiled ahead of them.
27
27
  - **Config flexibility**: Supports file paths (JSON, JSON5, JSONC, JS, MJS, TS),
28
28
  configuration objects, or functions — including registering custom formats at
29
29
  config-resolution time.
30
+ - **Error overlay**: A rebuild that fails under Vite's dev server is pushed to
31
+ the error overlay rather than only to the terminal, and cleared on the next
32
+ one that succeeds.
30
33
  - **Atomic writes**: Every generated file is written to a temporary sibling and
31
34
  renamed into place, so code importing a token file while it is being rebuilt
32
35
  never reads a half-written file.
@@ -191,6 +194,38 @@ source and no config file does not reach it, so a dev server editing unrelated
191
194
  project files leaves it alone — treat it as the place to prepare a build, not as
192
195
  a general file-change hook.
193
196
 
197
+ **It is handed what the host is doing**, so the configuration it returns can
198
+ depend on it — build an expensive platform on `vite build` and skip it while the
199
+ dev server runs:
200
+
201
+ ```typescript
202
+ styleDictionaryPlugin({
203
+ config: ({ command, mode, watch }) => ({
204
+ source: ['tokens/**/*.json'],
205
+ platforms: {
206
+ css: {
207
+ transformGroup: 'css',
208
+ buildPath: 'dist/',
209
+ files: [{ destination: 'vars.css', format: 'css/variables' }],
210
+ },
211
+ // Shells out to a native toolchain, so it is worth a minute of a real
212
+ // build and not worth a second of every rebuild.
213
+ ...(command === 'build' && !watch ? { ios: iosPlatform(mode) } : {}),
214
+ },
215
+ }),
216
+ })
217
+ ```
218
+
219
+ `command` is `'serve'` only under Vite's dev server; every other target builds.
220
+ `mode` is Vite's or webpack's own, and follows `command` on rollup and rolldown,
221
+ which have no such concept. `watch` is read from the host rather than inferred
222
+ from `command`, because `rollup --watch` both watches and builds. The full
223
+ contract is [`StyleDictionaryConfigContext`](#options-reference).
224
+
225
+ A function taking no arguments stays valid — TypeScript accepts one of fewer
226
+ parameters and JavaScript ignores the extra argument — so the example below
227
+ needs no change.
228
+
194
229
  ```typescript
195
230
  // Named `styleDictionaryPlugin` here to avoid colliding with the `StyleDictionary`
196
231
  // class imported from the `style-dictionary` package itself, below.
@@ -355,6 +390,41 @@ A compile that fails is reported at every level, including `'silent'`, which is
355
390
  why there is no `'error'`. `log.warnings` is never touched: if your
356
391
  configuration turns a warning into a thrown build, that stays your decision.
357
392
 
393
+ ### Where the messages go
394
+
395
+ Through your bundler, not straight to the console, and each one takes them its
396
+ own way:
397
+
398
+ | Target | Progress lines | A failed compile |
399
+ | ------------------ | ------------------------ | ---------------------------------- |
400
+ | Vite | `config.logger.info` | `config.logger.error` |
401
+ | Rollup, Rolldown | the plugin context's log | the context's warning channel |
402
+ | Webpack | the console | `compilation.warnings`, so `stats` |
403
+ | No host (one-shot) | the console | the console |
404
+
405
+ That is what makes a `customLogger` and `clearScreen` work under Vite, and what
406
+ puts a failed compile into `stats.toJson()` under webpack — where it reaches CI
407
+ annotations and anything else reading the build's own output.
408
+
409
+ **A failure is reported as a warning, never on the host's error channel.**
410
+ Rollup's `this.error` aborts the bundle, so reporting a failure through it would
411
+ stop every build that reported one — taking the decision `failOnError` exists to
412
+ make. The same reasoning puts webpack's report in `compilation.warnings` rather
413
+ than `compilation.errors`, so `failOnError: false` really does leave the build
414
+ passing.
415
+
416
+ **Your bundler's own log level applies.** `vite --logLevel silent` silences
417
+ Vite's logger, and the plugin's lines are Vite's logger's now, so they go too.
418
+ Nothing is lost by it that matters: `failOnError` decides whether a broken token
419
+ set stops the build, and it decides that whether or not anything was printed.
420
+
421
+ Colour follows the usual conventions, which it previously ignored entirely: no
422
+ escapes when `NO_COLOR` is set, or when the stream is not a terminal, or under
423
+ `TERM=dumb`; escapes when `FORCE_COLOR` is set to anything but `0`, including on
424
+ a non-terminal, which is what that variable is for. `NO_COLOR` wins over
425
+ `FORCE_COLOR`. stdout and stderr are decided separately, because they are
426
+ redirected separately.
427
+
358
428
  ## Failing the Build
359
429
 
360
430
  A token compile that fails stops the build. `vite build`, `rollup` and `webpack`
@@ -373,17 +443,68 @@ StyleDictionary({
373
443
  })
374
444
  ```
375
445
 
376
- A failure is always reported, whatever `failOnError` and `silent` are set to.
446
+ A failure is always reported by the plugin, whatever `failOnError` and `silent`
447
+ are set to — see [Where the messages go](#where-the-messages-go) for which
448
+ channel it arrives on, and for the one thing that can still suppress it.
449
+
450
+ Under Vite's dev server it is reported to the browser as well. A failed rebuild
451
+ is pushed to Vite's error overlay, naming this plugin and carrying Style
452
+ Dictionary's message, and the overlay is dismissed by the next rebuild that
453
+ succeeds — so a page left rendering the last good token file says so instead of
454
+ looking current. Set `errorOverlay: false` to keep the failure in the terminal
455
+ only.
456
+
457
+ `failOnError` and `errorOverlay` answer different questions and do not interact:
458
+ the first decides whether the host stops, the second whether the browser is
459
+ told. The dev server's default is not to stop, which is exactly when the overlay
460
+ is the only thing that can report the failure.
461
+
462
+ ## Build Hooks
463
+
464
+ Three optional callbacks, for work that has to happen around a compile rather
465
+ than inside one — formatting the generated files, type-checking them, telling
466
+ something else they have landed.
467
+
468
+ ```typescript
469
+ StyleDictionary({
470
+ config: 'sd.config.json',
471
+ onBuildStart: () => console.log('compiling tokens'),
472
+ onBuildEnd: async (files, durationMs) => {
473
+ console.log(`wrote ${files.length} files in ${durationMs}ms`)
474
+ await formatGeneratedFiles(files)
475
+ },
476
+ onBuildError: (error) => notifySomething(error),
477
+ })
478
+ ```
479
+
480
+ `files` holds the absolute, platform-native path of every file the build
481
+ declares, sorted. It is what the build declares rather than what it happened to
482
+ write this time: a configuration skipped because its output was already current
483
+ contributes its destinations too, so a post-processing step still sees the whole
484
+ set on a rebuild that changed one file.
485
+
486
+ A rebuild is a build, so all three fire again on every watch-triggered one.
487
+
488
+ **A hook cannot fail the build that called it.** The return value is not
489
+ awaited, so nothing waits for post-processing; a hook that throws is reported
490
+ and the build stands, and a promise that rejects is caught and reported rather
491
+ than reaching the host as an unhandled rejection — which would otherwise take a
492
+ dev server down from inside a step meant to reformat a file.
493
+
494
+ `onBuildError` fires whatever `failOnError` is set to, and before that option
495
+ decides whether to rethrow. The two answer different questions: one is whether
496
+ the host stops, the other is that a build went wrong.
377
497
 
378
498
  ## Public API
379
499
 
380
- Small on purpose. Four bundler entry points, one root entry, and one type.
500
+ Small on purpose. Four bundler entry points, one root entry, and two types.
381
501
 
382
502
  | Import | What it is |
383
503
  | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
384
504
  | `…/vite`, `…/rolldown`, `…/rollup`, `…/webpack` | Default export: the plugin for that bundler. Call it with the options below. |
385
505
  | `…` (the root) | Default export, also named `unplugin`: the unplugin instance, carrying `.vite`, `.rolldown`, `.rollup` and `.webpack`. |
386
506
  | `UnpluginStyleDictionaryOptions` | The options type, exported from every entry above. |
507
+ | `StyleDictionaryConfigContext` | What the function form of `config` is handed, exported from every entry above. |
387
508
 
388
509
  Anything not in that table is internal, whatever a build output happens to
389
510
  contain. In particular the watch filter and the raw unplugin factory are not
@@ -415,12 +536,72 @@ const plugin = styleDictionary.rollup({ config: 'sd.config.json' })
415
536
  * build (e.g. `tsdown`/`rolldown build` without `--watch`) only builds once, in
416
537
  * `buildStart`.
417
538
  *
539
+ * Everything the plugin says goes through the host rather than to the console:
540
+ * Vite's `config.logger`, the plugin context under rollup and rolldown, and
541
+ * `compilation.warnings` under webpack, which is what puts a failed compile in
542
+ * `stats.toJson()`. A failure is reported on the warning channel and never the
543
+ * error one — rollup's `this.error` aborts the bundle, and that decision is
544
+ * `failOnError`'s alone. Where no host offers a channel the console is used,
545
+ * with colour gated on `NO_COLOR`, `FORCE_COLOR` and whether the stream is a
546
+ * terminal.
547
+ *
548
+ * The three `onBuild*` hooks are called synchronously and their return value
549
+ * is not awaited, so a build never waits for one. A hook may still be written
550
+ * `async`: a promise it returns is left to run on its own, and a rejection is
551
+ * caught and reported rather than reaching the host as an unhandled one. A
552
+ * hook that throws is reported and does not fail the build that called it.
553
+ *
554
+ * They return `Promise<void> | void` rather than `void` for that reason. Both
555
+ * accept an `async` hook as far as the compiler is concerned, but `void` alone
556
+ * makes one a `no-misused-promises` error under the type-aware lint rules a
557
+ * consumer is likely to be running — for a hook this documents as supported.
558
+ *
418
559
  * Rolldown's watch mode is the exception, and it is not about glob patterns.
419
560
  * `addWatchFile` is accepted either way, but what happens next differs by
420
561
  * platform — on macOS a file registered through it is watched by nothing, while
421
562
  * on a Linux runner the same edit reaches a rebuild. Do not rely on a token
422
563
  * edit triggering a rebuild there.
423
564
  */
565
+ /**
566
+ * What the host is doing, handed to the function form of `config` so it can
567
+ * decide what to build.
568
+ *
569
+ * Only Vite reports all three. Where a host does not say, the value is
570
+ * derived rather than guessed at, and each field below says how.
571
+ */
572
+ export interface StyleDictionaryConfigContext {
573
+ /**
574
+ * Whether the host is serving or building.
575
+ *
576
+ * `'serve'` comes from Vite's own `config.command` and is the dev server.
577
+ * Every other target builds, so it is `'build'` there — rollup, rolldown and
578
+ * webpack have no serving mode of their own to report.
579
+ */
580
+ command: 'build' | 'serve'
581
+
582
+ /**
583
+ * The host's mode, as it names it.
584
+ *
585
+ * Vite reports its `config.mode` — `'development'` serving,
586
+ * `'production'` building, or whatever `--mode` named. webpack reports its
587
+ * `mode` option. rollup and rolldown have no such concept, so the value
588
+ * follows `command`: `'development'` when serving, `'production'` when
589
+ * building.
590
+ */
591
+ mode: string
592
+
593
+ /**
594
+ * Whether the host will keep rebuilding.
595
+ *
596
+ * `true` under Vite's dev server, `rollup --watch`, `rolldown.watch()` and
597
+ * `webpack --watch`; `false` for a one-shot build. It is read from the
598
+ * host — the plugin context's `meta.watchMode` on the three rollup-shaped
599
+ * targets, and `compiler.watchMode` on webpack — rather than inferred from
600
+ * `command`, because `rollup --watch` both watches and builds.
601
+ */
602
+ watch: boolean
603
+ }
604
+
424
605
  export interface UnpluginStyleDictionaryOptions {
425
606
  /**
426
607
  * Whether a configuration whose output is already up to date may skip its
@@ -462,18 +643,51 @@ export interface UnpluginStyleDictionaryOptions {
462
643
  * - A function that returns a config or array of configs (or resolves to them).
463
644
  * Useful for calling `StyleDictionary.registerFormat()` (or other `register*`
464
645
  * methods) before returning a config that references the custom format by name.
646
+ * It is handed a `StyleDictionaryConfigContext` describing what the host is
647
+ * doing, so an expensive platform can be built only when it is wanted —
648
+ * skipped under the dev server, built by `vite build`. A function taking no
649
+ * arguments stays valid: TypeScript accepts one of fewer parameters, and
650
+ * JavaScript ignores the extra argument.
465
651
  *
466
652
  * If not provided, the root directory is searched for 'sd.config.json',
467
653
  * 'config.json', 'sd.config.js' and 'sd.config.mjs', in that order. The
468
654
  * first one that exists wins, and the rest are not looked at.
469
655
  */
470
656
  config?:
471
- | (() => Config | Config[] | Promise<Config | Config[]>)
657
+ | ((
658
+ context: StyleDictionaryConfigContext,
659
+ ) => Config | Config[] | Promise<Config | Config[]>)
472
660
  | Config
473
661
  | Config[]
474
662
  | string
475
663
  | string[]
476
664
 
665
+ /**
666
+ * Whether a failed rebuild is pushed to Vite's error overlay.
667
+ *
668
+ * A rebuild that fails under the dev server used to reach the browser
669
+ * nowhere: the page went on rendering the last good generated file, and the
670
+ * only trace was one red terminal line the developer may not have been
671
+ * looking at. With this on, the failure is sent to the page as an error
672
+ * frame naming this plugin, and the overlay is dismissed on the next
673
+ * rebuild that succeeds.
674
+ *
675
+ * This is Vite's overlay, so it does nothing on the other three targets,
676
+ * and nothing under `vite build` — there is no page to draw on.
677
+ *
678
+ * It is not `failOnError`'s job, and the two are independent. `failOnError`
679
+ * decides whether the host stops; this decides whether the browser is told.
680
+ * A dev server deliberately keeps serving through a failed rebuild, which is
681
+ * precisely the case where the overlay is the only thing that can say so.
682
+ *
683
+ * A failure Style Dictionary raises before this plugin can catch it — a
684
+ * token file that is not valid JSON, which rejects out of band — reaches
685
+ * neither the overlay nor this option.
686
+ *
687
+ * @default true
688
+ */
689
+ errorOverlay?: boolean
690
+
477
691
  /**
478
692
  * Whether a compile that fails should throw rather than only be reported.
479
693
  *
@@ -513,11 +727,59 @@ export interface UnpluginStyleDictionaryOptions {
513
727
  * A compile that fails is reported at every level, so there is no
514
728
  * `'error'`: `'silent'` is the quietest and still reports a failure.
515
729
  *
730
+ * This option governs what the plugin says, not where it goes. The messages
731
+ * are handed to the host — Vite's `config.logger`, the rollup and rolldown
732
+ * plugin context, webpack's `compilation` — so a host silenced by its own
733
+ * log level suppresses them after this option has let them through. A
734
+ * failure still stops the build whenever `failOnError` says it should,
735
+ * printed or not.
736
+ *
516
737
  * @default undefined, which prints the plugin's own lines and leaves the
517
738
  * configuration's `log.verbosity` alone
518
739
  */
519
740
  logLevel?: 'info' | 'silent' | 'verbose' | 'warn'
520
741
 
742
+ /**
743
+ * Called once a build has finished, with every file it declares and how long
744
+ * it took in milliseconds.
745
+ *
746
+ * The paths are absolute and platform-native, sorted so two runs of the same
747
+ * configuration hand back the same order. They are what the build declares
748
+ * rather than what it wrote this time: a configuration skipped by `cache`
749
+ * contributes its destinations too, because they are on disk and current,
750
+ * and a post-processing step that ignored them would leave half the output
751
+ * untouched on a rebuild that changed one file.
752
+ *
753
+ * This is where formatting the generated files, type-checking them, or
754
+ * telling something else they have landed belongs.
755
+ *
756
+ * @default undefined
757
+ */
758
+ onBuildEnd?: (files: string[], durationMs: number) => Promise<void> | void
759
+
760
+ /**
761
+ * Called when a build fails, with whatever was thrown.
762
+ *
763
+ * It fires whatever `failOnError` is set to, and before that option decides
764
+ * whether to rethrow — the two answer different questions, and under a dev
765
+ * server the default is not to throw at all.
766
+ *
767
+ * The failure is reported to the console either way, so this is for reacting
768
+ * to one rather than for noticing it.
769
+ *
770
+ * @default undefined
771
+ */
772
+ onBuildError?: (error: unknown) => Promise<void> | void
773
+
774
+ /**
775
+ * Called before a build begins, once per build.
776
+ *
777
+ * A watch-triggered rebuild is a build, so this fires again for each one.
778
+ *
779
+ * @default undefined
780
+ */
781
+ onBuildStart?: () => Promise<void> | void
782
+
521
783
  /**
522
784
  * Whether the table of generated files and their sizes is produced.
523
785
  *
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { UnpluginStyleDictionaryOptions } from "./types.js";
1
+ import { StyleDictionaryConfigContext, UnpluginStyleDictionaryOptions } from "./types.js";
2
2
  //#region src/index.d.ts
3
3
  export declare const unplugin: import("unplugin").UnpluginInstance<UnpluginStyleDictionaryOptions | undefined, false>;
4
4
  //#endregion
5
- export { type UnpluginStyleDictionaryOptions, unplugin as default };
5
+ export { type StyleDictionaryConfigContext, type UnpluginStyleDictionaryOptions, unplugin as default };
6
6
  //# sourceMappingURL=index.d.ts.map
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,15 @@ 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 paint(code, value, allowed) {
40
+ return allowed ? `\u001B[${code}m${value}\u001B[0m` : value;
41
+ }
25
42
  function unwrapDefault(value) {
26
43
  return typeof value === "object" && value !== null && "default" in value ? value.default ?? value : value;
27
44
  }
@@ -123,11 +140,25 @@ function statOrNull(file) {
123
140
  }
124
141
  const unpluginFactory = (options = {}, meta) => {
125
142
  const isWebpack = meta.framework === "webpack";
126
- const { cache = true, failOnError = "build", logLevel, report = true, root: rootOption, silent = false } = options;
143
+ const { cache = true, errorOverlay = true, failOnError = "build", logLevel, onBuildEnd, onBuildError, onBuildStart, report = true, root: rootOption, silent = false } = options;
127
144
  const level = logLevel ?? (silent ? "silent" : void 0);
128
145
  const quiet = level === "silent" || level === "warn";
129
146
  const verbosity = level === void 0 ? void 0 : level === "verbose" ? "verbose" : level === "silent" ? "silent" : "default";
130
147
  const failsTheBuild = (context) => failOnError === true || (context === void 0 ? failOnError === "build" : failOnError === "serve");
148
+ let hostCommand = "build";
149
+ let hostMode;
150
+ let isWatching = false;
151
+ const configContext = () => ({
152
+ command: hostCommand,
153
+ mode: hostMode ?? (hostCommand === "serve" ? "development" : "production"),
154
+ watch: isWatching
155
+ });
156
+ const adoptWatchMode = (context) => {
157
+ const hookMeta = "meta" in context ? context.meta : void 0;
158
+ if (typeof hookMeta !== "object" || hookMeta === null) return;
159
+ const watching = "watchMode" in hookMeta ? hookMeta.watchMode : void 0;
160
+ if (typeof watching === "boolean") isWatching = watching;
161
+ };
131
162
  let root = rootOption ? path.resolve(process.cwd(), rootOption) : process.cwd();
132
163
  const generatedDestinations = /* @__PURE__ */ new Set();
133
164
  let cachedPatterns;
@@ -149,15 +180,52 @@ const unpluginFactory = (options = {}, meta) => {
149
180
  return Array.from(paths);
150
181
  };
151
182
  const isWatchedSource = (file, patterns) => !generatedDestinations.has(file.replace(/\\/g, "/")) && matchesWatchedFile(file, patterns);
183
+ const stdoutColour = colourAllowed(process.stdout);
184
+ const stderrColour = colourAllowed(process.stderr);
185
+ let host;
186
+ const adoptHost = (context) => {
187
+ if (host) return;
188
+ const warn = "warn" in context ? context.warn : void 0;
189
+ if (!isMessageChannel(warn)) return;
190
+ const info = "info" in context ? context.info : void 0;
191
+ host = {
192
+ error: (message) => {
193
+ warn.call(context, message);
194
+ },
195
+ info: isMessageChannel(info) ? (message) => {
196
+ info.call(context, message);
197
+ } : void 0
198
+ };
199
+ };
152
200
  const log = (message, type = "info") => {
153
201
  const prefix = "[unplugin-style-dictionary]";
154
202
  if (type === "error") {
155
- console.error(`\x1b[31m${prefix} ${message}\x1b[0m`);
203
+ if (host) {
204
+ host.error(`${prefix} ${message}`);
205
+ return;
206
+ }
207
+ console.error(paint("31", `${prefix} ${message}`, stderrColour));
156
208
  return;
157
209
  }
158
210
  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`);
211
+ if (host?.info) {
212
+ host.info(`${prefix} ${message}`);
213
+ return;
214
+ }
215
+ console.log(paint(type === "success" ? "32" : "36", `${prefix} ${message}`, stdoutColour));
216
+ };
217
+ const callHook = (name, hook, ...args) => {
218
+ let result;
219
+ try {
220
+ result = hook(...args);
221
+ } catch (err) {
222
+ log(`The ${name} hook threw: ${errorMessage(err)}`, "error");
223
+ return;
224
+ }
225
+ if (!isThenable(result)) return;
226
+ Promise.resolve(result).catch((err) => {
227
+ log(`The ${name} hook rejected: ${errorMessage(err)}`, "error");
228
+ });
161
229
  };
162
230
  const resolveConfigs = async () => {
163
231
  let rawConfig = options.config;
@@ -177,7 +245,7 @@ const unpluginFactory = (options = {}, meta) => {
177
245
  log("No configuration specified and no default config file found. Style Dictionary will not compile.", "error");
178
246
  return [];
179
247
  }
180
- if (typeof rawConfig === "function") rawConfig = await rawConfig();
248
+ if (typeof rawConfig === "function") rawConfig = await rawConfig(configContext());
181
249
  return (Array.isArray(rawConfig) ? rawConfig : [rawConfig]).map((conf) => {
182
250
  if (typeof conf === "string") {
183
251
  const fullPath = path.resolve(root, conf);
@@ -307,7 +375,7 @@ const unpluginFactory = (options = {}, meta) => {
307
375
  const displayPath = path.relative(root, filePath).replace(/\\/g, "/");
308
376
  const dir = path.dirname(displayPath);
309
377
  const base = path.basename(displayPath);
310
- const coloredPath = dir === "." ? `\x1b[32m${base}\x1b[0m` : `\x1b[90m${dir}/\x1b[0m\x1b[32m${base}\x1b[0m`;
378
+ const coloredPath = dir === "." ? paint("32", base, stdoutColour) : paint("90", `${dir}/`, stdoutColour) + paint("32", base, stdoutColour);
311
379
  try {
312
380
  const sizeStr = `${(fs.statSync(filePath).size / 1024).toFixed(2)} kB`;
313
381
  const content = fs.readFileSync(filePath);
@@ -326,7 +394,7 @@ const unpluginFactory = (options = {}, meta) => {
326
394
  for (const info of fileInfos) {
327
395
  const pathPadding = " ".repeat(Math.max(2, longestPathLength - info.relativeDisplayPath.length + 2));
328
396
  const sizePadded = info.sizeStr.padStart(longestSizeLength);
329
- console.log(`${info.coloredPath}${pathPadding}\x1b[90m${sizePadded} │ gzip: ${info.gzipSizeStr}\x1b[0m`);
397
+ console.log(info.coloredPath + pathPadding + paint("90", `${sizePadded} │ gzip: ${info.gzipSizeStr}`, stdoutColour));
330
398
  }
331
399
  }
332
400
  };
@@ -336,6 +404,7 @@ const unpluginFactory = (options = {}, meta) => {
336
404
  let skipped = 0;
337
405
  try {
338
406
  if (!context) log("Compiling design tokens...", "info");
407
+ if (onBuildStart) callHook("onBuildStart", onBuildStart);
339
408
  for (const item of resolvedConfigs) {
340
409
  const declared = cache ? await readConfigObject(item, false) : null;
341
410
  if (declared && await isUpToDate(item, declared)) {
@@ -366,10 +435,17 @@ const unpluginFactory = (options = {}, meta) => {
366
435
  } catch (err) {
367
436
  const duration = Date.now() - startTime;
368
437
  log(`Compilation failed after ${duration}ms: ${errorMessage(err)}`, "error");
438
+ notifyBuildOutcome?.(asError(err));
439
+ if (onBuildError) callHook("onBuildError", onBuildError, err);
369
440
  if (failsTheBuild(context)) throw err;
370
441
  return;
371
442
  }
443
+ notifyBuildOutcome?.(null);
372
444
  const duration = Date.now() - startTime;
445
+ if (onBuildEnd) {
446
+ const files = Array.from(generatedFiles).sort((left, right) => left.localeCompare(right));
447
+ callHook("onBuildEnd", onBuildEnd, files, duration);
448
+ }
373
449
  const everythingSkipped = skipped === resolvedConfigs.length;
374
450
  if (context) {
375
451
  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 +487,7 @@ const unpluginFactory = (options = {}, meta) => {
411
487
  let inFlight;
412
488
  let waiting = [];
413
489
  let refreshServerWatchList;
490
+ let notifyBuildOutcome;
414
491
  const drain = async () => {
415
492
  while (pendingReason !== void 0) {
416
493
  const reason = pendingReason;
@@ -430,7 +507,10 @@ const unpluginFactory = (options = {}, meta) => {
430
507
  }
431
508
  } catch (err) {
432
509
  failure = { error: err };
433
- if (!compiling) log(`Rebuild failed: ${errorMessage(err)}`, "error");
510
+ if (!compiling) {
511
+ log(`Rebuild failed: ${errorMessage(err)}`, "error");
512
+ notifyBuildOutcome?.(asError(err));
513
+ }
434
514
  }
435
515
  for (const settle of resolvers) settle(failure);
436
516
  }
@@ -453,6 +533,8 @@ const unpluginFactory = (options = {}, meta) => {
453
533
  };
454
534
  return {
455
535
  async buildStart() {
536
+ adoptHost(this);
537
+ adoptWatchMode(this);
456
538
  const resolved = await resolveConfigs();
457
539
  if (resolved.length === 0) return;
458
540
  const { paths } = await getWatchTargets(resolved);
@@ -469,8 +551,19 @@ const unpluginFactory = (options = {}, meta) => {
469
551
  vite: {
470
552
  configResolved(config) {
471
553
  if (rootOption === void 0) root = config.root || process.cwd();
554
+ hostCommand = config.command;
555
+ hostMode = config.mode;
556
+ host = {
557
+ error: (message) => {
558
+ config.logger.error(message);
559
+ },
560
+ info: (message) => {
561
+ config.logger.info(message);
562
+ }
563
+ };
472
564
  },
473
565
  async configureServer(server) {
566
+ isWatching = true;
474
567
  const resolved = await resolveConfigs();
475
568
  if (resolved.length === 0) return;
476
569
  let targets = await getWatchTargets(resolved);
@@ -479,6 +572,29 @@ const unpluginFactory = (options = {}, meta) => {
479
572
  targets = await getWatchTargets(rebuilt);
480
573
  server.watcher.add(targets.paths);
481
574
  };
575
+ if (errorOverlay) {
576
+ let overlayShowing = false;
577
+ notifyBuildOutcome = (error) => {
578
+ if (error) {
579
+ overlayShowing = true;
580
+ server.hot.send({
581
+ err: {
582
+ message: error.message,
583
+ plugin: "unplugin-style-dictionary",
584
+ stack: error.stack ?? ""
585
+ },
586
+ type: "error"
587
+ });
588
+ return;
589
+ }
590
+ if (!overlayShowing) return;
591
+ overlayShowing = false;
592
+ server.hot.send({
593
+ type: "update",
594
+ updates: []
595
+ });
596
+ };
597
+ }
482
598
  server.watcher.on("all", (_event, file) => {
483
599
  if (!isWatchedSource(file, targets.patterns)) return;
484
600
  schedule(path.basename(file)).catch(() => {});
@@ -486,6 +602,8 @@ const unpluginFactory = (options = {}, meta) => {
486
602
  }
487
603
  },
488
604
  async watchChange(id) {
605
+ adoptHost(this);
606
+ adoptWatchMode(this);
489
607
  watchRebuild = true;
490
608
  if (cachedPatterns && !isWatchedSource(id, cachedPatterns)) return;
491
609
  const resolved = await resolveConfigs();
@@ -497,7 +615,25 @@ const unpluginFactory = (options = {}, meta) => {
497
615
  },
498
616
  webpack(compiler) {
499
617
  if (rootOption === void 0) root = compiler.options.context ?? process.cwd();
618
+ hostMode = compiler.options.mode;
619
+ const pending = [];
620
+ host = { error: (message) => {
621
+ pending.push(message);
622
+ } };
623
+ compiler.hooks.compilation.tap("unplugin-style-dictionary", (compilation) => {
624
+ for (const message of pending.splice(0)) {
625
+ const reported = new Error(message);
626
+ reported.name = "UnpluginStyleDictionaryWarning";
627
+ compilation.warnings.push(reported);
628
+ }
629
+ });
630
+ const drainToConsole = () => {
631
+ for (const message of pending.splice(0)) console.error(paint("31", message, stderrColour));
632
+ };
633
+ compiler.hooks.failed.tap("unplugin-style-dictionary", drainToConsole);
634
+ compiler.hooks.done.tap("unplugin-style-dictionary", drainToConsole);
500
635
  compiler.hooks.beforeCompile.tapPromise("unplugin-style-dictionary", async () => {
636
+ isWatching = compiler.watchMode;
501
637
  const resolved = await resolveConfigs();
502
638
  if (resolved.length === 0) return;
503
639
  await compileOnceAcrossInstances(resolved);