@kanso-labs/unplugin-style-dictionary 0.10.11 → 0.10.13
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 +87 -33
- package/dist/compile.js +40 -8
- package/dist/compile.js.map +1 -1
- package/dist/index.js +36 -7
- package/dist/index.js.map +1 -1
- package/dist/patterns.js +53 -33
- package/dist/patterns.js.map +1 -1
- package/dist/types.d.ts +32 -8
- package/dist/up-to-date.js +142 -19
- package/dist/up-to-date.js.map +1 -1
- package/dist/watch-filter.js +4 -6
- package/dist/watch-filter.js.map +1 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -25,8 +25,9 @@ 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
|
|
29
|
-
|
|
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
|
|
30
31
|
[Watching, per target](#watching-per-target).
|
|
31
32
|
- **Config flexibility**: Supports file paths (JSON, JSON5, JSONC, JS, MJS, TS),
|
|
32
33
|
configuration objects, or functions — including registering custom formats at
|
|
@@ -335,13 +336,14 @@ Every target compiles tokens before the build that consumes them. What a later
|
|
|
335
336
|
change to a token file triggers is not the same everywhere, because it depends
|
|
336
337
|
on what the host bundler does with the watch list the plugin registers.
|
|
337
338
|
|
|
338
|
-
| Target
|
|
339
|
-
|
|
|
340
|
-
| **Vite**
|
|
341
|
-
| **
|
|
342
|
-
| **
|
|
343
|
-
| **
|
|
344
|
-
| **
|
|
339
|
+
| Target | Compiles before the build | Rebuilds on a token change | Safe from rebuild loops |
|
|
340
|
+
| ------------------------- | ------------------------- | --------------------------------- | ----------------------- |
|
|
341
|
+
| **Vite** | yes | yes, under the dev server | yes |
|
|
342
|
+
| **Vite**, `build --watch` | yes | yes on 6 and 7; on 8, as Rolldown | yes |
|
|
343
|
+
| **Rollup** | yes | yes, under `rollup --watch` | yes |
|
|
344
|
+
| **Webpack** | yes | yes, under `webpack --watch` | yes |
|
|
345
|
+
| **Rspack** | yes | yes, under `rspack --watch` | yes |
|
|
346
|
+
| **Rolldown** | yes | platform-dependent — see below | yes |
|
|
345
347
|
|
|
346
348
|
Patterns and literal paths behave the same way wherever rebuilds happen at all.
|
|
347
349
|
A `source` of `tokens/**/*.json` matches a file sitting directly in `tokens/` as
|
|
@@ -365,9 +367,15 @@ the files it registers, by name, on Vite 6, 7 and 8. The rest of `node_modules`
|
|
|
365
367
|
stays ignored, which matters: handing the whole dependency tree to the watcher
|
|
366
368
|
is thousands of files no token build reads.
|
|
367
369
|
|
|
370
|
+
`vite build --watch` had the same problem and gets the same fix on Vite 6 and 7,
|
|
371
|
+
which build on rollup's watcher and its chokidar ignore list: the plugin appends
|
|
372
|
+
the same negations to `build.watch.chokidar.ignored`. Vite 8 builds with
|
|
373
|
+
rolldown, which takes no ignore list from there, so what the Rolldown note above
|
|
374
|
+
says holds for it too.
|
|
375
|
+
|
|
368
376
|
Nothing is needed from you for that. If you had worked around it with a
|
|
369
|
-
`server.watch.ignored` negation of your own,
|
|
370
|
-
to your list rather than replacing it.
|
|
377
|
+
`server.watch.ignored` or `build.watch.chokidar.ignored` negation of your own,
|
|
378
|
+
it still works — the plugin appends to your list rather than replacing it.
|
|
371
379
|
|
|
372
380
|
"Safe from rebuild loops" is worth stating because consuming code imports the
|
|
373
381
|
generated file, so every regenerate is itself a change the host reacts to. The
|
|
@@ -429,26 +437,46 @@ after dropping `legacy.scss` from a configuration, a clean run left
|
|
|
429
437
|
|
|
430
438
|
## Skipping a Build That Would Change Nothing
|
|
431
439
|
|
|
432
|
-
A configuration whose output
|
|
440
|
+
A configuration whose output already reflects everything it reads is not
|
|
433
441
|
compiled again. `buildAllPlatforms` is around 80% of a build, and under Vite it
|
|
434
442
|
runs inside `server.listen()` — so without this the dev server refused
|
|
435
443
|
connections for the length of a compile whether or not a token had changed.
|
|
436
444
|
|
|
437
445
|
Three things are compared: every file the configuration reads (its `source` and
|
|
438
446
|
`include` matches, its own config file, and anything named by `watch`), every
|
|
439
|
-
file it declares, and
|
|
440
|
-
configuration
|
|
447
|
+
file it declares, and the configuration that last wrote them. That last one
|
|
448
|
+
covers more than the configuration's own contents. It includes the hooks the
|
|
449
|
+
configuration names — each `format`, `filter`, `fileHeader`, transform,
|
|
450
|
+
transform group, parser and preprocessor it refers to by name — as Style
|
|
451
|
+
Dictionary's registry holds them, and the Style Dictionary version. So a new
|
|
452
|
+
body registered under a name it uses, which is what editing the
|
|
453
|
+
[Custom Formats](#custom-formats) pattern does, rebuilds even though no file
|
|
454
|
+
moved, and so does an upgrade.
|
|
455
|
+
|
|
456
|
+
Once a configuration has been compiled, the files it reads are compared against
|
|
457
|
+
what that compile saw before it started reading. A token saved while a rebuild
|
|
458
|
+
is compiling is newer than that, so the rebuild queued for it runs, even though
|
|
459
|
+
the output the rebuild went on to write is newer still. The record of what each
|
|
460
|
+
compile read is kept in `node_modules/.cache/unplugin-style-dictionary`, beside
|
|
461
|
+
the nearest `package.json`, so it survives a restart. Where there is no record,
|
|
462
|
+
the files a configuration reads are compared against the files it declares.
|
|
463
|
+
|
|
464
|
+
A declared file that changed since the compile that wrote it is compiled over
|
|
465
|
+
rather than trusted, whatever its mtime says. Generated output is disposable, so
|
|
466
|
+
an edit made to it by hand does not survive the next build.
|
|
441
467
|
|
|
442
468
|
Two cases never skip, because neither can be settled from the filesystem:
|
|
443
469
|
|
|
444
|
-
| Case | Why
|
|
445
|
-
| ------------------------------------------------------------------------------------ |
|
|
446
|
-
| The first compile of a process, for a configuration given as an object or a function | There is no config file to stat, so an edit to the object inside `vite.config.ts` moves no mtime. Within one process the resolved configuration is compared against the one last built
|
|
447
|
-
| A platform declaring `actions` | An action writes what no `destination` names, so a skip would leave its work undone.
|
|
470
|
+
| Case | Why |
|
|
471
|
+
| ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
472
|
+
| The first compile of a process, for a configuration given as an object or a function | There is no config file to stat, so an edit to the object inside `vite.config.ts` moves no mtime. Within one process the resolved configuration is compared against the one last built. An earlier process's record is not trusted for it, because its functions are compared by their source and what they close over can differ. |
|
|
473
|
+
| A platform declaring `actions` | An action writes what no `destination` names, so a skip would leave its work undone. |
|
|
448
474
|
|
|
449
|
-
A
|
|
450
|
-
|
|
451
|
-
|
|
475
|
+
A hook's own code is compared, but not what it calls from another module. Name
|
|
476
|
+
that module in `watch` and an edit to it rebuilds on the next start. A custom
|
|
477
|
+
format that reads something off-disk — an environment variable, a network call —
|
|
478
|
+
cannot be detected this way either. Set `cache: false` where that is the case,
|
|
479
|
+
and every build runs.
|
|
452
480
|
|
|
453
481
|
## One Compile per Process
|
|
454
482
|
|
|
@@ -632,10 +660,12 @@ StyleDictionary({
|
|
|
632
660
|
```
|
|
633
661
|
|
|
634
662
|
`files` holds the absolute, platform-native path of every file the build
|
|
635
|
-
declares, sorted. It is what the build declares rather than what
|
|
636
|
-
write this time: a configuration skipped because its output was
|
|
637
|
-
contributes its destinations too, so a post-processing step
|
|
638
|
-
set on a rebuild that changed one file.
|
|
663
|
+
declares that is on disk, sorted. It is what the build declares rather than what
|
|
664
|
+
it happened to write this time: a configuration skipped because its output was
|
|
665
|
+
already current contributes its destinations too, so a post-processing step
|
|
666
|
+
still sees the whole set on a rebuild that changed one file. A declared file
|
|
667
|
+
Style Dictionary declined to write, such as one whose filter matched no tokens,
|
|
668
|
+
is left out.
|
|
639
669
|
|
|
640
670
|
A rebuild is a build, so all three fire again on every watch-triggered one.
|
|
641
671
|
|
|
@@ -747,8 +777,8 @@ export interface StyleDictionaryConfigContext {
|
|
|
747
777
|
/**
|
|
748
778
|
* Whether the host will keep rebuilding.
|
|
749
779
|
*
|
|
750
|
-
* `true` under Vite's dev server
|
|
751
|
-
* `webpack --watch`; `false` for a one-shot build. It is read from the
|
|
780
|
+
* `true` under Vite's dev server and `vite build --watch`, `rollup --watch`,
|
|
781
|
+
* `rolldown.watch()` and `webpack --watch`; `false` for a one-shot build. It is read from the
|
|
752
782
|
* host — the plugin context's `meta.watchMode` on the three rollup-shaped
|
|
753
783
|
* targets, and `compiler.watchMode` on webpack — rather than inferred from
|
|
754
784
|
* `command`, because `rollup --watch` both watches and builds.
|
|
@@ -765,8 +795,28 @@ export interface UnpluginStyleDictionaryOptions {
|
|
|
765
795
|
* 4,000-token, two-platform configuration — and under Vite it runs inside
|
|
766
796
|
* `server.listen()`, so the dev server does not accept a connection until
|
|
767
797
|
* it finishes whether or not a token changed. A configuration is treated as
|
|
768
|
-
* up to date when every file
|
|
769
|
-
*
|
|
798
|
+
* up to date when every file its last compile wrote exists and is as that
|
|
799
|
+
* compile left it, and nothing it reads, its own config file included, has
|
|
800
|
+
* changed since that compile read it. A file it declares and Style
|
|
801
|
+
* Dictionary declined to write — a filter matching no tokens — does not stop
|
|
802
|
+
* it being skipped, since that file was never going to be there.
|
|
803
|
+
*
|
|
804
|
+
* "Since" means since the moment that compile began reading, so a token
|
|
805
|
+
* saved while a rebuild is compiling is rebuilt rather than taken for part
|
|
806
|
+
* of the output the rebuild went on to write. What each compile read is
|
|
807
|
+
* kept in `node_modules/.cache/unplugin-style-dictionary` beside the nearest
|
|
808
|
+
* `package.json`, so a restart still knows it. Where nothing is recorded,
|
|
809
|
+
* the files a configuration reads are compared against the files it
|
|
810
|
+
* declares instead.
|
|
811
|
+
*
|
|
812
|
+
* A compile is also recorded against the configuration that ran it: its
|
|
813
|
+
* contents, the hooks it names as Style Dictionary's registry holds them —
|
|
814
|
+
* each `format`, `filter`, `fileHeader`, transform, transform group,
|
|
815
|
+
* parser and preprocessor it refers to by name — and the Style Dictionary
|
|
816
|
+
* version. A new body registered under a name it uses, or an upgrade,
|
|
817
|
+
* rebuilds even though no file moved. What a hook's code calls from another
|
|
818
|
+
* module is not part of that code, so an edit there is not seen; naming the
|
|
819
|
+
* module in `watch` makes the next start rebuild.
|
|
770
820
|
*
|
|
771
821
|
* Two things are never skipped, because neither can be told from the
|
|
772
822
|
* filesystem:
|
|
@@ -775,7 +825,9 @@ export interface UnpluginStyleDictionaryOptions {
|
|
|
775
825
|
* object or a function.** There is no config file to stat, so an edit to
|
|
776
826
|
* the object inside `vite.config.ts` moves no mtime. Within one process
|
|
777
827
|
* the resolved configuration is compared against the one that was last
|
|
778
|
-
* built
|
|
828
|
+
* built. An earlier process's record is not trusted for it, because its
|
|
829
|
+
* functions are compared by their source and what they close over can
|
|
830
|
+
* differ from one process to the next, so it builds.
|
|
779
831
|
* - **A platform declaring `actions`.** An action writes what no
|
|
780
832
|
* `destination` names, so a skip would leave its work undone.
|
|
781
833
|
*
|
|
@@ -907,15 +959,17 @@ export interface UnpluginStyleDictionaryOptions {
|
|
|
907
959
|
logLevel?: 'info' | 'silent' | 'verbose' | 'warn'
|
|
908
960
|
|
|
909
961
|
/**
|
|
910
|
-
* Called once a build has finished, with every file it declares
|
|
911
|
-
* it took in milliseconds.
|
|
962
|
+
* Called once a build has finished, with every file it declares that is on
|
|
963
|
+
* disk and how long it took in milliseconds.
|
|
912
964
|
*
|
|
913
965
|
* The paths are absolute and platform-native, sorted so two runs of the same
|
|
914
966
|
* configuration hand back the same order. They are what the build declares
|
|
915
967
|
* rather than what it wrote this time: a configuration skipped by `cache`
|
|
916
968
|
* contributes its destinations too, because they are on disk and current,
|
|
917
969
|
* and a post-processing step that ignored them would leave half the output
|
|
918
|
-
* untouched on a rebuild that changed one file.
|
|
970
|
+
* untouched on a rebuild that changed one file. A declared file Style
|
|
971
|
+
* Dictionary declined to write — a filter matching no tokens — is left out,
|
|
972
|
+
* since there is nothing there to open.
|
|
919
973
|
*
|
|
920
974
|
* This is where formatting the generated files, type-checking them, or
|
|
921
975
|
* telling something else they have landed belongs.
|
package/dist/compile.js
CHANGED
|
@@ -2,26 +2,40 @@ import { asError, errorMessage } from "./errors.js";
|
|
|
2
2
|
import { configForBuild, describeConfig, readConfigObject } from "./config.js";
|
|
3
3
|
import { patternsMatchingNothing, sourcePatternsOf } from "./patterns.js";
|
|
4
4
|
import { reportSizes } from "./size-report.js";
|
|
5
|
-
import { configFingerprint, declaredDestinations, isUpToDate, writtenDestination } from "./up-to-date.js";
|
|
5
|
+
import { configFingerprint, declaredDestinations, isUpToDate, newestSourceOf, readPersistedRecords, recordOf, recordsFileFor, writePersistedRecords, writtenDestination } from "./up-to-date.js";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
6
8
|
import StyleDictionary from "style-dictionary";
|
|
7
9
|
//#region src/compile.ts
|
|
8
10
|
async function runBuilds(instance, resolvedConfigs, context) {
|
|
9
|
-
const { cache,
|
|
11
|
+
const { cache, destinationRecords, failOnError, generatedDestinations, log, notifyBuildOutcome, onBuildEnd, onBuildError, onBuildStart, persistedRecordFiles, platformsOption, quiet, report, stdoutColour, verbosity, volume, watch } = instance;
|
|
10
12
|
const root = instance.root();
|
|
11
13
|
const startTime = Date.now();
|
|
12
14
|
const generatedFiles = /* @__PURE__ */ new Set();
|
|
13
15
|
let skipped = 0;
|
|
16
|
+
let recordsFile = cache ? persistedRecordFiles.get(root) : void 0;
|
|
17
|
+
if (cache && recordsFile === void 0) {
|
|
18
|
+
recordsFile = recordsFileFor(root);
|
|
19
|
+
persistedRecordFiles.set(root, recordsFile);
|
|
20
|
+
for (const [destination, record] of readPersistedRecords(recordsFile)) if (!destinationRecords.has(destination)) destinationRecords.set(destination, record);
|
|
21
|
+
}
|
|
22
|
+
const recorded = /* @__PURE__ */ new Map();
|
|
14
23
|
try {
|
|
15
24
|
if (!context) log("Compiling design tokens...", "info");
|
|
16
25
|
if (onBuildStart) callHook(log, "onBuildStart", onBuildStart);
|
|
17
26
|
for (const [index, item] of resolvedConfigs.entries()) {
|
|
18
27
|
const declared = cache ? await readConfigObject(item) : null;
|
|
19
28
|
const selectedPlatforms = platformsFor(platformsOption, context);
|
|
20
|
-
|
|
21
|
-
compiledFingerprints,
|
|
29
|
+
const newestSource = declared ? await newestSourceOf({
|
|
22
30
|
log,
|
|
23
31
|
root,
|
|
24
32
|
watch
|
|
33
|
+
}, item, declared) : null;
|
|
34
|
+
const fingerprint = declared ? configFingerprint(root, item, declared) : null;
|
|
35
|
+
if (declared && isUpToDate({
|
|
36
|
+
destinationRecords,
|
|
37
|
+
fingerprint,
|
|
38
|
+
newestSource
|
|
25
39
|
}, item, declared, selectedPlatforms)) {
|
|
26
40
|
for (const destination of declaredDestinations(declared)) generatedFiles.add(destination);
|
|
27
41
|
skipped++;
|
|
@@ -41,7 +55,10 @@ async function runBuilds(instance, resolvedConfigs, context) {
|
|
|
41
55
|
`Nothing was written. Set failOnError to false to build anyway.`
|
|
42
56
|
].join(" "));
|
|
43
57
|
}
|
|
44
|
-
|
|
58
|
+
const wrote = /* @__PURE__ */ new Set();
|
|
59
|
+
sd.volume = notingWrites(volume, wrote);
|
|
60
|
+
const writing = declaredDestinations({ platforms: sd.platforms }, selectedPlatforms);
|
|
61
|
+
for (const destination of writing) destinationRecords.delete(destination);
|
|
45
62
|
if (selectedPlatforms === void 0) await sd.buildAllPlatforms();
|
|
46
63
|
else {
|
|
47
64
|
const defined = Object.keys(sd.platforms);
|
|
@@ -50,9 +67,15 @@ async function runBuilds(instance, resolvedConfigs, context) {
|
|
|
50
67
|
for (const name of selectedPlatforms) await sd.buildPlatform(name);
|
|
51
68
|
}
|
|
52
69
|
for (const platform of Object.values(sd.platforms)) for (const file of platform.files ?? []) if (file.destination) generatedFiles.add(writtenDestination(platform.buildPath, file.destination));
|
|
53
|
-
|
|
54
|
-
|
|
70
|
+
if (fingerprint !== null) for (const destination of writing) {
|
|
71
|
+
const record = recordOf(destination, fingerprint, newestSource, wrote.has(destination));
|
|
72
|
+
if (record) {
|
|
73
|
+
destinationRecords.set(destination, record);
|
|
74
|
+
recorded.set(destination, record);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
55
77
|
}
|
|
78
|
+
if (recordsFile !== void 0 && recorded.size > 0) writePersistedRecords(recordsFile, recorded);
|
|
56
79
|
generatedDestinations.clear();
|
|
57
80
|
for (const destination of generatedFiles) generatedDestinations.add(destination.replace(/\\/g, "/"));
|
|
58
81
|
} catch (err) {
|
|
@@ -64,7 +87,7 @@ async function runBuilds(instance, resolvedConfigs, context) {
|
|
|
64
87
|
}
|
|
65
88
|
notifyBuildOutcome(null);
|
|
66
89
|
const duration = Date.now() - startTime;
|
|
67
|
-
if (onBuildEnd) callHook(log, "onBuildEnd", onBuildEnd, Array.from(generatedFiles).sort((left, right) => left.localeCompare(right)), duration);
|
|
90
|
+
if (onBuildEnd) callHook(log, "onBuildEnd", onBuildEnd, Array.from(generatedFiles).filter((file) => fs.existsSync(file)).sort((left, right) => left.localeCompare(right)), duration);
|
|
68
91
|
const everythingSkipped = skipped === resolvedConfigs.length;
|
|
69
92
|
if (context) {
|
|
70
93
|
log(everythingSkipped ? `Design tokens already up to date after change in ${context} (${duration}ms)` : `Rebuilt design tokens due to change in ${context} (${duration}ms)`, "success");
|
|
@@ -103,6 +126,15 @@ function failsTheBuild(failOnError, context) {
|
|
|
103
126
|
function isThenable(value) {
|
|
104
127
|
return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
|
|
105
128
|
}
|
|
129
|
+
function notingWrites(volume, into) {
|
|
130
|
+
const note = (file) => {
|
|
131
|
+
if (typeof file === "string") into.add(path.resolve(file));
|
|
132
|
+
};
|
|
133
|
+
return Object.create(volume, { promises: { value: Object.create(volume.promises, { writeFile: { value: async (...args) => {
|
|
134
|
+
note(args[0]);
|
|
135
|
+
await volume.promises.writeFile(...args);
|
|
136
|
+
} } }) } });
|
|
137
|
+
}
|
|
106
138
|
function platformsFor(platformsOption, context) {
|
|
107
139
|
if (platformsOption === void 0) return void 0;
|
|
108
140
|
if (Array.isArray(platformsOption)) return platformsOption;
|
package/dist/compile.js.map
CHANGED
|
@@ -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 type fs from 'node:fs'\n\nimport StyleDictionary from 'style-dictionary'\n\nimport type { ResolvedConfig } from './config.js'\nimport type { UnpluginStyleDictionaryOptions } from './types.js'\n\nimport { configForBuild, describeConfig, readConfigObject } from './config.js'\nimport { asError, errorMessage } from './errors.js'\nimport { patternsMatchingNothing, sourcePatternsOf } from './patterns.js'\nimport { reportSizes } from './size-report.js'\nimport {\n configFingerprint,\n declaredDestinations,\n isUpToDate,\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. `generatedDestinations` is the instance's own record of what it\n// wrote, and `compiledFingerprints` is the process's; neither lives here.\nexport interface PluginInstance {\n cache: boolean\n compiledFingerprints: Set<string>\n failOnError: NonNullable<UnpluginStyleDictionaryOptions['failOnError']>\n generatedDestinations: Set<string>\n log: (message: string, type: 'error' | 'info' | 'success') => void\n notifyBuildOutcome: (error: Error | null) => void\n onBuildEnd: UnpluginStyleDictionaryOptions['onBuildEnd']\n onBuildError: UnpluginStyleDictionaryOptions['onBuildError']\n onBuildStart: UnpluginStyleDictionaryOptions['onBuildStart']\n platformsOption: UnpluginStyleDictionaryOptions['platforms']\n quiet: boolean\n report: boolean\n root: () => string\n stdoutColour: boolean\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 compiledFingerprints,\n failOnError,\n generatedDestinations,\n log,\n notifyBuildOutcome,\n onBuildEnd,\n onBuildError,\n onBuildStart,\n platformsOption,\n quiet,\n report,\n stdoutColour,\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 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 if (\n declared &&\n (await isUpToDate(\n { compiledFingerprints, log, root, watch },\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 sd.extend(undefined, { mutateOriginal: true, verbosity })\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.\n sd.volume = volume\n\n if (selectedPlatforms === undefined) {\n await sd.buildAllPlatforms()\n } else {\n // Named, so a typo is an error rather than a platform silently not\n // built — which is what Style Dictionary's own CLI means by \"Must be\n // defined in the config\".\n const defined = Object.keys(sd.platforms)\n const unknown = selectedPlatforms.filter(\n (name) => !defined.includes(name),\n )\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 // 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 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 on it forever.\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 configuration whose build threw is never\n // treated as one this process has compiled.\n const fingerprint = configFingerprint(root, item)\n if (fingerprint !== null) compiledFingerprints.add(fingerprint)\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 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 log(`Compilation failed after ${duration}ms: ${errorMessage(err)}`, 'error')\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 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 // oxlint-disable-next-line unicorn/no-array-sort\n const files = Array.from(generatedFiles).sort((left, right) =>\n left.localeCompare(right),\n )\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 reportSizes({ root, stdoutColour }, 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// 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// 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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"}
|
|
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 { reportSizes } 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. `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 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 stdoutColour: boolean\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 notifyBuildOutcome,\n onBuildEnd,\n onBuildError,\n onBuildStart,\n persistedRecordFiles,\n platformsOption,\n quiet,\n report,\n stdoutColour,\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 // 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 sd.extend(undefined, { mutateOriginal: true, verbosity })\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 sd.buildAllPlatforms()\n } else {\n // Named, so a typo is an error rather than a platform silently not\n // built — which is what Style Dictionary's own CLI means by \"Must be\n // defined in the config\".\n const defined = Object.keys(sd.platforms)\n const unknown = selectedPlatforms.filter(\n (name) => !defined.includes(name),\n )\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 // 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 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 on it forever.\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 log(`Compilation failed after ${duration}ms: ${errorMessage(err)}`, 'error')\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 reportSizes({ root, stdoutColour }, 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// 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// `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/index.js
CHANGED
|
@@ -9,6 +9,17 @@ import fs from "node:fs";
|
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { createUnplugin } from "unplugin";
|
|
11
11
|
//#region src/index.ts
|
|
12
|
+
function appendChokidarIgnored(watch, negations) {
|
|
13
|
+
if (negations.length === 0) return;
|
|
14
|
+
const chokidar = Reflect.get(watch, "chokidar");
|
|
15
|
+
const options = typeof chokidar === "object" && chokidar !== null ? chokidar : {};
|
|
16
|
+
const existing = Reflect.get(options, "ignored");
|
|
17
|
+
const previous = Array.isArray(existing) ? existing : existing === void 0 ? [] : [existing];
|
|
18
|
+
Reflect.set(watch, "chokidar", {
|
|
19
|
+
...options,
|
|
20
|
+
ignored: [...previous, ...negations]
|
|
21
|
+
});
|
|
22
|
+
}
|
|
12
23
|
function discardTemporaryFile(temporary) {
|
|
13
24
|
try {
|
|
14
25
|
fs.rmSync(temporary, { force: true });
|
|
@@ -141,7 +152,8 @@ const atomicVolume = Object.create(fs, {
|
|
|
141
152
|
promises: { value: Object.create(fs.promises, { writeFile: { value: writeFileAtomic } }) },
|
|
142
153
|
writeFileSync: { value: writeFileSyncAtomic }
|
|
143
154
|
});
|
|
144
|
-
const
|
|
155
|
+
const destinationRecords = /* @__PURE__ */ new Map();
|
|
156
|
+
const persistedRecordFiles = /* @__PURE__ */ new Map();
|
|
145
157
|
const compilesInFlight = /* @__PURE__ */ new Map();
|
|
146
158
|
function buildKey(root, resolved) {
|
|
147
159
|
try {
|
|
@@ -231,7 +243,7 @@ const unpluginFactory = (options = {}, meta) => {
|
|
|
231
243
|
};
|
|
232
244
|
const instance = {
|
|
233
245
|
cache,
|
|
234
|
-
|
|
246
|
+
destinationRecords,
|
|
235
247
|
failOnError,
|
|
236
248
|
generatedDestinations,
|
|
237
249
|
log,
|
|
@@ -241,6 +253,7 @@ const unpluginFactory = (options = {}, meta) => {
|
|
|
241
253
|
onBuildEnd,
|
|
242
254
|
onBuildError,
|
|
243
255
|
onBuildStart,
|
|
256
|
+
persistedRecordFiles,
|
|
244
257
|
platformsOption,
|
|
245
258
|
quiet,
|
|
246
259
|
report,
|
|
@@ -339,10 +352,9 @@ const unpluginFactory = (options = {}, meta) => {
|
|
|
339
352
|
for (const file of paths) this.addWatchFile(file);
|
|
340
353
|
}
|
|
341
354
|
if (isWebpack) return;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
355
|
+
const reentry = watchRebuild && hasCompiled && hostCommand !== "serve";
|
|
356
|
+
watchRebuild = false;
|
|
357
|
+
if (reentry) return;
|
|
346
358
|
await compileOnceAcrossInstances(resolved);
|
|
347
359
|
hasCompiled = true;
|
|
348
360
|
},
|
|
@@ -367,11 +379,26 @@ const unpluginFactory = (options = {}, meta) => {
|
|
|
367
379
|
config.logger.info(message);
|
|
368
380
|
}
|
|
369
381
|
};
|
|
370
|
-
if (config.command !== "serve")
|
|
382
|
+
if (config.command !== "serve") {
|
|
383
|
+
if (config.build.watch) {
|
|
384
|
+
isWatching = true;
|
|
385
|
+
try {
|
|
386
|
+
const resolved = await resolveConfigs();
|
|
387
|
+
if (resolved.length > 0) {
|
|
388
|
+
const { paths } = await getWatchTargets(resolved);
|
|
389
|
+
appendChokidarIgnored(config.build.watch, nodeModulesNegations(paths));
|
|
390
|
+
}
|
|
391
|
+
} catch (err) {
|
|
392
|
+
log(`Could not read the configuration while preparing the watch list: ${errorMessage(err)}`, "error");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
371
397
|
isWatching = true;
|
|
372
398
|
try {
|
|
373
399
|
startupResolved = await resolveConfigs();
|
|
374
400
|
if (startupResolved.length === 0) return;
|
|
401
|
+
if (config.server.watch === null) return;
|
|
375
402
|
const { paths } = await getWatchTargets(startupResolved);
|
|
376
403
|
const negations = nodeModulesNegations(paths);
|
|
377
404
|
if (negations.length === 0) return;
|
|
@@ -393,7 +420,9 @@ const unpluginFactory = (options = {}, meta) => {
|
|
|
393
420
|
let targets = await getWatchTargets(resolved);
|
|
394
421
|
server.watcher.add(targets.paths);
|
|
395
422
|
const ownWatchers = /* @__PURE__ */ new Map();
|
|
423
|
+
const watcherDisabled = server.config.server.watch === null;
|
|
396
424
|
const watchNodeModules = (forPaths) => {
|
|
425
|
+
if (watcherDisabled) return;
|
|
397
426
|
const wanted = new Set(nodeModulesWatchDirectories(forPaths));
|
|
398
427
|
for (const [directory, watcher] of ownWatchers) {
|
|
399
428
|
if (wanted.has(directory)) continue;
|