@kanso-labs/unplugin-style-dictionary 0.4.29 → 0.5.1
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 +146 -1
- package/dist/index.js +137 -46
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +62 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ build on Rolldown/tsdown) that both need tokens compiled ahead of them.
|
|
|
20
20
|
rebuild-on-change is fully supported under Vite's dev server; other targets
|
|
21
21
|
rebuild on change wherever the host bundler itself runs a persistent watch
|
|
22
22
|
mode.
|
|
23
|
-
- **Config flexibility**: Supports file paths (JSON, JS, MJS,
|
|
23
|
+
- **Config flexibility**: Supports file paths (JSON, JSON5, JSONC, JS, MJS, TS),
|
|
24
24
|
configuration objects, or functions — including registering custom formats at
|
|
25
25
|
config-resolution time.
|
|
26
26
|
- **Atomic writes**: Every generated file is written to a temporary sibling and
|
|
@@ -106,6 +106,24 @@ _Note: that path needs Node 20.19+ or 22.12+, the versions that can `require` an
|
|
|
106
106
|
ES module. Every Node release still in support clears it. Importing from ESM has
|
|
107
107
|
no such floor._
|
|
108
108
|
|
|
109
|
+
### Config File Formats
|
|
110
|
+
|
|
111
|
+
A `config` path may be `.json`, `.json5`, `.jsonc`, `.js`, `.mjs` or `.ts`. The
|
|
112
|
+
JSON family is parsed as JSON5, so comments and trailing commas are accepted in
|
|
113
|
+
a `.json` file too — that is what Style Dictionary itself does, and the plugin
|
|
114
|
+
reads the file the same way so the watch list and the build never disagree about
|
|
115
|
+
what the configuration says.
|
|
116
|
+
|
|
117
|
+
Two limits worth knowing before you pick one:
|
|
118
|
+
|
|
119
|
+
- **A `.ts` config needs Node >= 22.18**, where type stripping is on by default.
|
|
120
|
+
Below that the build fails with `Could not import TypeScript file`. The
|
|
121
|
+
package's own engines floor is lower, so this is a per-config requirement
|
|
122
|
+
rather than a requirement of the plugin.
|
|
123
|
+
- **`.cjs` is not supported.** Style Dictionary has no branch for that extension
|
|
124
|
+
and parses it as JSON5, which fails on the first `module`. Rename the file to
|
|
125
|
+
`.js` in a CommonJS package, or pass a configuration object.
|
|
126
|
+
|
|
109
127
|
### Multiple Configurations
|
|
110
128
|
|
|
111
129
|
If you have multiple themes or sub-brands, pass an array of config paths or
|
|
@@ -149,6 +167,11 @@ including watch-triggered rebuilds under Vite — so the format is always
|
|
|
149
167
|
registered before it's needed; re-registering the same format name on every
|
|
150
168
|
rebuild is safe (Style Dictionary silently replaces the existing one).
|
|
151
169
|
|
|
170
|
+
It runs for builds and for nothing else. A file change that matches no token
|
|
171
|
+
source and no config file does not reach it, so a dev server editing unrelated
|
|
172
|
+
project files leaves it alone — treat it as the place to prepare a build, not as
|
|
173
|
+
a general file-change hook.
|
|
174
|
+
|
|
152
175
|
```typescript
|
|
153
176
|
// Named `styleDictionaryPlugin` here to avoid colliding with the `StyleDictionary`
|
|
154
177
|
// class imported from the `style-dictionary` package itself, below.
|
|
@@ -185,6 +208,81 @@ export default defineConfig({
|
|
|
185
208
|
})
|
|
186
209
|
```
|
|
187
210
|
|
|
211
|
+
## Where Paths Are Resolved From
|
|
212
|
+
|
|
213
|
+
Two bases, and which one applies depends on whose path it is.
|
|
214
|
+
|
|
215
|
+
**The `config` option is the plugin's**, so a relative path is looked up under
|
|
216
|
+
the host's root: Vite's `root`, webpack's `context`, and the working directory
|
|
217
|
+
for rollup and rolldown, which report none. `root` overrides that.
|
|
218
|
+
|
|
219
|
+
**Everything inside a Style Dictionary configuration is Style Dictionary's**, so
|
|
220
|
+
`source`, `include` and `buildPath` are resolved against the working directory.
|
|
221
|
+
That is what Style Dictionary itself does — `combineJSON` globs each pattern
|
|
222
|
+
with no directory of its own — so a configuration behaves the same here as it
|
|
223
|
+
does under the Style Dictionary CLI.
|
|
224
|
+
|
|
225
|
+
The consequence worth knowing: a configuration kept in a subdirectory names its
|
|
226
|
+
tokens relative to where the build runs, not relative to itself.
|
|
227
|
+
|
|
228
|
+
```jsonc
|
|
229
|
+
// tokens/config/sd.config.json, with the build run from the project root
|
|
230
|
+
{
|
|
231
|
+
// read from <project root>/tokens, not from tokens/config/tokens
|
|
232
|
+
"source": ["tokens/**/*.json"],
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Absolute paths sidestep the question entirely, and are worth reaching for when
|
|
237
|
+
the build might be run from more than one directory.
|
|
238
|
+
|
|
239
|
+
## Logging
|
|
240
|
+
|
|
241
|
+
Style Dictionary says useful things while it builds — a name collision, a
|
|
242
|
+
reference it could not resolve, `No tokens for vars.css. File not created.` —
|
|
243
|
+
and the plugin used to suppress all of it by overwriting `log.verbosity` on the
|
|
244
|
+
way past. It no longer touches that setting unless asked, so whatever your
|
|
245
|
+
configuration sets now reaches you.
|
|
246
|
+
|
|
247
|
+
`logLevel` overrides it from the plugin side:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
StyleDictionary({
|
|
251
|
+
config: 'sd.config.json',
|
|
252
|
+
// 'silent' | 'warn' | 'info' | 'verbose'. Unset leaves your config's own
|
|
253
|
+
// log.verbosity alone, which is the default.
|
|
254
|
+
logLevel: 'warn',
|
|
255
|
+
})
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`'warn'` is the level worth knowing about: Style Dictionary's warnings without
|
|
259
|
+
the plugin's own progress lines and size table. `silent: true` is an alias for
|
|
260
|
+
`'silent'`.
|
|
261
|
+
|
|
262
|
+
A compile that fails is reported at every level, including `'silent'`, which is
|
|
263
|
+
why there is no `'error'`. `log.warnings` is never touched: if your
|
|
264
|
+
configuration turns a warning into a thrown build, that stays your decision.
|
|
265
|
+
|
|
266
|
+
## Failing the Build
|
|
267
|
+
|
|
268
|
+
A token compile that fails stops the build. `vite build`, `rollup` and `webpack`
|
|
269
|
+
exit non-zero with Style Dictionary's own message, rather than finishing green
|
|
270
|
+
and shipping whatever the previous run wrote.
|
|
271
|
+
|
|
272
|
+
A watch-triggered rebuild only reports the failure, so a dev server survives a
|
|
273
|
+
half-typed token file. `failOnError` moves that line:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
StyleDictionary({
|
|
277
|
+
config: 'sd.config.json',
|
|
278
|
+
// 'build' is the default. 'serve' fails rebuilds instead, true fails both,
|
|
279
|
+
// false restores the old report-and-continue behaviour.
|
|
280
|
+
failOnError: true,
|
|
281
|
+
})
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
A failure is always reported, whatever `failOnError` and `silent` are set to.
|
|
285
|
+
|
|
188
286
|
## Options Reference
|
|
189
287
|
|
|
190
288
|
```typescript
|
|
@@ -216,8 +314,55 @@ export interface UnpluginStyleDictionaryOptions {
|
|
|
216
314
|
*/
|
|
217
315
|
watch?: string | string[]
|
|
218
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Whether a compile that fails should throw rather than only be reported.
|
|
319
|
+
*
|
|
320
|
+
* - 'build' (the default) throws on the one-shot compile in buildStart, and
|
|
321
|
+
* only reports a failed watch rebuild.
|
|
322
|
+
* - 'serve' is the reverse: a failed rebuild is thrown to whatever awaited
|
|
323
|
+
* it. Vite's dev server has no build to fail, so there it is reported.
|
|
324
|
+
* - true throws on both, false on neither.
|
|
325
|
+
*
|
|
326
|
+
* Reporting happens either way, and is not suppressed by 'silent'.
|
|
327
|
+
*
|
|
328
|
+
* @default 'build'
|
|
329
|
+
*/
|
|
330
|
+
failOnError?: 'build' | 'serve' | boolean
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The directory a relative config path is looked up in.
|
|
334
|
+
*
|
|
335
|
+
* Defaults to the host's own root — Vite's root, webpack's context — and to
|
|
336
|
+
* the working directory for rollup and rolldown, which offer none. A
|
|
337
|
+
* relative value here is resolved against the working directory, and it
|
|
338
|
+
* takes precedence over whatever the host reports.
|
|
339
|
+
*
|
|
340
|
+
* It does not move the paths inside a configuration. See "Where paths are
|
|
341
|
+
* resolved from" below.
|
|
342
|
+
*/
|
|
343
|
+
root?: string
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* How much this plugin and Style Dictionary say while building.
|
|
347
|
+
*
|
|
348
|
+
* - 'silent' — nothing from either.
|
|
349
|
+
* - 'warn' — Style Dictionary's warnings, and nothing from the plugin.
|
|
350
|
+
* - 'info' — the above, plus the plugin's progress lines and size table.
|
|
351
|
+
* - 'verbose' — the above, with Style Dictionary naming what it warned about.
|
|
352
|
+
*
|
|
353
|
+
* Leave it unset and the configuration's own log.verbosity stands.
|
|
354
|
+
* A compile that fails is reported at every level.
|
|
355
|
+
*
|
|
356
|
+
* @default undefined
|
|
357
|
+
*/
|
|
358
|
+
logLevel?: 'info' | 'silent' | 'verbose' | 'warn'
|
|
359
|
+
|
|
219
360
|
/**
|
|
220
361
|
* Disable console logging.
|
|
362
|
+
*
|
|
363
|
+
* An alias for logLevel: 'silent', which wins if both are set. A compile
|
|
364
|
+
* that fails is always reported.
|
|
365
|
+
*
|
|
221
366
|
* @default false
|
|
222
367
|
*/
|
|
223
368
|
silent?: boolean
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import JSON5 from "json5";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
@@ -14,6 +15,9 @@ function matchesWatchedFile(file, patterns) {
|
|
|
14
15
|
return normalizedPattern === normalizedFile || picomatch.isMatch(normalizedFile, normalizedPattern);
|
|
15
16
|
});
|
|
16
17
|
}
|
|
18
|
+
function asError(error) {
|
|
19
|
+
return error instanceof Error ? error : new Error(errorMessage(error));
|
|
20
|
+
}
|
|
17
21
|
function discardTemporaryFile(temporary) {
|
|
18
22
|
try {
|
|
19
23
|
fs.rmSync(temporary, { force: true });
|
|
@@ -86,15 +90,29 @@ const atomicVolume = Object.create(fs, {
|
|
|
86
90
|
writeFileSync: { value: writeFileSyncAtomic }
|
|
87
91
|
});
|
|
88
92
|
const GLOB_CHARACTERS = /[!*?[\]{}]/;
|
|
93
|
+
const IMPORTED_CONFIG_EXTENSIONS = [
|
|
94
|
+
".js",
|
|
95
|
+
".mjs",
|
|
96
|
+
".ts"
|
|
97
|
+
];
|
|
98
|
+
function isImportedConfig(file) {
|
|
99
|
+
return IMPORTED_CONFIG_EXTENSIONS.some((extension) => file.endsWith(extension));
|
|
100
|
+
}
|
|
89
101
|
function staticParentOf(pattern) {
|
|
90
102
|
const segments = pattern.split("/");
|
|
91
103
|
const firstGlob = segments.findIndex((segment) => GLOB_CHARACTERS.test(segment));
|
|
92
104
|
return firstGlob === -1 ? path.posix.dirname(pattern) : segments.slice(0, firstGlob).join("/");
|
|
93
105
|
}
|
|
94
|
-
const unpluginFactory = (options = {}) => {
|
|
95
|
-
const
|
|
96
|
-
|
|
106
|
+
const unpluginFactory = (options = {}, meta) => {
|
|
107
|
+
const isWebpack = meta.framework === "webpack";
|
|
108
|
+
const { failOnError = "build", logLevel, root: rootOption, silent = false } = options;
|
|
109
|
+
const level = logLevel ?? (silent ? "silent" : void 0);
|
|
110
|
+
const quiet = level === "silent" || level === "warn";
|
|
111
|
+
const verbosity = level === void 0 ? void 0 : level === "verbose" ? "verbose" : level === "silent" ? "silent" : "default";
|
|
112
|
+
const failsTheBuild = (context) => failOnError === true || (context === void 0 ? failOnError === "build" : failOnError === "serve");
|
|
113
|
+
let root = rootOption ? path.resolve(process.cwd(), rootOption) : process.cwd();
|
|
97
114
|
const generatedDestinations = /* @__PURE__ */ new Set();
|
|
115
|
+
let cachedPatterns;
|
|
98
116
|
let watchRebuild = false;
|
|
99
117
|
let hasCompiled = false;
|
|
100
118
|
const expandPatterns = async (patterns) => {
|
|
@@ -114,10 +132,13 @@ const unpluginFactory = (options = {}) => {
|
|
|
114
132
|
};
|
|
115
133
|
const isWatchedSource = (file, patterns) => !generatedDestinations.has(file.replace(/\\/g, "/")) && matchesWatchedFile(file, patterns);
|
|
116
134
|
const log = (message, type = "info") => {
|
|
117
|
-
if (silent) return;
|
|
118
135
|
const prefix = "[unplugin-style-dictionary]";
|
|
119
|
-
if (type === "error")
|
|
120
|
-
|
|
136
|
+
if (type === "error") {
|
|
137
|
+
console.error(`\x1b[31m${prefix} ${message}\x1b[0m`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (quiet) return;
|
|
141
|
+
if (type === "success") console.log(`\x1b[32m${prefix} ${message}\x1b[0m`);
|
|
121
142
|
else console.log(`\x1b[36m${prefix} ${message}\x1b[0m`);
|
|
122
143
|
};
|
|
123
144
|
const resolveConfigs = async () => {
|
|
@@ -144,37 +165,41 @@ const unpluginFactory = (options = {}) => {
|
|
|
144
165
|
const fullPath = path.resolve(root, conf);
|
|
145
166
|
return {
|
|
146
167
|
config: fullPath,
|
|
147
|
-
dir: path.dirname(fullPath),
|
|
148
168
|
file: fullPath
|
|
149
169
|
};
|
|
150
|
-
} else return {
|
|
151
|
-
config: conf,
|
|
152
|
-
dir: root
|
|
153
|
-
};
|
|
170
|
+
} else return { config: conf };
|
|
154
171
|
});
|
|
155
172
|
};
|
|
173
|
+
const importConfigModule = async (file) => {
|
|
174
|
+
let version;
|
|
175
|
+
try {
|
|
176
|
+
version = fs.statSync(file).mtimeMs;
|
|
177
|
+
} catch {
|
|
178
|
+
version = Date.now();
|
|
179
|
+
}
|
|
180
|
+
const key = String(version).replace(".", "_");
|
|
181
|
+
return unwrapDefault(await import(`${pathToFileURL(file).href}?t=${key}`));
|
|
182
|
+
};
|
|
183
|
+
const readConfigObject = async (item, report) => {
|
|
184
|
+
if (typeof item.config !== "string") return item.config;
|
|
185
|
+
try {
|
|
186
|
+
const loaded = isImportedConfig(item.config) ? await importConfigModule(item.config) : JSON5.parse(fs.readFileSync(item.config, "utf-8"));
|
|
187
|
+
if (isConfig(loaded)) return loaded;
|
|
188
|
+
if (report) log(`Config file did not resolve to a configuration object: ${item.config}`, "error");
|
|
189
|
+
} catch (err) {
|
|
190
|
+
if (report) log(`Failed to parse config file: ${item.config}. Error: ${errorMessage(err)}`, "error");
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
};
|
|
156
194
|
const getWatchTargets = async (resolvedConfigs) => {
|
|
157
195
|
const filesToWatch = /* @__PURE__ */ new Set();
|
|
158
196
|
for (const item of resolvedConfigs) {
|
|
159
197
|
if (item.file) filesToWatch.add(item.file.replace(/\\/g, "/"));
|
|
160
|
-
|
|
161
|
-
if (typeof item.config === "string") try {
|
|
162
|
-
let loaded;
|
|
163
|
-
if (item.config.endsWith(".json")) loaded = JSON.parse(fs.readFileSync(item.config, "utf-8"));
|
|
164
|
-
else {
|
|
165
|
-
const fileUrl = pathToFileURL(item.config).href;
|
|
166
|
-
loaded = unwrapDefault(await import(`${fileUrl}?t=${Date.now()}`));
|
|
167
|
-
}
|
|
168
|
-
if (isConfig(loaded)) configObj = loaded;
|
|
169
|
-
else log(`Config file did not resolve to a configuration object: ${item.config}`, "error");
|
|
170
|
-
} catch (err) {
|
|
171
|
-
log(`Failed to parse config file: ${item.config}. Error: ${errorMessage(err)}`, "error");
|
|
172
|
-
}
|
|
173
|
-
else configObj = item.config;
|
|
198
|
+
const configObj = await readConfigObject(item, true);
|
|
174
199
|
if (configObj) {
|
|
175
200
|
const addPattern = (pattern) => {
|
|
176
201
|
if (typeof pattern === "string") {
|
|
177
|
-
const normalized = (path.isAbsolute(pattern) ? pattern : path.resolve(
|
|
202
|
+
const normalized = (path.isAbsolute(pattern) ? pattern : path.resolve(process.cwd(), pattern)).replace(/\\/g, "/");
|
|
178
203
|
filesToWatch.add(normalized);
|
|
179
204
|
}
|
|
180
205
|
};
|
|
@@ -196,23 +221,37 @@ const unpluginFactory = (options = {}) => {
|
|
|
196
221
|
}
|
|
197
222
|
}
|
|
198
223
|
const patterns = Array.from(filesToWatch);
|
|
224
|
+
cachedPatterns = patterns;
|
|
199
225
|
return {
|
|
200
226
|
paths: await expandPatterns(patterns),
|
|
201
227
|
patterns
|
|
202
228
|
};
|
|
203
229
|
};
|
|
230
|
+
const configForBuild = async (item) => {
|
|
231
|
+
const { config } = item;
|
|
232
|
+
if (typeof config !== "string" || !isImportedConfig(config)) return config;
|
|
233
|
+
const loaded = await readConfigObject(item, false);
|
|
234
|
+
if (!loaded) return item.config;
|
|
235
|
+
try {
|
|
236
|
+
return structuredClone(loaded);
|
|
237
|
+
} catch {
|
|
238
|
+
return loaded;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
204
241
|
const runBuilds = async (resolvedConfigs, context) => {
|
|
205
242
|
const startTime = Date.now();
|
|
206
243
|
try {
|
|
207
244
|
if (!context) log("Compiling design tokens...", "info");
|
|
208
245
|
const generatedFiles = /* @__PURE__ */ new Set();
|
|
209
246
|
for (const item of resolvedConfigs) {
|
|
210
|
-
const sd = new StyleDictionary(item
|
|
211
|
-
await sd.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
247
|
+
const sd = new StyleDictionary(await configForBuild(item), { init: false });
|
|
248
|
+
await sd.extend(void 0, {
|
|
249
|
+
mutateOriginal: true,
|
|
250
|
+
verbosity
|
|
251
|
+
});
|
|
252
|
+
sd.volume = atomicVolume;
|
|
253
|
+
await sd.buildAllPlatforms();
|
|
254
|
+
for (const platform of Object.values(sd.platforms)) {
|
|
216
255
|
const buildPath = platform.buildPath ?? "";
|
|
217
256
|
for (const file of platform.files ?? []) if (file.destination) {
|
|
218
257
|
const absoluteBuildPath = path.isAbsolute(buildPath) ? buildPath : path.resolve(root, buildPath);
|
|
@@ -226,7 +265,7 @@ const unpluginFactory = (options = {}) => {
|
|
|
226
265
|
const duration = Date.now() - startTime;
|
|
227
266
|
if (context) log(`Rebuilt design tokens due to change in ${context} (${duration}ms)`, "success");
|
|
228
267
|
else {
|
|
229
|
-
if (!
|
|
268
|
+
if (!quiet && generatedFiles.size > 0) {
|
|
230
269
|
const fileInfos = [];
|
|
231
270
|
for (const filePath of generatedFiles) if (fs.existsSync(filePath)) {
|
|
232
271
|
const displayPath = path.relative(root, filePath).replace(/\\/g, "/");
|
|
@@ -260,14 +299,62 @@ const unpluginFactory = (options = {}) => {
|
|
|
260
299
|
} catch (err) {
|
|
261
300
|
const duration = Date.now() - startTime;
|
|
262
301
|
log(`Compilation failed after ${duration}ms: ${errorMessage(err)}`, "error");
|
|
302
|
+
if (failsTheBuild(context)) throw err;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const REBUILD_DEBOUNCE_MS = 50;
|
|
306
|
+
let debounceTimer;
|
|
307
|
+
let pendingReason;
|
|
308
|
+
let inFlight;
|
|
309
|
+
let waiting = [];
|
|
310
|
+
let refreshServerWatchList;
|
|
311
|
+
const drain = async () => {
|
|
312
|
+
while (pendingReason !== void 0) {
|
|
313
|
+
const reason = pendingReason;
|
|
314
|
+
pendingReason = void 0;
|
|
315
|
+
const resolvers = waiting;
|
|
316
|
+
waiting = [];
|
|
317
|
+
let failure;
|
|
318
|
+
let compiling = false;
|
|
319
|
+
try {
|
|
320
|
+
const resolved = await resolveConfigs();
|
|
321
|
+
if (resolved.length > 0) {
|
|
322
|
+
compiling = true;
|
|
323
|
+
await runBuilds(resolved, reason);
|
|
324
|
+
compiling = false;
|
|
325
|
+
hasCompiled = true;
|
|
326
|
+
await refreshServerWatchList?.(resolved);
|
|
327
|
+
}
|
|
328
|
+
} catch (err) {
|
|
329
|
+
failure = { error: err };
|
|
330
|
+
if (!compiling) log(`Rebuild failed: ${errorMessage(err)}`, "error");
|
|
331
|
+
}
|
|
332
|
+
for (const settle of resolvers) settle(failure);
|
|
263
333
|
}
|
|
264
334
|
};
|
|
335
|
+
const schedule = async (reason) => {
|
|
336
|
+
pendingReason = reason;
|
|
337
|
+
const covered = new Promise((resolve, reject) => {
|
|
338
|
+
waiting.push((failure) => {
|
|
339
|
+
if (failure) reject(asError(failure.error));
|
|
340
|
+
else resolve();
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
344
|
+
debounceTimer = setTimeout(() => {
|
|
345
|
+
debounceTimer = void 0;
|
|
346
|
+
inFlight = (inFlight ?? Promise.resolve()).then(drain);
|
|
347
|
+
}, REBUILD_DEBOUNCE_MS);
|
|
348
|
+
debounceTimer.unref();
|
|
349
|
+
return covered;
|
|
350
|
+
};
|
|
265
351
|
return {
|
|
266
352
|
async buildStart() {
|
|
267
353
|
const resolved = await resolveConfigs();
|
|
268
354
|
if (resolved.length === 0) return;
|
|
269
355
|
const { paths } = await getWatchTargets(resolved);
|
|
270
356
|
for (const file of paths) this.addWatchFile(file);
|
|
357
|
+
if (isWebpack) return;
|
|
271
358
|
if (watchRebuild && hasCompiled) {
|
|
272
359
|
watchRebuild = false;
|
|
273
360
|
return;
|
|
@@ -278,37 +365,41 @@ const unpluginFactory = (options = {}) => {
|
|
|
278
365
|
name: "unplugin-style-dictionary",
|
|
279
366
|
vite: {
|
|
280
367
|
configResolved(config) {
|
|
281
|
-
root = config.root || process.cwd();
|
|
368
|
+
if (rootOption === void 0) root = config.root || process.cwd();
|
|
282
369
|
},
|
|
283
370
|
async configureServer(server) {
|
|
284
371
|
const resolved = await resolveConfigs();
|
|
285
372
|
if (resolved.length === 0) return;
|
|
286
373
|
let targets = await getWatchTargets(resolved);
|
|
287
374
|
server.watcher.add(targets.paths);
|
|
375
|
+
refreshServerWatchList = async (rebuilt) => {
|
|
376
|
+
targets = await getWatchTargets(rebuilt);
|
|
377
|
+
server.watcher.add(targets.paths);
|
|
378
|
+
};
|
|
288
379
|
server.watcher.on("all", (_event, file) => {
|
|
289
380
|
if (!isWatchedSource(file, targets.patterns)) return;
|
|
290
|
-
(
|
|
291
|
-
try {
|
|
292
|
-
const currentResolved = await resolveConfigs();
|
|
293
|
-
await runBuilds(currentResolved, path.basename(file));
|
|
294
|
-
targets = await getWatchTargets(currentResolved);
|
|
295
|
-
server.watcher.add(targets.paths);
|
|
296
|
-
} catch (err) {
|
|
297
|
-
log(`Rebuild failed: ${errorMessage(err)}`, "error");
|
|
298
|
-
}
|
|
299
|
-
})();
|
|
381
|
+
schedule(path.basename(file)).catch(() => {});
|
|
300
382
|
});
|
|
301
383
|
}
|
|
302
384
|
},
|
|
303
385
|
async watchChange(id) {
|
|
304
386
|
watchRebuild = true;
|
|
387
|
+
if (cachedPatterns && !isWatchedSource(id, cachedPatterns)) return;
|
|
305
388
|
const resolved = await resolveConfigs();
|
|
306
389
|
if (resolved.length === 0) return;
|
|
307
390
|
const { patterns } = await getWatchTargets(resolved);
|
|
308
391
|
if (!isWatchedSource(id, patterns)) return;
|
|
309
|
-
await
|
|
310
|
-
hasCompiled = true;
|
|
392
|
+
if (!isWebpack) await schedule(path.basename(id));
|
|
311
393
|
for (const file of await expandPatterns(patterns)) this.addWatchFile(file);
|
|
394
|
+
},
|
|
395
|
+
webpack(compiler) {
|
|
396
|
+
if (rootOption === void 0) root = compiler.options.context ?? process.cwd();
|
|
397
|
+
compiler.hooks.beforeCompile.tapPromise("unplugin-style-dictionary", async () => {
|
|
398
|
+
const resolved = await resolveConfigs();
|
|
399
|
+
if (resolved.length === 0) return;
|
|
400
|
+
await runBuilds(resolved);
|
|
401
|
+
hasCompiled = true;
|
|
402
|
+
});
|
|
312
403
|
}
|
|
313
404
|
};
|
|
314
405
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Config } from 'style-dictionary'\nimport type { UnpluginFactory } from 'unplugin'\nimport type { ViteDevServer } from 'vite'\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport zlib from 'node:zlib'\nimport picomatch from 'picomatch'\nimport StyleDictionary from 'style-dictionary'\nimport { glob } from 'tinyglobby'\nimport { createUnplugin } from 'unplugin'\n\nimport type { UnpluginStyleDictionaryOptions } from './types.js'\n\nexport type * from './types.js'\n\n// Whether `file` matches one of the resolved config/token watch patterns.\n// Shared by the Vite-specific `configureServer` watcher and the universal\n// `watchChange` hook — both need it, and both must skip files that don't\n// match: without this filter, `watchChange` reacts to *any* changed\n// module-graph file, including this plugin's own generated output (since\n// consuming code imports it). Every regenerate is itself a \"change\", which\n// without filtering re-triggers a rebuild forever.\n//\n// Matching a pattern is only half of that, and this function is only the half\n// it can answer. A `buildPath` inside a `source` directory is a supported\n// layout, and under any correct matcher its output matches the very glob that\n// produced it — so the caller also subtracts what the last build wrote. See\n// `generatedDestinations` and `isWatchedSource` in the factory below.\n//\n// The patterns are Style Dictionary's own `source` and `include` globs, so the\n// filter has to admit exactly what the build reads — which is why the matching\n// is a real globber's rather than hand-rolled. The version this replaces was\n// wrong in both directions at once: it stripped `/**` out of a pattern and\n// prefix-matched the remainder, so `tokens/**/*.json` matched nothing sitting\n// directly in `tokens/` and `tokens/**` matched a `tokens-backup/` sibling,\n// while its regex branch mapped every `*` to `.*` — crossing `/` — and tested\n// it unanchored, so generated output under a watched directory matched its own\n// source glob and rebuilt forever.\n//\n// picomatch rather than `path.matchesGlob`, which would need no dependency at\n// all: that function is documented experimental, and on Node 20 — the floor\n// `engines` declares — it prints `ExperimentalWarning: glob is an experimental\n// feature and might change at any time` into the consumer's build output. The\n// dependency is free in practice, since `unplugin` depends on the same\n// picomatch and is already installed wherever this plugin is. Its `dot: false`\n// default is deliberate: it is what glob, and so Style Dictionary, reads\n// sources with, so a dotfile is invisible to the filter and to the build alike.\nexport function matchesWatchedFile(file: string, patterns: string[]): boolean {\n const normalizedFile = file.replace(/\\\\/g, '/')\n\n return patterns.some((pattern) => {\n const normalizedPattern = pattern.replace(/\\\\/g, '/')\n\n // A config file reaches this function as its own literal path, which is\n // both the common case and the one shape that is not a glob at all.\n return (\n normalizedPattern === normalizedFile ||\n picomatch.isMatch(normalizedFile, normalizedPattern)\n )\n })\n}\n\n// Best-effort cleanup of a temporary file whose write or rename failed. The\n// original failure is what the caller reports, so nothing here may throw.\nfunction discardTemporaryFile(temporary: string): void {\n try {\n fs.rmSync(temporary, { force: true })\n } catch {\n // Ignore: a leftover temporary file is not worth masking the real error.\n }\n}\n\n// `catch` binds `unknown`, and a thrown non-Error — a string, a rejected\n// value out of a config module — carries no `.message`. The `as Error` casts\n// this replaces claimed otherwise and printed `undefined` for exactly those\n// cases, which is the least useful thing a failure log can say.\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\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// 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// Style Dictionary writes every generated file with a plain `writeFile` on the\n// volume it was handed, which truncates the destination and then streams the\n// new contents into it. Anything reading that file inside the window sees a\n// partial file: a consuming test run whose tokens are rebuilt mid-suite, or a\n// dev-server request landing on a rebuild, gets a truncated module and fails\n// to parse it. Writing a sibling temporary file and renaming it over the\n// destination closes the window — `rename` is atomic within a filesystem, so a\n// concurrent reader sees either the whole old file or the whole new one.\n\n// Temporary path for an atomic write of `destination`.\n//\n// It has to be a sibling of the destination, because `rename` is only atomic\n// within one filesystem and the system temp directory is often a different\n// mount. The final extension is dropped rather than kept, so the temporary\n// file cannot match a pattern written for the generated file's own extension.\n// That was load-bearing while `matchesWatchedFile` tested its globs\n// unanchored, where a leftover `vars.css.tmp` matched a `*.css` watch; it is\n// belt-and-braces now that the matcher anchors and, like the globber Style\n// Dictionary reads sources with, does not match the leading dot this name\n// already starts with. Both stay, because a temporary file only outlives its\n// rename when a write failed, and hiding one costs a string. The pid and\n// counter make the name unique, so two writes of the same destination —\n// parallel platforms in one build, or two builds overlapping — never share a\n// temporary file.\nlet temporaryFileCounter = 0\n\n// Whether the freshly rendered `temporary` holds exactly what `destination`\n// already holds. A rebuild whose inputs did not change renders byte-identical\n// output, and renaming that over the destination is a filesystem event the\n// host bundler reacts to — which is the whole of the rebuild loop, since\n// consuming code imports the generated file and every regenerate is therefore\n// a module-graph change. Comparing the two files rather than the `data`\n// argument keeps this indifferent to whether the caller passed a string, a\n// buffer or a stream, and to the encoding it passed with it.\n//\n// A destination that cannot be read is not identical, which covers the\n// ordinary case of it not existing yet.\nasync function rendersWhatIsAlreadyThere(\n temporary: string,\n destination: string,\n): Promise<boolean> {\n try {\n const [existing, rendered] = await Promise.all([\n fs.promises.readFile(destination),\n fs.promises.readFile(temporary),\n ])\n\n return existing.equals(rendered)\n } catch {\n return false\n }\n}\n\nfunction rendersWhatIsAlreadyThereSync(\n temporary: string,\n destination: string,\n): boolean {\n try {\n return fs.readFileSync(destination).equals(fs.readFileSync(temporary))\n } catch {\n return false\n }\n}\n\nfunction temporaryPathFor(destination: string): string {\n const extension = path.extname(destination)\n\n return path.join(\n path.dirname(destination),\n `.${path.basename(destination, extension)}.${process.pid}.${temporaryFileCounter++}.tmp`,\n )\n}\n\nconst writeFileAtomic: typeof fs.promises.writeFile = async (\n file,\n data,\n options,\n) => {\n // A file handle or descriptor is already-open state that a rename cannot\n // stand in for, so only a path is written atomically.\n if (typeof file !== 'string') {\n return fs.promises.writeFile(file, data, options)\n }\n\n const temporary = temporaryPathFor(file)\n\n try {\n await fs.promises.writeFile(temporary, data, options)\n\n // The check sits in front of the rename rather than in place of it: the\n // temporary file is still written, so a destination that does need\n // replacing is still replaced in one atomic step and a concurrent reader\n // still never sees a partial file.\n if (await rendersWhatIsAlreadyThere(temporary, file)) {\n discardTemporaryFile(temporary)\n return\n }\n\n await fs.promises.rename(temporary, file)\n } catch (err) {\n discardTemporaryFile(temporary)\n throw err\n }\n}\n\nconst writeFileSyncAtomic: typeof fs.writeFileSync = (file, data, options) => {\n if (typeof file !== 'string') {\n fs.writeFileSync(file, data, options)\n return\n }\n\n const temporary = temporaryPathFor(file)\n\n try {\n fs.writeFileSync(temporary, data, options)\n\n if (rendersWhatIsAlreadyThereSync(temporary, file)) {\n discardTemporaryFile(temporary)\n return\n }\n\n fs.renameSync(temporary, file)\n } catch (err) {\n discardTemporaryFile(temporary)\n throw err\n }\n}\n\n// `node:fs` with both write entry points swapped for their atomic\n// equivalents, handed to Style Dictionary as the volume it builds through.\n// Everything else — reads, `mkdir`, `access`, the `promises` namespace — is\n// inherited from `node:fs` unchanged, so only the moment a file becomes\n// visible to readers changes. Custom actions receive this volume too, so\n// whatever they emit is written the same way.\n//\n// It is assigned onto the instance rather than passed as the `volume`\n// constructor option on purpose: that option marks the volume as a custom\n// filesystem shim, which switches Style Dictionary's path resolution off for\n// every read as well.\n// `Object.create` is declared as returning `any`, so pinning the result to\n// `typeof fs` is a claim no type guard can replace. The prototype link is the\n// whole point — see the note above — so rebuilding this with a spread, which\n// copies own properties and drops the chain, is not a substitute.\n/* oxlint-disable typescript/no-unsafe-type-assertion */\nconst atomicVolume = Object.create(fs, {\n promises: {\n value: Object.create(fs.promises, {\n writeFile: { value: writeFileAtomic },\n }) as typeof fs.promises,\n },\n writeFileSync: { value: writeFileSyncAtomic },\n}) as typeof fs\n/* oxlint-enable typescript/no-unsafe-type-assertion */\n\n// A pattern is a glob when any of these appear in it. Deliberately the set\n// picomatch and tinyglobby act on, since those two are what match and expand\n// here — a path containing one of these characters literally is not\n// distinguishable from a pattern, and would not be matchable either.\nconst GLOB_CHARACTERS = /[!*?[\\]{}]/\n\n// The leading run of a pattern that contains no glob character —\n// `/p/tokens` for `/p/tokens/**/*.json`. Registering it alongside the files\n// that match today is what makes a token file created tomorrow visible:\n// watching only the current matches can never see a path that did not exist\n// when the watcher was built.\nfunction staticParentOf(pattern: string): string {\n const segments = pattern.split('/')\n const firstGlob = segments.findIndex((segment) =>\n GLOB_CHARACTERS.test(segment),\n )\n\n return firstGlob === -1\n ? path.posix.dirname(pattern)\n : segments.slice(0, firstGlob).join('/')\n}\n\nexport const unpluginFactory: UnpluginFactory<\n undefined | UnpluginStyleDictionaryOptions,\n false\n> = (options = {}) => {\n const { silent = false } = options\n let root = process.cwd()\n\n // Every absolute destination the last completed build wrote, spelled with\n // forward slashes so it compares against a normalised watcher path. This is\n // the half of the rebuild-loop guard that pattern matching cannot supply:\n // output written under a watched directory matches the source glob that\n // produced it, so without subtracting this set a supported layout rebuilds\n // on its own writes for as long as the dev server runs.\n const generatedDestinations = new Set<string>()\n\n // Whether `watchChange` has fired since the last `buildStart`, and whether\n // anything has been compiled yet. Rollup, rolldown and webpack all run\n // `watchChange` for every changed file and only then re-enter `buildStart`\n // — unplugin's webpack adapter awaits both in one `make` tap — so a flag\n // raised in the first is still standing in the second, and is what tells it\n // this is a watch rebuild rather than the first build of the process.\n let watchRebuild = false\n let hasCompiled = false\n\n // What a watcher is handed, and what a changed path is tested against, are\n // not the same list, and conflating them is why a glob source was watched by\n // nothing at all. Every watcher in play takes filenames rather than\n // patterns: Vite's chokidar and rollup's `FileWatcher` are both constructed\n // with `disableGlobbing: true`, Vite's `addWatchFile` drops anything that\n // fails `fs.existsSync`, and webpack never globs `fileDependencies`. So the\n // patterns stay for matching and the paths are expanded for registering.\n const expandPatterns = async (patterns: string[]): Promise<string[]> => {\n const paths = new Set<string>()\n const globs: string[] = []\n\n for (const pattern of patterns) {\n if (GLOB_CHARACTERS.test(pattern)) {\n globs.push(pattern)\n\n // Watching the directory as well as its current contents. chokidar\n // reports a creation inside a watched directory, which is the only\n // way a token file added later is ever noticed.\n const parent = staticParentOf(pattern)\n if (parent && fs.existsSync(parent)) paths.add(parent)\n } else {\n paths.add(pattern)\n }\n }\n\n if (globs.length > 0) {\n try {\n // tinyglobby matches with picomatch, which is what\n // `matchesWatchedFile` tests with, so what is registered here and what\n // is accepted there cannot disagree.\n for (const match of await glob(globs, { absolute: true })) {\n paths.add(match.replace(/\\\\/g, '/'))\n }\n } catch (err) {\n log(`Failed to expand watch patterns: ${errorMessage(err)}`, 'error')\n }\n }\n\n return Array.from(paths)\n }\n\n // Whether a changed file is a token or config source rather than something\n // this plugin just wrote. Both watch entry points ask through here, so\n // neither can react to its own output.\n const isWatchedSource = (file: string, patterns: string[]): boolean =>\n !generatedDestinations.has(file.replace(/\\\\/g, '/')) &&\n matchesWatchedFile(file, patterns)\n\n // Helper to log if not silent\n const log = (\n message: string,\n type: 'error' | 'info' | 'success' = 'info',\n ) => {\n if (silent) return\n const prefix = '[unplugin-style-dictionary]'\n if (type === 'error') {\n console.error(`\\x1b[31m${prefix} ${message}\\x1b[0m`)\n } else if (type === 'success') {\n console.log(`\\x1b[32m${prefix} ${message}\\x1b[0m`)\n } else {\n console.log(`\\x1b[36m${prefix} ${message}\\x1b[0m`)\n }\n }\n\n // Resolve config file paths / objects\n const resolveConfigs = async (): Promise<\n Array<{ config: Config | string; dir: string; file?: string }>\n > => {\n let rawConfig = options.config\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 for (const file of defaults) {\n const fullPath = path.resolve(root, file)\n if (fs.existsSync(fullPath)) {\n rawConfig = file\n break\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()\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 {\n config: fullPath,\n dir: path.dirname(fullPath),\n file: fullPath,\n }\n } else {\n return {\n config: conf,\n dir: root,\n }\n }\n })\n }\n\n // Parse token files to watch\n const getWatchTargets = async (\n resolvedConfigs: Array<{\n config: Config | string\n dir: string\n file?: string\n }>,\n ): Promise<{ paths: string[]; patterns: string[] }> => {\n const filesToWatch = new Set<string>()\n\n for (const item of resolvedConfigs) {\n if (item.file) {\n filesToWatch.add(item.file.replace(/\\\\/g, '/'))\n }\n\n let configObj: Config | null = null\n\n if (typeof item.config === 'string') {\n try {\n let loaded: unknown\n\n if (item.config.endsWith('.json')) {\n loaded = JSON.parse(fs.readFileSync(item.config, 'utf-8'))\n } else {\n const fileUrl = pathToFileURL(item.config).href\n // Sequential on purpose: a config module runs arbitrary code at\n // import time — `registerFormat` and friends — and Style\n // Dictionary's registries are global, so importing several at\n // once would interleave those registrations.\n loaded = unwrapDefault(await import(`${fileUrl}?t=${Date.now()}`))\n }\n\n if (isConfig(loaded)) {\n configObj = loaded\n } else {\n log(\n `Config file did not resolve to a configuration object: ${item.config}`,\n 'error',\n )\n }\n } catch (err) {\n log(\n `Failed to parse config file: ${item.config}. Error: ${errorMessage(err)}`,\n 'error',\n )\n }\n } else {\n configObj = item.config\n }\n\n if (configObj) {\n const addPattern = (pattern: unknown) => {\n if (typeof pattern === 'string') {\n const absolutePattern = path.isAbsolute(pattern)\n ? pattern\n : path.resolve(item.dir, pattern)\n const normalized = absolutePattern.replace(/\\\\/g, '/')\n filesToWatch.add(normalized)\n }\n }\n\n if (configObj.source) {\n if (Array.isArray(configObj.source)) {\n configObj.source.forEach(addPattern)\n } else {\n addPattern(configObj.source)\n }\n }\n\n if (configObj.include) {\n if (Array.isArray(configObj.include)) {\n configObj.include.forEach(addPattern)\n } else {\n addPattern(configObj.include)\n }\n }\n }\n }\n\n // Add manually configured watch files\n if (options.watch) {\n const extraWatches = Array.isArray(options.watch)\n ? options.watch\n : [options.watch]\n for (const pattern of extraWatches) {\n const absolutePattern = path.isAbsolute(pattern)\n ? pattern\n : path.resolve(root, pattern)\n filesToWatch.add(absolutePattern.replace(/\\\\/g, '/'))\n }\n }\n\n const patterns = Array.from(filesToWatch)\n\n return { paths: await expandPatterns(patterns), patterns }\n }\n\n // Compile design tokens\n const runBuilds = async (\n resolvedConfigs: Array<{ config: Config | string; dir: string }>,\n context?: string,\n ) => {\n const startTime = Date.now()\n try {\n if (!context) {\n log('Compiling design tokens...', 'info')\n }\n\n const generatedFiles = new Set<string>()\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 item of resolvedConfigs) {\n const sd = new StyleDictionary(item.config)\n await sd.hasInitialized\n const silentSD = await sd.extend({\n log: {\n verbosity: 'silent',\n },\n })\n // Swap in the atomic volume only now that the instance has finished\n // reading its configs and token sources, so every write below lands\n // through `rename` while the read path stays exactly as it was.\n silentSD.volume = atomicVolume\n await silentSD.buildAllPlatforms()\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 for (const platform of Object.values(silentSD.platforms)) {\n const buildPath = platform.buildPath ?? ''\n for (const file of platform.files ?? []) {\n if (file.destination) {\n const absoluteBuildPath = path.isAbsolute(buildPath)\n ? buildPath\n : path.resolve(root, buildPath)\n const absoluteDestination = path.isAbsolute(file.destination)\n ? file.destination\n : path.resolve(absoluteBuildPath, file.destination)\n generatedFiles.add(absoluteDestination)\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 generatedDestinations.clear()\n for (const destination of generatedFiles) {\n generatedDestinations.add(destination.replace(/\\\\/g, '/'))\n }\n\n const duration = Date.now() - startTime\n\n if (context) {\n log(\n `Rebuilt design tokens due to change in ${context} (${duration}ms)`,\n 'success',\n )\n } else {\n if (!silent && generatedFiles.size > 0) {\n const fileInfos: Array<{\n coloredPath: string\n gzipSizeStr: string\n relativeDisplayPath: string\n sizeStr: string\n }> = []\n\n for (const filePath of generatedFiles) {\n if (fs.existsSync(filePath)) {\n const displayPath = path\n .relative(root, filePath)\n .replace(/\\\\/g, '/')\n const dir = path.dirname(displayPath)\n const base = path.basename(displayPath)\n const coloredPath =\n dir === '.'\n ? `\\x1b[32m${base}\\x1b[0m`\n : `\\x1b[90m${dir}/\\x1b[0m\\x1b[32m${base}\\x1b[0m`\n\n try {\n const stats = fs.statSync(filePath)\n const bytes = stats.size\n const sizeStr = `${(bytes / 1024).toFixed(2)} kB`\n\n const content = fs.readFileSync(filePath)\n const gzipBytes = zlib.gzipSync(content).length\n const gzipSizeStr = `${(gzipBytes / 1024).toFixed(2)} kB`\n\n fileInfos.push({\n coloredPath,\n gzipSizeStr,\n relativeDisplayPath: displayPath,\n sizeStr,\n })\n } catch {\n // Ignore errors reading individual files\n }\n }\n }\n\n if (fileInfos.length > 0) {\n const longestPathLength = Math.max(\n ...fileInfos.map((f) => f.relativeDisplayPath.length),\n 0,\n )\n const longestSizeLength = Math.max(\n ...fileInfos.map((f) => f.sizeStr.length),\n 0,\n )\n\n for (const info of fileInfos) {\n const pathPadding = ' '.repeat(\n Math.max(\n 2,\n longestPathLength - info.relativeDisplayPath.length + 2,\n ),\n )\n const sizePadded = info.sizeStr.padStart(longestSizeLength)\n console.log(\n `${info.coloredPath}${pathPadding}\\x1b[90m${sizePadded} │ gzip: ${info.gzipSizeStr}\\x1b[0m`,\n )\n }\n }\n }\n\n log(`Compiled successfully! (${duration}ms)`, 'success')\n }\n } catch (err) {\n const duration = Date.now() - startTime\n log(\n `Compilation failed after ${duration}ms: ${errorMessage(err)}`,\n 'error',\n )\n }\n }\n\n return {\n async buildStart() {\n const resolved = await resolveConfigs()\n if (resolved.length === 0) return\n\n // Register token/config files with the host bundler's watch mode.\n // Works out of the box wherever the host runs a persistent watcher\n // (e.g. `rollup --watch`); Vite's dev server is additionally handled\n // below via the `vite.configureServer` escape hatch, since token\n // sources are plain JSON/JS files outside the module graph and\n // `watchChange` is not reliably invoked by Vite while serving.\n const { paths } = await getWatchTargets(resolved)\n for (const file of paths) {\n this.addWatchFile(file)\n }\n\n // Every watch rebuild re-enters this hook, and compiling here as well as\n // in `watchChange` is what closed the loop: consuming code imports the\n // generated file, so writing it is itself a module-graph change, which\n // re-enters `buildStart`, which writes it again. `watchChange` has\n // already run for every file in this cycle and rebuilt if any of them\n // was a source, so the only thing left for a re-entry to do is the\n // re-registration above.\n //\n // `hasCompiled` is the floor under that: a host that fires\n // `watchChange` without ever re-entering here would otherwise leave the\n // flag standing, and no first compile of a process may ever be skipped —\n // the tokens have to exist before the build that consumes them.\n if (watchRebuild && hasCompiled) {\n watchRebuild = false\n return\n }\n\n await runBuilds(resolved)\n hasCompiled = true\n },\n\n name: 'unplugin-style-dictionary',\n\n vite: {\n configResolved(config) {\n root = config.root || process.cwd()\n },\n\n async configureServer(server: ViteDevServer) {\n const resolved = await resolveConfigs()\n if (resolved.length === 0) return\n\n // Reassigned after every rebuild below, so a configuration that gains\n // a source is matched against its new patterns rather than the ones\n // read at start-up.\n let targets = await getWatchTargets(resolved)\n\n // Watch configuration files and token files\n server.watcher.add(targets.paths)\n\n // chokidar types its listener as returning void and does not await\n // what it is handed, so handing it an async function left every\n // rejection floating — `runBuilds` catches its own, but\n // `resolveConfigs` and `getWatchTargets` do not. Launching the work\n // explicitly and catching here is what keeps a bad config on disk\n // from surfacing as an unhandled rejection that kills the dev server.\n server.watcher.on('all', (_event, file) => {\n if (!isWatchedSource(file, targets.patterns)) return\n\n void (async () => {\n try {\n // Re-resolve configs to handle added/removed configs or changes\n // to config itself\n const currentResolved = await resolveConfigs()\n await runBuilds(currentResolved, path.basename(file))\n\n // Dynamically update the watch list in case the configurations\n // changed\n targets = await getWatchTargets(currentResolved)\n server.watcher.add(targets.paths)\n } catch (err) {\n log(`Rebuild failed: ${errorMessage(err)}`, 'error')\n }\n })()\n })\n },\n },\n\n // Rollup types `watchChange` as returning void, yet awaits it as a\n // sequential hook — and the work here is inherently asynchronous. The\n // signature is the thing that is wrong, so the rule is silenced rather\n // than the hook made to lie about finishing.\n // oxlint-disable-next-line typescript/no-misused-promises\n async watchChange(id) {\n // Raised before any decision about `id`, because whatever this change\n // was, the host is now on its way back into `buildStart`.\n watchRebuild = true\n\n const resolved = await resolveConfigs()\n if (resolved.length === 0) return\n\n const { patterns } = await getWatchTargets(resolved)\n // Without this check, watchChange fires for *any* changed file in the\n // host bundler's module graph — including our own generated output,\n // since consuming code imports it. Every regenerate is itself a\n // \"change\", so skipping what is not a source here is what keeps this\n // from rebuilding forever — both the files that match no pattern and\n // the ones that match only because this plugin wrote them.\n if (!isWatchedSource(id, patterns)) return\n\n await runBuilds(resolved, path.basename(id))\n hasCompiled = true\n\n // Expanded again after the build rather than reusing the list from\n // before it, so a token file the build itself produced is registered.\n for (const file of await expandPatterns(patterns)) {\n this.addWatchFile(file)\n }\n },\n }\n}\n\nexport const unplugin = /* #__PURE__ */ createUnplugin(unpluginFactory)\n\nexport default unplugin\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Config } from 'style-dictionary'\nimport type { UnpluginFactory } from 'unplugin'\nimport type { ViteDevServer } from 'vite'\n\nimport JSON5 from 'json5'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport zlib from 'node:zlib'\nimport picomatch from 'picomatch'\nimport StyleDictionary from 'style-dictionary'\nimport { glob } from 'tinyglobby'\nimport { createUnplugin } from 'unplugin'\n\nimport type { UnpluginStyleDictionaryOptions } from './types.js'\n\nexport type * from './types.js'\n\n// Whether `file` matches one of the resolved config/token watch patterns.\n// Shared by the Vite-specific `configureServer` watcher and the universal\n// `watchChange` hook — both need it, and both must skip files that don't\n// match: without this filter, `watchChange` reacts to *any* changed\n// module-graph file, including this plugin's own generated output (since\n// consuming code imports it). Every regenerate is itself a \"change\", which\n// without filtering re-triggers a rebuild forever.\n//\n// Matching a pattern is only half of that, and this function is only the half\n// it can answer. A `buildPath` inside a `source` directory is a supported\n// layout, and under any correct matcher its output matches the very glob that\n// produced it — so the caller also subtracts what the last build wrote. See\n// `generatedDestinations` and `isWatchedSource` in the factory below.\n//\n// The patterns are Style Dictionary's own `source` and `include` globs, so the\n// filter has to admit exactly what the build reads — which is why the matching\n// is a real globber's rather than hand-rolled. The version this replaces was\n// wrong in both directions at once: it stripped `/**` out of a pattern and\n// prefix-matched the remainder, so `tokens/**/*.json` matched nothing sitting\n// directly in `tokens/` and `tokens/**` matched a `tokens-backup/` sibling,\n// while its regex branch mapped every `*` to `.*` — crossing `/` — and tested\n// it unanchored, so generated output under a watched directory matched its own\n// source glob and rebuilt forever.\n//\n// picomatch rather than `path.matchesGlob`, which would need no dependency at\n// all: that function is documented experimental, and on Node 20 — the floor\n// `engines` declares — it prints `ExperimentalWarning: glob is an experimental\n// feature and might change at any time` into the consumer's build output. The\n// dependency is free in practice, since `unplugin` depends on the same\n// picomatch and is already installed wherever this plugin is. Its `dot: false`\n// default is deliberate: it is what glob, and so Style Dictionary, reads\n// sources with, so a dotfile is invisible to the filter and to the build alike.\nexport function matchesWatchedFile(file: string, patterns: string[]): boolean {\n const normalizedFile = file.replace(/\\\\/g, '/')\n\n return patterns.some((pattern) => {\n const normalizedPattern = pattern.replace(/\\\\/g, '/')\n\n // A config file reaches this function as its own literal path, which is\n // both the common case and the one shape that is not a glob at all.\n return (\n normalizedPattern === normalizedFile ||\n picomatch.isMatch(normalizedFile, normalizedPattern)\n )\n })\n}\n\n// `catch` binds `unknown`, and a thrown non-Error — a string, a rejected\n// value out of a config module — carries no `.message`. The `as Error` casts\n// this replaces claimed otherwise and printed `undefined` for exactly those\n// cases, which is the least useful thing a failure log can say.\n// A rejected promise must carry an Error, and `catch` binds `unknown`. What\n// Style Dictionary throws is already one; anything else is wrapped rather than\n// handed on raw.\nfunction asError(error: unknown): Error {\n return error instanceof Error ? error : new Error(errorMessage(error))\n}\n\n// Best-effort cleanup of a temporary file whose write or rename failed. The\n// original failure is what the caller reports, so nothing here may throw.\nfunction discardTemporaryFile(temporary: string): void {\n try {\n fs.rmSync(temporary, { force: true })\n } catch {\n // Ignore: a leftover temporary file is not worth masking the real error.\n }\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\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// 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// Style Dictionary writes every generated file with a plain `writeFile` on the\n// volume it was handed, which truncates the destination and then streams the\n// new contents into it. Anything reading that file inside the window sees a\n// partial file: a consuming test run whose tokens are rebuilt mid-suite, or a\n// dev-server request landing on a rebuild, gets a truncated module and fails\n// to parse it. Writing a sibling temporary file and renaming it over the\n// destination closes the window — `rename` is atomic within a filesystem, so a\n// concurrent reader sees either the whole old file or the whole new one.\n\n// Temporary path for an atomic write of `destination`.\n//\n// It has to be a sibling of the destination, because `rename` is only atomic\n// within one filesystem and the system temp directory is often a different\n// mount. The final extension is dropped rather than kept, so the temporary\n// file cannot match a pattern written for the generated file's own extension.\n// That was load-bearing while `matchesWatchedFile` tested its globs\n// unanchored, where a leftover `vars.css.tmp` matched a `*.css` watch; it is\n// belt-and-braces now that the matcher anchors and, like the globber Style\n// Dictionary reads sources with, does not match the leading dot this name\n// already starts with. Both stay, because a temporary file only outlives its\n// rename when a write failed, and hiding one costs a string. The pid and\n// counter make the name unique, so two writes of the same destination —\n// parallel platforms in one build, or two builds overlapping — never share a\n// temporary file.\nlet temporaryFileCounter = 0\n\n// Whether the freshly rendered `temporary` holds exactly what `destination`\n// already holds. A rebuild whose inputs did not change renders byte-identical\n// output, and renaming that over the destination is a filesystem event the\n// host bundler reacts to — which is the whole of the rebuild loop, since\n// consuming code imports the generated file and every regenerate is therefore\n// a module-graph change. Comparing the two files rather than the `data`\n// argument keeps this indifferent to whether the caller passed a string, a\n// buffer or a stream, and to the encoding it passed with it.\n//\n// A destination that cannot be read is not identical, which covers the\n// ordinary case of it not existing yet.\nasync function rendersWhatIsAlreadyThere(\n temporary: string,\n destination: string,\n): Promise<boolean> {\n try {\n const [existing, rendered] = await Promise.all([\n fs.promises.readFile(destination),\n fs.promises.readFile(temporary),\n ])\n\n return existing.equals(rendered)\n } catch {\n return false\n }\n}\n\nfunction rendersWhatIsAlreadyThereSync(\n temporary: string,\n destination: string,\n): boolean {\n try {\n return fs.readFileSync(destination).equals(fs.readFileSync(temporary))\n } catch {\n return false\n }\n}\n\nfunction temporaryPathFor(destination: string): string {\n const extension = path.extname(destination)\n\n return path.join(\n path.dirname(destination),\n `.${path.basename(destination, extension)}.${process.pid}.${temporaryFileCounter++}.tmp`,\n )\n}\n\nconst writeFileAtomic: typeof fs.promises.writeFile = async (\n file,\n data,\n options,\n) => {\n // A file handle or descriptor is already-open state that a rename cannot\n // stand in for, so only a path is written atomically.\n if (typeof file !== 'string') {\n return fs.promises.writeFile(file, data, options)\n }\n\n const temporary = temporaryPathFor(file)\n\n try {\n await fs.promises.writeFile(temporary, data, options)\n\n // The check sits in front of the rename rather than in place of it: the\n // temporary file is still written, so a destination that does need\n // replacing is still replaced in one atomic step and a concurrent reader\n // still never sees a partial file.\n if (await rendersWhatIsAlreadyThere(temporary, file)) {\n discardTemporaryFile(temporary)\n return\n }\n\n await fs.promises.rename(temporary, file)\n } catch (err) {\n discardTemporaryFile(temporary)\n throw err\n }\n}\n\nconst writeFileSyncAtomic: typeof fs.writeFileSync = (file, data, options) => {\n if (typeof file !== 'string') {\n fs.writeFileSync(file, data, options)\n return\n }\n\n const temporary = temporaryPathFor(file)\n\n try {\n fs.writeFileSync(temporary, data, options)\n\n if (rendersWhatIsAlreadyThereSync(temporary, file)) {\n discardTemporaryFile(temporary)\n return\n }\n\n fs.renameSync(temporary, file)\n } catch (err) {\n discardTemporaryFile(temporary)\n throw err\n }\n}\n\n// `node:fs` with both write entry points swapped for their atomic\n// equivalents, handed to Style Dictionary as the volume it builds through.\n// Everything else — reads, `mkdir`, `access`, the `promises` namespace — is\n// inherited from `node:fs` unchanged, so only the moment a file becomes\n// visible to readers changes. Custom actions receive this volume too, so\n// whatever they emit is written the same way.\n//\n// It is assigned onto the instance rather than passed as the `volume`\n// constructor option on purpose: that option marks the volume as a custom\n// filesystem shim, which switches Style Dictionary's path resolution off for\n// every read as well.\n// `Object.create` is declared as returning `any`, so pinning the result to\n// `typeof fs` is a claim no type guard can replace. The prototype link is the\n// whole point — see the note above — so rebuilding this with a spread, which\n// copies own properties and drops the chain, is not a substitute.\n/* oxlint-disable typescript/no-unsafe-type-assertion */\nconst atomicVolume = Object.create(fs, {\n promises: {\n value: Object.create(fs.promises, {\n writeFile: { value: writeFileAtomic },\n }) as typeof fs.promises,\n },\n writeFileSync: { value: writeFileSyncAtomic },\n}) as typeof fs\n/* oxlint-enable typescript/no-unsafe-type-assertion */\n\n// A pattern is a glob when any of these appear in it. Deliberately the set\n// picomatch and tinyglobby act on, since those two are what match and expand\n// here — a path containing one of these characters literally is not\n// distinguishable from a pattern, and would not be matchable either.\nconst GLOB_CHARACTERS = /[!*?[\\]{}]/\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// A configuration as `resolveConfigs` 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.\ninterface ResolvedConfig {\n config: Config | string\n file?: string\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\n// The leading run of a pattern that contains no glob character —\n// `/p/tokens` for `/p/tokens/**/*.json`. Registering it alongside the files\n// that match today is what makes a token file created tomorrow visible:\n// watching only the current matches can never see a path that did not exist\n// when the watcher was built.\nfunction staticParentOf(pattern: string): string {\n const segments = pattern.split('/')\n const firstGlob = segments.findIndex((segment) =>\n GLOB_CHARACTERS.test(segment),\n )\n\n return firstGlob === -1\n ? path.posix.dirname(pattern)\n : segments.slice(0, firstGlob).join('/')\n}\n\nexport const unpluginFactory: UnpluginFactory<\n undefined | UnpluginStyleDictionaryOptions,\n false\n> = (options = {}, meta) => {\n // webpack is the one target whose `buildStart` does not run before the\n // module graph is resolved: unplugin taps it on `make`, an\n // `AsyncParallelHook` that `EntryPlugin` taps too. The `webpack` key below\n // compiles on `beforeCompile` instead, which webpack awaits before the\n // compilation exists.\n const isWebpack = meta.framework === 'webpack'\n const {\n failOnError = 'build',\n logLevel,\n root: rootOption,\n silent = false,\n } = options\n\n // `silent` predates `logLevel` and names its quietest level, so it is read\n // as one. `logLevel` wins when a consumer sets both.\n const level = logLevel ?? (silent ? 'silent' : undefined)\n\n // Whether the plugin keeps its own progress lines and size table to itself.\n // A failure is reported at every level, which is why this gate is not on the\n // error branch below.\n const quiet = level === 'silent' || level === 'warn'\n\n // What Style Dictionary is told, if anything. `undefined` is the point of\n // this: it leaves whatever the consumer's own `log.verbosity` asked for\n // standing, where the plugin used to overwrite it on every build. Style\n // Dictionary has three levels to this option's four, so `'warn'` and\n // `'info'` both map to its default — they differ in what the plugin itself\n // says, not in what Style Dictionary does.\n const verbosity =\n level === undefined\n ? undefined\n : level === 'verbose'\n ? 'verbose'\n : level === 'silent'\n ? 'silent'\n : 'default'\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.\n const failsTheBuild = (context: string | undefined): boolean =>\n failOnError === true ||\n (context === undefined ? failOnError === 'build' : failOnError === 'serve')\n // Where a relative `config` path is looked up. The host sets it below\n // unless the consumer named one, which is why an explicit option wins: a\n // layout the host cannot describe is exactly what it is for.\n let root = rootOption\n ? path.resolve(process.cwd(), rootOption)\n : process.cwd()\n\n // Every absolute destination the last completed build wrote, spelled with\n // forward slashes so it compares against a normalised watcher path. This is\n // the half of the rebuild-loop guard that pattern matching cannot supply:\n // output written under a watched directory matches the source glob that\n // produced it, so without subtracting this set a supported layout rebuilds\n // on its own writes for as long as the dev server runs.\n const generatedDestinations = new Set<string>()\n\n // The patterns the last `getWatchTargets` derived. `watchChange` tests a\n // changed path against these before it resolves anything, so a file the\n // plugin does not care about costs one glob match instead of a full config\n // resolution — which, when `config` is a function, is the consumer's own\n // code, and the place the README tells them to register custom formats.\n //\n // It is safe to filter on a list that may be one build out of date because\n // the list always contains the config files themselves: an edit that adds a\n // source matches as a config change, which re-resolves and re-derives. The\n // one thing it cannot see is a `config` function that starts returning\n // different sources with no file changing at all, and that was never\n // observable without a rebuild to observe it in.\n let cachedPatterns: string[] | undefined\n\n // Whether `watchChange` has fired since the last `buildStart`, and whether\n // anything has been compiled yet. Rollup, rolldown and webpack all run\n // `watchChange` for every changed file and only then re-enter `buildStart`\n // — unplugin's webpack adapter awaits both in one `make` tap — so a flag\n // raised in the first is still standing in the second, and is what tells it\n // this is a watch rebuild rather than the first build of the process.\n let watchRebuild = false\n let hasCompiled = false\n\n // What a watcher is handed, and what a changed path is tested against, are\n // not the same list, and conflating them is why a glob source was watched by\n // nothing at all. Every watcher in play takes filenames rather than\n // patterns: Vite's chokidar and rollup's `FileWatcher` are both constructed\n // with `disableGlobbing: true`, Vite's `addWatchFile` drops anything that\n // fails `fs.existsSync`, and webpack never globs `fileDependencies`. So the\n // patterns stay for matching and the paths are expanded for registering.\n const expandPatterns = async (patterns: string[]): Promise<string[]> => {\n const paths = new Set<string>()\n const globs: string[] = []\n\n for (const pattern of patterns) {\n if (GLOB_CHARACTERS.test(pattern)) {\n globs.push(pattern)\n\n // Watching the directory as well as its current contents. chokidar\n // reports a creation inside a watched directory, which is the only\n // way a token file added later is ever noticed.\n const parent = staticParentOf(pattern)\n if (parent && fs.existsSync(parent)) paths.add(parent)\n } else {\n paths.add(pattern)\n }\n }\n\n if (globs.length > 0) {\n try {\n // tinyglobby matches with picomatch, which is what\n // `matchesWatchedFile` tests with, so what is registered here and what\n // is accepted there cannot disagree.\n for (const match of await glob(globs, { absolute: true })) {\n paths.add(match.replace(/\\\\/g, '/'))\n }\n } catch (err) {\n log(`Failed to expand watch patterns: ${errorMessage(err)}`, 'error')\n }\n }\n\n return Array.from(paths)\n }\n\n // Whether a changed file is a token or config source rather than something\n // this plugin just wrote. Both watch entry points ask through here, so\n // neither can react to its own output.\n const isWatchedSource = (file: string, patterns: string[]): boolean =>\n !generatedDestinations.has(file.replace(/\\\\/g, '/')) &&\n matchesWatchedFile(file, patterns)\n\n // Helper to log at the configured level\n const log = (\n message: string,\n type: 'error' | 'info' | 'success' = 'info',\n ) => {\n const prefix = '[unplugin-style-dictionary]'\n\n // Ahead of the `silent` gate on purpose. `silent` is about the progress\n // lines and the size table; a compile that failed is not noise, and\n // hiding it left a broken token set shipping with nothing said at all.\n if (type === 'error') {\n console.error(`\\x1b[31m${prefix} ${message}\\x1b[0m`)\n return\n }\n\n if (quiet) return\n if (type === 'success') {\n console.log(`\\x1b[32m${prefix} ${message}\\x1b[0m`)\n } else {\n console.log(`\\x1b[36m${prefix} ${message}\\x1b[0m`)\n }\n }\n\n // Resolve config file paths / objects\n const resolveConfigs = async (): Promise<ResolvedConfig[]> => {\n let rawConfig = options.config\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 for (const file of defaults) {\n const fullPath = path.resolve(root, file)\n if (fs.existsSync(fullPath)) {\n rawConfig = file\n break\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()\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 const importConfigModule = async (file: string): Promise<unknown> => {\n let version: number\n try {\n version = fs.statSync(file).mtimeMs\n } catch {\n // A config that cannot be stat'd is about to fail its import too. The\n // old key is what keeps that failure the import's to report.\n version = Date.now()\n }\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\n // query first — `sd.config.ts?t=1789565080284.6606` is then a `.6606`\n // file, and a TypeScript config gets parsed as JavaScript. Replacing the\n // one dot keeps every distinct mtime a distinct key.\n const key = String(version).replace('.', '_')\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 // What a configuration item says, as an object. `report` is what stops the\n // two readers of this from saying the same thing twice: a bad config has\n // nowhere else to surface when the watch list is being built, while a build\n // falls back to handing Style Dictionary the path and lets its message\n // through instead.\n const readConfigObject = async (\n item: ResolvedConfig,\n report: boolean,\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)\n : JSON5.parse(fs.readFileSync(item.config, 'utf-8'))\n\n if (isConfig(loaded)) return loaded\n\n if (report) {\n log(\n `Config file did not resolve to a configuration object: ${item.config}`,\n 'error',\n )\n }\n } catch (err) {\n if (report) {\n log(\n `Failed to parse config file: ${item.config}. Error: ${errorMessage(err)}`,\n 'error',\n )\n }\n }\n\n return null\n }\n\n // Parse token files to watch\n const getWatchTargets = async (\n resolvedConfigs: ResolvedConfig[],\n ): Promise<{ paths: string[]; patterns: string[] }> => {\n const filesToWatch = new Set<string>()\n\n for (const item of resolvedConfigs) {\n if (item.file) {\n filesToWatch.add(item.file.replace(/\\\\/g, '/'))\n }\n\n const configObj = await readConfigObject(item, true)\n\n if (configObj) {\n const addPattern = (pattern: unknown) => {\n if (typeof pattern === 'string') {\n // Against the working directory, because that is where Style\n // Dictionary resolves it: `combineJSON` globs each pattern with\n // no `cwd` of its own. Resolving against the configuration file's\n // directory instead is how the watch list came to name paths the\n // build never reads — a configuration in a subdirectory built\n // correctly and watched nothing at all.\n const absolutePattern = path.isAbsolute(pattern)\n ? pattern\n : path.resolve(process.cwd(), pattern)\n const normalized = absolutePattern.replace(/\\\\/g, '/')\n filesToWatch.add(normalized)\n }\n }\n\n if (configObj.source) {\n if (Array.isArray(configObj.source)) {\n configObj.source.forEach(addPattern)\n } else {\n addPattern(configObj.source)\n }\n }\n\n if (configObj.include) {\n if (Array.isArray(configObj.include)) {\n configObj.include.forEach(addPattern)\n } else {\n addPattern(configObj.include)\n }\n }\n }\n }\n\n // Add manually configured watch files\n if (options.watch) {\n const extraWatches = Array.isArray(options.watch)\n ? options.watch\n : [options.watch]\n for (const pattern of extraWatches) {\n const absolutePattern = path.isAbsolute(pattern)\n ? pattern\n : path.resolve(root, pattern)\n filesToWatch.add(absolutePattern.replace(/\\\\/g, '/'))\n }\n }\n\n const patterns = Array.from(filesToWatch)\n\n // Recorded here rather than at each call site, so every path that derives\n // a watch list refreshes the one `watchChange` filters against.\n cachedPatterns = patterns\n\n return { paths: await expandPatterns(patterns), patterns }\n }\n\n // What `new StyleDictionary` is handed for an item. Only a path in the JS\n // family becomes an object, because those are exactly the extensions Style\n // Dictionary's own `loadFile` reaches with `import` — the ones whose module\n // record Node then caches forever, and so the only ones a build could read\n // stale. The JSON5 family stays a path because there is nothing to gain:\n // those are read from disk on every pass either way, so a build can never\n // see one as it stood earlier in the process.\n const configForBuild = async (\n item: ResolvedConfig,\n ): Promise<Config | string> => {\n const { config } = item\n\n if (typeof config !== 'string' || !isImportedConfig(config)) return config\n\n const loaded = await readConfigObject(item, false)\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 try {\n return structuredClone(loaded)\n } catch {\n return loaded\n }\n }\n\n // Compile design tokens\n const runBuilds = async (\n resolvedConfigs: ResolvedConfig[],\n context?: string,\n ) => {\n const startTime = Date.now()\n try {\n if (!context) {\n log('Compiling design tokens...', 'info')\n }\n\n const generatedFiles = new Set<string>()\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 item of resolvedConfigs) {\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 // Swap in the atomic volume only now that the instance has finished\n // reading its configs and token sources, so every write below lands\n // through `rename` while the read path stays exactly as it was.\n sd.volume = atomicVolume\n await sd.buildAllPlatforms()\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 for (const platform of Object.values(sd.platforms)) {\n const buildPath = platform.buildPath ?? ''\n for (const file of platform.files ?? []) {\n if (file.destination) {\n const absoluteBuildPath = path.isAbsolute(buildPath)\n ? buildPath\n : path.resolve(root, buildPath)\n const absoluteDestination = path.isAbsolute(file.destination)\n ? file.destination\n : path.resolve(absoluteBuildPath, file.destination)\n generatedFiles.add(absoluteDestination)\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 generatedDestinations.clear()\n for (const destination of generatedFiles) {\n generatedDestinations.add(destination.replace(/\\\\/g, '/'))\n }\n\n const duration = Date.now() - startTime\n\n if (context) {\n log(\n `Rebuilt design tokens due to change in ${context} (${duration}ms)`,\n 'success',\n )\n } else {\n if (!quiet && generatedFiles.size > 0) {\n const fileInfos: Array<{\n coloredPath: string\n gzipSizeStr: string\n relativeDisplayPath: string\n sizeStr: string\n }> = []\n\n for (const filePath of generatedFiles) {\n if (fs.existsSync(filePath)) {\n const displayPath = path\n .relative(root, filePath)\n .replace(/\\\\/g, '/')\n const dir = path.dirname(displayPath)\n const base = path.basename(displayPath)\n const coloredPath =\n dir === '.'\n ? `\\x1b[32m${base}\\x1b[0m`\n : `\\x1b[90m${dir}/\\x1b[0m\\x1b[32m${base}\\x1b[0m`\n\n try {\n const stats = fs.statSync(filePath)\n const bytes = stats.size\n const sizeStr = `${(bytes / 1024).toFixed(2)} kB`\n\n const content = fs.readFileSync(filePath)\n const gzipBytes = zlib.gzipSync(content).length\n const gzipSizeStr = `${(gzipBytes / 1024).toFixed(2)} kB`\n\n fileInfos.push({\n coloredPath,\n gzipSizeStr,\n relativeDisplayPath: displayPath,\n sizeStr,\n })\n } catch {\n // Ignore errors reading individual files\n }\n }\n }\n\n if (fileInfos.length > 0) {\n const longestPathLength = Math.max(\n ...fileInfos.map((f) => f.relativeDisplayPath.length),\n 0,\n )\n const longestSizeLength = Math.max(\n ...fileInfos.map((f) => f.sizeStr.length),\n 0,\n )\n\n for (const info of fileInfos) {\n const pathPadding = ' '.repeat(\n Math.max(\n 2,\n longestPathLength - info.relativeDisplayPath.length + 2,\n ),\n )\n const sizePadded = info.sizeStr.padStart(longestSizeLength)\n console.log(\n `${info.coloredPath}${pathPadding}\\x1b[90m${sizePadded} │ gzip: ${info.gzipSizeStr}\\x1b[0m`,\n )\n }\n }\n }\n\n log(`Compiled successfully! (${duration}ms)`, 'success')\n }\n } catch (err) {\n const duration = Date.now() - startTime\n log(\n `Compilation failed after ${duration}ms: ${errorMessage(err)}`,\n 'error',\n )\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(context)) throw err\n }\n }\n\n // One rebuild per burst of watcher events, and never two at once.\n //\n // Two things went wrong without this. A single token edit under Vite's dev\n // server reached both the `configureServer` listener and `watchChange` —\n // Vite 6, 7 and 8 all invoke plugin `watchChange` while serving — and each\n // started its own build, so one write produced two. And nothing serialised\n // them: a four-file change started one build per file, all overlapping.\n // `runBuilds` builds its configurations one after another precisely so two\n // instances never write the same destination at once, and concurrent calls\n // to it reintroduced that one level up.\n //\n // The trailing debounce collapses the burst; the in-flight chain means a\n // trigger arriving mid-build queues exactly one follow-up rather than\n // starting a second build beside it.\n const REBUILD_DEBOUNCE_MS = 50\n\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n let pendingReason: string | undefined\n let inFlight: Promise<void> | undefined\n let waiting: Array<(failure?: { error: unknown }) => void> = []\n\n // Set by `configureServer`. A dev server's watcher is long-lived, so its\n // list has to follow a configuration that changes; every other target\n // re-registers on each build through `addWatchFile` instead.\n let refreshServerWatchList:\n | ((resolved: ResolvedConfig[]) => Promise<void>)\n | undefined\n\n const drain = async (): Promise<void> => {\n // A loop rather than a single pass: anything scheduled while the build\n // below is running is picked up here instead of starting a second one.\n while (pendingReason !== undefined) {\n const reason = pendingReason\n pendingReason = undefined\n\n // Captured before the await, so a trigger arriving mid-build waits for\n // the next pass rather than being told this one covered it.\n const resolvers = waiting\n waiting = []\n\n let failure: undefined | { error: unknown }\n let compiling = false\n\n try {\n const resolved = await resolveConfigs()\n if (resolved.length > 0) {\n compiling = true\n await runBuilds(resolved, reason)\n compiling = false\n hasCompiled = true\n await refreshServerWatchList?.(resolved)\n }\n } catch (err) {\n failure = { error: err }\n\n // `runBuilds` reports its own failure before rethrowing, so only the\n // other things that can throw here — a `config` function of the\n // consumer's that raises, a watch list that cannot be rebuilt — need\n // reporting.\n if (!compiling) log(`Rebuild failed: ${errorMessage(err)}`, 'error')\n }\n\n // Handed on to whatever awaited this rebuild, which is `watchChange`\n // and so the host under a watching bundler. Vite's dev-server listener\n // has no build to fail and catches it.\n for (const settle of resolvers) settle(failure)\n }\n }\n\n // Resolves once a rebuild covering this trigger has finished.\n const schedule = async (reason: string): Promise<void> => {\n pendingReason = reason\n\n const covered = new Promise<void>((resolve, reject) => {\n waiting.push((failure) => {\n if (failure) reject(asError(failure.error))\n else resolve()\n })\n })\n\n if (debounceTimer) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n debounceTimer = undefined\n inFlight = (inFlight ?? Promise.resolve()).then(drain)\n }, REBUILD_DEBOUNCE_MS)\n\n // A pending rebuild must not be what keeps a process alive; whatever is\n // watching already is.\n debounceTimer.unref()\n\n return covered\n }\n\n return {\n async buildStart() {\n const resolved = await resolveConfigs()\n if (resolved.length === 0) return\n\n // Register token/config files with the host bundler's watch mode.\n // Works out of the box wherever the host runs a persistent watcher\n // (e.g. `rollup --watch`). Vite's dev server is additionally handled\n // below via the `vite.configureServer` escape hatch — not because\n // `watchChange` is missing there, which it is not on any Vite this\n // package supports, but because the declared peer range is wider than\n // what has been measured and the scheduler above makes a duplicate\n // trigger free.\n const { paths } = await getWatchTargets(resolved)\n for (const file of paths) {\n this.addWatchFile(file)\n }\n\n // Registering the watch list is all this hook does on webpack, and it\n // has to happen here rather than beside the compile: `addWatchFile`\n // reaches `compilation.fileDependencies`, and `beforeCompile` runs\n // before there is a compilation to add to. Compiling here as well would\n // put the race back, and run every webpack build twice.\n if (isWebpack) return\n\n // Every watch rebuild re-enters this hook, and compiling here as well as\n // in `watchChange` is what closed the loop: consuming code imports the\n // generated file, so writing it is itself a module-graph change, which\n // re-enters `buildStart`, which writes it again. `watchChange` has\n // already run for every file in this cycle and rebuilt if any of them\n // was a source, so the only thing left for a re-entry to do is the\n // re-registration above.\n //\n // `hasCompiled` is the floor under that: a host that fires\n // `watchChange` without ever re-entering here would otherwise leave the\n // flag standing, and no first compile of a process may ever be skipped —\n // the tokens have to exist before the build that consumes them.\n if (watchRebuild && hasCompiled) {\n watchRebuild = false\n return\n }\n\n await runBuilds(resolved)\n hasCompiled = true\n },\n\n name: 'unplugin-style-dictionary',\n\n vite: {\n configResolved(config) {\n if (rootOption === undefined) root = config.root || process.cwd()\n },\n\n async configureServer(server: ViteDevServer) {\n const resolved = await resolveConfigs()\n if (resolved.length === 0) return\n\n // Reassigned after every rebuild below, so a configuration that gains\n // a source is matched against its new patterns rather than the ones\n // read at start-up.\n let targets = await getWatchTargets(resolved)\n\n // Watch configuration files and token files\n server.watcher.add(targets.paths)\n\n // Runs once per rebuild rather than once per event, which is why it\n // is handed to the scheduler rather than done in the listener.\n refreshServerWatchList = async (rebuilt) => {\n targets = await getWatchTargets(rebuilt)\n server.watcher.add(targets.paths)\n }\n\n // chokidar types its listener as returning void and does not await\n // what it is handed, so an async listener left every rejection\n // floating. `schedule` owns the whole rebuild including its errors,\n // so there is nothing here left to reject.\n server.watcher.on('all', (_event, file) => {\n if (!isWatchedSource(file, targets.patterns)) return\n\n // A dev server has no build to fail, so a rebuild that throws is\n // reported by the scheduler and the server keeps serving.\n void schedule(path.basename(file)).catch(() => {})\n })\n },\n },\n\n // Rollup types `watchChange` as returning void, yet awaits it as a\n // sequential hook — and the work here is inherently asynchronous. The\n // signature is the thing that is wrong, so the rule is silenced rather\n // than the hook made to lie about finishing.\n // oxlint-disable-next-line typescript/no-misused-promises\n async watchChange(id) {\n // Raised before any decision about `id`, because whatever this change\n // was, the host is now on its way back into `buildStart`.\n watchRebuild = true\n\n // The cheap half of the decision, taken before anything is resolved.\n // Under Vite the scope this hook sees is the whole project root rather\n // than the module graph, so most of what arrives here has nothing to do\n // with tokens, and resolving every configuration only to discard the\n // answer ran a consumer's `config` function once per unrelated file.\n // Skipped until a build has derived a list to filter against.\n if (cachedPatterns && !isWatchedSource(id, cachedPatterns)) return\n\n const resolved = await resolveConfigs()\n if (resolved.length === 0) return\n\n // Derived again rather than trusted from the cache, because the cache\n // is what decided this path was worth resolving and not what decides a\n // rebuild. A config edit reaches here through its own filename and can\n // have dropped the very source the cached list matched.\n const { patterns } = await getWatchTargets(resolved)\n // Without this check, watchChange fires for *any* changed file in the\n // host bundler's module graph — including our own generated output,\n // since consuming code imports it. Every regenerate is itself a\n // \"change\", so skipping what is not a source here is what keeps this\n // from rebuilding forever — both the files that match no pattern and\n // the ones that match only because this plugin wrote them.\n if (!isWatchedSource(id, patterns)) return\n\n // Same division as `buildStart`: on webpack the compile belongs to\n // `beforeCompile`, which has already run for this compilation, so all\n // that is left is to re-register the watch list below.\n if (!isWebpack) await schedule(path.basename(id))\n\n // Expanded again after the build rather than reusing the list from\n // before it, so a token file the build itself produced is registered.\n for (const file of await expandPatterns(patterns)) {\n this.addWatchFile(file)\n }\n },\n\n // unplugin calls this inside `apply(compiler)`, one line before it taps\n // `make`, so the root is in place before the first compile. Without it a\n // webpack build whose `context` is not the working directory looked for\n // the configuration in the wrong place and reported ENOENT.\n webpack(compiler) {\n if (rootOption === undefined) {\n root = compiler.options.context ?? process.cwd()\n }\n\n // `beforeCompile` is awaited before the compilation exists, so the\n // tokens are on disk before webpack resolves the module that imports\n // them. Tapped on every compilation rather than only the first: a watch\n // rebuild needs the same guarantee, and a compile that renders what is\n // already there skips its own write.\n compiler.hooks.beforeCompile.tapPromise(\n 'unplugin-style-dictionary',\n async () => {\n const resolved = await resolveConfigs()\n if (resolved.length === 0) return\n\n await runBuilds(resolved)\n hasCompiled = true\n },\n )\n },\n }\n}\n\nexport const unplugin = /* #__PURE__ */ createUnplugin(unpluginFactory)\n\nexport default unplugin\n"],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -26,8 +26,70 @@ export interface UnpluginStyleDictionaryOptions {
|
|
|
26
26
|
* If not provided, it will look for 'sd.config.json' or 'config.json' in the root directory.
|
|
27
27
|
*/
|
|
28
28
|
config?: (() => Config | Config[] | Promise<Config | Config[]>) | Config | Config[] | string | string[];
|
|
29
|
+
/**
|
|
30
|
+
* Whether a compile that fails should throw rather than only be reported.
|
|
31
|
+
*
|
|
32
|
+
* A failed compile used to be logged and swallowed, so `vite build`,
|
|
33
|
+
* `rollup` and `webpack` all exited 0 and shipped whatever the previous
|
|
34
|
+
* run had written — the stale values, presented as current.
|
|
35
|
+
*
|
|
36
|
+
* - `'build'` (the default) throws on the one-shot compile that runs in
|
|
37
|
+
* `buildStart`, and only reports a failed watch rebuild, so a dev server
|
|
38
|
+
* survives a half-typed token file.
|
|
39
|
+
* - `'serve'` is the reverse: a failed rebuild is thrown to whatever awaited
|
|
40
|
+
* it, which is the host under `rollup --watch`. Vite's dev server has no
|
|
41
|
+
* build to fail, so there it is reported and the server keeps serving.
|
|
42
|
+
* - `true` throws on both, `false` on neither.
|
|
43
|
+
*
|
|
44
|
+
* Reporting happens either way, and is not suppressed by `silent`.
|
|
45
|
+
*
|
|
46
|
+
* @default 'build'
|
|
47
|
+
*/
|
|
48
|
+
failOnError?: 'build' | 'serve' | boolean;
|
|
49
|
+
/**
|
|
50
|
+
* How much this plugin and Style Dictionary say while building.
|
|
51
|
+
*
|
|
52
|
+
* Style Dictionary's own warnings — a name collision, a reference that
|
|
53
|
+
* cannot be resolved, `No tokens for vars.css. File not created.` — used to
|
|
54
|
+
* be suppressed unconditionally, because the plugin overwrote
|
|
55
|
+
* `log.verbosity` on the way past. Leave this unset and whatever the
|
|
56
|
+
* configuration asked for stands.
|
|
57
|
+
*
|
|
58
|
+
* - `'silent'` — nothing from either.
|
|
59
|
+
* - `'warn'` — Style Dictionary's warnings, and nothing from the plugin.
|
|
60
|
+
* - `'info'` — the above, plus the plugin's progress lines and size table.
|
|
61
|
+
* - `'verbose'` — the above, with Style Dictionary naming what it warned
|
|
62
|
+
* about rather than pointing at its own `--verbose` flag.
|
|
63
|
+
*
|
|
64
|
+
* A compile that fails is reported at every level, so there is no
|
|
65
|
+
* `'error'`: `'silent'` is the quietest and still reports a failure.
|
|
66
|
+
*
|
|
67
|
+
* @default undefined, which prints the plugin's own lines and leaves the
|
|
68
|
+
* configuration's `log.verbosity` alone
|
|
69
|
+
*/
|
|
70
|
+
logLevel?: 'info' | 'silent' | 'verbose' | 'warn';
|
|
71
|
+
/**
|
|
72
|
+
* The directory a relative `config` path is looked up in.
|
|
73
|
+
*
|
|
74
|
+
* Defaults to the host's own root — Vite's `root`, webpack's `context` —
|
|
75
|
+
* and to the working directory for rollup and rolldown, which offer none.
|
|
76
|
+
* A relative value here is resolved against the working directory, and it
|
|
77
|
+
* takes precedence over whatever the host reports.
|
|
78
|
+
*
|
|
79
|
+
* It does not move the paths **inside** a configuration. Style Dictionary
|
|
80
|
+
* resolves every relative `source`, `include` and `buildPath` against the
|
|
81
|
+
* working directory, and this plugin reads them the same way, so a
|
|
82
|
+
* configuration behaves identically here and under Style Dictionary's own
|
|
83
|
+
* CLI. A configuration kept in a subdirectory therefore names its tokens
|
|
84
|
+
* relative to where the build runs, not relative to itself.
|
|
85
|
+
*/
|
|
86
|
+
root?: string;
|
|
29
87
|
/**
|
|
30
88
|
* Disable console logging.
|
|
89
|
+
*
|
|
90
|
+
* An alias for `logLevel: 'silent'`, which wins if both are set. A compile
|
|
91
|
+
* that fails is always reported.
|
|
92
|
+
*
|
|
31
93
|
* @default false
|
|
32
94
|
*/
|
|
33
95
|
silent?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kanso-labs/unplugin-style-dictionary",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Compile Style Dictionary design tokens ahead of your bundler (Vite, Rolldown, Rollup, or Webpack) from a single unplugin-based plugin, with automatic watching and rebuilding under Vite",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"unplugin",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"test:coverage": "vitest run --coverage"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
+
"json5": "2.2.3",
|
|
69
70
|
"picomatch": "4.0.7",
|
|
70
71
|
"tinyglobby": "0.2.17",
|
|
71
72
|
"unplugin": "3.3.0"
|
|
@@ -74,7 +75,7 @@
|
|
|
74
75
|
"@commitlint/cli": "21.2.2",
|
|
75
76
|
"@commitlint/config-conventional": "21.2.2",
|
|
76
77
|
"@eslint/js": "10.0.1",
|
|
77
|
-
"@types/node": "26.
|
|
78
|
+
"@types/node": "26.6.1",
|
|
78
79
|
"@types/picomatch": "4.0.3",
|
|
79
80
|
"@vitest/coverage-v8": "5.0.1",
|
|
80
81
|
"eslint": "10.10.0",
|