@kanso-labs/unplugin-style-dictionary 0.10.14 → 0.10.16
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 +36 -8
- package/dist/compile.js +13 -5
- package/dist/compile.js.map +1 -1
- package/dist/config.js +3 -3
- package/dist/config.js.map +1 -1
- package/dist/index.js +44 -20
- package/dist/index.js.map +1 -1
- package/dist/options.js +40 -0
- package/dist/options.js.map +1 -0
- package/dist/types.d.ts +20 -4
- package/dist/watch-filter.js +8 -3
- package/dist/watch-filter.js.map +1 -1
- package/package.json +12 -12
package/README.md
CHANGED
|
@@ -165,10 +165,15 @@ With no `config`, the root is searched for `sd.config.json`, `config.json`,
|
|
|
165
165
|
`sd.config.js` and `sd.config.mjs`, in that order — and the first one that
|
|
166
166
|
**looks like a Style Dictionary configuration** wins. That means declaring at
|
|
167
167
|
least one of `platforms`, `source`, `include` or `tokens`. A candidate that
|
|
168
|
-
fails the check is
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
so which configuration a build used is answerable from the
|
|
168
|
+
fails the check is skipped rather than adopted, so an unrelated `config.json` —
|
|
169
|
+
an extremely common name for something else — no longer gets compiled over and
|
|
170
|
+
added to the watch set. The path that was picked is printed, along with any file
|
|
171
|
+
skipped on the way, so which configuration a build used is answerable from the
|
|
172
|
+
console. When nothing is adopted, the skipped files are reported as an error.
|
|
173
|
+
|
|
174
|
+
A configuration problem — nothing found, or a named `config` file that is
|
|
175
|
+
missing or will not parse — is said once rather than on every rebuild, and again
|
|
176
|
+
only if it comes back after a build that worked.
|
|
172
177
|
|
|
173
178
|
`config.json` stays in the list because Style Dictionary's own CLI defaults to
|
|
174
179
|
it, so a project relying on that default keeps working.
|
|
@@ -693,6 +698,13 @@ dev server down from inside a step meant to reformat a file.
|
|
|
693
698
|
decides whether to rethrow. The two answer different questions: one is whether
|
|
694
699
|
the host stops, the other is that a build went wrong.
|
|
695
700
|
|
|
701
|
+
**A failure names the configuration it came from.** Style Dictionary's own
|
|
702
|
+
errors name neither the configuration nor the file behind them, so the plugin
|
|
703
|
+
wraps them: the message opens with the configuration's path, or its position in
|
|
704
|
+
the list, and Style Dictionary's error is the `cause`. A failed rebuild also
|
|
705
|
+
names the change it followed — the last file to change, which is not necessarily
|
|
706
|
+
the one that broke the build.
|
|
707
|
+
|
|
696
708
|
## Public API
|
|
697
709
|
|
|
698
710
|
Small on purpose. Five bundler entry points, one root entry, and two types.
|
|
@@ -881,9 +893,10 @@ export interface UnpluginStyleDictionaryOptions {
|
|
|
881
893
|
* first one that *looks like a Style Dictionary configuration* wins — it has
|
|
882
894
|
* to declare at least one of `platforms`, `source`, `include` or `tokens` —
|
|
883
895
|
* and the path it picked is announced, so which file a build used is
|
|
884
|
-
* answerable from the console. A candidate that fails that check is
|
|
885
|
-
*
|
|
886
|
-
*
|
|
896
|
+
* answerable from the console. A candidate that fails that check is skipped
|
|
897
|
+
* rather than adopted, because `config.json` is an extremely common name for
|
|
898
|
+
* something else entirely. It is named in that announcement when a later
|
|
899
|
+
* candidate is adopted, and reported as an error when none is.
|
|
887
900
|
*
|
|
888
901
|
* **`false` turns discovery off.** Two of the four names are modules rather
|
|
889
902
|
* than data, and reading a module means running it: a `sd.config.js` in the
|
|
@@ -946,6 +959,10 @@ export interface UnpluginStyleDictionaryOptions {
|
|
|
946
959
|
*
|
|
947
960
|
* Reporting happens either way, and is not suppressed by `silent`.
|
|
948
961
|
*
|
|
962
|
+
* Any other value throws a `TypeError` when the plugin is created — the
|
|
963
|
+
* string `'true'` from an environment variable included, which used to
|
|
964
|
+
* behave as `false`.
|
|
965
|
+
*
|
|
949
966
|
* @default 'build'
|
|
950
967
|
*/
|
|
951
968
|
failOnError?: 'build' | 'serve' | boolean
|
|
@@ -966,7 +983,9 @@ export interface UnpluginStyleDictionaryOptions {
|
|
|
966
983
|
* about rather than pointing at its own `--verbose` flag.
|
|
967
984
|
*
|
|
968
985
|
* A compile that fails is reported at every level, so there is no
|
|
969
|
-
* `'error'`: `'silent'` is the quietest and still reports a failure.
|
|
986
|
+
* `'error'`: `'silent'` is the quietest and still reports a failure. Any
|
|
987
|
+
* other value, `'error'` included, throws a `TypeError` when the plugin is
|
|
988
|
+
* created.
|
|
970
989
|
*
|
|
971
990
|
* This option governs what the plugin says, not where it goes. The messages
|
|
972
991
|
* are handed to the host — Vite's `config.logger`, the rollup and rolldown
|
|
@@ -1003,6 +1022,10 @@ export interface UnpluginStyleDictionaryOptions {
|
|
|
1003
1022
|
/**
|
|
1004
1023
|
* Called when a build fails, with whatever was thrown.
|
|
1005
1024
|
*
|
|
1025
|
+
* A failure inside Style Dictionary arrives wrapped in an `Error` whose
|
|
1026
|
+
* message names the configuration it came from, with Style Dictionary's own
|
|
1027
|
+
* error as its `cause`. One the plugin raises itself already names it.
|
|
1028
|
+
*
|
|
1006
1029
|
* It fires whatever `failOnError` is set to, and before that option decides
|
|
1007
1030
|
* whether to rethrow — the two answer different questions, and under a dev
|
|
1008
1031
|
* server the default is not to throw at all.
|
|
@@ -1061,6 +1084,11 @@ export interface UnpluginStyleDictionaryOptions {
|
|
|
1061
1084
|
* stale output for the rest. Scope the watch half rather than the build half
|
|
1062
1085
|
* unless that is what you want.
|
|
1063
1086
|
*
|
|
1087
|
+
* Any other shape throws a `TypeError` when the plugin is created: a single
|
|
1088
|
+
* string, or an object with a key other than `build` and `watch` — `serve`
|
|
1089
|
+
* is the easy slip, since `failOnError` says `'serve'`. Both used to build
|
|
1090
|
+
* every platform.
|
|
1091
|
+
*
|
|
1064
1092
|
* @default undefined, which builds every platform
|
|
1065
1093
|
*/
|
|
1066
1094
|
platforms?: string[] | { build?: string[]; watch?: string[] }
|
package/dist/compile.js
CHANGED
|
@@ -43,10 +43,10 @@ async function runBuilds(instance, resolvedConfigs, context) {
|
|
|
43
43
|
continue;
|
|
44
44
|
}
|
|
45
45
|
const sd = new StyleDictionary(await configForBuild(item), { init: false });
|
|
46
|
-
await sd.extend(void 0, {
|
|
46
|
+
await namingConfig(item, index, async () => sd.extend(void 0, {
|
|
47
47
|
mutateOriginal: true,
|
|
48
48
|
verbosity
|
|
49
|
-
});
|
|
49
|
+
}));
|
|
50
50
|
if (sd.allTokens.length === 0) {
|
|
51
51
|
const asObject = await readConfigObject(item);
|
|
52
52
|
const barren = asObject ? await patternsMatchingNothing(sourcePatternsOf(asObject)) : [];
|
|
@@ -60,10 +60,10 @@ async function runBuilds(instance, resolvedConfigs, context) {
|
|
|
60
60
|
sd.volume = notingWrites(volume, wrote);
|
|
61
61
|
const writing = declaredDestinations({ platforms: sd.platforms }, selectedPlatforms);
|
|
62
62
|
for (const destination of writing) destinationRecords.delete(destination);
|
|
63
|
-
if (selectedPlatforms === void 0) await sd.buildAllPlatforms();
|
|
63
|
+
if (selectedPlatforms === void 0) await namingConfig(item, index, async () => sd.buildAllPlatforms());
|
|
64
64
|
else {
|
|
65
65
|
checkSelection(item, index, selectedPlatforms, sd.platforms);
|
|
66
|
-
for (const name of selectedPlatforms) await sd.buildPlatform(name);
|
|
66
|
+
for (const name of selectedPlatforms) await namingConfig(item, index, async () => sd.buildPlatform(name));
|
|
67
67
|
}
|
|
68
68
|
for (const platform of Object.values(sd.platforms)) for (const file of platform.files ?? []) if (file.destination) generatedFiles.add(writtenDestination(platform.buildPath, file.destination));
|
|
69
69
|
if (fingerprint !== null) for (const destination of writing) {
|
|
@@ -78,7 +78,8 @@ async function runBuilds(instance, resolvedConfigs, context) {
|
|
|
78
78
|
generatedDestinations.clear();
|
|
79
79
|
for (const destination of generatedFiles) generatedDestinations.add(destination.replace(/\\/g, "/"));
|
|
80
80
|
} catch (err) {
|
|
81
|
-
|
|
81
|
+
const duration = Date.now() - startTime;
|
|
82
|
+
log(context ? `Compilation failed after a change in ${context} (${duration}ms): ${errorMessage(err)}` : `Compilation failed after ${duration}ms: ${errorMessage(err)}`, "error");
|
|
82
83
|
notifyBuildOutcome(asError(err));
|
|
83
84
|
if (onBuildError) callHook(log, "onBuildError", onBuildError, err);
|
|
84
85
|
if (failsTheBuild(failOnError, context)) throw err;
|
|
@@ -131,6 +132,13 @@ function failsTheBuild(failOnError, context) {
|
|
|
131
132
|
function isThenable(value) {
|
|
132
133
|
return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
|
|
133
134
|
}
|
|
135
|
+
async function namingConfig(item, index, step) {
|
|
136
|
+
try {
|
|
137
|
+
return await step();
|
|
138
|
+
} catch (err) {
|
|
139
|
+
throw new Error(`${describeConfig(item, index)} failed: ${errorMessage(err)}`, { cause: err });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
134
142
|
function notingWrites(volume, into) {
|
|
135
143
|
const note = (file) => {
|
|
136
144
|
if (typeof file === "string") into.add(path.resolve(file));
|
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 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 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 // `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 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 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 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// `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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"}
|
|
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":"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"}
|
package/dist/config.js
CHANGED
|
@@ -44,7 +44,7 @@ async function readConfigObject(item, report) {
|
|
|
44
44
|
if (isConfig(loaded)) return loaded;
|
|
45
45
|
if (report) report(`Config file did not resolve to a configuration object: ${item.config}`, "error");
|
|
46
46
|
} catch (err) {
|
|
47
|
-
if (report) report(`Failed to parse config file: ${item.config}. Error: ${errorMessage(err)}`, "error");
|
|
47
|
+
if (report) report(fs.existsSync(item.config) ? `Failed to parse config file: ${item.config}. Error: ${errorMessage(err)}` : `Config file not found: ${item.config}`, "error");
|
|
48
48
|
}
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
@@ -71,12 +71,12 @@ async function resolveConfigOption({ config, configContext, discovery, log, root
|
|
|
71
71
|
}
|
|
72
72
|
if (!discovery.announced) {
|
|
73
73
|
discovery.announced = true;
|
|
74
|
-
log(`Using the configuration it found at ${fullPath}`, "info");
|
|
74
|
+
log(rejected.length > 0 ? `Using the configuration it found at ${fullPath}, after ignoring ${rejected.join(", ")}: nothing there declares platforms, source, include or tokens. Name ${file} with the config option to stop looking.` : `Using the configuration it found at ${fullPath}`, "info");
|
|
75
75
|
}
|
|
76
76
|
rawConfig = file;
|
|
77
77
|
break;
|
|
78
78
|
}
|
|
79
|
-
if (rejected.length > 0) log(`Ignored ${rejected.join(", ")} in ${root}: nothing there declares platforms, source, include or tokens, so it does not look like a Style Dictionary configuration. Name it with the config option if it is one, or set config to false to stop looking.`, "error");
|
|
79
|
+
if (!rawConfig && rejected.length > 0) log(`Ignored ${rejected.join(", ")} in ${root}: nothing there declares platforms, source, include or tokens, so it does not look like a Style Dictionary configuration. Name it with the config option if it is one, or set config to false to stop looking.`, "error");
|
|
80
80
|
}
|
|
81
81
|
if (!rawConfig) {
|
|
82
82
|
log("No configuration specified and no default config file found. Style Dictionary will not compile.", "error");
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","names":[],"sources":["../src/config.ts"],"sourcesContent":["// Where a configuration comes from and what it says: the `config` option\n// resolved into a list, each item read as an object, and the form Style\n// Dictionary is handed to build from.\n//\n// Nothing here holds state. One module serves every plugin instance in the\n// process, so what an instance owns — its root, its logger, whether it has\n// announced a discovery yet — is handed in on each call.\n\nimport type { Config } from 'style-dictionary'\nimport type { DesignTokens } from 'style-dictionary/types'\n\nimport JSON5 from 'json5'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport StyleDictionary from 'style-dictionary'\n\nimport type {\n StyleDictionaryConfigContext,\n UnpluginStyleDictionaryOptions,\n} from './types.js'\n\nimport { errorMessage } from './errors.js'\n\n// The plugin's logger, narrowed to the two levels anything here reports at. It\n// is handed on as a function rather than as the host it writes to, so it goes\n// on reading whichever host has claimed the plugin's messages by the time it\n// is called.\nexport type Log = (message: string, type: 'error' | 'info') => void\n\n// A configuration as `resolveConfigOption` hands it on: either the object the\n// consumer passed or the path it was read from, plus the directory relative\n// paths inside it resolve against.\nexport interface ResolvedConfig {\n config: Config | string\n file?: string\n // The files the plugin's `watch` option names, expanded. A config module's\n // import is keyed on their mtimes beside its own, so a data file it reads\n // at load time — and that a consumer named in `watch` to trigger a rebuild\n // — re-evaluates it rather than only rebuilding with what it read before.\n watched?: string[]\n}\n\n// The config extensions Style Dictionary loads with `import` rather than by\n// parsing the file — the `case` list in its own `loadFile`. They are the only\n// ones Node's permanent module cache applies to, and so the only ones this\n// plugin has to read on the build's behalf.\n//\n// Everything else Style Dictionary parses as JSON5, including `.json`, and\n// this list is what makes the plugin split the same way. Reading the two\n// halves apart is what silently unwatched a whole family of configurations:\n// a `.json5` or `.jsonc` file went down the import branch and failed there\n// while the build succeeded, and a `.json` file carrying a comment or a\n// trailing comma failed strict `JSON.parse` for the same reason.\nconst IMPORTED_CONFIG_EXTENSIONS = ['.js', '.mjs', '.ts']\n\n// What `new StyleDictionary` is handed for an item: always an object, carrying\n// the plugin's own parser for token modules — see `withTokenModuleParser`.\n//\n// A path in the JS family is read here because those are exactly the\n// extensions Style Dictionary's own `loadFile` reaches with `import`, whose\n// module record Node then caches forever, so a build handed the path could\n// read the config stale. A JSON5-family path is read here too, so the parser\n// can ride along; Style Dictionary would have parsed the same file the same\n// way.\nexport async function configForBuild(\n item: ResolvedConfig,\n): Promise<Config | string> {\n const { config } = item\n\n if (typeof config !== 'string') return withTokenModuleParser(config)\n\n const loaded = await readConfigObject(item)\n\n // A config that could not be read falls back to the path, so the failure\n // stays Style Dictionary's to report — it knows more about why an import\n // failed than this does, a `.ts` config without type stripping especially.\n if (!loaded) return item.config\n\n // `loadFile` clones what it imports before handing it on, and passing an\n // object skips that. It matters more here than it does there: the module\n // record now outlives the build, and `extend` is called with\n // `mutateOriginal`. Cloning throws on a config carrying functions — an\n // inline transform — and Style Dictionary's own fallback in that case is\n // to use the original, so this one matches it.\n if (!isImportedConfig(config)) return withTokenModuleParser(loaded)\n\n try {\n return withTokenModuleParser(structuredClone(loaded))\n } catch {\n return withTokenModuleParser(loaded)\n }\n}\n\n// The name the plugin's token-module parser is registered and applied under.\nconst TOKEN_MODULE_PARSER = 'unplugin-style-dictionary/token-module'\n\n// Matches a `.js`, `.mjs` or `.ts` token source, unless another parser the\n// configuration applies matches it too. Style Dictionary runs every parser\n// whose pattern matches and keeps the last result, and a global parser comes\n// ahead of a configuration's own whatever the order in `parsers` — measured,\n// `['plugin-own', 'consumer-global']` ran in the order\n// `['consumer-global', 'plugin-own']`. So order cannot make the consumer's\n// win, and stepping aside is what does. A `RegExp` whose `Symbol.match` is\n// overridden, because `filePath.match(pattern)` is how Style Dictionary asks.\nclass TokenModulePattern extends RegExp {\n private readonly others: RegExp[]\n\n constructor(others: RegExp[]) {\n super(String.raw`\\.(?:js|mjs|ts)$`)\n this.others = others\n }\n\n override [Symbol.match](value: string): null | RegExpMatchArray {\n if (this.others.some((other) => value.match(other) !== null)) return null\n\n return super[Symbol.match](value)\n }\n}\n\n// How to name a configuration in a message. A path is what a consumer\n// recognises; a configuration passed as an object or returned by a function has\n// no name, so it is identified by where it sits in the list rather than by a\n// stringified dump of itself.\nexport function describeConfig(item: ResolvedConfig, index: number): string {\n return item.file\n ? `The configuration ${item.file}`\n : `The configuration at position ${index + 1}`\n}\n\n// What a configuration item says, as an object. `report` is where a failure is\n// said, and leaving it out is what stops the two readers of this from saying\n// the same thing twice: a bad config has nowhere else to surface when the\n// watch list is being built, while a build falls back to handing Style\n// Dictionary the path and lets its message through instead.\nexport async function readConfigObject(\n item: ResolvedConfig,\n report?: Log,\n): Promise<Config | null> {\n if (typeof item.config !== 'string') return item.config\n\n try {\n // JSON5 rather than `JSON.parse`, because that is what Style Dictionary\n // reads these files with — it is a superset, so a plain `.json` config\n // parses identically and one carrying a comment stops being a config\n // the build understands and the watch list does not.\n const loaded: unknown = isImportedConfig(item.config)\n ? await importConfigModule(item.config, item.watched)\n : JSON5.parse(fs.readFileSync(item.config, 'utf-8'))\n\n if (isConfig(loaded)) return loaded\n\n if (report) {\n report(\n `Config file did not resolve to a configuration object: ${item.config}`,\n 'error',\n )\n }\n } catch (err) {\n if (report) {\n report(\n `Failed to parse config file: ${item.config}. Error: ${errorMessage(err)}`,\n 'error',\n )\n }\n }\n\n return null\n}\n\n// Resolve config file paths / objects\n//\n// Everything a plugin instance owns arrives as an argument, and none of it is\n// kept. `root` is read by the caller on each call because the host assigns it\n// after the factory has run, and `configContext` is a function rather than a\n// value for the same reason: it is asked only once a consumer's `config`\n// function is about to run.\nexport async function resolveConfigOption({\n config,\n configContext,\n discovery,\n log,\n root,\n}: {\n config: UnpluginStyleDictionaryOptions['config']\n configContext: () => StyleDictionaryConfigContext\n discovery: { announced: boolean }\n log: Log\n root: string\n}): Promise<ResolvedConfig[]> {\n let rawConfig = config\n\n // Checked ahead of the discovery below, and by identity rather than\n // truthiness: `false` is falsy, so the `!rawConfig` test that triggers\n // discovery would treat \"do not discover anything\" as \"go and look\".\n if (rawConfig === false) return []\n\n // If config is not defined, look for default configuration files\n if (!rawConfig) {\n const defaults = [\n 'sd.config.json',\n 'config.json',\n 'sd.config.js',\n 'sd.config.mjs',\n ]\n\n const rejected: string[] = []\n\n for (const file of defaults) {\n const fullPath = path.resolve(root, file)\n if (!fs.existsSync(fullPath)) continue\n\n // Read before adopting. For the two `.json` names this is a parse and\n // nothing more; for the two module names it is an import, and the\n // module has already run by the time there is anything to check —\n // which is what `config: false` exists for and why validation alone\n // does not cover them.\n const candidate = await readConfigObject({\n config: fullPath,\n file: fullPath,\n })\n\n if (!looksLikeConfig(candidate)) {\n rejected.push(file)\n continue\n }\n\n // Announced, because \"which configuration did it pick\" was not\n // answerable from the console at all, and discovery picks from four\n // generic names.\n if (!discovery.announced) {\n discovery.announced = true\n log(`Using the configuration it found at ${fullPath}`, 'info')\n }\n\n rawConfig = file\n break\n }\n\n // Said whether or not something usable turned up after them. A skipped\n // candidate is the interesting half of \"no configuration found\": the\n // file is right there, and the reason it was not used is not guessable.\n if (rejected.length > 0) {\n log(\n `Ignored ${rejected.join(', ')} in ${root}: nothing there declares platforms, source, include or tokens, so it does not look like a Style Dictionary configuration. Name it with the config option if it is one, or set config to false to stop looking.`,\n 'error',\n )\n }\n }\n\n if (!rawConfig) {\n log(\n 'No configuration specified and no default config file found. Style Dictionary will not compile.',\n 'error',\n )\n return []\n }\n\n // Evaluate function if provided\n if (typeof rawConfig === 'function') {\n rawConfig = await rawConfig(configContext())\n }\n\n const configs = Array.isArray(rawConfig) ? rawConfig : [rawConfig]\n\n return configs.map((conf) => {\n if (typeof conf === 'string') {\n const fullPath = path.resolve(root, conf)\n return { config: fullPath, file: fullPath }\n } else {\n return { config: conf }\n }\n })\n}\n\n// Imports a config module, re-evaluating it only when the file itself has\n// changed. The query string is what decides that, and it is not decoration:\n// Node's ESM cache is permanent and keyed on the specifier, so a config\n// imported without one is evaluated once and never read again — which is\n// how an edited `.mjs` config went on building the platform map the process\n// started with, for the rest of the session.\n//\n// `Date.now()` fixed that staleness and bought two problems. Every watcher\n// event registered another module record in a map nothing prunes, re-running\n// the config's own `registerFormat` side effects for a file nobody touched.\n// And its millisecond granularity meant an edit landing inside the same\n// millisecond as the previous import shared that import's key, and was\n// served the old module anyway. `mtimeMs` carries sub-millisecond\n// resolution and only moves when the file does.\n//\n// Keyed on the files the plugin's `watch` option names as well: a config that\n// reads a data file at load time went on building what that file said at\n// startup. A module the config itself imports is out of reach — a new query\n// on the config re-evaluates the config, not what it imports — so naming one\n// in `watch` buys a rebuild and not a re-read.\nasync function importConfigModule(\n file: string,\n watched: string[] = [],\n): Promise<unknown> {\n const key = versionKey([file, ...watched])\n\n // Sequential on purpose: a config module runs arbitrary code at import\n // time — `registerFormat` and friends — and Style Dictionary's registries\n // are global, so importing several at once would interleave those\n // registrations.\n return unwrapDefault(await import(`${pathToFileURL(file).href}?t=${key}`))\n}\n\n// A token module, read the way `importConfigModule` reads a config: under a\n// query keyed on its mtime, and cloned as Style Dictionary's own `loadFile`\n// clones it. Style Dictionary reads one with a bare `import`, and Node's\n// module cache keeps that first evaluation for the life of the process — so\n// under a long-lived watcher an edited `.mjs` token file rebuilt, reported\n// success, and wrote the values the process started with.\nasync function importTokenModule(\n filePath: string | undefined,\n): Promise<DesignTokens> {\n if (filePath === undefined) return {}\n\n const module: unknown = await import(\n `${pathToFileURL(filePath).href}?t=${versionKey([filePath])}`\n )\n\n // `.default`, exactly as `loadFile` reads it: a module without one carries\n // no tokens.\n const tokens: unknown =\n typeof module === 'object' && module !== null && 'default' in module\n ? module.default\n : undefined\n if (!isTokens(tokens)) return {}\n\n try {\n return structuredClone(tokens)\n } catch {\n return tokens\n }\n}\n\n// A config file is an untyped boundary: `JSON.parse` and a dynamic `import`\n// both hand back `any`, and an `any` assigned to `configObj` spreads through\n// every read of it downstream. These two narrow that boundary once, here.\n// They are type predicates rather than assertions on purpose — a predicate is\n// a check the compiler verifies, where a cast is only a claim.\nfunction isConfig(value: unknown): value is Config {\n return typeof value === 'object' && value !== null\n}\n\n// Whether a config path is one Style Dictionary imports rather than parses.\nfunction isImportedConfig(file: string): boolean {\n return IMPORTED_CONFIG_EXTENSIONS.some((extension) =>\n file.endsWith(extension),\n )\n}\n\nfunction isTokens(value: unknown): value is DesignTokens {\n return typeof value === 'object' && value !== null\n}\n\n// Whether a discovered file looks like a Style Dictionary configuration at all.\n//\n// Only applied to a file the plugin went looking for, never to one a consumer\n// named: an explicit `config` is their choice and second-guessing it would\n// reject shapes Style Dictionary accepts and this does not know about.\n//\n// `config.json` is an extremely common name for something else entirely, and\n// the plugin used to adopt whatever it found under that name, add it to the\n// watch set, and report a successful compile over it.\nfunction looksLikeConfig(value: unknown): boolean {\n if (typeof value !== 'object' || value === null) return false\n\n // The four keys any usable configuration has at least one of. `platforms`\n // alone is enough because a configuration can declare its tokens inline\n // under `tokens`, or read them through `source`/`include`.\n return ['include', 'platforms', 'source', 'tokens'].some(\n (key) => key in value,\n )\n}\n\n// A config module may expose its config as a `default` export or as the\n// namespace itself. `'default' in value` is what lets the compiler reach\n// `.default` without a cast.\nfunction unwrapDefault(value: unknown): unknown {\n return typeof value === 'object' && value !== null && 'default' in value\n ? (value.default ?? value)\n : value\n}\n\n// The cache key for a module read under `files`: the newest of their mtimes.\n//\n// The dot goes, and that is not cosmetic. `mtimeMs` is fractional, so the\n// query it produces ends in something that reads as a file extension to\n// anything deriving a loader from the specifier without stripping the query\n// first — `sd.config.ts?t=1789565080284.6606` is then a `.6606` file, and a\n// TypeScript config gets parsed as JavaScript. Replacing the one dot keeps\n// every distinct mtime a distinct key.\n//\n// A file that cannot be stat'd is about to fail its import too, and falls\n// back to the time now, which keeps that failure the import's to report.\nfunction versionKey(files: string[]): string {\n let newest = -1\n for (const file of files) {\n try {\n newest = Math.max(newest, fs.statSync(file).mtimeMs)\n } catch {\n // Named, and not there: a data file created later is fine to miss.\n }\n }\n\n return String(newest < 0 ? Date.now() : newest).replace('.', '_')\n}\n\n// `config` with the plugin's token-module parser registered in `hooks.parsers`\n// and named in `parsers` — Style Dictionary applies a parser only when both\n// are true. A copy, so the consumer's object is not changed.\nfunction withTokenModuleParser(config: Config): Config {\n const applied = config.parsers ?? []\n\n // The patterns of every other parser the configuration applies, from its\n // own hooks or the global registry, since either can be named.\n // Typed partial, because a name nothing registered indexes to nothing.\n const own: Partial<Record<string, { pattern: RegExp }>> =\n config.hooks?.parsers ?? {}\n const registered: Partial<Record<string, { pattern: RegExp }>> =\n StyleDictionary.hooks.parsers\n const others = applied.flatMap((name) => {\n const parser = own[name] ?? registered[name]\n return parser ? [parser.pattern] : []\n })\n\n return {\n ...config,\n hooks: {\n ...config.hooks,\n parsers: {\n ...config.hooks?.parsers,\n [TOKEN_MODULE_PARSER]: {\n parser: async ({ filePath }) => importTokenModule(filePath),\n pattern: new TokenModulePattern(others),\n },\n },\n },\n parsers: [...applied, TOKEN_MODULE_PARSER],\n }\n}\n"],"mappings":";;;;;;;AAsDA,MAAM,6BAA6B;CAAC;CAAO;CAAQ;AAAK;AAWxD,eAAsB,eACpB,MAC0B;CAC1B,MAAM,EAAE,WAAW;CAEnB,IAAI,OAAO,WAAW,UAAU,OAAO,sBAAsB,MAAM;CAEnE,MAAM,SAAS,MAAM,iBAAiB,IAAI;CAK1C,IAAI,CAAC,QAAQ,OAAO,KAAK;CAQzB,IAAI,CAAC,iBAAiB,MAAM,GAAG,OAAO,sBAAsB,MAAM;CAElE,IAAI;EACF,OAAO,sBAAsB,gBAAgB,MAAM,CAAC;CACtD,QAAQ;EACN,OAAO,sBAAsB,MAAM;CACrC;AACF;AAGA,MAAM,sBAAsB;AAU5B,IAAM,qBAAN,cAAiC,OAAO;CACtC;CAEA,YAAY,QAAkB;EAC5B,MAAM,OAAO,GAAG,kBAAkB;EAClC,KAAK,SAAS;CAChB;CAEA,CAAU,OAAO,OAAO,OAAwC;EAC9D,IAAI,KAAK,OAAO,MAAM,UAAU,MAAM,MAAM,KAAK,MAAM,IAAI,GAAG,OAAO;EAErE,OAAO,MAAM,OAAO,MAAM,CAAC,KAAK;CAClC;AACF;AAMA,SAAgB,eAAe,MAAsB,OAAuB;CAC1E,OAAO,KAAK,OACR,qBAAqB,KAAK,SAC1B,iCAAiC,QAAQ;AAC/C;AAOA,eAAsB,iBACpB,MACA,QACwB;CACxB,IAAI,OAAO,KAAK,WAAW,UAAU,OAAO,KAAK;CAEjD,IAAI;EAKF,MAAM,SAAkB,iBAAiB,KAAK,MAAM,IAChD,MAAM,mBAAmB,KAAK,QAAQ,KAAK,OAAO,IAClD,MAAM,MAAM,GAAG,aAAa,KAAK,QAAQ,OAAO,CAAC;EAErD,IAAI,SAAS,MAAM,GAAG,OAAO;EAE7B,IAAI,QACF,OACE,0DAA0D,KAAK,UAC/D,OACF;CAEJ,SAAS,KAAK;EACZ,IAAI,QACF,OACE,gCAAgC,KAAK,OAAO,WAAW,aAAa,GAAG,KACvE,OACF;CAEJ;CAEA,OAAO;AACT;AASA,eAAsB,oBAAoB,EACxC,QACA,eACA,WACA,KACA,QAO4B;CAC5B,IAAI,YAAY;CAKhB,IAAI,cAAc,OAAO,OAAO,CAAC;CAGjC,IAAI,CAAC,WAAW;EACd,MAAM,WAAW;GACf;GACA;GACA;GACA;EACF;EAEA,MAAM,WAAqB,CAAC;EAE5B,KAAK,MAAM,QAAQ,UAAU;GAC3B,MAAM,WAAW,KAAK,QAAQ,MAAM,IAAI;GACxC,IAAI,CAAC,GAAG,WAAW,QAAQ,GAAG;GAY9B,IAAI,CAAC,gBAAgB,MALG,iBAAiB;IACvC,QAAQ;IACR,MAAM;GACR,CAAC,CAE6B,GAAG;IAC/B,SAAS,KAAK,IAAI;IAClB;GACF;GAKA,IAAI,CAAC,UAAU,WAAW;IACxB,UAAU,YAAY;IACtB,IAAI,uCAAuC,YAAY,MAAM;GAC/D;GAEA,YAAY;GACZ;EACF;EAKA,IAAI,SAAS,SAAS,GACpB,IACE,WAAW,SAAS,KAAK,IAAI,EAAE,MAAM,KAAK,iNAC1C,OACF;CAEJ;CAEA,IAAI,CAAC,WAAW;EACd,IACE,mGACA,OACF;EACA,OAAO,CAAC;CACV;CAGA,IAAI,OAAO,cAAc,YACvB,YAAY,MAAM,UAAU,cAAc,CAAC;CAK7C,QAFgB,MAAM,QAAQ,SAAS,IAAI,YAAY,CAAC,SAAS,EAAA,CAElD,KAAK,SAAS;EAC3B,IAAI,OAAO,SAAS,UAAU;GAC5B,MAAM,WAAW,KAAK,QAAQ,MAAM,IAAI;GACxC,OAAO;IAAE,QAAQ;IAAU,MAAM;GAAS;EAC5C,OACE,OAAO,EAAE,QAAQ,KAAK;CAE1B,CAAC;AACH;AAsBA,eAAe,mBACb,MACA,UAAoB,CAAC,GACH;CAClB,MAAM,MAAM,WAAW,CAAC,MAAM,GAAG,OAAO,CAAC;CAMzC,OAAO,cAAc,MAAM,OAAO,GAAG,cAAc,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM;AAC3E;AAQA,eAAe,kBACb,UACuB;CACvB,IAAI,aAAa,KAAA,GAAW,OAAO,CAAC;CAEpC,MAAM,SAAkB,MAAM,OAC5B,GAAG,cAAc,QAAQ,CAAC,CAAC,KAAK,KAAK,WAAW,CAAC,QAAQ,CAAC;CAK5D,MAAM,SACJ,OAAO,WAAW,YAAY,WAAW,QAAQ,aAAa,SAC1D,OAAO,UACP,KAAA;CACN,IAAI,CAAC,SAAS,MAAM,GAAG,OAAO,CAAC;CAE/B,IAAI;EACF,OAAO,gBAAgB,MAAM;CAC/B,QAAQ;EACN,OAAO;CACT;AACF;AAOA,SAAS,SAAS,OAAiC;CACjD,OAAO,OAAO,UAAU,YAAY,UAAU;AAChD;AAGA,SAAS,iBAAiB,MAAuB;CAC/C,OAAO,2BAA2B,MAAM,cACtC,KAAK,SAAS,SAAS,CACzB;AACF;AAEA,SAAS,SAAS,OAAuC;CACvD,OAAO,OAAO,UAAU,YAAY,UAAU;AAChD;AAWA,SAAS,gBAAgB,OAAyB;CAChD,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CAKxD,OAAO;EAAC;EAAW;EAAa;EAAU;CAAQ,CAAC,CAAC,MACjD,QAAQ,OAAO,KAClB;AACF;AAKA,SAAS,cAAc,OAAyB;CAC9C,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,aAAa,QAC9D,MAAM,WAAW,QAClB;AACN;AAaA,SAAS,WAAW,OAAyB;CAC3C,IAAI,SAAS;CACb,KAAK,MAAM,QAAQ,OACjB,IAAI;EACF,SAAS,KAAK,IAAI,QAAQ,GAAG,SAAS,IAAI,CAAC,CAAC,OAAO;CACrD,QAAQ,CAER;CAGF,OAAO,OAAO,SAAS,IAAI,KAAK,IAAI,IAAI,MAAM,CAAC,CAAC,QAAQ,KAAK,GAAG;AAClE;AAKA,SAAS,sBAAsB,QAAwB;CACrD,MAAM,UAAU,OAAO,WAAW,CAAC;CAKnC,MAAM,MACJ,OAAO,OAAO,WAAW,CAAC;CAC5B,MAAM,aACJ,gBAAgB,MAAM;CACxB,MAAM,SAAS,QAAQ,SAAS,SAAS;EACvC,MAAM,SAAS,IAAI,SAAS,WAAW;EACvC,OAAO,SAAS,CAAC,OAAO,OAAO,IAAI,CAAC;CACtC,CAAC;CAED,OAAO;EACL,GAAG;EACH,OAAO;GACL,GAAG,OAAO;GACV,SAAS;IACP,GAAG,OAAO,OAAO;KAChB,sBAAsB;KACrB,QAAQ,OAAO,EAAE,eAAe,kBAAkB,QAAQ;KAC1D,SAAS,IAAI,mBAAmB,MAAM;IACxC;GACF;EACF;EACA,SAAS,CAAC,GAAG,SAAS,mBAAmB;CAC3C;AACF"}
|
|
1
|
+
{"version":3,"file":"config.js","names":[],"sources":["../src/config.ts"],"sourcesContent":["// Where a configuration comes from and what it says: the `config` option\n// resolved into a list, each item read as an object, and the form Style\n// Dictionary is handed to build from.\n//\n// Nothing here holds state. One module serves every plugin instance in the\n// process, so what an instance owns — its root, its logger, whether it has\n// announced a discovery yet — is handed in on each call.\n\nimport type { Config } from 'style-dictionary'\nimport type { DesignTokens } from 'style-dictionary/types'\n\nimport JSON5 from 'json5'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport StyleDictionary from 'style-dictionary'\n\nimport type {\n StyleDictionaryConfigContext,\n UnpluginStyleDictionaryOptions,\n} from './types.js'\n\nimport { errorMessage } from './errors.js'\n\n// The plugin's logger, narrowed to the two levels anything here reports at. It\n// is handed on as a function rather than as the host it writes to, so it goes\n// on reading whichever host has claimed the plugin's messages by the time it\n// is called.\nexport type Log = (message: string, type: 'error' | 'info') => void\n\n// A configuration as `resolveConfigOption` hands it on: either the object the\n// consumer passed or the path it was read from, plus the directory relative\n// paths inside it resolve against.\nexport interface ResolvedConfig {\n config: Config | string\n file?: string\n // The files the plugin's `watch` option names, expanded. A config module's\n // import is keyed on their mtimes beside its own, so a data file it reads\n // at load time — and that a consumer named in `watch` to trigger a rebuild\n // — re-evaluates it rather than only rebuilding with what it read before.\n watched?: string[]\n}\n\n// The config extensions Style Dictionary loads with `import` rather than by\n// parsing the file — the `case` list in its own `loadFile`. They are the only\n// ones Node's permanent module cache applies to, and so the only ones this\n// plugin has to read on the build's behalf.\n//\n// Everything else Style Dictionary parses as JSON5, including `.json`, and\n// this list is what makes the plugin split the same way. Reading the two\n// halves apart is what silently unwatched a whole family of configurations:\n// a `.json5` or `.jsonc` file went down the import branch and failed there\n// while the build succeeded, and a `.json` file carrying a comment or a\n// trailing comma failed strict `JSON.parse` for the same reason.\nconst IMPORTED_CONFIG_EXTENSIONS = ['.js', '.mjs', '.ts']\n\n// What `new StyleDictionary` is handed for an item: always an object, carrying\n// the plugin's own parser for token modules — see `withTokenModuleParser`.\n//\n// A path in the JS family is read here because those are exactly the\n// extensions Style Dictionary's own `loadFile` reaches with `import`, whose\n// module record Node then caches forever, so a build handed the path could\n// read the config stale. A JSON5-family path is read here too, so the parser\n// can ride along; Style Dictionary would have parsed the same file the same\n// way.\nexport async function configForBuild(\n item: ResolvedConfig,\n): Promise<Config | string> {\n const { config } = item\n\n if (typeof config !== 'string') return withTokenModuleParser(config)\n\n const loaded = await readConfigObject(item)\n\n // A config that could not be read falls back to the path, so the failure\n // stays Style Dictionary's to report — it knows more about why an import\n // failed than this does, a `.ts` config without type stripping especially.\n if (!loaded) return item.config\n\n // `loadFile` clones what it imports before handing it on, and passing an\n // object skips that. It matters more here than it does there: the module\n // record now outlives the build, and `extend` is called with\n // `mutateOriginal`. Cloning throws on a config carrying functions — an\n // inline transform — and Style Dictionary's own fallback in that case is\n // to use the original, so this one matches it.\n if (!isImportedConfig(config)) return withTokenModuleParser(loaded)\n\n try {\n return withTokenModuleParser(structuredClone(loaded))\n } catch {\n return withTokenModuleParser(loaded)\n }\n}\n\n// The name the plugin's token-module parser is registered and applied under.\nconst TOKEN_MODULE_PARSER = 'unplugin-style-dictionary/token-module'\n\n// Matches a `.js`, `.mjs` or `.ts` token source, unless another parser the\n// configuration applies matches it too. Style Dictionary runs every parser\n// whose pattern matches and keeps the last result, and a global parser comes\n// ahead of a configuration's own whatever the order in `parsers` — measured,\n// `['plugin-own', 'consumer-global']` ran in the order\n// `['consumer-global', 'plugin-own']`. So order cannot make the consumer's\n// win, and stepping aside is what does. A `RegExp` whose `Symbol.match` is\n// overridden, because `filePath.match(pattern)` is how Style Dictionary asks.\nclass TokenModulePattern extends RegExp {\n private readonly others: RegExp[]\n\n constructor(others: RegExp[]) {\n super(String.raw`\\.(?:js|mjs|ts)$`)\n this.others = others\n }\n\n override [Symbol.match](value: string): null | RegExpMatchArray {\n if (this.others.some((other) => value.match(other) !== null)) return null\n\n return super[Symbol.match](value)\n }\n}\n\n// How to name a configuration in a message. A path is what a consumer\n// recognises; a configuration passed as an object or returned by a function has\n// no name, so it is identified by where it sits in the list rather than by a\n// stringified dump of itself.\nexport function describeConfig(item: ResolvedConfig, index: number): string {\n return item.file\n ? `The configuration ${item.file}`\n : `The configuration at position ${index + 1}`\n}\n\n// What a configuration item says, as an object. `report` is where a failure is\n// said, and leaving it out is what stops the two readers of this from saying\n// the same thing twice: a bad config has nowhere else to surface when the\n// watch list is being built, while a build falls back to handing Style\n// Dictionary the path and lets its message through instead.\nexport async function readConfigObject(\n item: ResolvedConfig,\n report?: Log,\n): Promise<Config | null> {\n if (typeof item.config !== 'string') return item.config\n\n try {\n // JSON5 rather than `JSON.parse`, because that is what Style Dictionary\n // reads these files with — it is a superset, so a plain `.json` config\n // parses identically and one carrying a comment stops being a config\n // the build understands and the watch list does not.\n const loaded: unknown = isImportedConfig(item.config)\n ? await importConfigModule(item.config, item.watched)\n : JSON5.parse(fs.readFileSync(item.config, 'utf-8'))\n\n if (isConfig(loaded)) return loaded\n\n if (report) {\n report(\n `Config file did not resolve to a configuration object: ${item.config}`,\n 'error',\n )\n }\n } catch (err) {\n // Asked only once reading has failed, and of the file rather than the\n // error: a module config that imports something missing fails with the\n // same code as a config that is not there at all.\n if (report) {\n report(\n fs.existsSync(item.config)\n ? `Failed to parse config file: ${item.config}. Error: ${errorMessage(err)}`\n : `Config file not found: ${item.config}`,\n 'error',\n )\n }\n }\n\n return null\n}\n\n// Resolve config file paths / objects\n//\n// Everything a plugin instance owns arrives as an argument, and none of it is\n// kept. `root` is read by the caller on each call because the host assigns it\n// after the factory has run, and `configContext` is a function rather than a\n// value for the same reason: it is asked only once a consumer's `config`\n// function is about to run.\nexport async function resolveConfigOption({\n config,\n configContext,\n discovery,\n log,\n root,\n}: {\n config: UnpluginStyleDictionaryOptions['config']\n configContext: () => StyleDictionaryConfigContext\n discovery: { announced: boolean }\n log: Log\n root: string\n}): Promise<ResolvedConfig[]> {\n let rawConfig = config\n\n // Checked ahead of the discovery below, and by identity rather than\n // truthiness: `false` is falsy, so the `!rawConfig` test that triggers\n // discovery would treat \"do not discover anything\" as \"go and look\".\n if (rawConfig === false) return []\n\n // If config is not defined, look for default configuration files\n if (!rawConfig) {\n const defaults = [\n 'sd.config.json',\n 'config.json',\n 'sd.config.js',\n 'sd.config.mjs',\n ]\n\n const rejected: string[] = []\n\n for (const file of defaults) {\n const fullPath = path.resolve(root, file)\n if (!fs.existsSync(fullPath)) continue\n\n // Read before adopting. For the two `.json` names this is a parse and\n // nothing more; for the two module names it is an import, and the\n // module has already run by the time there is anything to check —\n // which is what `config: false` exists for and why validation alone\n // does not cover them.\n const candidate = await readConfigObject({\n config: fullPath,\n file: fullPath,\n })\n\n if (!looksLikeConfig(candidate)) {\n rejected.push(file)\n continue\n }\n\n // Announced, because \"which configuration did it pick\" was not\n // answerable from the console at all, and discovery picks from four\n // generic names. A candidate skipped on the way is named here rather\n // than as an error: the build has a configuration, and the advice is\n // to name it, since `config: false` would stop discovery finding it.\n if (!discovery.announced) {\n discovery.announced = true\n log(\n rejected.length > 0\n ? `Using the configuration it found at ${fullPath}, after ignoring ${rejected.join(', ')}: nothing there declares platforms, source, include or tokens. Name ${file} with the config option to stop looking.`\n : `Using the configuration it found at ${fullPath}`,\n 'info',\n )\n }\n\n rawConfig = file\n break\n }\n\n // A skipped candidate is the interesting half of \"no configuration\n // found\": the file is right there, and the reason it was not used is not\n // guessable.\n if (!rawConfig && rejected.length > 0) {\n log(\n `Ignored ${rejected.join(', ')} in ${root}: nothing there declares platforms, source, include or tokens, so it does not look like a Style Dictionary configuration. Name it with the config option if it is one, or set config to false to stop looking.`,\n 'error',\n )\n }\n }\n\n if (!rawConfig) {\n log(\n 'No configuration specified and no default config file found. Style Dictionary will not compile.',\n 'error',\n )\n return []\n }\n\n // Evaluate function if provided\n if (typeof rawConfig === 'function') {\n rawConfig = await rawConfig(configContext())\n }\n\n const configs = Array.isArray(rawConfig) ? rawConfig : [rawConfig]\n\n return configs.map((conf) => {\n if (typeof conf === 'string') {\n const fullPath = path.resolve(root, conf)\n return { config: fullPath, file: fullPath }\n } else {\n return { config: conf }\n }\n })\n}\n\n// Imports a config module, re-evaluating it only when the file itself has\n// changed. The query string is what decides that, and it is not decoration:\n// Node's ESM cache is permanent and keyed on the specifier, so a config\n// imported without one is evaluated once and never read again — which is\n// how an edited `.mjs` config went on building the platform map the process\n// started with, for the rest of the session.\n//\n// `Date.now()` fixed that staleness and bought two problems. Every watcher\n// event registered another module record in a map nothing prunes, re-running\n// the config's own `registerFormat` side effects for a file nobody touched.\n// And its millisecond granularity meant an edit landing inside the same\n// millisecond as the previous import shared that import's key, and was\n// served the old module anyway. `mtimeMs` carries sub-millisecond\n// resolution and only moves when the file does.\n//\n// Keyed on the files the plugin's `watch` option names as well: a config that\n// reads a data file at load time went on building what that file said at\n// startup. A module the config itself imports is out of reach — a new query\n// on the config re-evaluates the config, not what it imports — so naming one\n// in `watch` buys a rebuild and not a re-read.\nasync function importConfigModule(\n file: string,\n watched: string[] = [],\n): Promise<unknown> {\n const key = versionKey([file, ...watched])\n\n // Sequential on purpose: a config module runs arbitrary code at import\n // time — `registerFormat` and friends — and Style Dictionary's registries\n // are global, so importing several at once would interleave those\n // registrations.\n return unwrapDefault(await import(`${pathToFileURL(file).href}?t=${key}`))\n}\n\n// A token module, read the way `importConfigModule` reads a config: under a\n// query keyed on its mtime, and cloned as Style Dictionary's own `loadFile`\n// clones it. Style Dictionary reads one with a bare `import`, and Node's\n// module cache keeps that first evaluation for the life of the process — so\n// under a long-lived watcher an edited `.mjs` token file rebuilt, reported\n// success, and wrote the values the process started with.\nasync function importTokenModule(\n filePath: string | undefined,\n): Promise<DesignTokens> {\n if (filePath === undefined) return {}\n\n const module: unknown = await import(\n `${pathToFileURL(filePath).href}?t=${versionKey([filePath])}`\n )\n\n // `.default`, exactly as `loadFile` reads it: a module without one carries\n // no tokens.\n const tokens: unknown =\n typeof module === 'object' && module !== null && 'default' in module\n ? module.default\n : undefined\n if (!isTokens(tokens)) return {}\n\n try {\n return structuredClone(tokens)\n } catch {\n return tokens\n }\n}\n\n// A config file is an untyped boundary: `JSON.parse` and a dynamic `import`\n// both hand back `any`, and an `any` assigned to `configObj` spreads through\n// every read of it downstream. These two narrow that boundary once, here.\n// They are type predicates rather than assertions on purpose — a predicate is\n// a check the compiler verifies, where a cast is only a claim.\nfunction isConfig(value: unknown): value is Config {\n return typeof value === 'object' && value !== null\n}\n\n// Whether a config path is one Style Dictionary imports rather than parses.\nfunction isImportedConfig(file: string): boolean {\n return IMPORTED_CONFIG_EXTENSIONS.some((extension) =>\n file.endsWith(extension),\n )\n}\n\nfunction isTokens(value: unknown): value is DesignTokens {\n return typeof value === 'object' && value !== null\n}\n\n// Whether a discovered file looks like a Style Dictionary configuration at all.\n//\n// Only applied to a file the plugin went looking for, never to one a consumer\n// named: an explicit `config` is their choice and second-guessing it would\n// reject shapes Style Dictionary accepts and this does not know about.\n//\n// `config.json` is an extremely common name for something else entirely, and\n// the plugin used to adopt whatever it found under that name, add it to the\n// watch set, and report a successful compile over it.\nfunction looksLikeConfig(value: unknown): boolean {\n if (typeof value !== 'object' || value === null) return false\n\n // The four keys any usable configuration has at least one of. `platforms`\n // alone is enough because a configuration can declare its tokens inline\n // under `tokens`, or read them through `source`/`include`.\n return ['include', 'platforms', 'source', 'tokens'].some(\n (key) => key in value,\n )\n}\n\n// A config module may expose its config as a `default` export or as the\n// namespace itself. `'default' in value` is what lets the compiler reach\n// `.default` without a cast.\nfunction unwrapDefault(value: unknown): unknown {\n return typeof value === 'object' && value !== null && 'default' in value\n ? (value.default ?? value)\n : value\n}\n\n// The cache key for a module read under `files`: the newest of their mtimes.\n//\n// The dot goes, and that is not cosmetic. `mtimeMs` is fractional, so the\n// query it produces ends in something that reads as a file extension to\n// anything deriving a loader from the specifier without stripping the query\n// first — `sd.config.ts?t=1789565080284.6606` is then a `.6606` file, and a\n// TypeScript config gets parsed as JavaScript. Replacing the one dot keeps\n// every distinct mtime a distinct key.\n//\n// A file that cannot be stat'd is about to fail its import too, and falls\n// back to the time now, which keeps that failure the import's to report.\nfunction versionKey(files: string[]): string {\n let newest = -1\n for (const file of files) {\n try {\n newest = Math.max(newest, fs.statSync(file).mtimeMs)\n } catch {\n // Named, and not there: a data file created later is fine to miss.\n }\n }\n\n return String(newest < 0 ? Date.now() : newest).replace('.', '_')\n}\n\n// `config` with the plugin's token-module parser registered in `hooks.parsers`\n// and named in `parsers` — Style Dictionary applies a parser only when both\n// are true. A copy, so the consumer's object is not changed.\nfunction withTokenModuleParser(config: Config): Config {\n const applied = config.parsers ?? []\n\n // The patterns of every other parser the configuration applies, from its\n // own hooks or the global registry, since either can be named.\n // Typed partial, because a name nothing registered indexes to nothing.\n const own: Partial<Record<string, { pattern: RegExp }>> =\n config.hooks?.parsers ?? {}\n const registered: Partial<Record<string, { pattern: RegExp }>> =\n StyleDictionary.hooks.parsers\n const others = applied.flatMap((name) => {\n const parser = own[name] ?? registered[name]\n return parser ? [parser.pattern] : []\n })\n\n return {\n ...config,\n hooks: {\n ...config.hooks,\n parsers: {\n ...config.hooks?.parsers,\n [TOKEN_MODULE_PARSER]: {\n parser: async ({ filePath }) => importTokenModule(filePath),\n pattern: new TokenModulePattern(others),\n },\n },\n },\n parsers: [...applied, TOKEN_MODULE_PARSER],\n }\n}\n"],"mappings":"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"}
|