@kanso-labs/unplugin-style-dictionary 0.10.18 → 0.10.20

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
@@ -25,9 +25,8 @@ them.
25
25
  bullet follows it rather than standing beside it.
26
26
  - **Automatic watching**: Reads the `source` and `include` patterns from your
27
27
  Style Dictionary configurations and watches the files they match, including a
28
- token package resolved through `node_modules` in a workspace — except under
29
- `vite build --watch` on Vite 8, whose build watcher is rolldown's. What a
30
- change then triggers depends on the target — see
28
+ token package resolved through `node_modules` in a workspace. What a change
29
+ then triggers depends on the target — see
31
30
  [Watching, per target](#watching-per-target).
32
31
  - **Config flexibility**: Supports file paths (JSON, JSON5, JSONC, JS, MJS, TS),
33
32
  configuration objects, or functions — including registering custom formats at
@@ -74,10 +73,12 @@ npm error Could not resolve dependency:
74
73
  npm error peer style-dictionary@"^5.0.0" from @kanso-labs/unplugin-style-dictionary@0.9.0
75
74
  ```
76
75
 
77
- **This package needs Node 22.12 or newer.** The floor is Style Dictionary v5's,
78
- not this plugin's: every 5.x release declares `engines.node >= 22.0.0`, and it
79
- is a required peer rather than an optional one, so an older Node cannot install
80
- a working set at all. Node 20 reached end of life on 30 April 2026.
76
+ **This package needs Node 22.12 or newer.** The floor is where two ranges meet,
77
+ neither of them this plugin's own: `unplugin`, a runtime dependency, declares
78
+ `engines.node` `^20.19.0 || >=22.12.0`, and every Style Dictionary 5.x release,
79
+ a required peer rather than an optional one, declares `>=22.0.0`. Only 22.12 and
80
+ newer satisfies both, so an older Node cannot install a working set at all. Node
81
+ 20 reached end of life on 30 April 2026.
81
82
 
82
83
  ## Usage
83
84
 
@@ -99,6 +100,13 @@ export default defineConfig({
99
100
  })
100
101
  ```
101
102
 
103
+ Keep the generated files out of Vite's `build.outDir`, which is `dist` unless
104
+ you change it. When that directory sits inside the project root, `vite build`
105
+ empties it once this plugin has compiled, and again on every
106
+ `vite build --watch` rebuild, so tokens written there are gone by the time the
107
+ build finishes. Write them somewhere else, such as `build/`, or set
108
+ `build.emptyOutDir` to `false`.
109
+
102
110
  ### Rolldown
103
111
 
104
112
  ```typescript
@@ -118,8 +126,8 @@ typically run as a one-shot build rather than a long-lived dev server. There the
118
126
  plugin compiles tokens once in `buildStart`, which is enough to guarantee
119
127
  generated token files exist before the rest of the build consumes them.
120
128
 
121
- Under a real `rolldown.watch()`, do not rely on a token edit triggering a
122
- rebuild — see [Watching, per target](#watching-per-target).
129
+ Under a real `rolldown.watch()`, a token edit rebuilds, as it does under the
130
+ other targets' watch modes — see [Watching, per target](#watching-per-target).
123
131
 
124
132
  ### Rollup / Webpack / Rspack
125
133
 
@@ -236,7 +244,7 @@ StyleDictionary({
236
244
  platforms: {
237
245
  css: {
238
246
  transformGroup: 'css',
239
- buildPath: 'dist/css/',
247
+ buildPath: 'build/css/',
240
248
  files: [{ destination: 'variables.css', format: 'css/variables' }],
241
249
  },
242
250
  },
@@ -270,7 +278,7 @@ styleDictionaryPlugin({
270
278
  platforms: {
271
279
  css: {
272
280
  transformGroup: 'css',
273
- buildPath: 'dist/',
281
+ buildPath: 'build/',
274
282
  files: [{ destination: 'vars.css', format: 'css/variables' }],
275
283
  },
276
284
  // Shells out to a native toolchain, so it is worth a minute of a real
@@ -314,7 +322,7 @@ export default defineConfig({
314
322
  platforms: {
315
323
  custom: {
316
324
  transformGroup: 'css',
317
- buildPath: 'dist/',
325
+ buildPath: 'build/',
318
326
  files: [
319
327
  { destination: 'tokens.txt', format: 'custom/my-format' },
320
328
  ],
@@ -350,26 +358,28 @@ Every target compiles tokens before the build that consumes them. What a later
350
358
  change to a token file triggers is not the same everywhere, because it depends
351
359
  on what the host bundler does with the watch list the plugin registers.
352
360
 
353
- | Target | Compiles before the build | Rebuilds on a token change | Safe from rebuild loops |
354
- | ------------------------- | ------------------------- | --------------------------------- | ----------------------- |
355
- | **Vite** | yes | yes, under the dev server | yes |
356
- | **Vite**, `build --watch` | yes | yes on 6 and 7; on 8, as Rolldown | yes |
357
- | **Rollup** | yes | yes, under `rollup --watch` | yes |
358
- | **Webpack** | yes | yes, under `webpack --watch` | yes |
359
- | **Rspack** | yes | yes, under `rspack --watch` | yes |
360
- | **Rolldown** | yes | platform-dependent — see below | yes |
361
+ | Target | Compiles before the build | Rebuilds on a token change | Safe from rebuild loops |
362
+ | ------------------------- | ------------------------- | ----------------------------- | ----------------------- |
363
+ | **Vite** | yes | yes, under the dev server | yes |
364
+ | **Vite**, `build --watch` | yes | yes, on 6, 7 and 8 | yes |
365
+ | **Rollup** | yes | yes, under `rollup --watch` | yes |
366
+ | **Webpack** | yes | yes, under `webpack --watch` | yes |
367
+ | **Rspack** | yes | yes, under `rspack --watch` | yes |
368
+ | **Rolldown** | yes | yes, under `rolldown.watch()` | yes |
361
369
 
362
370
  Patterns and literal paths behave the same way wherever rebuilds happen at all.
363
371
  A `source` of `tokens/**/*.json` matches a file sitting directly in `tokens/` as
364
372
  well as one in a subdirectory, and a file created after the watcher started is
365
373
  picked up too.
366
374
 
367
- **Rolldown is the exception, and it is not about globs.** `this.addWatchFile()`
368
- is accepted by rolldown either way, and what happens next differs by platform:
369
- on macOS a file registered through it is watched by nothing, so a token edit
370
- reaches no hook, while on a Linux runner the same edit reaches a rebuild. Treat
371
- rolldown's watch mode as compiling once and not tracking tokens, and reach for a
372
- one-shot build or another target if you need rebuild-on-change.
375
+ **Rolldown rebuilds on macOS too, a token reached through a symbolic link
376
+ included.** Its watcher there drops the events for a path registered through a
377
+ link — a workspace package linked into `node_modules`, or a project under `/var`
378
+ or `/tmp`, both of which are links on macOS — so the plugin registers each such
379
+ path by its realpath as well, and maps the event back to the spelling your
380
+ `source` patterns use. This table used to call rolldown platform-dependent, and
381
+ it was the link: the measurement behind that ran in a directory under `/var`.
382
+ The same cases rebuild on Linux and on Windows, where CI runs them.
373
383
 
374
384
  **A token package resolved through `node_modules` is watched too, and that took
375
385
  a fix.** In a workspace — `app/node_modules/@acme/tokens` symlinked to
@@ -384,8 +394,9 @@ is thousands of files no token build reads.
384
394
  `vite build --watch` had the same problem and gets the same fix on Vite 6 and 7,
385
395
  which build on rollup's watcher and its chokidar ignore list: the plugin appends
386
396
  the same negations to `build.watch.chokidar.ignored`. Vite 8 builds with
387
- rolldown, which takes no ignore list from there, so what the Rolldown note above
388
- says holds for it too.
397
+ rolldown, which takes no ignore list from there and needs none: a linked token
398
+ package rebuilds there through the realpath registration the Rolldown note above
399
+ describes.
389
400
 
390
401
  Nothing is needed from you for that. If you had worked around it with a
391
402
  `server.watch.ignored` or `build.watch.chokidar.ignored` negation of your own,
@@ -442,6 +453,14 @@ So treat the build directory as disposable: delete it when a configuration
442
453
  changes shape, and keep it out of version control and out of any directory
443
454
  holding hand-written files.
444
455
 
456
+ Keep it out of the bundler's own output directory as well, which is the opposite
457
+ problem. Vite empties `build.outDir` when it starts writing its bundle, after
458
+ this plugin has compiled, whenever that directory sits inside the project root —
459
+ `dist`, by default — so a `buildPath` under it leaves nothing behind after
460
+ `vite build`, and nothing after a `vite build --watch` rebuild either. An
461
+ `outDir` outside the root is not emptied unless `build.emptyOutDir` asks for it.
462
+ Put the `buildPath` somewhere else, or set `build.emptyOutDir` to `false`.
463
+
445
464
  There is deliberately no `clean` option. Style Dictionary's
446
465
  `cleanAllPlatforms()` does not solve this — it removes the destinations the
447
466
  _current_ configuration declares, which are exactly the files that are not
@@ -476,8 +495,19 @@ the nearest `package.json`, so it survives a restart. Where there is no record,
476
495
  the files a configuration reads are compared against the files it declares.
477
496
 
478
497
  A declared file that changed since the compile that wrote it is compiled over
479
- rather than trusted, whatever its mtime says. Generated output is disposable, so
480
- an edit made to it by hand does not survive the next build.
498
+ rather than trusted, whatever its mtime says. Each compile records the mtime,
499
+ size and inode of every file it wrote, and a file that no longer has all three
500
+ is built again: a rewrite of another length moves the size, and a file replaced
501
+ by a rename, which is how atomic-save editors and `rsync` write, moves the
502
+ inode, even where the mtime was put back or never moved. Generated output is
503
+ disposable, so an edit made to it by hand does not survive the next build.
504
+
505
+ One rewrite is still not seen: one of the same length, made in place, inside the
506
+ timestamp tick the compile wrote in — about 15ms on Windows, and a whole second
507
+ or more on HFS+, ext3 and FAT. It moves none of the three, and only reading
508
+ every output on every build could catch it, which is the cost the skip exists to
509
+ avoid. Set `cache: false` where something rewrites generated output that
510
+ quickly.
481
511
 
482
512
  Two cases never skip, because neither can be settled from the filesystem:
483
513
 
@@ -709,12 +739,12 @@ the one that broke the build.
709
739
 
710
740
  Small on purpose. Five bundler entry points, one root entry, and two types.
711
741
 
712
- | Import | What it is |
713
- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
714
- | `…/vite`, `…/rolldown`, `…/rollup`, `…/rspack`, `…/webpack` | Default export: the plugin for that bundler. Call it with the options below. |
715
- | `…` (the root) | Default export, also named `unplugin`: the unplugin instance, carrying `.vite`, `.rolldown`, `.rollup`, `.rspack` and `.webpack`. |
716
- | `UnpluginStyleDictionaryOptions` | The options type, exported from every entry above. |
717
- | `StyleDictionaryConfigContext` | What the function form of `config` is handed, exported from every entry above. |
742
+ | Import | What it is |
743
+ | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
744
+ | `…/vite`, `…/rolldown`, `…/rollup`, `…/rspack`, `…/webpack` | Default export: the plugin for that bundler. Call it with the options below. |
745
+ | `…` (the root) | Default export, also named `unplugin`: the unplugin instance. Its `.vite`, `.rolldown`, `.rollup`, `.rspack` and `.webpack` are the five supported targets, the same plugins the subpaths export. It also carries unplugin's own `.esbuild`, `.farm`, `.unloader`, `.rsbuild`, `.bun` and `.raw`, which are not supported here: `.esbuild` throws on its first build, and nothing tests the others. |
746
+ | `UnpluginStyleDictionaryOptions` | The options type, exported from every entry above. |
747
+ | `StyleDictionaryConfigContext` | What the function form of `config` is handed, exported from every entry above. |
718
748
 
719
749
  Anything not in that table is internal, whatever a build output happens to
720
750
  contain. In particular the watch filter and the raw unplugin factory are not
@@ -723,62 +753,27 @@ takes a second `meta` argument — the bundler-identifying `UnpluginContextMeta`
723
753
  that a consumer would have to construct by hand, so calling it the obvious way
724
754
  is a type error rather than a plugin.
725
755
 
726
- Reach for the root entry when you need a target that has no subpath of its own,
727
- or when one configuration object feeds more than one bundler:
756
+ Reach for the root entry when one configuration object feeds more than one
757
+ bundler:
728
758
 
729
759
  ```typescript
730
760
  import styleDictionary from '@kanso-labs/unplugin-style-dictionary'
731
761
 
732
- const plugin = styleDictionary.rollup({ config: 'sd.config.json' })
762
+ const options = { config: 'sd.config.json' }
763
+
764
+ const forRollup = styleDictionary.rollup(options)
765
+ const forWebpack = styleDictionary.webpack(options)
733
766
  ```
734
767
 
768
+ It is not a way to reach a bundler that has no subpath. Every target without one
769
+ is unplugin's own and unsupported here. esbuild in particular fails outright:
770
+ the plugin registers its watch list before it compiles, and unplugin's esbuild
771
+ context throws on that registration, so `.esbuild` writes nothing even for a
772
+ one-shot build.
773
+
735
774
  ## Options Reference
736
775
 
737
776
  ````typescript
738
- /**
739
- * Options for the Style Dictionary unplugin factory, shared across all bundler
740
- * targets (Vite, Rolldown, Rollup, Webpack).
741
- *
742
- * Every target compiles tokens before the build that consumes them. Live
743
- * rebuild-on-change is driven by the host bundler's watch mode, because token
744
- * source files sit outside the module graph: Vite's dev server, `rollup
745
- * --watch` and `webpack --watch` all rebuild on a token change, and a one-shot
746
- * build (e.g. `tsdown`/`rolldown build` without `--watch`) only builds once, in
747
- * `buildStart`.
748
- *
749
- * What the plugin says goes through the host wherever the host has a channel
750
- * for it: Vite's `config.logger`, the plugin context under rollup and
751
- * rolldown, and `compilation.warnings` under webpack and rspack, which is what
752
- * puts a failed compile in `stats.toJson()`. The progress lines and the size
753
- * table take the host's `info` channel, and webpack and rspack have none, so
754
- * there both go to the console. A failure is reported on the warning channel
755
- * and never the error one — rollup's `this.error` aborts the bundle, and that
756
- * decision is `failOnError`'s alone. Where no host offers a channel the console
757
- * is used, with colour gated on `NO_COLOR`, `FORCE_COLOR` and whether the
758
- * stream is a terminal.
759
- *
760
- * Style Dictionary's own lines are not the plugin's. The platform name and the
761
- * `✔︎` for each file it wrote are its own `console` calls, so they reach the
762
- * console under every host, whatever the host's log level. `logLevel: 'silent'`
763
- * quiets them, as does `log.verbosity: 'silent'` in the configuration.
764
- *
765
- * The three `onBuild*` hooks are called synchronously and their return value
766
- * is not awaited, so a build never waits for one. A hook may still be written
767
- * `async`: a promise it returns is left to run on its own, and a rejection is
768
- * caught and reported rather than reaching the host as an unhandled one. A
769
- * hook that throws is reported and does not fail the build that called it.
770
- *
771
- * They return `Promise<void> | void` rather than `void` for that reason. Both
772
- * accept an `async` hook as far as the compiler is concerned, but `void` alone
773
- * makes one a `no-misused-promises` error under the type-aware lint rules a
774
- * consumer is likely to be running — for a hook this documents as supported.
775
- *
776
- * Rolldown's watch mode is the exception, and it is not about glob patterns.
777
- * `addWatchFile` is accepted either way, but what happens next differs by
778
- * platform — on macOS a file registered through it is watched by nothing, while
779
- * on a Linux runner the same edit reaches a rebuild. Do not rely on a token
780
- * edit triggering a rebuild there.
781
- */
782
777
  /**
783
778
  * What the host is doing, handed to the function form of `config` so it can
784
779
  * decide what to build.
@@ -819,6 +814,51 @@ export interface StyleDictionaryConfigContext {
819
814
  watch: boolean
820
815
  }
821
816
 
817
+ /**
818
+ * Options for the Style Dictionary unplugin factory, shared across all bundler
819
+ * targets (Vite, Rolldown, Rollup, Webpack, Rspack).
820
+ *
821
+ * Every target compiles tokens before the build that consumes them. Live
822
+ * rebuild-on-change is driven by the host bundler's watch mode, because token
823
+ * source files sit outside the module graph: Vite's dev server, `rollup
824
+ * --watch`, `rolldown.watch()`, `webpack --watch` and `rspack --watch` all
825
+ * rebuild on a token change, and a one-shot build (e.g. `tsdown`/`rolldown
826
+ * build` without `--watch`) only builds once, in `buildStart`.
827
+ *
828
+ * What the plugin says goes through the host wherever the host has a channel
829
+ * for it: Vite's `config.logger`, the plugin context under rollup and
830
+ * rolldown, and `compilation.warnings` under webpack and rspack, which is what
831
+ * puts a failed compile in `stats.toJson()`. The progress lines and the size
832
+ * table take the host's `info` channel, and webpack and rspack have none, so
833
+ * there both go to the console. A failure is reported on the warning channel
834
+ * and never the error one — rollup's `this.error` aborts the bundle, and that
835
+ * decision is `failOnError`'s alone. Where no host offers a channel the console
836
+ * is used, with colour gated on `NO_COLOR`, `FORCE_COLOR` and whether the
837
+ * stream is a terminal.
838
+ *
839
+ * Style Dictionary's own lines are not the plugin's. The platform name and the
840
+ * `✔︎` for each file it wrote are its own `console` calls, so they reach the
841
+ * console under every host, whatever the host's log level. `logLevel: 'silent'`
842
+ * quiets them, as does `log.verbosity: 'silent'` in the configuration.
843
+ *
844
+ * The three `onBuild*` hooks are called synchronously and their return value
845
+ * is not awaited, so a build never waits for one. A hook may still be written
846
+ * `async`: a promise it returns is left to run on its own, and a rejection is
847
+ * caught and reported rather than reaching the host as an unhandled one. A
848
+ * hook that throws is reported and does not fail the build that called it.
849
+ *
850
+ * They return `Promise<void> | void` rather than `void` for that reason. Both
851
+ * accept an `async` hook as far as the compiler is concerned, but `void` alone
852
+ * makes one a `no-misused-promises` error under the type-aware lint rules a
853
+ * consumer is likely to be running — for a hook this documents as supported.
854
+ *
855
+ * Rolldown's watch mode rebuilds on macOS as well, a token reached through a
856
+ * symbolic link included. Its watcher there drops the events for a path
857
+ * registered through a link — a workspace package linked into `node_modules`,
858
+ * or a project under `/var` or `/tmp`, both links on macOS — so each such path
859
+ * is registered by its realpath beside it, and the event is mapped back to the
860
+ * spelling the `source` patterns use.
861
+ */
822
862
  export interface UnpluginStyleDictionaryOptions {
823
863
  /**
824
864
  * Whether a configuration whose output is already up to date may skip its
@@ -926,7 +966,7 @@ export interface UnpluginStyleDictionaryOptions {
926
966
  * frame naming this plugin, and the overlay is dismissed on the next
927
967
  * rebuild that succeeds.
928
968
  *
929
- * This is Vite's overlay, so it does nothing on the other three targets,
969
+ * This is Vite's overlay, so it does nothing on the other four targets,
930
970
  * and nothing under `vite build` — there is no page to draw on.
931
971
  *
932
972
  * It is not `failOnError`'s job, and the two are independent. `failOnError`
@@ -934,9 +974,9 @@ export interface UnpluginStyleDictionaryOptions {
934
974
  * A dev server deliberately keeps serving through a failed rebuild, which is
935
975
  * precisely the case where the overlay is the only thing that can say so.
936
976
  *
937
- * A failure Style Dictionary raises before this plugin can catch it — a
938
- * token file that is not valid JSON, which rejects out of band — reaches
939
- * neither the overlay nor this option.
977
+ * A token file that fails to parse is reported like any other failed
978
+ * compile: it reaches the overlay, and `onBuildError`, with the parser's
979
+ * message.
940
980
  *
941
981
  * @default true
942
982
  */
@@ -1 +1 @@
1
- {"version":3,"file":"compile.js","names":[],"sources":["../src/compile.ts"],"sourcesContent":["// One compile of every resolved configuration: building each one through the\n// atomic volume, recording what it wrote, and saying how it went.\n//\n// Nothing here holds state. What a plugin instance owns is handed in as a\n// `PluginInstance`, which the factory builds once, and the two parts of it\n// that are assigned after the factory has run — `root` and the overlay\n// callback — are reached through it when a compile runs rather than copied\n// when it was built.\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport StyleDictionary from 'style-dictionary'\n\nimport type { ResolvedConfig } from './config.js'\nimport type { UnpluginStyleDictionaryOptions } from './types.js'\nimport type { DestinationRecord } from './up-to-date.js'\n\nimport { configForBuild, describeConfig, readConfigObject } from './config.js'\nimport { asError, errorMessage } from './errors.js'\nimport { patternsMatchingNothing, sourcePatternsOf } from './patterns.js'\nimport { sizeTable } from './size-report.js'\nimport {\n configFingerprint,\n declaredDestinations,\n isUpToDate,\n newestSourceOf,\n readPersistedRecords,\n recordOf,\n recordsFileFor,\n writePersistedRecords,\n writtenDestination,\n} from './up-to-date.js'\n\n// What `runBuilds` reads from the plugin instance it compiles for.\n//\n// `root` and `notifyBuildOutcome` are functions for the same reason: each is\n// assigned after the factory has run — `root` by `adoptCompiler` or\n// `configResolved`, the overlay callback by `configureServer` — so a value\n// copied when this was built would be the working directory and `undefined`\n// for good. `log` and `logTable` read the message host when they are called,\n// for the same reason. `generatedDestinations` is the instance's own record of what it\n// wrote, and `destinationRecords` is the process's record of which\n// configuration last wrote each file and what it read, with\n// `persistedRecordFiles` naming the file each root's copy persists to;\n// none of them lives here.\nexport interface PluginInstance {\n cache: boolean\n destinationRecords: Map<string, DestinationRecord>\n failOnError: NonNullable<UnpluginStyleDictionaryOptions['failOnError']>\n generatedDestinations: Set<string>\n log: (message: string, type: 'error' | 'info' | 'success') => void\n logTable: (render: (colour: boolean) => string[]) => void\n notifyBuildOutcome: (error: Error | null) => void\n onBuildEnd: UnpluginStyleDictionaryOptions['onBuildEnd']\n onBuildError: UnpluginStyleDictionaryOptions['onBuildError']\n onBuildStart: UnpluginStyleDictionaryOptions['onBuildStart']\n persistedRecordFiles: Map<string, string>\n platformsOption: UnpluginStyleDictionaryOptions['platforms']\n quiet: boolean\n report: boolean\n root: () => string\n verbosity: 'default' | 'silent' | 'verbose' | undefined\n volume: typeof fs\n watch: UnpluginStyleDictionaryOptions['watch']\n}\n\n// Compile design tokens\nexport async function runBuilds(\n instance: PluginInstance,\n resolvedConfigs: ResolvedConfig[],\n context?: string,\n): Promise<void> {\n const {\n cache,\n destinationRecords,\n failOnError,\n generatedDestinations,\n log,\n logTable,\n notifyBuildOutcome,\n onBuildEnd,\n onBuildError,\n onBuildStart,\n persistedRecordFiles,\n platformsOption,\n quiet,\n report,\n verbosity,\n volume,\n watch,\n } = instance\n\n // Read when this compile starts rather than when the instance was built,\n // because the host assigns it after the factory has run.\n const root = instance.root()\n\n const startTime = Date.now()\n\n // Ahead of the `try` rather than inside it, because the reporting below\n // reads it and that reporting is deliberately outside.\n const generatedFiles = new Set<string>()\n\n // How many configurations were already up to date. Read by the reporting\n // below, which is why it sits out here with `generatedFiles`.\n let skipped = 0\n\n // The records an earlier process persisted for this root, loaded the first\n // time this process compiles for it and never over a record this process\n // wrote itself. Only with `cache` on, the one case that reads or keeps\n // them: every other build compiles regardless.\n let recordsFile = cache ? persistedRecordFiles.get(root) : undefined\n if (cache && recordsFile === undefined) {\n recordsFile = recordsFileFor(root)\n persistedRecordFiles.set(root, recordsFile)\n for (const [destination, record] of readPersistedRecords(recordsFile)) {\n if (!destinationRecords.has(destination)) {\n destinationRecords.set(destination, record)\n }\n }\n }\n\n // What this compile recorded, and so what it adds to the persisted copy.\n const recorded = new Map<string, DestinationRecord>()\n\n try {\n if (!context) {\n log('Compiling design tokens...', 'info')\n }\n\n // Before anything is resolved or built, and once per build — a watch\n // rebuild is a build, so this fires again for each one.\n if (onBuildStart) callHook(log, 'onBuildStart', onBuildStart)\n\n // Configurations are built one after another rather than with\n // `Promise.all`, and that is load-bearing. Two configurations may name\n // the same destination file, and each instance gets the atomic volume\n // swapped onto it below — overlapping builds would interleave those\n // writes and hand a reader a file assembled from both.\n for (const [index, item] of resolvedConfigs.entries()) {\n // Read ahead of the instance, because avoiding the instance is the\n // point: construction plus `extend` is the 15-30% of a build that\n // parses the token sources, and `buildAllPlatforms` is the rest.\n //\n // Without a `report`, so a configuration that will not parse says\n // nothing here — the build below hands Style Dictionary the path and\n // lets its own message through, which is more specific than anything\n // this could say.\n const declared = cache ? await readConfigObject(item) : null\n const selectedPlatforms = platformsFor(platformsOption, context)\n\n // Ahead of the up-to-date check, which judges a selection by the\n // destinations the configuration declares for it and so cannot see a\n // name it does not define: `['css', 'andriod']` was judged on `css`\n // alone, and with `css` current the whole build was skipped and the typo\n // went unreported until a source changed. The build path checks again\n // below, for the builds that reach it without `declared`.\n if (declared) {\n checkSelection(item, index, selectedPlatforms, declared.platforms)\n }\n\n // What this configuration reads, as it stands before the compile below\n // reads any of it. The up-to-date check needs it now, and the record\n // needs it afterwards: it is what the next check measures \"changed\n // since\" against. Only asked with `cache` on, the one case that reads it.\n const newestSource = declared\n ? await newestSourceOf({ log, root, watch }, item, declared)\n : null\n\n // Who this configuration is: its contents, the hooks it names as the\n // registry holds them now, and the Style Dictionary version. Taken here,\n // after the `config` function's `register*` calls have landed, and once:\n // the check compares it with each destination's record, and the record\n // keeps it. Only with `cache` on, because nothing compares it otherwise\n // and reading a configuration module to compute it would run it again.\n const fingerprint = declared\n ? configFingerprint(root, item, declared)\n : null\n\n if (\n declared &&\n isUpToDate(\n { destinationRecords, fingerprint, newestSource },\n item,\n declared,\n selectedPlatforms,\n )\n ) {\n // The destinations still have to be collected. They are what stops\n // the plugin's own output being treated as a watched source, so a\n // skipped configuration that contributed none would have its files\n // rebuild the moment a watcher noticed them.\n for (const destination of declaredDestinations(declared)) {\n generatedFiles.add(destination)\n }\n\n skipped++\n continue\n }\n\n // `{ init: false }` is the escape hatch Style Dictionary documents on\n // this constructor, and it is what makes a bad configuration\n // catchable. Left to itself the constructor ends in a call to\n // `init()` whose promise it neither stores nor returns, so a config\n // that fails to load rejects a promise nobody holds: the `catch`\n // below never runs, and the host dies with a raw stack or — where an\n // `unhandledRejection` handler suppresses it — hangs on a\n // `buildStart` that never settles. `await sd.hasInitialized` cannot\n // observe it either, since that promise is only ever resolved, at the\n // tail of a successful extend.\n //\n // It is handed the configuration as an object rather than as a path\n // for the same reason: Style Dictionary imports a path with no\n // cache-busting query of its own, so under a long-lived dev server\n // every rebuild after the first built the config the process started\n // with while the watch list followed the edit.\n const sd = new StyleDictionary(await configForBuild(item), {\n init: false,\n })\n\n // One initialisation rather than two. `init()` is `extend()` with\n // `mutateOriginal`, so the old pair loaded the configuration and\n // combined every source twice — running a custom parser or\n // preprocessor twice with it — and the first of the two ran at\n // default verbosity, which is how Style Dictionary's own warnings\n // escaped this plugin's `silent`. `config` defaults to the one the\n // constructor was handed.\n //\n // `verbosity` is `undefined` unless a consumer asked for a level, and\n // Style Dictionary falls through an unset one to the configuration's\n // own `log.verbosity`. Overwriting it here is what silenced the one\n // line explaining why a build wrote nothing. `log.warnings` is not\n // touched either way: a consumer's `warnings: 'error'` turning a\n // missing output file into a thrown build is their decision.\n await namingConfig(item, index, async () =>\n sd.extend(undefined, { mutateOriginal: true, verbosity }),\n )\n\n // **Before the build, and that is the whole of it.** A token set that\n // resolved to nothing is not an error anywhere in this stack: Style\n // Dictionary writes the file with no custom properties in it, prints\n // its usual `✔︎` line at any verbosity, and returns. So a token file\n // deleted mid-session took the generated output down with it and\n // reported `Rebuilt design tokens` while doing it, and a `source`\n // matching nothing shipped an empty stylesheet from a build that\n // exited 0.\n //\n // Checked here because `buildAllPlatforms` truncates and rewrites the\n // destination: one line later the previous good output is already gone\n // and an error would be accurate and useless.\n if (sd.allTokens.length === 0) {\n // The configuration as an object, so its own patterns can be named.\n // Without a `report`, because a configuration that will not parse\n // never reaches here — the `extend` above would have thrown first.\n const asObject = await readConfigObject(item)\n const barren = asObject\n ? await patternsMatchingNothing(sourcePatternsOf(asObject))\n : []\n\n // Thrown rather than reported, so it takes the path `failOnError`\n // already owns — the same decision, made in one place, rather than a\n // second way for a build to fail.\n throw new Error(\n [\n `${describeConfig(item, index)} resolved no tokens, so its output would be emptied.`,\n barren.length > 0\n ? `These patterns matched no files: ${barren.join(', ')}`\n : `It declares no source or include patterns that matched anything.`,\n `Nothing was written. Set failOnError to false to build anyway.`,\n ].join(' '),\n )\n }\n\n // Swap in the atomic volume only now that the instance has finished\n // reading its configs and token sources, so every write below lands\n // through `rename` while the read path stays exactly as it was. Seen\n // through a layer that notes what this build writes, which the record\n // below needs: a declared file is not necessarily a written one.\n const wrote = new Set<string>()\n sd.volume = notingWrites(volume, wrote)\n\n // The files this compile is about to write, named exactly as the\n // up-to-date check names them, and only the selected platforms' — a\n // scoped build leaves the others' files, and whatever wrote them, alone.\n const writing = declaredDestinations(\n { platforms: sd.platforms },\n selectedPlatforms,\n )\n\n // Forgotten before the first write rather than overwritten after the\n // last, because a build that throws partway has already replaced some\n // of these files. A record left standing would vouch for output that\n // no longer matches the configuration it names.\n for (const destination of writing) {\n destinationRecords.delete(destination)\n }\n\n if (selectedPlatforms === undefined) {\n await namingConfig(item, index, async () => sd.buildAllPlatforms())\n } else {\n // `cache: false`, and a configuration that would not parse ahead of\n // `extend`, arrive here without the check above having run.\n checkSelection(item, index, selectedPlatforms, sd.platforms)\n\n // One after another, matching the loop this sits inside: two\n // platforms may name the same destination, and `buildAllPlatforms`\n // fanning its own out with `Promise.all` is Style Dictionary's\n // choice over configurations it owns, not this plugin's over a\n // selection a consumer wrote.\n for (const name of selectedPlatforms) {\n await namingConfig(item, index, async () => sd.buildPlatform(name))\n }\n }\n\n // Every declared platform, not only the ones this compile built. A\n // file an unselected platform wrote on an earlier build is still the\n // plugin's own output, and dropping it from this set would let a\n // watcher treat it as a token source and rebuild once for every change\n // to it. Not a loop: a scoped rebuild never rewrites that file.\n //\n // Collected on every build rather than only on the ones whose size\n // report prints it below. The set is also what keeps a rebuild from\n // being triggered by the write it just made, and a rebuild passes a\n // `context` — so gating the collection on `!context` left it empty on\n // exactly the builds a watcher is live for.\n //\n // Named by `writtenDestination`, the one place that knows where Style\n // Dictionary writes a file. Every copy of that rule this code has had\n // named files that were never written — against `root` until #362, and\n // an absolute destination until #367 — and handed them to `onBuildEnd`,\n // the size report and this very set.\n for (const platform of Object.values(sd.platforms)) {\n for (const file of platform.files ?? []) {\n if (file.destination) {\n generatedFiles.add(\n writtenDestination(platform.buildPath, file.destination),\n )\n }\n }\n }\n\n // Recorded only now, so a build that threw vouches for none of what it\n // wrote before throwing. Every configuration is recorded, a file-backed\n // one included: an object configuration naming the same file has to\n // see that something else wrote it last.\n //\n // `newestSource` goes in beside the fingerprint. It was taken before\n // this compile read anything, so a source saved while it ran is newer\n // than it, and the rebuild queued for that save is not skipped. So does\n // each file's mtime as this compile left it, byte-identical skip and\n // all: a later check trusts the record only while the file still has\n // it. A file Style Dictionary declined to write is recorded as such, so\n // a later check does not go looking for it; one it wrote and that is\n // already gone gets no record, so the next build compiles.\n //\n // Only a compile with `cache` on has a fingerprint to record. One with it\n // off has still forgotten what it rewrote, above, which is what keeps\n // every other instance from vouching for its output.\n if (fingerprint !== null) {\n for (const destination of writing) {\n const record = recordOf(\n destination,\n fingerprint,\n newestSource,\n wrote.has(destination),\n )\n if (record) {\n destinationRecords.set(destination, record)\n recorded.set(destination, record)\n }\n }\n }\n }\n\n // Replaced wholesale rather than added to, so a destination dropped from\n // a configuration stops being treated as ours and becomes watchable\n // again. A build that throws never reaches this and leaves the previous\n // set standing, which is the safe direction: the files it wrote before\n // failing are still ours.\n // Once per compile rather than per configuration, and only after every\n // configuration succeeded. A compile that throws persists nothing, which\n // is safe: whatever it rewrote no longer has the mtime its old record\n // names, so that record vouches for nothing.\n if (recordsFile !== undefined && recorded.size > 0) {\n writePersistedRecords(recordsFile, recorded)\n }\n\n generatedDestinations.clear()\n for (const destination of generatedFiles) {\n generatedDestinations.add(destination.replace(/\\\\/g, '/'))\n }\n } catch (err) {\n const duration = Date.now() - startTime\n\n // The change a rebuild followed, worded as a change rather than as the\n // culprit: after a debounced burst `context` is the last file that\n // changed, which is not necessarily the one that broke the build.\n log(\n context\n ? `Compilation failed after a change in ${context} (${duration}ms): ${errorMessage(err)}`\n : `Compilation failed after ${duration}ms: ${errorMessage(err)}`,\n 'error',\n )\n\n // Ahead of the throw decision on purpose, so the overlay sees a failure\n // whatever `failOnError` does with it. Under the dev server's default\n // the line below does not throw, and reading the outcome from a caller's\n // `catch` would see a rebuild that looked like it succeeded.\n notifyBuildOutcome(asError(err))\n\n // Ahead of the throw decision for the same reason as the line above: a\n // rebuild under the dev server's default does not throw, and a hook that\n // only fired when something else was about to fail would be silent on\n // exactly the builds a consumer is watching.\n if (onBuildError) callHook(log, 'onBuildError', onBuildError, err)\n\n // Reported, and then rethrown so the host stops. Swallowing it left\n // every target exiting 0 with the previous run's tokens still on disk\n // and in the bundle — a green build shipping stale values.\n if (failsTheBuild(failOnError, context)) throw err\n\n // Explicit, now that the reporting below sits outside the `try`. This\n // `catch` used to end the function by falling off the end of it; a\n // failure that is not rethrown would otherwise carry on to announce a\n // compile that did not happen.\n return\n }\n\n // The compile is what the overlay reflects, so this is said here rather\n // than at the end: everything below is reporting, it returns early in\n // three places, and a size table that throws must not leave a successful\n // build looking unfinished.\n notifyBuildOutcome(null)\n\n // One measurement, read by the hook below and by the reporting under it.\n const duration = Date.now() - startTime\n\n // Beside the overlay notification, and for the same reason it sits here\n // rather than at the end of the function: the reporting below returns\n // early in three places, and a build that finished has finished whether or\n // not a size table gets printed for it.\n //\n // Sorted, so two runs of one configuration hand back the same order —\n // `generatedFiles` is a set in platform-then-file order, which is stable\n // in practice and guaranteed by nothing. The paths stay platform-native:\n // this is a list a consumer is going to open files with, not one the\n // watcher compares against.\n //\n // Only the declared files that are on disk. One whose filter matched no\n // tokens is declared and never written, and a hook opening it got an ENOENT.\n if (onBuildEnd) {\n // `toSorted` is what the linter asks for and what this cannot use:\n // `lib` is ES2022 here and `toSorted` is ES2023, so it types as an error\n // even though every Node this package supports has it. The rule guards\n // against mutating an array someone else holds, and this one was built\n // from the set on the line it appears on.\n const files = Array.from(generatedFiles)\n .filter((file) => fs.existsSync(file))\n // oxlint-disable-next-line unicorn/no-array-sort\n .sort((left, right) => left.localeCompare(right))\n callHook(log, 'onBuildEnd', onBuildEnd, files, duration)\n }\n\n // The `try` ends above, and everything from here down is reporting. Style\n // Dictionary has finished writing by now and `generatedDestinations` is\n // already replaced, so nothing below can put a file on disk in doubt —\n // which is why a throw from it must not be caught as a compile failure.\n // It used to be: a fault in the padding arithmetic printed `Compilation\n // failed after 19ms` over a build whose every token file was correct, and\n // with `failOnError` defaulting to `'build'` that stopped the bundler.\n\n // Every configuration was already current, so nothing was written. Said\n // rather than left implied: a build that prints its opening line and then\n // finishes in two milliseconds reads as one that silently did nothing.\n const everythingSkipped = skipped === resolvedConfigs.length\n\n if (context) {\n log(\n everythingSkipped\n ? `Design tokens already up to date after change in ${context} (${duration}ms)`\n : `Rebuilt design tokens due to change in ${context} (${duration}ms)`,\n 'success',\n )\n return\n }\n\n // The table is skipped when nothing was written, on top of `report` and\n // `quiet`. It reads every generated file in full and gzips it, and\n // reprinting the sizes of files this build did not touch is the one case\n // where that cost buys nothing at all.\n if (report && !quiet && !everythingSkipped && generatedFiles.size > 0) {\n try {\n logTable((colour) => sizeTable({ colour, root }, generatedFiles))\n } catch (err) {\n // At `'error'`, so it is said at every level including `silent`,\n // exactly as a compile failure is — and worded so it cannot be read\n // as one. Not rethrown: the build succeeded.\n log(\n `Failed to report generated file sizes: ${errorMessage(err)}`,\n 'error',\n )\n }\n }\n\n if (everythingSkipped) {\n log(`Design tokens are already up to date (${duration}ms)`, 'success')\n return\n }\n\n log(\n skipped > 0\n ? `Compiled successfully! (${duration}ms, ${skipped} already up to date)`\n : `Compiled successfully! (${duration}ms)`,\n 'success',\n )\n}\n\n// Runs one of the consumer's `onBuild*` hooks without letting it decide the\n// fate of the build that called it.\n//\n// Two ways a hook can go wrong, and neither may propagate. A throw is caught\n// here, because a post-processing step that fails must not undo a compile the\n// plugin itself completed — the files are written and correct. A rejected\n// promise is the quieter one: the return value is deliberately not awaited,\n// so a rejection has nothing holding it and reaches the host as an unhandled\n// rejection, which under Node's default takes the process down — a dev server\n// killed from inside a hook that was only meant to reformat a file.\n//\n// Both are reported at `'error'`, so they are said at every level including\n// `silent`, and worded so neither can be read as the compile having failed.\nfunction callHook<A extends unknown[]>(\n log: PluginInstance['log'],\n name: string,\n hook: (...args: A) => Promise<void> | void,\n ...args: A\n): void {\n let result: unknown\n\n try {\n // Captured rather than dropped, because the promise an `async` hook\n // returns is the thing the check below needs. Wrapping this call in a\n // block-bodied arrow — which is what the linter asks for when the return\n // type is plain `void` — discarded it, and the rejection escaped exactly\n // as it had before any of this existed.\n result = hook(...args)\n } catch (err) {\n log(`The ${name} hook threw: ${errorMessage(err)}`, 'error')\n return\n }\n\n if (!isThenable(result)) return\n\n void Promise.resolve(result).catch((err: unknown) => {\n log(`The ${name} hook rejected: ${errorMessage(err)}`, 'error')\n })\n}\n\n// Throws when `selected` names a platform that `platforms` does not define.\n// Named, so a typo is an error rather than a platform silently not built —\n// which is what Style Dictionary's own CLI means by \"Must be defined in the\n// config\" — and one function, so the check ahead of the up-to-date skip and\n// the one on the build path say exactly the same thing.\nfunction checkSelection(\n item: ResolvedConfig,\n index: number,\n selected: string[] | undefined,\n platforms: Record<string, unknown> | undefined,\n): void {\n if (selected === undefined) return\n\n const defined = Object.keys(platforms ?? {})\n const unknown = selected.filter((name) => !defined.includes(name))\n if (unknown.length > 0) {\n throw new Error(\n `${describeConfig(item, index)} does not define the platform(s) ${unknown.join(', ')}. It defines ${defined.join(', ')}.`,\n )\n }\n}\n\n// Whether a failure in this compile should be thrown rather than only\n// reported. The two compiles are told apart by `runBuilds`'s `context`,\n// which only the rebuild paths pass.\nfunction failsTheBuild(\n failOnError: NonNullable<UnpluginStyleDictionaryOptions['failOnError']>,\n context: string | undefined,\n): boolean {\n return (\n failOnError === true ||\n (context === undefined ? failOnError === 'build' : failOnError === 'serve')\n )\n}\n\n// A hook is the consumer's code, and what it hands back is not this plugin's to\n// assume. A predicate rather than `instanceof Promise`, which answers `false`\n// for a thenable from another realm or from a promise library — exactly the\n// case where letting a rejection escape does the damage.\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'then' in value &&\n typeof value.then === 'function'\n )\n}\n\n// Runs one of Style Dictionary's own steps for a configuration, and names the\n// configuration in anything it throws. Its messages name neither the\n// configuration nor the file behind them, so with several configurations a\n// failure could have come from any of them — in the log line, the overlay and\n// `onBuildError` alike. `cause` keeps its own error reachable.\n//\n// Only Style Dictionary's steps, never the whole loop: the plugin's own errors\n// already open with `describeConfig`, and wrapping those would name the\n// configuration twice.\nasync function namingConfig<T>(\n item: ResolvedConfig,\n index: number,\n step: () => Promise<T>,\n): Promise<T> {\n try {\n return await step()\n } catch (err) {\n throw new Error(\n `${describeConfig(item, index)} failed: ${errorMessage(err)}`,\n { cause: err },\n )\n }\n}\n\n// `volume`, with every path Style Dictionary writes through it noted in\n// `into`, resolved as `writtenDestination` resolves a declared one. Each file\n// a build writes reaches `promises.writeFile` (`buildPlatform` in\n// `lib/StyleDictionary.js`) — a byte-identical one included, since the atomic\n// writer decides to skip the rename only once it has been handed the file — so\n// `into` ends up holding exactly what the build wrote, and a file it declined\n// to write never appears in it. Built per compile, over the atomic volume\n// rather than in place of it.\n//\n// The sync writer is inherited untouched. Only an action reaches it, a\n// configuration with actions is never skipped, and `tests/index.test.ts`\n// checks that an action is handed the atomic writer itself.\nfunction notingWrites(volume: typeof fs, into: Set<string>): typeof fs {\n const note = (file: unknown) => {\n if (typeof file === 'string') into.add(path.resolve(file))\n }\n\n /* oxlint-disable typescript/no-unsafe-type-assertion */\n return Object.create(volume, {\n promises: {\n value: Object.create(volume.promises, {\n writeFile: {\n value: async (...args: Parameters<typeof fs.promises.writeFile>) => {\n note(args[0])\n await volume.promises.writeFile(...args)\n },\n },\n }) as typeof fs.promises,\n },\n }) as typeof fs\n /* oxlint-enable typescript/no-unsafe-type-assertion */\n}\n\n// Which platforms this compile covers, or `undefined` for all of them.\n//\n// The array form applies to every build; the object form splits the first\n// compile from the watch rebuilds, and `context` is what tells them apart —\n// only the rebuild paths pass one. An absent key means every platform, so\n// `{ watch: ['css'] }` builds everything once and then only css.\nfunction platformsFor(\n platformsOption: UnpluginStyleDictionaryOptions['platforms'],\n context: string | undefined,\n): string[] | undefined {\n if (platformsOption === undefined) return undefined\n if (Array.isArray(platformsOption)) return platformsOption\n\n return context === undefined ? platformsOption.build : platformsOption.watch\n}\n"],"mappings":";;;;;;;;;AAmEA,eAAsB,UACpB,UACA,iBACA,SACe;CACf,MAAM,EACJ,OACA,oBACA,aACA,uBACA,KACA,UACA,oBACA,YACA,cACA,cACA,sBACA,iBACA,OACA,QACA,WACA,QACA,UACE;CAIJ,MAAM,OAAO,SAAS,KAAK;CAE3B,MAAM,YAAY,KAAK,IAAI;CAI3B,MAAM,iCAAiB,IAAI,IAAY;CAIvC,IAAI,UAAU;CAMd,IAAI,cAAc,QAAQ,qBAAqB,IAAI,IAAI,IAAI,KAAA;CAC3D,IAAI,SAAS,gBAAgB,KAAA,GAAW;EACtC,cAAc,eAAe,IAAI;EACjC,qBAAqB,IAAI,MAAM,WAAW;EAC1C,KAAK,MAAM,CAAC,aAAa,WAAW,qBAAqB,WAAW,GAClE,IAAI,CAAC,mBAAmB,IAAI,WAAW,GACrC,mBAAmB,IAAI,aAAa,MAAM;CAGhD;CAGA,MAAM,2BAAW,IAAI,IAA+B;CAEpD,IAAI;EACF,IAAI,CAAC,SACH,IAAI,8BAA8B,MAAM;EAK1C,IAAI,cAAc,SAAS,KAAK,gBAAgB,YAAY;EAO5D,KAAK,MAAM,CAAC,OAAO,SAAS,gBAAgB,QAAQ,GAAG;GASrD,MAAM,WAAW,QAAQ,MAAM,iBAAiB,IAAI,IAAI;GACxD,MAAM,oBAAoB,aAAa,iBAAiB,OAAO;GAQ/D,IAAI,UACF,eAAe,MAAM,OAAO,mBAAmB,SAAS,SAAS;GAOnE,MAAM,eAAe,WACjB,MAAM,eAAe;IAAE;IAAK;IAAM;GAAM,GAAG,MAAM,QAAQ,IACzD;GAQJ,MAAM,cAAc,WAChB,kBAAkB,MAAM,MAAM,QAAQ,IACtC;GAEJ,IACE,YACA,WACE;IAAE;IAAoB;IAAa;GAAa,GAChD,MACA,UACA,iBACF,GACA;IAKA,KAAK,MAAM,eAAe,qBAAqB,QAAQ,GACrD,eAAe,IAAI,WAAW;IAGhC;IACA;GACF;GAkBA,MAAM,KAAK,IAAI,gBAAgB,MAAM,eAAe,IAAI,GAAG,EACzD,MAAM,MACR,CAAC;GAgBD,MAAM,aAAa,MAAM,OAAO,YAC9B,GAAG,OAAO,KAAA,GAAW;IAAE,gBAAgB;IAAM;GAAU,CAAC,CAC1D;GAcA,IAAI,GAAG,UAAU,WAAW,GAAG;IAI7B,MAAM,WAAW,MAAM,iBAAiB,IAAI;IAC5C,MAAM,SAAS,WACX,MAAM,wBAAwB,iBAAiB,QAAQ,CAAC,IACxD,CAAC;IAKL,MAAM,IAAI,MACR;KACE,GAAG,eAAe,MAAM,KAAK,EAAE;KAC/B,OAAO,SAAS,IACZ,oCAAoC,OAAO,KAAK,IAAI,MACpD;KACJ;IACF,CAAC,CAAC,KAAK,GAAG,CACZ;GACF;GAOA,MAAM,wBAAQ,IAAI,IAAY;GAC9B,GAAG,SAAS,aAAa,QAAQ,KAAK;GAKtC,MAAM,UAAU,qBACd,EAAE,WAAW,GAAG,UAAU,GAC1B,iBACF;GAMA,KAAK,MAAM,eAAe,SACxB,mBAAmB,OAAO,WAAW;GAGvC,IAAI,sBAAsB,KAAA,GACxB,MAAM,aAAa,MAAM,OAAO,YAAY,GAAG,kBAAkB,CAAC;QAC7D;IAGL,eAAe,MAAM,OAAO,mBAAmB,GAAG,SAAS;IAO3D,KAAK,MAAM,QAAQ,mBACjB,MAAM,aAAa,MAAM,OAAO,YAAY,GAAG,cAAc,IAAI,CAAC;GAEtE;GAmBA,KAAK,MAAM,YAAY,OAAO,OAAO,GAAG,SAAS,GAC/C,KAAK,MAAM,QAAQ,SAAS,SAAS,CAAC,GACpC,IAAI,KAAK,aACP,eAAe,IACb,mBAAmB,SAAS,WAAW,KAAK,WAAW,CACzD;GAsBN,IAAI,gBAAgB,MAClB,KAAK,MAAM,eAAe,SAAS;IACjC,MAAM,SAAS,SACb,aACA,aACA,cACA,MAAM,IAAI,WAAW,CACvB;IACA,IAAI,QAAQ;KACV,mBAAmB,IAAI,aAAa,MAAM;KAC1C,SAAS,IAAI,aAAa,MAAM;IAClC;GACF;EAEJ;EAWA,IAAI,gBAAgB,KAAA,KAAa,SAAS,OAAO,GAC/C,sBAAsB,aAAa,QAAQ;EAG7C,sBAAsB,MAAM;EAC5B,KAAK,MAAM,eAAe,gBACxB,sBAAsB,IAAI,YAAY,QAAQ,OAAO,GAAG,CAAC;CAE7D,SAAS,KAAK;EACZ,MAAM,WAAW,KAAK,IAAI,IAAI;EAK9B,IACE,UACI,wCAAwC,QAAQ,IAAI,SAAS,OAAO,aAAa,GAAG,MACpF,4BAA4B,SAAS,MAAM,aAAa,GAAG,KAC/D,OACF;EAMA,mBAAmB,QAAQ,GAAG,CAAC;EAM/B,IAAI,cAAc,SAAS,KAAK,gBAAgB,cAAc,GAAG;EAKjE,IAAI,cAAc,aAAa,OAAO,GAAG,MAAM;EAM/C;CACF;CAMA,mBAAmB,IAAI;CAGvB,MAAM,WAAW,KAAK,IAAI,IAAI;CAe9B,IAAI,YAUF,SAAS,KAAK,cAAc,YAJd,MAAM,KAAK,cAAc,CAAC,CACrC,QAAQ,SAAS,GAAG,WAAW,IAAI,CAAC,CAAC,CAErC,MAAM,MAAM,UAAU,KAAK,cAAc,KAAK,CACL,GAAG,QAAQ;CAczD,MAAM,oBAAoB,YAAY,gBAAgB;CAEtD,IAAI,SAAS;EACX,IACE,oBACI,oDAAoD,QAAQ,IAAI,SAAS,OACzE,0CAA0C,QAAQ,IAAI,SAAS,MACnE,SACF;EACA;CACF;CAMA,IAAI,UAAU,CAAC,SAAS,CAAC,qBAAqB,eAAe,OAAO,GAClE,IAAI;EACF,UAAU,WAAW,UAAU;GAAE;GAAQ;EAAK,GAAG,cAAc,CAAC;CAClE,SAAS,KAAK;EAIZ,IACE,0CAA0C,aAAa,GAAG,KAC1D,OACF;CACF;CAGF,IAAI,mBAAmB;EACrB,IAAI,yCAAyC,SAAS,MAAM,SAAS;EACrE;CACF;CAEA,IACE,UAAU,IACN,2BAA2B,SAAS,MAAM,QAAQ,wBAClD,2BAA2B,SAAS,MACxC,SACF;AACF;AAeA,SAAS,SACP,KACA,MACA,MACA,GAAG,MACG;CACN,IAAI;CAEJ,IAAI;EAMF,SAAS,KAAK,GAAG,IAAI;CACvB,SAAS,KAAK;EACZ,IAAI,OAAO,KAAK,eAAe,aAAa,GAAG,KAAK,OAAO;EAC3D;CACF;CAEA,IAAI,CAAC,WAAW,MAAM,GAAG;CAEzB,QAAa,QAAQ,MAAM,CAAC,CAAC,OAAO,QAAiB;EACnD,IAAI,OAAO,KAAK,kBAAkB,aAAa,GAAG,KAAK,OAAO;CAChE,CAAC;AACH;AAOA,SAAS,eACP,MACA,OACA,UACA,WACM;CACN,IAAI,aAAa,KAAA,GAAW;CAE5B,MAAM,UAAU,OAAO,KAAK,aAAa,CAAC,CAAC;CAC3C,MAAM,UAAU,SAAS,QAAQ,SAAS,CAAC,QAAQ,SAAS,IAAI,CAAC;CACjE,IAAI,QAAQ,SAAS,GACnB,MAAM,IAAI,MACR,GAAG,eAAe,MAAM,KAAK,EAAE,mCAAmC,QAAQ,KAAK,IAAI,EAAE,eAAe,QAAQ,KAAK,IAAI,EAAE,EACzH;AAEJ;AAKA,SAAS,cACP,aACA,SACS;CACT,OACE,gBAAgB,SACf,YAAY,KAAA,IAAY,gBAAgB,UAAU,gBAAgB;AAEvE;AAMA,SAAS,WAAW,OAA+C;CACjE,OACE,OAAO,UAAU,YACjB,UAAU,QACV,UAAU,SACV,OAAO,MAAM,SAAS;AAE1B;AAWA,eAAe,aACb,MACA,OACA,MACY;CACZ,IAAI;EACF,OAAO,MAAM,KAAK;CACpB,SAAS,KAAK;EACZ,MAAM,IAAI,MACR,GAAG,eAAe,MAAM,KAAK,EAAE,WAAW,aAAa,GAAG,KAC1D,EAAE,OAAO,IAAI,CACf;CACF;AACF;AAcA,SAAS,aAAa,QAAmB,MAA8B;CACrE,MAAM,QAAQ,SAAkB;EAC9B,IAAI,OAAO,SAAS,UAAU,KAAK,IAAI,KAAK,QAAQ,IAAI,CAAC;CAC3D;CAGA,OAAO,OAAO,OAAO,QAAQ,EAC3B,UAAU,EACR,OAAO,OAAO,OAAO,OAAO,UAAU,EACpC,WAAW,EACT,OAAO,OAAO,GAAG,SAAmD;EAClE,KAAK,KAAK,EAAE;EACZ,MAAM,OAAO,SAAS,UAAU,GAAG,IAAI;CACzC,EACF,EACF,CAAC,EACH,EACF,CAAC;AAEH;AAQA,SAAS,aACP,iBACA,SACsB;CACtB,IAAI,oBAAoB,KAAA,GAAW,OAAO,KAAA;CAC1C,IAAI,MAAM,QAAQ,eAAe,GAAG,OAAO;CAE3C,OAAO,YAAY,KAAA,IAAY,gBAAgB,QAAQ,gBAAgB;AACzE"}
1
+ {"version":3,"file":"compile.js","names":[],"sources":["../src/compile.ts"],"sourcesContent":["// One compile of every resolved configuration: building each one through the\n// atomic volume, recording what it wrote, and saying how it went.\n//\n// Nothing here holds state. What a plugin instance owns is handed in as a\n// `PluginInstance`, which the factory builds once, and the two parts of it\n// that are assigned after the factory has run — `root` and the overlay\n// callback — are reached through it when a compile runs rather than copied\n// when it was built.\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport StyleDictionary from 'style-dictionary'\n\nimport type { ResolvedConfig } from './config.js'\nimport type { UnpluginStyleDictionaryOptions } from './types.js'\nimport type { DestinationRecord } from './up-to-date.js'\n\nimport { configForBuild, describeConfig, readConfigObject } from './config.js'\nimport { asError, errorMessage } from './errors.js'\nimport { patternsMatchingNothing, sourcePatternsOf } from './patterns.js'\nimport { sizeTable } from './size-report.js'\nimport {\n configFingerprint,\n declaredDestinations,\n isUpToDate,\n newestSourceOf,\n readPersistedRecords,\n recordOf,\n recordsFileFor,\n writePersistedRecords,\n writtenDestination,\n} from './up-to-date.js'\n\n// What `runBuilds` reads from the plugin instance it compiles for.\n//\n// `root` and `notifyBuildOutcome` are functions for the same reason: each is\n// assigned after the factory has run — `root` by `adoptCompiler` or\n// `configResolved`, the overlay callback by `configureServer` — so a value\n// copied when this was built would be the working directory and `undefined`\n// for good. `log` and `logTable` read the message host when they are called,\n// for the same reason. `generatedDestinations` is the instance's own record of what it\n// wrote, and `destinationRecords` is the process's record of which\n// configuration last wrote each file and what it read, with\n// `persistedRecordFiles` naming the file each root's copy persists to;\n// none of them lives here.\nexport interface PluginInstance {\n cache: boolean\n destinationRecords: Map<string, DestinationRecord>\n failOnError: NonNullable<UnpluginStyleDictionaryOptions['failOnError']>\n generatedDestinations: Set<string>\n log: (message: string, type: 'error' | 'info' | 'success') => void\n logTable: (render: (colour: boolean) => string[]) => void\n notifyBuildOutcome: (error: Error | null) => void\n onBuildEnd: UnpluginStyleDictionaryOptions['onBuildEnd']\n onBuildError: UnpluginStyleDictionaryOptions['onBuildError']\n onBuildStart: UnpluginStyleDictionaryOptions['onBuildStart']\n persistedRecordFiles: Map<string, string>\n platformsOption: UnpluginStyleDictionaryOptions['platforms']\n quiet: boolean\n report: boolean\n root: () => string\n verbosity: 'default' | 'silent' | 'verbose' | undefined\n volume: typeof fs\n watch: UnpluginStyleDictionaryOptions['watch']\n}\n\n// Compile design tokens\nexport async function runBuilds(\n instance: PluginInstance,\n resolvedConfigs: ResolvedConfig[],\n context?: string,\n): Promise<void> {\n const {\n cache,\n destinationRecords,\n failOnError,\n generatedDestinations,\n log,\n logTable,\n notifyBuildOutcome,\n onBuildEnd,\n onBuildError,\n onBuildStart,\n persistedRecordFiles,\n platformsOption,\n quiet,\n report,\n verbosity,\n volume,\n watch,\n } = instance\n\n // Read when this compile starts rather than when the instance was built,\n // because the host assigns it after the factory has run.\n const root = instance.root()\n\n const startTime = Date.now()\n\n // Ahead of the `try` rather than inside it, because the reporting below\n // reads it and that reporting is deliberately outside.\n const generatedFiles = new Set<string>()\n\n // How many configurations were already up to date. Read by the reporting\n // below, which is why it sits out here with `generatedFiles`.\n let skipped = 0\n\n // The records an earlier process persisted for this root, loaded the first\n // time this process compiles for it and never over a record this process\n // wrote itself. Only with `cache` on, the one case that reads or keeps\n // them: every other build compiles regardless.\n let recordsFile = cache ? persistedRecordFiles.get(root) : undefined\n if (cache && recordsFile === undefined) {\n recordsFile = recordsFileFor(root)\n persistedRecordFiles.set(root, recordsFile)\n for (const [destination, record] of readPersistedRecords(recordsFile)) {\n if (!destinationRecords.has(destination)) {\n destinationRecords.set(destination, record)\n }\n }\n }\n\n // What this compile recorded, and so what it adds to the persisted copy.\n const recorded = new Map<string, DestinationRecord>()\n\n try {\n if (!context) {\n log('Compiling design tokens...', 'info')\n }\n\n // Before anything is resolved or built, and once per build — a watch\n // rebuild is a build, so this fires again for each one.\n if (onBuildStart) callHook(log, 'onBuildStart', onBuildStart)\n\n // Configurations are built one after another rather than with\n // `Promise.all`, and that is load-bearing. Two configurations may name\n // the same destination file, and each instance gets the atomic volume\n // swapped onto it below — overlapping builds would interleave those\n // writes and hand a reader a file assembled from both.\n for (const [index, item] of resolvedConfigs.entries()) {\n // Read ahead of the instance, because avoiding the instance is the\n // point: construction plus `extend` is the 15-30% of a build that\n // parses the token sources, and `buildAllPlatforms` is the rest.\n //\n // Without a `report`, so a configuration that will not parse says\n // nothing here — the build below hands Style Dictionary the path and\n // lets its own message through, which is more specific than anything\n // this could say.\n const declared = cache ? await readConfigObject(item) : null\n const selectedPlatforms = platformsFor(platformsOption, context)\n\n // Ahead of the up-to-date check, which judges a selection by the\n // destinations the configuration declares for it and so cannot see a\n // name it does not define: `['css', 'andriod']` was judged on `css`\n // alone, and with `css` current the whole build was skipped and the typo\n // went unreported until a source changed. The build path checks again\n // below, for the builds that reach it without `declared`.\n if (declared) {\n checkSelection(item, index, selectedPlatforms, declared.platforms)\n }\n\n // What this configuration reads, as it stands before the compile below\n // reads any of it. The up-to-date check needs it now, and the record\n // needs it afterwards: it is what the next check measures \"changed\n // since\" against. Only asked with `cache` on, the one case that reads it.\n const newestSource = declared\n ? await newestSourceOf({ log, root, watch }, item, declared)\n : null\n\n // Who this configuration is: its contents, the hooks it names as the\n // registry holds them now, and the Style Dictionary version. Taken here,\n // after the `config` function's `register*` calls have landed, and once:\n // the check compares it with each destination's record, and the record\n // keeps it. Only with `cache` on, because nothing compares it otherwise\n // and reading a configuration module to compute it would run it again.\n const fingerprint = declared\n ? configFingerprint(root, item, declared)\n : null\n\n if (\n declared &&\n isUpToDate(\n { destinationRecords, fingerprint, newestSource },\n item,\n declared,\n selectedPlatforms,\n )\n ) {\n // The destinations still have to be collected. They are what stops\n // the plugin's own output being treated as a watched source, so a\n // skipped configuration that contributed none would have its files\n // rebuild the moment a watcher noticed them.\n for (const destination of declaredDestinations(declared)) {\n generatedFiles.add(destination)\n }\n\n skipped++\n continue\n }\n\n // `{ init: false }` is the escape hatch Style Dictionary documents on\n // this constructor, and it is what makes a bad configuration\n // catchable. Left to itself the constructor ends in a call to\n // `init()` whose promise it neither stores nor returns, so a config\n // that fails to load rejects a promise nobody holds: the `catch`\n // below never runs, and the host dies with a raw stack or — where an\n // `unhandledRejection` handler suppresses it — hangs on a\n // `buildStart` that never settles. `await sd.hasInitialized` cannot\n // observe it either, since that promise is only ever resolved, at the\n // tail of a successful extend.\n //\n // It is handed the configuration as an object rather than as a path\n // for the same reason: Style Dictionary imports a path with no\n // cache-busting query of its own, so under a long-lived dev server\n // every rebuild after the first built the config the process started\n // with while the watch list followed the edit.\n const sd = new StyleDictionary(await configForBuild(item), {\n init: false,\n })\n\n // One initialisation rather than two. `init()` is `extend()` with\n // `mutateOriginal`, so the old pair loaded the configuration and\n // combined every source twice — running a custom parser or\n // preprocessor twice with it — and the first of the two ran at\n // default verbosity, which is how Style Dictionary's own warnings\n // escaped this plugin's `silent`. `config` defaults to the one the\n // constructor was handed.\n //\n // `verbosity` is `undefined` unless a consumer asked for a level, and\n // Style Dictionary falls through an unset one to the configuration's\n // own `log.verbosity`. Overwriting it here is what silenced the one\n // line explaining why a build wrote nothing. `log.warnings` is not\n // touched either way: a consumer's `warnings: 'error'` turning a\n // missing output file into a thrown build is their decision.\n await namingConfig(item, index, async () =>\n sd.extend(undefined, { mutateOriginal: true, verbosity }),\n )\n\n // **Before the build, and that is the whole of it.** A token set that\n // resolved to nothing is not an error anywhere in this stack: Style\n // Dictionary writes the file with no custom properties in it, prints\n // its usual `✔︎` line at any verbosity, and returns. So a token file\n // deleted mid-session took the generated output down with it and\n // reported `Rebuilt design tokens` while doing it, and a `source`\n // matching nothing shipped an empty stylesheet from a build that\n // exited 0.\n //\n // Checked here because `buildAllPlatforms` truncates and rewrites the\n // destination: one line later the previous good output is already gone\n // and an error would be accurate and useless.\n if (sd.allTokens.length === 0) {\n // The configuration as an object, so its own patterns can be named.\n // Without a `report`, because a configuration that will not parse\n // never reaches here — the `extend` above would have thrown first.\n const asObject = await readConfigObject(item)\n const barren = asObject\n ? await patternsMatchingNothing(sourcePatternsOf(asObject))\n : []\n\n // Thrown rather than reported, so it takes the path `failOnError`\n // already owns — the same decision, made in one place, rather than a\n // second way for a build to fail.\n throw new Error(\n [\n `${describeConfig(item, index)} resolved no tokens, so its output would be emptied.`,\n barren.length > 0\n ? `These patterns matched no files: ${barren.join(', ')}`\n : `It declares no source or include patterns that matched anything.`,\n `Nothing was written. Set failOnError to false to build anyway.`,\n ].join(' '),\n )\n }\n\n // Swap in the atomic volume only now that the instance has finished\n // reading its configs and token sources, so every write below lands\n // through `rename` while the read path stays exactly as it was. Seen\n // through a layer that notes what this build writes, which the record\n // below needs: a declared file is not necessarily a written one.\n const wrote = new Set<string>()\n sd.volume = notingWrites(volume, wrote)\n\n // The files this compile is about to write, named exactly as the\n // up-to-date check names them, and only the selected platforms' — a\n // scoped build leaves the others' files, and whatever wrote them, alone.\n const writing = declaredDestinations(\n { platforms: sd.platforms },\n selectedPlatforms,\n )\n\n // Forgotten before the first write rather than overwritten after the\n // last, because a build that throws partway has already replaced some\n // of these files. A record left standing would vouch for output that\n // no longer matches the configuration it names.\n for (const destination of writing) {\n destinationRecords.delete(destination)\n }\n\n if (selectedPlatforms === undefined) {\n await namingConfig(item, index, async () => sd.buildAllPlatforms())\n } else {\n // `cache: false`, and a configuration that would not parse ahead of\n // `extend`, arrive here without the check above having run.\n checkSelection(item, index, selectedPlatforms, sd.platforms)\n\n // One after another, matching the loop this sits inside: two\n // platforms may name the same destination, and `buildAllPlatforms`\n // fanning its own out with `Promise.all` is Style Dictionary's\n // choice over configurations it owns, not this plugin's over a\n // selection a consumer wrote.\n for (const name of selectedPlatforms) {\n await namingConfig(item, index, async () => sd.buildPlatform(name))\n }\n }\n\n // Every declared platform, not only the ones this compile built. A\n // file an unselected platform wrote on an earlier build is still the\n // plugin's own output, and dropping it from this set would let a\n // watcher treat it as a token source and rebuild once for every change\n // to it. Not a loop: a scoped rebuild never rewrites that file.\n //\n // Collected on every build rather than only on the ones whose size\n // report prints it below. The set is also what keeps a rebuild from\n // being triggered by the write it just made, and a rebuild passes a\n // `context` — so gating the collection on `!context` left it empty on\n // exactly the builds a watcher is live for.\n //\n // Named by `writtenDestination`, the one place that knows where Style\n // Dictionary writes a file. Every copy of that rule this code has had\n // named files that were never written — against `root` until #362, and\n // an absolute destination until #367 — and handed them to `onBuildEnd`,\n // the size report and this very set.\n for (const platform of Object.values(sd.platforms)) {\n for (const file of platform.files ?? []) {\n if (file.destination) {\n generatedFiles.add(\n writtenDestination(platform.buildPath, file.destination),\n )\n }\n }\n }\n\n // Recorded only now, so a build that threw vouches for none of what it\n // wrote before throwing. Every configuration is recorded, a file-backed\n // one included: an object configuration naming the same file has to\n // see that something else wrote it last.\n //\n // `newestSource` goes in beside the fingerprint. It was taken before\n // this compile read anything, so a source saved while it ran is newer\n // than it, and the rebuild queued for that save is not skipped. So do\n // each file's mtime, size and inode as this compile left it,\n // byte-identical skip and all: a later check trusts the record only\n // while the file still has all three. A file Style Dictionary declined\n // to write is recorded as such, so a later check does not go looking\n // for it; one it wrote and that is already gone gets no record, so the\n // next build compiles.\n //\n // Only a compile with `cache` on has a fingerprint to record. One with it\n // off has still forgotten what it rewrote, above, which is what keeps\n // every other instance from vouching for its output.\n if (fingerprint !== null) {\n for (const destination of writing) {\n const record = recordOf(\n destination,\n fingerprint,\n newestSource,\n wrote.has(destination),\n )\n if (record) {\n destinationRecords.set(destination, record)\n recorded.set(destination, record)\n }\n }\n }\n }\n\n // Replaced wholesale rather than added to, so a destination dropped from\n // a configuration stops being treated as ours and becomes watchable\n // again. A build that throws never reaches this and leaves the previous\n // set standing, which is the safe direction: the files it wrote before\n // failing are still ours.\n // Once per compile rather than per configuration, and only after every\n // configuration succeeded. A compile that throws persists nothing, which\n // is safe: whatever it rewrote was replaced rather than written in place,\n // so it no longer has the inode its old record names, and that record\n // vouches for nothing.\n if (recordsFile !== undefined && recorded.size > 0) {\n writePersistedRecords(recordsFile, recorded)\n }\n\n generatedDestinations.clear()\n for (const destination of generatedFiles) {\n generatedDestinations.add(destination.replace(/\\\\/g, '/'))\n }\n } catch (err) {\n const duration = Date.now() - startTime\n\n // The change a rebuild followed, worded as a change rather than as the\n // culprit: after a debounced burst `context` is the last file that\n // changed, which is not necessarily the one that broke the build.\n log(\n context\n ? `Compilation failed after a change in ${context} (${duration}ms): ${errorMessage(err)}`\n : `Compilation failed after ${duration}ms: ${errorMessage(err)}`,\n 'error',\n )\n\n // Ahead of the throw decision on purpose, so the overlay sees a failure\n // whatever `failOnError` does with it. Under the dev server's default\n // the line below does not throw, and reading the outcome from a caller's\n // `catch` would see a rebuild that looked like it succeeded.\n notifyBuildOutcome(asError(err))\n\n // Ahead of the throw decision for the same reason as the line above: a\n // rebuild under the dev server's default does not throw, and a hook that\n // only fired when something else was about to fail would be silent on\n // exactly the builds a consumer is watching.\n if (onBuildError) callHook(log, 'onBuildError', onBuildError, err)\n\n // Reported, and then rethrown so the host stops. Swallowing it left\n // every target exiting 0 with the previous run's tokens still on disk\n // and in the bundle — a green build shipping stale values.\n if (failsTheBuild(failOnError, context)) throw err\n\n // Explicit, now that the reporting below sits outside the `try`. This\n // `catch` used to end the function by falling off the end of it; a\n // failure that is not rethrown would otherwise carry on to announce a\n // compile that did not happen.\n return\n }\n\n // The compile is what the overlay reflects, so this is said here rather\n // than at the end: everything below is reporting, it returns early in\n // three places, and a size table that throws must not leave a successful\n // build looking unfinished.\n notifyBuildOutcome(null)\n\n // One measurement, read by the hook below and by the reporting under it.\n const duration = Date.now() - startTime\n\n // Beside the overlay notification, and for the same reason it sits here\n // rather than at the end of the function: the reporting below returns\n // early in three places, and a build that finished has finished whether or\n // not a size table gets printed for it.\n //\n // Sorted, so two runs of one configuration hand back the same order —\n // `generatedFiles` is a set in platform-then-file order, which is stable\n // in practice and guaranteed by nothing. The paths stay platform-native:\n // this is a list a consumer is going to open files with, not one the\n // watcher compares against.\n //\n // Only the declared files that are on disk. One whose filter matched no\n // tokens is declared and never written, and a hook opening it got an ENOENT.\n if (onBuildEnd) {\n // `toSorted` is what the linter asks for and what this cannot use:\n // `lib` is ES2022 here and `toSorted` is ES2023, so it types as an error\n // even though every Node this package supports has it. The rule guards\n // against mutating an array someone else holds, and this one was built\n // from the set on the line it appears on.\n const files = Array.from(generatedFiles)\n .filter((file) => fs.existsSync(file))\n // oxlint-disable-next-line unicorn/no-array-sort\n .sort((left, right) => left.localeCompare(right))\n callHook(log, 'onBuildEnd', onBuildEnd, files, duration)\n }\n\n // The `try` ends above, and everything from here down is reporting. Style\n // Dictionary has finished writing by now and `generatedDestinations` is\n // already replaced, so nothing below can put a file on disk in doubt —\n // which is why a throw from it must not be caught as a compile failure.\n // It used to be: a fault in the padding arithmetic printed `Compilation\n // failed after 19ms` over a build whose every token file was correct, and\n // with `failOnError` defaulting to `'build'` that stopped the bundler.\n\n // Every configuration was already current, so nothing was written. Said\n // rather than left implied: a build that prints its opening line and then\n // finishes in two milliseconds reads as one that silently did nothing.\n const everythingSkipped = skipped === resolvedConfigs.length\n\n if (context) {\n log(\n everythingSkipped\n ? `Design tokens already up to date after change in ${context} (${duration}ms)`\n : `Rebuilt design tokens due to change in ${context} (${duration}ms)`,\n 'success',\n )\n return\n }\n\n // The table is skipped when nothing was written, on top of `report` and\n // `quiet`. It reads every generated file in full and gzips it, and\n // reprinting the sizes of files this build did not touch is the one case\n // where that cost buys nothing at all.\n if (report && !quiet && !everythingSkipped && generatedFiles.size > 0) {\n try {\n logTable((colour) => sizeTable({ colour, root }, generatedFiles))\n } catch (err) {\n // At `'error'`, so it is said at every level including `silent`,\n // exactly as a compile failure is — and worded so it cannot be read\n // as one. Not rethrown: the build succeeded.\n log(\n `Failed to report generated file sizes: ${errorMessage(err)}`,\n 'error',\n )\n }\n }\n\n if (everythingSkipped) {\n log(`Design tokens are already up to date (${duration}ms)`, 'success')\n return\n }\n\n log(\n skipped > 0\n ? `Compiled successfully! (${duration}ms, ${skipped} already up to date)`\n : `Compiled successfully! (${duration}ms)`,\n 'success',\n )\n}\n\n// Runs one of the consumer's `onBuild*` hooks without letting it decide the\n// fate of the build that called it.\n//\n// Two ways a hook can go wrong, and neither may propagate. A throw is caught\n// here, because a post-processing step that fails must not undo a compile the\n// plugin itself completed — the files are written and correct. A rejected\n// promise is the quieter one: the return value is deliberately not awaited,\n// so a rejection has nothing holding it and reaches the host as an unhandled\n// rejection, which under Node's default takes the process down — a dev server\n// killed from inside a hook that was only meant to reformat a file.\n//\n// Both are reported at `'error'`, so they are said at every level including\n// `silent`, and worded so neither can be read as the compile having failed.\nfunction callHook<A extends unknown[]>(\n log: PluginInstance['log'],\n name: string,\n hook: (...args: A) => Promise<void> | void,\n ...args: A\n): void {\n let result: unknown\n\n try {\n // Captured rather than dropped, because the promise an `async` hook\n // returns is the thing the check below needs. Wrapping this call in a\n // block-bodied arrow — which is what the linter asks for when the return\n // type is plain `void` — discarded it, and the rejection escaped exactly\n // as it had before any of this existed.\n result = hook(...args)\n } catch (err) {\n log(`The ${name} hook threw: ${errorMessage(err)}`, 'error')\n return\n }\n\n if (!isThenable(result)) return\n\n void Promise.resolve(result).catch((err: unknown) => {\n log(`The ${name} hook rejected: ${errorMessage(err)}`, 'error')\n })\n}\n\n// Throws when `selected` names a platform that `platforms` does not define.\n// Named, so a typo is an error rather than a platform silently not built —\n// which is what Style Dictionary's own CLI means by \"Must be defined in the\n// config\" — and one function, so the check ahead of the up-to-date skip and\n// the one on the build path say exactly the same thing.\nfunction checkSelection(\n item: ResolvedConfig,\n index: number,\n selected: string[] | undefined,\n platforms: Record<string, unknown> | undefined,\n): void {\n if (selected === undefined) return\n\n const defined = Object.keys(platforms ?? {})\n const unknown = selected.filter((name) => !defined.includes(name))\n if (unknown.length > 0) {\n throw new Error(\n `${describeConfig(item, index)} does not define the platform(s) ${unknown.join(', ')}. It defines ${defined.join(', ')}.`,\n )\n }\n}\n\n// Whether a failure in this compile should be thrown rather than only\n// reported. The two compiles are told apart by `runBuilds`'s `context`,\n// which only the rebuild paths pass.\nfunction failsTheBuild(\n failOnError: NonNullable<UnpluginStyleDictionaryOptions['failOnError']>,\n context: string | undefined,\n): boolean {\n return (\n failOnError === true ||\n (context === undefined ? failOnError === 'build' : failOnError === 'serve')\n )\n}\n\n// A hook is the consumer's code, and what it hands back is not this plugin's to\n// assume. A predicate rather than `instanceof Promise`, which answers `false`\n// for a thenable from another realm or from a promise library — exactly the\n// case where letting a rejection escape does the damage.\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'then' in value &&\n typeof value.then === 'function'\n )\n}\n\n// Runs one of Style Dictionary's own steps for a configuration, and names the\n// configuration in anything it throws. Its messages name neither the\n// configuration nor the file behind them, so with several configurations a\n// failure could have come from any of them — in the log line, the overlay and\n// `onBuildError` alike. `cause` keeps its own error reachable.\n//\n// Only Style Dictionary's steps, never the whole loop: the plugin's own errors\n// already open with `describeConfig`, and wrapping those would name the\n// configuration twice.\nasync function namingConfig<T>(\n item: ResolvedConfig,\n index: number,\n step: () => Promise<T>,\n): Promise<T> {\n try {\n return await step()\n } catch (err) {\n throw new Error(\n `${describeConfig(item, index)} failed: ${errorMessage(err)}`,\n { cause: err },\n )\n }\n}\n\n// `volume`, with every path Style Dictionary writes through it noted in\n// `into`, resolved as `writtenDestination` resolves a declared one. Each file\n// a build writes reaches `promises.writeFile` (`buildPlatform` in\n// `lib/StyleDictionary.js`) — a byte-identical one included, since the atomic\n// writer decides to skip the rename only once it has been handed the file — so\n// `into` ends up holding exactly what the build wrote, and a file it declined\n// to write never appears in it. Built per compile, over the atomic volume\n// rather than in place of it.\n//\n// The sync writer is inherited untouched. Only an action reaches it, a\n// configuration with actions is never skipped, and `tests/index.test.ts`\n// checks that an action is handed the atomic writer itself.\nfunction notingWrites(volume: typeof fs, into: Set<string>): typeof fs {\n const note = (file: unknown) => {\n if (typeof file === 'string') into.add(path.resolve(file))\n }\n\n /* oxlint-disable typescript/no-unsafe-type-assertion */\n return Object.create(volume, {\n promises: {\n value: Object.create(volume.promises, {\n writeFile: {\n value: async (...args: Parameters<typeof fs.promises.writeFile>) => {\n note(args[0])\n await volume.promises.writeFile(...args)\n },\n },\n }) as typeof fs.promises,\n },\n }) as typeof fs\n /* oxlint-enable typescript/no-unsafe-type-assertion */\n}\n\n// Which platforms this compile covers, or `undefined` for all of them.\n//\n// The array form applies to every build; the object form splits the first\n// compile from the watch rebuilds, and `context` is what tells them apart —\n// only the rebuild paths pass one. An absent key means every platform, so\n// `{ watch: ['css'] }` builds everything once and then only css.\nfunction platformsFor(\n platformsOption: UnpluginStyleDictionaryOptions['platforms'],\n context: string | undefined,\n): string[] | undefined {\n if (platformsOption === undefined) return undefined\n if (Array.isArray(platformsOption)) return platformsOption\n\n return context === undefined ? platformsOption.build : platformsOption.watch\n}\n"],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -1,49 +1,5 @@
1
1
  import { Config } from "style-dictionary";
2
2
  //#region src/types.d.ts
3
- /**
4
- * Options for the Style Dictionary unplugin factory, shared across all bundler
5
- * targets (Vite, Rolldown, Rollup, Webpack).
6
- *
7
- * Every target compiles tokens before the build that consumes them. Live
8
- * rebuild-on-change is driven by the host bundler's watch mode, because token
9
- * source files sit outside the module graph: Vite's dev server, `rollup
10
- * --watch` and `webpack --watch` all rebuild on a token change, and a one-shot
11
- * build (e.g. `tsdown`/`rolldown build` without `--watch`) only builds once, in
12
- * `buildStart`.
13
- *
14
- * What the plugin says goes through the host wherever the host has a channel
15
- * for it: Vite's `config.logger`, the plugin context under rollup and
16
- * rolldown, and `compilation.warnings` under webpack and rspack, which is what
17
- * puts a failed compile in `stats.toJson()`. The progress lines and the size
18
- * table take the host's `info` channel, and webpack and rspack have none, so
19
- * there both go to the console. A failure is reported on the warning channel
20
- * and never the error one — rollup's `this.error` aborts the bundle, and that
21
- * decision is `failOnError`'s alone. Where no host offers a channel the console
22
- * is used, with colour gated on `NO_COLOR`, `FORCE_COLOR` and whether the
23
- * stream is a terminal.
24
- *
25
- * Style Dictionary's own lines are not the plugin's. The platform name and the
26
- * `✔︎` for each file it wrote are its own `console` calls, so they reach the
27
- * console under every host, whatever the host's log level. `logLevel: 'silent'`
28
- * quiets them, as does `log.verbosity: 'silent'` in the configuration.
29
- *
30
- * The three `onBuild*` hooks are called synchronously and their return value
31
- * is not awaited, so a build never waits for one. A hook may still be written
32
- * `async`: a promise it returns is left to run on its own, and a rejection is
33
- * caught and reported rather than reaching the host as an unhandled one. A
34
- * hook that throws is reported and does not fail the build that called it.
35
- *
36
- * They return `Promise<void> | void` rather than `void` for that reason. Both
37
- * accept an `async` hook as far as the compiler is concerned, but `void` alone
38
- * makes one a `no-misused-promises` error under the type-aware lint rules a
39
- * consumer is likely to be running — for a hook this documents as supported.
40
- *
41
- * Rolldown's watch mode is the exception, and it is not about glob patterns.
42
- * `addWatchFile` is accepted either way, but what happens next differs by
43
- * platform — on macOS a file registered through it is watched by nothing, while
44
- * on a Linux runner the same edit reaches a rebuild. Do not rely on a token
45
- * edit triggering a rebuild there.
46
- */
47
3
  /**
48
4
  * What the host is doing, handed to the function form of `config` so it can
49
5
  * decide what to build.
@@ -81,6 +37,51 @@ export interface StyleDictionaryConfigContext {
81
37
  */
82
38
  watch: boolean;
83
39
  }
40
+ /**
41
+ * Options for the Style Dictionary unplugin factory, shared across all bundler
42
+ * targets (Vite, Rolldown, Rollup, Webpack, Rspack).
43
+ *
44
+ * Every target compiles tokens before the build that consumes them. Live
45
+ * rebuild-on-change is driven by the host bundler's watch mode, because token
46
+ * source files sit outside the module graph: Vite's dev server, `rollup
47
+ * --watch`, `rolldown.watch()`, `webpack --watch` and `rspack --watch` all
48
+ * rebuild on a token change, and a one-shot build (e.g. `tsdown`/`rolldown
49
+ * build` without `--watch`) only builds once, in `buildStart`.
50
+ *
51
+ * What the plugin says goes through the host wherever the host has a channel
52
+ * for it: Vite's `config.logger`, the plugin context under rollup and
53
+ * rolldown, and `compilation.warnings` under webpack and rspack, which is what
54
+ * puts a failed compile in `stats.toJson()`. The progress lines and the size
55
+ * table take the host's `info` channel, and webpack and rspack have none, so
56
+ * there both go to the console. A failure is reported on the warning channel
57
+ * and never the error one — rollup's `this.error` aborts the bundle, and that
58
+ * decision is `failOnError`'s alone. Where no host offers a channel the console
59
+ * is used, with colour gated on `NO_COLOR`, `FORCE_COLOR` and whether the
60
+ * stream is a terminal.
61
+ *
62
+ * Style Dictionary's own lines are not the plugin's. The platform name and the
63
+ * `✔︎` for each file it wrote are its own `console` calls, so they reach the
64
+ * console under every host, whatever the host's log level. `logLevel: 'silent'`
65
+ * quiets them, as does `log.verbosity: 'silent'` in the configuration.
66
+ *
67
+ * The three `onBuild*` hooks are called synchronously and their return value
68
+ * is not awaited, so a build never waits for one. A hook may still be written
69
+ * `async`: a promise it returns is left to run on its own, and a rejection is
70
+ * caught and reported rather than reaching the host as an unhandled one. A
71
+ * hook that throws is reported and does not fail the build that called it.
72
+ *
73
+ * They return `Promise<void> | void` rather than `void` for that reason. Both
74
+ * accept an `async` hook as far as the compiler is concerned, but `void` alone
75
+ * makes one a `no-misused-promises` error under the type-aware lint rules a
76
+ * consumer is likely to be running — for a hook this documents as supported.
77
+ *
78
+ * Rolldown's watch mode rebuilds on macOS as well, a token reached through a
79
+ * symbolic link included. Its watcher there drops the events for a path
80
+ * registered through a link — a workspace package linked into `node_modules`,
81
+ * or a project under `/var` or `/tmp`, both links on macOS — so each such path
82
+ * is registered by its realpath beside it, and the event is mapped back to the
83
+ * spelling the `source` patterns use.
84
+ */
84
85
  export interface UnpluginStyleDictionaryOptions {
85
86
  /**
86
87
  * Whether a configuration whose output is already up to date may skip its
@@ -178,7 +179,7 @@ export interface UnpluginStyleDictionaryOptions {
178
179
  * frame naming this plugin, and the overlay is dismissed on the next
179
180
  * rebuild that succeeds.
180
181
  *
181
- * This is Vite's overlay, so it does nothing on the other three targets,
182
+ * This is Vite's overlay, so it does nothing on the other four targets,
182
183
  * and nothing under `vite build` — there is no page to draw on.
183
184
  *
184
185
  * It is not `failOnError`'s job, and the two are independent. `failOnError`
@@ -186,9 +187,9 @@ export interface UnpluginStyleDictionaryOptions {
186
187
  * A dev server deliberately keeps serving through a failed rebuild, which is
187
188
  * precisely the case where the overlay is the only thing that can say so.
188
189
  *
189
- * A failure Style Dictionary raises before this plugin can catch it — a
190
- * token file that is not valid JSON, which rejects out of band — reaches
191
- * neither the overlay nor this option.
190
+ * A token file that fails to parse is reported like any other failed
191
+ * compile: it reaches the overlay, and `onBuildError`, with the parser's
192
+ * message.
192
193
  *
193
194
  * @default true
194
195
  */
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import StyleDictionary from "style-dictionary";
5
5
  import { createHash } from "node:crypto";
6
6
  //#region src/up-to-date.ts
7
- const RECORDS_VERSION = 3;
7
+ const RECORDS_VERSION = 4;
8
8
  function configFingerprint(root, item, configObj) {
9
9
  let serialised;
10
10
  try {
@@ -43,7 +43,7 @@ function isUpToDate({ destinationRecords, fingerprint, newestSource }, item, con
43
43
  }
44
44
  const stats = statOrNull(destination);
45
45
  if (!stats) return false;
46
- if (writtenByThis && record.written !== stats.mtimeMs) return false;
46
+ if (writtenByThis && (record.written !== stats.mtimeMs || record.size !== stats.size || record.ino !== stats.ino)) return false;
47
47
  const lastRead = writtenByThis ? record.newestSource : null;
48
48
  if (lastRead === null ? stats.mtimeMs <= newestSource : newestSource > lastRead) return false;
49
49
  }
@@ -81,10 +81,12 @@ function readPersistedRecords(file) {
81
81
  const stored = parsed.records;
82
82
  for (const destination of Object.keys(stored)) {
83
83
  const value = Reflect.get(stored, destination);
84
- if (typeof value === "object" && value !== null && "fingerprint" in value && typeof value.fingerprint === "string" && "newestSource" in value && (value.newestSource === null || typeof value.newestSource === "number") && "written" in value && (value.written === null || typeof value.written === "number")) records.set(destination, {
84
+ if (typeof value === "object" && value !== null && "fingerprint" in value && typeof value.fingerprint === "string" && "ino" in value && (value.ino === null || typeof value.ino === "number") && "newestSource" in value && (value.newestSource === null || typeof value.newestSource === "number") && "size" in value && (value.size === null || typeof value.size === "number") && "written" in value && (value.written === null || typeof value.written === "number")) records.set(destination, {
85
85
  fingerprint: value.fingerprint,
86
+ ino: value.ino,
86
87
  newestSource: value.newestSource,
87
88
  persisted: true,
89
+ size: value.size,
88
90
  written: value.written
89
91
  });
90
92
  }
@@ -93,13 +95,17 @@ function readPersistedRecords(file) {
93
95
  function recordOf(destination, fingerprint, newestSource, wrote) {
94
96
  if (!wrote) return {
95
97
  fingerprint,
98
+ ino: null,
96
99
  newestSource,
100
+ size: null,
97
101
  written: null
98
102
  };
99
103
  const stats = statOrNull(destination);
100
104
  return stats ? {
101
105
  fingerprint,
106
+ ino: stats.ino,
102
107
  newestSource,
108
+ size: stats.size,
103
109
  written: stats.mtimeMs
104
110
  } : null;
105
111
  }
@@ -119,11 +125,16 @@ function writePersistedRecords(file, recorded) {
119
125
  const merged = readPersistedRecords(file);
120
126
  for (const [destination, record] of recorded) merged.set(destination, record);
121
127
  const records = {};
122
- for (const [destination, { fingerprint, newestSource, written }] of merged) if (written === null ? statOrNull(path.dirname(destination)) : statOrNull(destination)) records[destination] = {
123
- fingerprint,
124
- newestSource,
125
- written
126
- };
128
+ for (const [destination, record] of merged) {
129
+ const { fingerprint, ino, newestSource, size, written } = record;
130
+ if (written === null ? statOrNull(path.dirname(destination)) : statOrNull(destination)) records[destination] = {
131
+ fingerprint,
132
+ ino,
133
+ newestSource,
134
+ size,
135
+ written
136
+ };
137
+ }
127
138
  const temporary = `${file}.${String(process.pid)}.tmp`;
128
139
  try {
129
140
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -1 +1 @@
1
- {"version":3,"file":"up-to-date.js","names":[],"sources":["../src/up-to-date.ts"],"sourcesContent":["// Whether a configuration's compile can be skipped, and the destinations that\n// question is asked about.\n//\n// Nothing here holds state — not even the record of which configuration last\n// wrote each destination, which is process-wide and lives in `index.ts` beside\n// `compilesInFlight`. What a plugin instance owns is handed in on each call.\n// The functions that read and write the persisted copy of that record work on\n// a file they are handed and keep nothing.\n\nimport type { Config, Hooks } from 'style-dictionary/types'\n\nimport { createHash } from 'node:crypto'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport StyleDictionary from 'style-dictionary'\n\nimport type { Log, ResolvedConfig } from './config.js'\nimport type { UnpluginStyleDictionaryOptions } from './types.js'\n\nimport {\n expandPatterns,\n sourcePatternsOf,\n watchOptionPatterns,\n} from './patterns.js'\n\n// What is known about the compile that last built one destination: which\n// configuration it was, the newest source it saw before it read any of them,\n// and the file's mtime once it was done. `newestSource` is `null` where that\n// compile did not look, which is when `cache` was off or the sources could not\n// be established. `written` is `null` where the compile declared the file and\n// Style Dictionary declined to write it — a filter matching no tokens — so\n// there is nothing on disk for it to vouch for. `persisted` marks a record an\n// earlier process left.\nexport interface DestinationRecord {\n fingerprint: string\n newestSource: null | number\n persisted?: true\n written: null | number\n}\n\n// Bumped whenever what a record or a fingerprint means changes, so a file an\n// older version of the plugin wrote is ignored rather than misread. 2 added\n// the hooks a configuration names and the Style Dictionary version to the\n// fingerprint, and started comparing it for a configuration given as a path.\n// 3 records a declared file the compile did not write, with `written: null`.\nconst RECORDS_VERSION = 3\n\n// A stable identity for one resolved configuration, or `null` where it\n// cannot have one. Functions are serialised by source rather than dropped,\n// because an inline `format` or `transform` is exactly the edit a\n// fingerprint has to notice, and `JSON.stringify` omits a function outright.\n//\n// It carries more than the configuration: the hooks it names, as the registry\n// holds them now, and the Style Dictionary version. A hook named by a string is\n// only that string in the configuration, and its body lives in the registry,\n// which a `register*` call changes without anything on disk moving — the\n// README's own Custom Formats pattern registers inside the `config` function.\n// An upgrade changes the built-in hooks the same way. Neither is a file the\n// up-to-date check reads, so neither could be seen before this.\n//\n// A digest of that serialisation rather than the serialisation itself,\n// because it is held against every destination a build writes, and a\n// configuration carrying its tokens inline serialises to all of them.\nexport function configFingerprint(\n root: string,\n item: ResolvedConfig,\n configObj: Config,\n): null | string {\n let serialised: string\n try {\n serialised = JSON.stringify(\n [\n root,\n item.file ?? null,\n configObj,\n namedHooksOf(configObj),\n StyleDictionary.VERSION,\n ],\n (_key, value: unknown) =>\n typeof value === 'function' ? `[fn]${String(value)}` : value,\n )\n } catch {\n // Circular, or holding a BigInt. It takes no identity rather than a\n // wrong one, so it compiles every time exactly as it did before.\n return null\n }\n\n return createHash('sha256').update(serialised).digest('hex')\n}\n\n// Every absolute destination a configuration declares, read off the\n// configuration itself rather than off an extended Style Dictionary\n// instance. Reading it here is the whole point: constructing the instance\n// is what the skip exists to avoid.\n//\n// Each one is named by `writtenDestination`, which `runBuilds` asks as well,\n// so the up-to-date check and the record of what was built name each file\n// where Style Dictionary writes it. Whether it did is the record's to say: a\n// file whose filter matches no tokens is declared and never written.\n//\n// `only` narrows this to named platforms, and exactly one caller wants that:\n// the up-to-date check, which asks whether the work *this* compile would do\n// is already done. Everywhere else the answer has to cover every declared\n// platform, because a file an unselected platform wrote earlier is still the\n// plugin's own output and has to stay out of the watch list.\nexport function declaredDestinations(\n configObj: Config,\n only?: string[],\n): string[] {\n const destinations: string[] = []\n\n const entries = Object.entries(configObj.platforms ?? {})\n const selected = only\n ? entries.filter(([name]) => only.includes(name))\n : entries\n\n for (const [, platform] of selected) {\n for (const file of platform.files ?? []) {\n if (file.destination) {\n destinations.push(\n writtenDestination(platform.buildPath, file.destination),\n )\n }\n }\n }\n\n return destinations\n}\n\n// Whether a configuration's compile can be skipped: every file its last compile\n// wrote still exists as it was left, and nothing it reads has changed since\n// that compile read it. A declared file Style Dictionary declined to write is\n// not asked about, because it was never going to be on disk — asking made a\n// configuration with an empty filter compile on every build.\n//\n// \"Since\" is measured against what that compile read wherever it left a\n// record, in this process or persisted by an earlier one. The record holds the\n// newest source it saw before reading, and a source newer than that is one it\n// never read. Comparing against the output instead lost a save that landed\n// while a rebuild ran, because the rebuild wrote its output after the save.\n// It also never skipped again once an edit left one destination unchanged:\n// the atomic write keeps a byte-identical file's mtime, so the edited source\n// stayed newer than it for good. Only with no record at all is the output\n// compared.\n//\n// Conservative in every direction it can be: anything it cannot establish —\n// a destination that is missing, a source it cannot stat, a configuration\n// declaring no destinations at all — is a reason to build rather than to\n// skip.\nexport function isUpToDate(\n {\n destinationRecords,\n fingerprint,\n newestSource,\n }: {\n destinationRecords: ReadonlyMap<string, DestinationRecord>\n fingerprint: null | string\n newestSource: null | number\n },\n item: ResolvedConfig,\n configObj: Config,\n only?: string[],\n): boolean {\n // An action writes what no `destination` names, so there is nothing for\n // the comparison below to check and skipping would leave its work undone.\n const hasActions = Object.values(configObj.platforms ?? {}).some(\n (platform) => (platform.actions?.length ?? 0) > 0,\n )\n if (hasActions) return false\n\n const destinations = declaredDestinations(configObj, only)\n if (destinations.length === 0) return false\n\n if (newestSource === null) return false\n\n for (const destination of destinations) {\n const record = destinationRecords.get(destination)\n const writtenByThis =\n fingerprint !== null && record?.fingerprint === fingerprint\n\n // Recorded against something else: another configuration, or this one\n // before a hook it names changed or Style Dictionary was upgraded. The\n // file holds that compile's output, and nothing the comparison below\n // reads can say so — which is why a configuration given as a path is\n // held to this as much as one given as an object.\n if (record !== undefined && !writtenByThis) return false\n\n // A configuration given as a path has its own file among the sources, so\n // an edit to it is accounted for and the skip holds across processes. One\n // given as an object or a function has not, so it skips only over files\n // this process wrote for it. Having built it at some point is not enough:\n // an edit that was then undone left the edit's output standing, and so\n // did a build of it that threw partway. An earlier process's record is not\n // enough either, which is why the first compile of a process builds it:\n // its functions are fingerprinted by their source, and what they close\n // over can differ from one process to the next.\n if (!item.file && (!writtenByThis || record.persisted === true)) {\n return false\n }\n\n // Declared and left unwritten by this configuration's last compile. There\n // is no file to stat, so all that can have changed is what it reads.\n if (writtenByThis && record.written === null) {\n if (record.newestSource === null || newestSource > record.newestSource) {\n return false\n }\n continue\n }\n\n const stats = statOrNull(destination)\n if (!stats) return false\n\n // The file has moved since this configuration wrote it, so something\n // wrote it afterwards and nothing here can say what it holds. Its mtime\n // is no evidence either way: an edit to generated output is newer than\n // every source, and would have been kept.\n if (writtenByThis && record.written !== stats.mtimeMs) return false\n\n const lastRead = writtenByThis ? record.newestSource : null\n\n if (\n lastRead === null\n ? stats.mtimeMs <= newestSource\n : newestSource > lastRead\n ) {\n return false\n }\n }\n\n return true\n}\n\n// The newest mtime among the files a configuration reads, or `null` where that\n// cannot be established: a source that cannot be stat'ed, or patterns that\n// matched no file at all.\n//\n// Asked before a compile reads anything, and recorded against what it writes,\n// so that the next `isUpToDate` compares against what the compile actually\n// read. A save that lands afterwards is newer than this, however much newer\n// the compile's own output turns out to be. Both sides of that comparison are\n// mtimes of the files themselves, so no clock is involved and nothing depends\n// on how finely the filesystem stamps a write.\nexport async function newestSourceOf(\n {\n log,\n root,\n watch,\n }: {\n log: Log\n root: string\n watch: UnpluginStyleDictionaryOptions['watch']\n },\n item: ResolvedConfig,\n configObj: Config,\n): Promise<null | number> {\n // `options.watch` belongs in here as much as `source` does. A consumer\n // names an extra file because something in the build reads it — a custom\n // format's own data file, most obviously — and leaving it out let a change\n // to it be skipped over while the watcher dutifully reported it.\n const read = sourcePatternsOf(configObj)\n const extra = watchOptionPatterns(root, watch)\n\n const sources = await expandPatterns(\n {\n globs: [...read.globs, ...extra.globs],\n literals: [...read.literals, ...extra.literals],\n },\n log,\n )\n if (item.file) sources.push(item.file.replace(/\\\\/g, '/'))\n\n if (sources.length === 0) return null\n\n let newestSource = -Infinity\n let sawFile = false\n\n for (const source of sources) {\n const stats = statOrNull(source)\n if (!stats) return null\n\n // Directories are in this list on purpose — `expandPatterns` registers\n // each pattern's static parent so a token file created later is\n // noticed — but their mtime cannot be read as an input signal here. A\n // directory's mtime moves whenever an entry is added or renamed inside\n // it, and the atomic write renames every generated file into place, so\n // a `buildPath` inside a watched directory made the build itself the\n // newest thing the comparison could see. Nothing was ever up to date.\n if (stats.isDirectory()) continue\n\n sawFile = true\n newestSource = Math.max(newestSource, stats.mtimeMs)\n }\n\n // Every pattern expanded to directories alone, so nothing was actually\n // read. Style Dictionary would build an empty dictionary from that, and a\n // skip would present the empty result as current.\n return sawFile ? newestSource : null\n}\n\n// The records an earlier process persisted, each marked as such. Anything\n// that does not read as this version's records — a missing file, one another\n// version wrote, one cut short — is no records at all, which costs a compile\n// and never a wrong skip.\nexport function readPersistedRecords(\n file: string,\n): Map<string, DestinationRecord> {\n const records = new Map<string, DestinationRecord>()\n\n let parsed: unknown\n try {\n parsed = JSON.parse(fs.readFileSync(file, 'utf8'))\n } catch {\n return records\n }\n\n if (\n typeof parsed !== 'object' ||\n parsed === null ||\n !('version' in parsed) ||\n parsed.version !== RECORDS_VERSION ||\n !('records' in parsed) ||\n typeof parsed.records !== 'object' ||\n parsed.records === null\n ) {\n return records\n }\n\n const stored = parsed.records\n for (const destination of Object.keys(stored)) {\n const value: unknown = Reflect.get(stored, destination)\n if (\n typeof value === 'object' &&\n value !== null &&\n 'fingerprint' in value &&\n typeof value.fingerprint === 'string' &&\n 'newestSource' in value &&\n (value.newestSource === null || typeof value.newestSource === 'number') &&\n 'written' in value &&\n (value.written === null || typeof value.written === 'number')\n ) {\n records.set(destination, {\n fingerprint: value.fingerprint,\n newestSource: value.newestSource,\n persisted: true,\n written: value.written,\n })\n }\n }\n\n return records\n}\n\n// The record a compile leaves for one destination it declared. One it wrote\n// is recorded with the file's mtime, and one Style Dictionary declined to write\n// with `written: null`. `null` comes back where a file it wrote is not there\n// to vouch for, so nothing is recorded for it and the next build compiles.\nexport function recordOf(\n destination: string,\n fingerprint: string,\n newestSource: null | number,\n wrote: boolean,\n): DestinationRecord | null {\n if (!wrote) return { fingerprint, newestSource, written: null }\n\n const stats = statOrNull(destination)\n return stats ? { fingerprint, newestSource, written: stats.mtimeMs } : null\n}\n\n// Where one project's records persist: under the nearest `package.json`'s\n// `node_modules/.cache`, the directory build tools share for this, and where\n// Vite keeps its own cache. The nearest package rather than `root`, because a\n// host's root can be a subdirectory — Vite's `root: 'src'` — and a cache\n// there would put a `node_modules` inside the consumer's sources. No `.json`\n// extension, so no `source` glob a consumer writes can read it as tokens.\nexport function recordsFileFor(root: string): string {\n let directory = root\n while (!fs.existsSync(path.join(directory, 'package.json'))) {\n const parent = path.dirname(directory)\n if (parent === directory) {\n directory = root\n break\n }\n directory = parent\n }\n\n return path.join(\n directory,\n 'node_modules',\n '.cache',\n 'unplugin-style-dictionary',\n 'records',\n )\n}\n\n// Merges what one compile recorded into the persisted records.\n//\n// Read again right before the write rather than trusted from the load, so a\n// record another process added since is kept. Two processes writing at once\n// can still lose one's entries, and that only costs a compile on the next\n// start. An entry whose destination is gone is dropped, which bounds the file\n// by the files that exist — and for a file the compile left unwritten, which is\n// never there, by the directory it would have been written into. Written\n// through a temporary file and a rename, so a reader never sees half of it.\nexport function writePersistedRecords(\n file: string,\n recorded: ReadonlyMap<string, DestinationRecord>,\n): void {\n const merged = readPersistedRecords(file)\n for (const [destination, record] of recorded) merged.set(destination, record)\n\n const records: Record<string, Omit<DestinationRecord, 'persisted'>> = {}\n for (const [destination, { fingerprint, newestSource, written }] of merged) {\n const stillThere =\n written === null\n ? statOrNull(path.dirname(destination))\n : statOrNull(destination)\n if (stillThere) {\n records[destination] = { fingerprint, newestSource, written }\n }\n }\n\n const temporary = `${file}.${String(process.pid)}.tmp`\n try {\n fs.mkdirSync(path.dirname(file), { recursive: true })\n fs.writeFileSync(\n temporary,\n JSON.stringify({ records, version: RECORDS_VERSION }),\n )\n fs.renameSync(temporary, file)\n } catch {\n // Only a cache: failing to write it costs a compile on the next start,\n // and never this build.\n try {\n fs.rmSync(temporary, { force: true })\n } catch {\n // Nothing more to do about a temporary file that cannot be removed.\n }\n }\n}\n\n// Where Style Dictionary writes one file, as an absolute path.\n//\n// It joins the destination onto the platform's `buildPath` when there is one\n// and writes the result relative to the working directory. **Joins, not\n// resolves**: an absolute destination lands under the build path rather than\n// replacing it. Measured with `buildPath: 'gen/'` and an absolute destination,\n// Style Dictionary wrote `gen/<that path>`, while the plugin had named the\n// absolute path on its own — a file that was never written.\n//\n// `root` has no say. It is where a relative `config` path is looked up and\n// nothing more, and reading `buildPath` against it named files that did not\n// exist whenever the two differed (#362). Style Dictionary joins with\n// `path-unified/posix`; the platform's own `join` names the same file, and\n// resolving the result is what normalises the separators either way.\nexport function writtenDestination(\n buildPath: string | undefined,\n destination: string,\n): string {\n return path.resolve(\n process.cwd(),\n buildPath ? path.join(buildPath, destination) : destination,\n )\n}\n\n// The hooks a configuration names by string, each as the registry holds it\n// now: the parsers and preprocessors it lists, each platform's transforms and\n// the transforms its `transformGroup` expands to, and each file's `format`,\n// `filter` and `fileHeader`. A hook given inline is part of the configuration\n// already. The configuration's own `hooks` win over the registry, as they do\n// when Style Dictionary builds.\n//\n// **Only the hooks it names, never the whole registry.** The registry is\n// shared by the whole process, so fingerprinting all of it would let one\n// configuration's `register*` call defeat every other configuration's skip.\nfunction namedHooksOf(\n configObj: Config,\n): Record<string, Record<string, unknown>> {\n const own: Hooks = configObj.hooks ?? {}\n const registry = StyleDictionary.hooks\n const named: Record<string, Record<string, unknown>> = {}\n\n const add = (kind: keyof Hooks, name: unknown) => {\n if (typeof name !== 'string') return\n const hook: unknown = own[kind]?.[name] ?? registry[kind][name]\n named[kind] ??= {}\n named[kind][name] = hook ?? null\n }\n\n for (const name of configObj.parsers ?? []) add('parsers', name)\n for (const name of configObj.preprocessors ?? []) add('preprocessors', name)\n\n for (const platform of Object.values(configObj.platforms ?? {})) {\n for (const name of platform.preprocessors ?? []) add('preprocessors', name)\n for (const transform of platform.transforms ?? []) {\n add('transforms', transform)\n }\n\n const group = platform.transformGroup\n if (group !== undefined) {\n add('transformGroups', group)\n\n // `unknown`, because a group nobody registered is `undefined` here\n // whatever the registry's type says.\n const members: unknown =\n own.transformGroups?.[group] ?? registry.transformGroups[group]\n if (Array.isArray(members)) {\n for (const name of members) add('transforms', name)\n }\n }\n\n add('fileHeaders', platform.options?.fileHeader)\n for (const file of platform.files ?? []) {\n add('formats', file.format)\n add('filters', file.filter)\n add('fileHeaders', file.options?.fileHeader)\n }\n }\n\n return named\n}\n\n// `fs.statSync` without the throw. A file that is missing, or that cannot be\n// read, is the same answer to every caller here: nothing to compare against.\nfunction statOrNull(file: string): fs.Stats | null {\n try {\n return fs.statSync(file)\n } catch {\n return null\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"up-to-date.js","names":[],"sources":["../src/up-to-date.ts"],"sourcesContent":["// Whether a configuration's compile can be skipped, and the destinations that\n// question is asked about.\n//\n// Nothing here holds state — not even the record of which configuration last\n// wrote each destination, which is process-wide and lives in `index.ts` beside\n// `compilesInFlight`. What a plugin instance owns is handed in on each call.\n// The functions that read and write the persisted copy of that record work on\n// a file they are handed and keep nothing.\n\nimport type { Config, Hooks } from 'style-dictionary/types'\n\nimport { createHash } from 'node:crypto'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport StyleDictionary from 'style-dictionary'\n\nimport type { Log, ResolvedConfig } from './config.js'\nimport type { UnpluginStyleDictionaryOptions } from './types.js'\n\nimport {\n expandPatterns,\n sourcePatternsOf,\n watchOptionPatterns,\n} from './patterns.js'\n\n// What is known about the compile that last built one destination: which\n// configuration it was, the newest source it saw before it read any of them,\n// and the file's mtime, size and inode once it was done. `newestSource` is\n// `null` where that compile did not look, which is when `cache` was off or the\n// sources could not be established. `written` is `null` where the compile\n// declared the file and Style Dictionary declined to write it — a filter\n// matching no tokens — so there is nothing on disk for it to vouch for, and\n// `size` and `ino` are `null` with it. `persisted` marks a record an earlier\n// process left.\n//\n// The mtime alone cannot vouch for the file. It moves only by a whole\n// timestamp tick — about 15ms on Windows, a second or more on HFS+, ext3 and\n// FAT — so a rewrite inside the tick the compile wrote in keeps it. `size`\n// sees a rewrite of another length, and `ino` a file replaced by a rename,\n// which is how atomic-save editors, `rsync` and this plugin's own writer\n// write. Both come from the stat the check already makes, so neither costs a\n// read. **Not `ctimeMs`**: it is stamped from the same clock as the mtime, so\n// it misses the same rewrites, and it moves on a `chmod` or an extended\n// attribute that changes nothing a build would.\nexport interface DestinationRecord {\n fingerprint: string\n ino: null | number\n newestSource: null | number\n persisted?: true\n size: null | number\n written: null | number\n}\n\n// Bumped whenever what a record or a fingerprint means changes, so a file an\n// older version of the plugin wrote is ignored rather than misread. 2 added\n// the hooks a configuration names and the Style Dictionary version to the\n// fingerprint, and started comparing it for a configuration given as a path.\n// 3 records a declared file the compile did not write, with `written: null`.\n// 4 records each written file's `size` and `ino` beside its mtime, which a\n// version-3 record carries neither of and must not be read as matching.\nconst RECORDS_VERSION = 4\n\n// A stable identity for one resolved configuration, or `null` where it\n// cannot have one. Functions are serialised by source rather than dropped,\n// because an inline `format` or `transform` is exactly the edit a\n// fingerprint has to notice, and `JSON.stringify` omits a function outright.\n//\n// It carries more than the configuration: the hooks it names, as the registry\n// holds them now, and the Style Dictionary version. A hook named by a string is\n// only that string in the configuration, and its body lives in the registry,\n// which a `register*` call changes without anything on disk moving — the\n// README's own Custom Formats pattern registers inside the `config` function.\n// An upgrade changes the built-in hooks the same way. Neither is a file the\n// up-to-date check reads, so neither could be seen before this.\n//\n// A digest of that serialisation rather than the serialisation itself,\n// because it is held against every destination a build writes, and a\n// configuration carrying its tokens inline serialises to all of them.\nexport function configFingerprint(\n root: string,\n item: ResolvedConfig,\n configObj: Config,\n): null | string {\n let serialised: string\n try {\n serialised = JSON.stringify(\n [\n root,\n item.file ?? null,\n configObj,\n namedHooksOf(configObj),\n StyleDictionary.VERSION,\n ],\n (_key, value: unknown) =>\n typeof value === 'function' ? `[fn]${String(value)}` : value,\n )\n } catch {\n // Circular, or holding a BigInt. It takes no identity rather than a\n // wrong one, so it compiles every time exactly as it did before.\n return null\n }\n\n return createHash('sha256').update(serialised).digest('hex')\n}\n\n// Every absolute destination a configuration declares, read off the\n// configuration itself rather than off an extended Style Dictionary\n// instance. Reading it here is the whole point: constructing the instance\n// is what the skip exists to avoid.\n//\n// Each one is named by `writtenDestination`, which `runBuilds` asks as well,\n// so the up-to-date check and the record of what was built name each file\n// where Style Dictionary writes it. Whether it did is the record's to say: a\n// file whose filter matches no tokens is declared and never written.\n//\n// `only` narrows this to named platforms, and exactly one caller wants that:\n// the up-to-date check, which asks whether the work *this* compile would do\n// is already done. Everywhere else the answer has to cover every declared\n// platform, because a file an unselected platform wrote earlier is still the\n// plugin's own output and has to stay out of the watch list.\nexport function declaredDestinations(\n configObj: Config,\n only?: string[],\n): string[] {\n const destinations: string[] = []\n\n const entries = Object.entries(configObj.platforms ?? {})\n const selected = only\n ? entries.filter(([name]) => only.includes(name))\n : entries\n\n for (const [, platform] of selected) {\n for (const file of platform.files ?? []) {\n if (file.destination) {\n destinations.push(\n writtenDestination(platform.buildPath, file.destination),\n )\n }\n }\n }\n\n return destinations\n}\n\n// Whether a configuration's compile can be skipped: every file its last compile\n// wrote still exists as it was left, and nothing it reads has changed since\n// that compile read it. A declared file Style Dictionary declined to write is\n// not asked about, because it was never going to be on disk — asking made a\n// configuration with an empty filter compile on every build.\n//\n// \"Since\" is measured against what that compile read wherever it left a\n// record, in this process or persisted by an earlier one. The record holds the\n// newest source it saw before reading, and a source newer than that is one it\n// never read. Comparing against the output instead lost a save that landed\n// while a rebuild ran, because the rebuild wrote its output after the save.\n// It also never skipped again once an edit left one destination unchanged:\n// the atomic write keeps a byte-identical file's mtime, so the edited source\n// stayed newer than it for good. Only with no record at all is the output\n// compared.\n//\n// Conservative in every direction it can be: anything it cannot establish —\n// a destination that is missing, a source it cannot stat, a configuration\n// declaring no destinations at all — is a reason to build rather than to\n// skip.\nexport function isUpToDate(\n {\n destinationRecords,\n fingerprint,\n newestSource,\n }: {\n destinationRecords: ReadonlyMap<string, DestinationRecord>\n fingerprint: null | string\n newestSource: null | number\n },\n item: ResolvedConfig,\n configObj: Config,\n only?: string[],\n): boolean {\n // An action writes what no `destination` names, so there is nothing for\n // the comparison below to check and skipping would leave its work undone.\n const hasActions = Object.values(configObj.platforms ?? {}).some(\n (platform) => (platform.actions?.length ?? 0) > 0,\n )\n if (hasActions) return false\n\n const destinations = declaredDestinations(configObj, only)\n if (destinations.length === 0) return false\n\n if (newestSource === null) return false\n\n for (const destination of destinations) {\n const record = destinationRecords.get(destination)\n const writtenByThis =\n fingerprint !== null && record?.fingerprint === fingerprint\n\n // Recorded against something else: another configuration, or this one\n // before a hook it names changed or Style Dictionary was upgraded. The\n // file holds that compile's output, and nothing the comparison below\n // reads can say so — which is why a configuration given as a path is\n // held to this as much as one given as an object.\n if (record !== undefined && !writtenByThis) return false\n\n // A configuration given as a path has its own file among the sources, so\n // an edit to it is accounted for and the skip holds across processes. One\n // given as an object or a function has not, so it skips only over files\n // this process wrote for it. Having built it at some point is not enough:\n // an edit that was then undone left the edit's output standing, and so\n // did a build of it that threw partway. An earlier process's record is not\n // enough either, which is why the first compile of a process builds it:\n // its functions are fingerprinted by their source, and what they close\n // over can differ from one process to the next.\n if (!item.file && (!writtenByThis || record.persisted === true)) {\n return false\n }\n\n // Declared and left unwritten by this configuration's last compile. There\n // is no file to stat, so all that can have changed is what it reads.\n if (writtenByThis && record.written === null) {\n if (record.newestSource === null || newestSource > record.newestSource) {\n return false\n }\n continue\n }\n\n const stats = statOrNull(destination)\n if (!stats) return false\n\n // The file has moved since this configuration wrote it, so something\n // wrote it afterwards and nothing here can say what it holds. Its mtime\n // is no evidence either way: an edit to generated output is newer than\n // every source, and would have been kept. The size and the inode are\n // asked beside the mtime, because a rewrite inside the timestamp tick the\n // compile wrote in keeps the mtime: one of another length moves the size,\n // and one renamed over the file moves the inode.\n if (\n writtenByThis &&\n (record.written !== stats.mtimeMs ||\n record.size !== stats.size ||\n record.ino !== stats.ino)\n ) {\n return false\n }\n\n const lastRead = writtenByThis ? record.newestSource : null\n\n if (\n lastRead === null\n ? stats.mtimeMs <= newestSource\n : newestSource > lastRead\n ) {\n return false\n }\n }\n\n return true\n}\n\n// The newest mtime among the files a configuration reads, or `null` where that\n// cannot be established: a source that cannot be stat'ed, or patterns that\n// matched no file at all.\n//\n// Asked before a compile reads anything, and recorded against what it writes,\n// so that the next `isUpToDate` compares against what the compile actually\n// read. A save that lands afterwards is newer than this, however much newer\n// the compile's own output turns out to be. Both sides of that comparison are\n// mtimes of the files themselves, so no clock is involved and nothing depends\n// on how finely the filesystem stamps a write.\nexport async function newestSourceOf(\n {\n log,\n root,\n watch,\n }: {\n log: Log\n root: string\n watch: UnpluginStyleDictionaryOptions['watch']\n },\n item: ResolvedConfig,\n configObj: Config,\n): Promise<null | number> {\n // `options.watch` belongs in here as much as `source` does. A consumer\n // names an extra file because something in the build reads it — a custom\n // format's own data file, most obviously — and leaving it out let a change\n // to it be skipped over while the watcher dutifully reported it.\n const read = sourcePatternsOf(configObj)\n const extra = watchOptionPatterns(root, watch)\n\n const sources = await expandPatterns(\n {\n globs: [...read.globs, ...extra.globs],\n literals: [...read.literals, ...extra.literals],\n },\n log,\n )\n if (item.file) sources.push(item.file.replace(/\\\\/g, '/'))\n\n if (sources.length === 0) return null\n\n let newestSource = -Infinity\n let sawFile = false\n\n for (const source of sources) {\n const stats = statOrNull(source)\n if (!stats) return null\n\n // Directories are in this list on purpose — `expandPatterns` registers\n // each pattern's static parent so a token file created later is\n // noticed — but their mtime cannot be read as an input signal here. A\n // directory's mtime moves whenever an entry is added or renamed inside\n // it, and the atomic write renames every generated file into place, so\n // a `buildPath` inside a watched directory made the build itself the\n // newest thing the comparison could see. Nothing was ever up to date.\n if (stats.isDirectory()) continue\n\n sawFile = true\n newestSource = Math.max(newestSource, stats.mtimeMs)\n }\n\n // Every pattern expanded to directories alone, so nothing was actually\n // read. Style Dictionary would build an empty dictionary from that, and a\n // skip would present the empty result as current.\n return sawFile ? newestSource : null\n}\n\n// The records an earlier process persisted, each marked as such. Anything\n// that does not read as this version's records — a missing file, one another\n// version wrote, one cut short — is no records at all, which costs a compile\n// and never a wrong skip.\nexport function readPersistedRecords(\n file: string,\n): Map<string, DestinationRecord> {\n const records = new Map<string, DestinationRecord>()\n\n let parsed: unknown\n try {\n parsed = JSON.parse(fs.readFileSync(file, 'utf8'))\n } catch {\n return records\n }\n\n if (\n typeof parsed !== 'object' ||\n parsed === null ||\n !('version' in parsed) ||\n parsed.version !== RECORDS_VERSION ||\n !('records' in parsed) ||\n typeof parsed.records !== 'object' ||\n parsed.records === null\n ) {\n return records\n }\n\n const stored = parsed.records\n for (const destination of Object.keys(stored)) {\n const value: unknown = Reflect.get(stored, destination)\n if (\n typeof value === 'object' &&\n value !== null &&\n 'fingerprint' in value &&\n typeof value.fingerprint === 'string' &&\n 'ino' in value &&\n (value.ino === null || typeof value.ino === 'number') &&\n 'newestSource' in value &&\n (value.newestSource === null || typeof value.newestSource === 'number') &&\n 'size' in value &&\n (value.size === null || typeof value.size === 'number') &&\n 'written' in value &&\n (value.written === null || typeof value.written === 'number')\n ) {\n records.set(destination, {\n fingerprint: value.fingerprint,\n ino: value.ino,\n newestSource: value.newestSource,\n persisted: true,\n size: value.size,\n written: value.written,\n })\n }\n }\n\n return records\n}\n\n// The record a compile leaves for one destination it declared. One it wrote\n// is recorded with the file's mtime, size and inode as it stood once written,\n// and one Style Dictionary declined to write with all three `null`. `null`\n// comes back where a file it wrote is not there to vouch for, so nothing is\n// recorded for it and the next build compiles.\nexport function recordOf(\n destination: string,\n fingerprint: string,\n newestSource: null | number,\n wrote: boolean,\n): DestinationRecord | null {\n if (!wrote) {\n return { fingerprint, ino: null, newestSource, size: null, written: null }\n }\n\n const stats = statOrNull(destination)\n return stats\n ? {\n fingerprint,\n ino: stats.ino,\n newestSource,\n size: stats.size,\n written: stats.mtimeMs,\n }\n : null\n}\n\n// Where one project's records persist: under the nearest `package.json`'s\n// `node_modules/.cache`, the directory build tools share for this, and where\n// Vite keeps its own cache. The nearest package rather than `root`, because a\n// host's root can be a subdirectory — Vite's `root: 'src'` — and a cache\n// there would put a `node_modules` inside the consumer's sources. No `.json`\n// extension, so no `source` glob a consumer writes can read it as tokens.\nexport function recordsFileFor(root: string): string {\n let directory = root\n while (!fs.existsSync(path.join(directory, 'package.json'))) {\n const parent = path.dirname(directory)\n if (parent === directory) {\n directory = root\n break\n }\n directory = parent\n }\n\n return path.join(\n directory,\n 'node_modules',\n '.cache',\n 'unplugin-style-dictionary',\n 'records',\n )\n}\n\n// Merges what one compile recorded into the persisted records.\n//\n// Read again right before the write rather than trusted from the load, so a\n// record another process added since is kept. Two processes writing at once\n// can still lose one's entries, and that only costs a compile on the next\n// start. An entry whose destination is gone is dropped, which bounds the file\n// by the files that exist — and for a file the compile left unwritten, which is\n// never there, by the directory it would have been written into. Written\n// through a temporary file and a rename, so a reader never sees half of it.\nexport function writePersistedRecords(\n file: string,\n recorded: ReadonlyMap<string, DestinationRecord>,\n): void {\n const merged = readPersistedRecords(file)\n for (const [destination, record] of recorded) merged.set(destination, record)\n\n const records: Record<string, Omit<DestinationRecord, 'persisted'>> = {}\n for (const [destination, record] of merged) {\n const { fingerprint, ino, newestSource, size, written } = record\n const stillThere =\n written === null\n ? statOrNull(path.dirname(destination))\n : statOrNull(destination)\n if (stillThere) {\n records[destination] = { fingerprint, ino, newestSource, size, written }\n }\n }\n\n const temporary = `${file}.${String(process.pid)}.tmp`\n try {\n fs.mkdirSync(path.dirname(file), { recursive: true })\n fs.writeFileSync(\n temporary,\n JSON.stringify({ records, version: RECORDS_VERSION }),\n )\n fs.renameSync(temporary, file)\n } catch {\n // Only a cache: failing to write it costs a compile on the next start,\n // and never this build.\n try {\n fs.rmSync(temporary, { force: true })\n } catch {\n // Nothing more to do about a temporary file that cannot be removed.\n }\n }\n}\n\n// Where Style Dictionary writes one file, as an absolute path.\n//\n// It joins the destination onto the platform's `buildPath` when there is one\n// and writes the result relative to the working directory. **Joins, not\n// resolves**: an absolute destination lands under the build path rather than\n// replacing it. Measured with `buildPath: 'gen/'` and an absolute destination,\n// Style Dictionary wrote `gen/<that path>`, while the plugin had named the\n// absolute path on its own — a file that was never written.\n//\n// `root` has no say. It is where a relative `config` path is looked up and\n// nothing more, and reading `buildPath` against it named files that did not\n// exist whenever the two differed (#362). Style Dictionary joins with\n// `path-unified/posix`; the platform's own `join` names the same file, and\n// resolving the result is what normalises the separators either way.\nexport function writtenDestination(\n buildPath: string | undefined,\n destination: string,\n): string {\n return path.resolve(\n process.cwd(),\n buildPath ? path.join(buildPath, destination) : destination,\n )\n}\n\n// The hooks a configuration names by string, each as the registry holds it\n// now: the parsers and preprocessors it lists, each platform's transforms and\n// the transforms its `transformGroup` expands to, and each file's `format`,\n// `filter` and `fileHeader`. A hook given inline is part of the configuration\n// already. The configuration's own `hooks` win over the registry, as they do\n// when Style Dictionary builds.\n//\n// **Only the hooks it names, never the whole registry.** The registry is\n// shared by the whole process, so fingerprinting all of it would let one\n// configuration's `register*` call defeat every other configuration's skip.\nfunction namedHooksOf(\n configObj: Config,\n): Record<string, Record<string, unknown>> {\n const own: Hooks = configObj.hooks ?? {}\n const registry = StyleDictionary.hooks\n const named: Record<string, Record<string, unknown>> = {}\n\n const add = (kind: keyof Hooks, name: unknown) => {\n if (typeof name !== 'string') return\n const hook: unknown = own[kind]?.[name] ?? registry[kind][name]\n named[kind] ??= {}\n named[kind][name] = hook ?? null\n }\n\n for (const name of configObj.parsers ?? []) add('parsers', name)\n for (const name of configObj.preprocessors ?? []) add('preprocessors', name)\n\n for (const platform of Object.values(configObj.platforms ?? {})) {\n for (const name of platform.preprocessors ?? []) add('preprocessors', name)\n for (const transform of platform.transforms ?? []) {\n add('transforms', transform)\n }\n\n const group = platform.transformGroup\n if (group !== undefined) {\n add('transformGroups', group)\n\n // `unknown`, because a group nobody registered is `undefined` here\n // whatever the registry's type says.\n const members: unknown =\n own.transformGroups?.[group] ?? registry.transformGroups[group]\n if (Array.isArray(members)) {\n for (const name of members) add('transforms', name)\n }\n }\n\n add('fileHeaders', platform.options?.fileHeader)\n for (const file of platform.files ?? []) {\n add('formats', file.format)\n add('filters', file.filter)\n add('fileHeaders', file.options?.fileHeader)\n }\n }\n\n return named\n}\n\n// `fs.statSync` without the throw. A file that is missing, or that cannot be\n// read, is the same answer to every caller here: nothing to compare against.\nfunction statOrNull(file: string): fs.Stats | null {\n try {\n return fs.statSync(file)\n } catch {\n return null\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@kanso-labs/unplugin-style-dictionary",
3
- "version": "0.10.18",
4
- "description": "Compile Style Dictionary design tokens ahead of your bundler (Vite, Rolldown, Rollup, or Webpack) from a single unplugin-based plugin, with automatic watching and rebuilding under Vite",
3
+ "version": "0.10.20",
4
+ "description": "Compile Style Dictionary design tokens ahead of your bundler (Vite, Rolldown, Rollup, Webpack or Rspack) from a single unplugin-based plugin, rebuilding on a token change under Vite's dev server and each bundler's watch mode",
5
5
  "keywords": [
6
6
  "unplugin",
7
7
  "vite",
8
8
  "rolldown",
9
9
  "rollup",
10
10
  "webpack",
11
+ "rspack",
11
12
  "style-dictionary",
12
13
  "design-tokens",
13
14
  "tokens"
@@ -102,7 +103,7 @@
102
103
  "rolldown": "1.2.12",
103
104
  "rollup": "4.64.0",
104
105
  "semver": "7.8.5",
105
- "style-dictionary": "5.5.5",
106
+ "style-dictionary": "5.6.0",
106
107
  "tsdown": "0.23.0",
107
108
  "typescript": "6.0.3",
108
109
  "typescript-eslint": "8.71.0",