@kanso-labs/unplugin-style-dictionary 0.10.6 → 0.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1011,7 +1011,8 @@ export interface UnpluginStyleDictionaryOptions {
1011
1011
  report?: boolean
1012
1012
 
1013
1013
  /**
1014
- * The directory a relative `config` path is looked up in.
1014
+ * The directory a relative `config` path is looked up in, and a relative
1015
+ * `watch` entry is resolved against.
1015
1016
  *
1016
1017
  * Defaults to the host's own root — Vite's `root`, webpack's `context` —
1017
1018
  * and to the working directory for rollup and rolldown, which offer none.
@@ -1047,6 +1048,13 @@ export interface UnpluginStyleDictionaryOptions {
1047
1048
  * pattern's own directory is registered alongside them, so a token file
1048
1049
  * created later is noticed too.
1049
1050
  *
1051
+ * A relative entry is resolved against `root`, like a relative `config`: both
1052
+ * are options of this plugin rather than paths inside a configuration. That
1053
+ * is not the base `source` and `include` use — Style Dictionary reads those
1054
+ * against the working directory — so where the host's root is somewhere
1055
+ * else, name a file here relative to the root, and in `source` relative to
1056
+ * where the build runs.
1057
+ *
1050
1058
  * What a change to a watched file then triggers is the host's to decide —
1051
1059
  * see the interface documentation above.
1052
1060
  */
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { UnpluginFactory } from 'unplugin'\nimport type { ViteDevServer } from 'vite'\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { createUnplugin } from 'unplugin'\n\nimport type { PluginInstance } from './compile.js'\nimport type { ResolvedConfig } from './config.js'\nimport type {\n StyleDictionaryConfigContext,\n UnpluginStyleDictionaryOptions,\n} from './types.js'\n\nimport { colourAllowed, paint } from './colour.js'\nimport { runBuilds } from './compile.js'\nimport { resolveConfigOption } from './config.js'\nimport { asError, errorMessage } from './errors.js'\nimport { expandPatterns, watchPatternsOf } from './patterns.js'\nimport { createScheduler } from './scheduler.js'\nimport { isWatchedSource } from './watch-filter.js'\n\nexport type * from './types.js'\n\n// Where the plugin's own lines go when a host offers somewhere better than the\n// console: Vite's `config.logger`, rollup's and rolldown's plugin context, or\n// webpack's `compilation`.\n//\n// **There is no `error` channel that merely reports.** Rollup's `this.error`\n// aborts the bundle — measured: a `buildStart` calling it ends the run with\n// `THREW: [plugin err-probe] fatal?` — so routing a failure report through it\n// would stop every build that reported one and silently override `failOnError`,\n// whose entire job is deciding that. A failure is therefore reported on the\n// host's warning channel, and whether the build stops stays `failOnError`'s\n// decision alone.\ninterface HostMessenger {\n error: (message: string) => void\n\n // Optional because not every host has somewhere for a progress line to go.\n // webpack's `stats` carries warnings and errors and nothing else, and\n // `Compiling design tokens...` is neither — so there it stays on the\n // console rather than being dressed up as a warning.\n info?: (message: string) => void\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// A host's message channel, narrowed by a predicate rather than asserted: what\n// a plugin context carries under `warn` is the host's business, and a cast\n// would only claim it is callable.\nfunction isMessageChannel(value: unknown): value is (message: string) => void {\n return typeof value === 'function'\n}\n\n// Vite builds its dev-server watcher with a fixed ignore list — `**/.git/**`,\n// `**/node_modules/**`, `**/test-results/**` and the cache directory — and\n// spreads the consumer's own `server.watch.ignored` entries in *after* them.\n// Entries are appended, never subtracted, so `server.watcher.add()` cannot\n// reach a path an earlier entry already covers.\n//\n// That makes a token package resolved through `node_modules` — the shape of\n// every workspace, `app/node_modules/@acme/tokens` symlinked to\n// `packages/tokens` — build correctly once and then never rebuild, with\n// nothing said about it. Measured on Vite 6.4.3, 7.3.6 and 8.3.0: zero watcher\n// events for an edit, while a token file outside the root but outside\n// `node_modules` rebuilt in the same run.\n//\n// A negation naming the file exactly is what un-ignores it, and is deliberately\n// the narrowest form that works. `!**/node_modules/**` would restore the whole\n// dependency tree to the watcher.\nfunction nodeModulesNegations(paths: string[]): string[] {\n const negations = new Set<string>()\n\n for (const file of paths) {\n const normalised = file.replace(/\\\\/g, '/')\n if (normalised.includes('/node_modules/')) negations.add(`!${normalised}`)\n }\n\n return Array.from(negations)\n}\n\n// The directories holding token files that resolve through `node_modules`.\n//\n// Vite's ignore list cannot be argued with on Windows. The negation below is\n// honoured on Linux and macOS, and there a token inside `node_modules` rebuilds\n// through the dev-server watcher like any other. On Windows it is not, and no\n// spelling of the negation changes that — measured on a `windows-latest`\n// runner: the file path, every ancestor directory, and the package subtree as\n// a globstar all leave the edit reaching no rebuild, while the same fixture\n// outside `node_modules` rebuilds. `server.watcher.add()` does not reach it\n// either, which is the same limit AGENTS.md already records for a path an\n// earlier ignore entry covers.\n//\n// A symlink is *not* what distinguishes them, which is worth stating because it\n// is the obvious suspect: a real directory inside `node_modules` fails exactly\n// as the symlinked one does, and a symlink outside it succeeds.\n//\n// So these directories get a watcher of the plugin's own, which Vite's ignore\n// list has no say over. It runs on every platform rather than behind a\n// `process.platform` check: one path that is exercised everywhere beats a\n// Windows-only branch that nothing else executes, and the scheduler already\n// collapses the duplicate trigger this produces where the negation also works.\nfunction nodeModulesWatchDirectories(paths: string[]): string[] {\n const directories = new Set<string>()\n\n for (const file of paths) {\n const normalised = file.replace(/\\\\/g, '/')\n if (!normalised.includes('/node_modules/')) continue\n\n // The directory rather than the file: `fs.watch` on a file stops reporting\n // once an editor replaces it by rename, which is what an atomic save does.\n directories.add(path.dirname(normalised))\n }\n\n return Array.from(directories)\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\n// Windows refuses a rename over a destination another process holds open, and\n// that is exactly the case the atomic write exists to serve: measured on a\n// `windows-latest` runner, the suite's own concurrent-reader case fails with\n// `EPERM: operation not permitted, rename`. So the feature inverts — the\n// compile fails rather than the read being protected.\n//\n// This **refutes** the reasoning that put the case in doubt. It was argued that\n// libuv opens files with `FILE_SHARE_DELETE`, so a concurrent reader would most\n// likely not block the rename. It blocks it.\n//\n// The blocking handle is transient — a reader, an indexer, a virus scanner —\n// so a short bounded backoff clears it. The bound matters as much as the retry:\n// a rename that genuinely cannot succeed has to fail rather than hang a dev\n// server, and the existing failure path already reports and lets `failOnError`\n// decide.\n//\n// Unreachable on Linux and macOS, where a rename over an open file succeeds.\nconst RENAME_RETRY_CODES = new Set(['EBUSY', 'EPERM'])\n\n// Doubling rather than a fixed interval, so the common case — a handle already\n// gone by the first retry — costs a millisecond rather than the whole budget.\n//\n// **The total is sized by what a dev server can tolerate waiting, not by how\n// long a handle usually persists.** An earlier version stopped at about 255ms,\n// which clears a transient hold and is not the situation that matters: a\n// consumer polling the generated file holds it for a large fraction of wall\n// time, and then no budget wins every race. What that produced was a rebuild\n// that failed roughly once in a hundred renames — which the suite's own\n// concurrent-reader case turns into a failure about once a run, because it\n// performs twenty of them. Intermittent, and on a platform where the whole\n// point of the atomic write is that a reader never sees a partial file.\n//\n// About two seconds is therefore the bound. A rebuild that takes a second is\n// something a dev server absorbs; one that fails is not. `write-file-atomic`\n// and `graceful-fs` both take this approach, the latter retrying for up to a\n// minute, so this is still the conservative end.\n//\n// The bound stays a bound: a rename that genuinely cannot succeed has to fail\n// rather than hang, and the unguarded attempt after the loop is what makes it.\nconst RENAME_RETRY_DELAYS_MS = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]\n\n// **The synchronous path gets a shorter budget, and the asymmetry is the\n// point.** `renameWithRetry` waits on a timer, so the event loop keeps serving\n// while it does; `renameWithRetrySync` waits on `Atomics.wait`, which blocks\n// everything — a dev server holding its main thread for two seconds is worse\n// than the failed rebuild the wait is trying to avoid.\n//\n// It is reached only through a Style Dictionary custom action's own\n// `vol.writeFileSync`, which is rare, and it keeps roughly the budget the async\n// path had before this change.\nconst RENAME_RETRY_DELAYS_SYNC_MS = [1, 2, 4, 8, 16, 32, 64, 128]\n\nfunction isRetryableRenameError(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'code' in error &&\n typeof error.code === 'string' &&\n RENAME_RETRY_CODES.has(error.code)\n )\n}\n\n// `Atomics.wait` rather than a spin on `Date.now()`, because the sync path has\n// no event loop to yield to and a busy loop would hold the CPU for the whole\n// backoff — on the one platform where the handle it is waiting for belongs to\n// another process.\nconst sleepSync = (ms: number): void => {\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)\n}\n\nasync function renameWithRetry(\n temporary: string,\n destination: string,\n): Promise<void> {\n for (const delay of RENAME_RETRY_DELAYS_MS) {\n try {\n await fs.promises.rename(temporary, destination)\n return\n } catch (err) {\n if (!isRetryableRenameError(err)) throw err\n await new Promise((resolve) => setTimeout(resolve, delay))\n }\n }\n\n // The last attempt is deliberately outside the loop and unguarded: the bound\n // is a bound, so whatever it throws here is what the caller sees.\n await fs.promises.rename(temporary, destination)\n}\n\nfunction renameWithRetrySync(temporary: string, destination: string): void {\n for (const delay of RENAME_RETRY_DELAYS_SYNC_MS) {\n try {\n fs.renameSync(temporary, destination)\n return\n } catch (err) {\n if (!isRetryableRenameError(err)) throw err\n sleepSync(delay)\n }\n }\n\n fs.renameSync(temporary, destination)\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 renameWithRetry(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 renameWithRetrySync(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// The fingerprints of configurations this process has compiled at least once.\n// It is what lets a configuration given as an object or a function be skipped\n// at all: such a configuration has no file to stat, so an edit to it inside\n// `vite.config.ts` moves no mtime and the filesystem cannot tell the two\n// apart. Having built it here, the plugin can — the fingerprint changes with\n// the configuration.\n//\n// Module scope rather than the factory's, for the same reason `compilesInFlight`\n// below is: the instances that would otherwise repeat the work are different\n// instances, so per-instance state cannot see them. A `vitest run` stands up\n// several, and a function configuration — the form the README recommends for\n// registering custom formats — would be the one form that never skipped.\n//\n// The fingerprint carries the root, so two projects in one process never share\n// one. A configuration given as a path needs none of this: its own file is one\n// of the sources the mtime comparison reads, so an edit to it is visible across\n// processes as well as within one.\nconst compiledFingerprints = new Set<string>()\n\n// A compile that is running right now, keyed by `buildKey`, so bundler\n// instances in one process wait on each other rather than each starting their\n// own.\n//\n// Generated token files are a side effect on the filesystem, not per-bundler\n// output, and one process routinely holds several instances of this plugin. A\n// single `vitest run` on a project with two test projects and browser mode\n// stands up five Vite servers — the root one, one per project, and one more\n// per project once its HTTP server listens — and every one of them runs\n// `buildStart`. `hasCompiled` cannot see any of that: it is closure state\n// inside the factory, so each instance has its own and each compiles.\n//\n// Module scope is the only place a shared answer can live, since the\n// instances know nothing about each other. It stays a claim about identical\n// work, never about identity: the key carries the root and the resolved\n// configurations, so one script building two packages shares nothing.\nconst compilesInFlight = new Map<string, Promise<void>>()\n\n// The slice of a webpack-shaped compiler this plugin touches, named rather\n// than imported. webpack and rspack each ship their own `Compiler` type, and\n// neither is assignable to the other, so a hook written against one cannot be\n// handed to the other's key. Both satisfy this structurally, which is what\n// makes `adoptCompiler` one function instead of two copies drifting apart.\ninterface BundlerCompiler {\n hooks: {\n beforeCompile: {\n tapPromise: (name: string, handler: () => Promise<void>) => void\n }\n compilation: {\n tap: (name: string, handler: (compilation: Compilation) => void) => void\n }\n done: { tap: (name: string, handler: () => void) => void }\n failed: { tap: (name: string, handler: () => void) => void }\n }\n options: { context?: string | undefined; mode?: string | undefined }\n watchMode: boolean\n}\n\n// Only the one array a report is pushed onto. webpack types it `WebpackError[]`\n// and rspack `Error[]`; both are arrays of something extending `Error`, so a\n// plain one is what this pushes on either.\ninterface Compilation {\n warnings: Error[]\n}\n\n// A stable identity for a set of resolved configurations, or `null` for one\n// that cannot have a stable identity at all.\n//\n// Functions are serialised by source rather than dropped, because a `format`\n// or `transform` written inline is exactly what distinguishes two otherwise\n// identical configurations — and `JSON.stringify` omits a function outright,\n// which would make two different builds look like one.\nfunction buildKey(root: string, resolved: ResolvedConfig[]): null | string {\n try {\n return JSON.stringify(\n [root, resolved.map((item) => item.file ?? item.config)],\n (_key, value: unknown) =>\n typeof value === 'function' ? `[fn]${String(value)}` : value,\n )\n } catch {\n // A configuration that will not serialise — a circular reference, a\n // BigInt — takes no shared identity rather than a wrong one, and compiles\n // exactly as it did before.\n return null\n }\n}\n\n// Not exported. It cannot be called in the form a reader would guess —\n// unplugin types the factory as `(options, meta)`, and `meta` is the\n// bundler-identifying `UnpluginContextMeta` a consumer would have to build by\n// hand — so publishing it offered a name that answered nothing. What a\n// consumer imports is the default export of the entry for their bundler.\nconst unpluginFactory: UnpluginFactory<\n undefined | UnpluginStyleDictionaryOptions,\n false\n> = (options = {}, meta) => {\n // webpack and rspack are the targets whose `buildStart` does not run before\n // the module graph is resolved: unplugin taps it on `make`, an\n // `AsyncParallelHook` that `EntryPlugin` taps too. The `webpack` and\n // `rspack` keys below compile on `beforeCompile` instead, which both await\n // before the compilation exists. rspack reimplements webpack's plugin API,\n // so everything this plugin does with a compiler is the same on either —\n // but unplugin dispatches them by separate keys, so the flag names both.\n const isWebpack = meta.framework === 'webpack' || meta.framework === 'rspack'\n const {\n cache = true,\n errorOverlay = true,\n failOnError = 'build',\n logLevel,\n onBuildEnd,\n onBuildError,\n onBuildStart,\n platforms: platformsOption,\n report = true,\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 // What the host is doing, for the function form of `config`. Populated where\n // each host knows the answer and read when that function is called — the\n // same shape as `root` and the message host above, and for the same reason:\n // `resolveConfigs` is reached from five places now, and threading a context\n // parameter through all five would make every caller restate what only the\n // host can say.\n let hostCommand: 'build' | 'serve' = 'build'\n let hostMode: string | undefined\n let isWatching = false\n\n // `mode` is derived rather than invented where a host has no notion of one.\n // rollup and rolldown report nothing, and following `command` is the answer\n // Vite itself would give: its default mode is `development` serving and\n // `production` building.\n const configContext = (): StyleDictionaryConfigContext => ({\n command: hostCommand,\n mode: hostMode ?? (hostCommand === 'serve' ? 'development' : 'production'),\n watch: isWatching,\n })\n\n // Whether the host will keep rebuilding, as the plugin context reports it.\n // Read from `meta.watchMode`, which rollup, rolldown and Vite all carry and\n // webpack does not — there it comes off the compiler instead.\n const adoptWatchMode = (context: object): void => {\n const hookMeta: unknown = 'meta' in context ? context.meta : undefined\n if (typeof hookMeta !== 'object' || hookMeta === null) return\n\n const watching: unknown =\n 'watchMode' in hookMeta ? hookMeta.watchMode : undefined\n if (typeof watching === 'boolean') isWatching = watching\n }\n\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 // Whether the host has shut its watcher down. `await watcher.close()` is not\n // a promise that no build is in flight: rollup's `Watcher.close` clears the\n // pending build timeout, closes each task's file watcher and emits `close`,\n // and never awaits `run` — while `Task.run` checks `closed` only *after*\n // `rollupInternal` has resolved. So a build that has already entered\n // `rollupInternal` runs its `buildStart` hooks through to completion after\n // `close()` has returned to its caller, against a project that may be half\n // torn down by then. Measured on rollup 4.63.3 with no plugin of ours: a\n // `buildStart` reading a file 300ms after `close()` resolved gets ENOENT.\n //\n // `closeWatcher` is what makes that answerable. It runs synchronously inside\n // `close()`, and so before the in-flight hook resumes, which is the whole\n // reason a flag set there is worth setting. What it must not be is\n // `closeBundle`: that fires once per bundle — every `BUNDLE_END` a consumer\n // calls `result.close()` on — and would read as a shutdown on every rebuild.\n let hostClosed = false\n\n // Decided once, when the plugin is constructed, and held for its life. The\n // two streams are asked separately because they are redirected separately —\n // `build 2>err.log` leaves stdout a terminal and stderr a file.\n const stdoutColour = colourAllowed(process.stdout)\n const stderrColour = colourAllowed(process.stderr)\n\n // Where a message goes once a host has offered somewhere better than the\n // console. Set by `configResolved` under Vite, by the build hooks under\n // rollup and rolldown, and by the `webpack` block; left undefined when no\n // host has claimed it, which is every unit test binding its own context.\n let host: HostMessenger | undefined\n\n // Adopts a plugin context as the message host, if it has the channels — the\n // unit tests bind a context carrying `addWatchFile` and nothing else, and a\n // hook calling `this.warn` against that throws in a way that reads as a\n // plugin bug rather than as a missing stub.\n //\n // Only when nothing has claimed the host yet. Under Vite `configResolved`\n // has already installed the dev server's own logger, and `buildStart` runs\n // after it with a rollup-shaped context that would otherwise replace it.\n const adoptHost = (context: object): void => {\n if (host) return\n\n const warn: unknown = 'warn' in context ? context.warn : undefined\n if (!isMessageChannel(warn)) return\n\n const info: unknown = 'info' in context ? context.info : undefined\n\n host = {\n // `warn`, never `error`. Rollup's `this.error` aborts the bundle, so\n // reporting through it would stop every build that reported anything and\n // take the decision `failOnError` exists to make.\n error: (message) => {\n warn.call(context, message)\n },\n info: isMessageChannel(info)\n ? (message) => {\n info.call(context, message)\n }\n : undefined,\n }\n }\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 // The host renders and colours its own output, so nothing painted here\n // is handed to one — an escape inside a webpack `stats` entry survives\n // into `stats.toJson()` and into whatever reads it.\n if (host) {\n host.error(`${prefix} ${message}`)\n return\n }\n\n console.error(paint('31', `${prefix} ${message}`, stderrColour))\n return\n }\n\n if (quiet) return\n\n if (host?.info) {\n host.info(`${prefix} ${message}`)\n return\n }\n\n console.log(\n paint(\n type === 'success' ? '32' : '36',\n `${prefix} ${message}`,\n stdoutColour,\n ),\n )\n }\n\n // Resolve config file paths / objects. The work is `resolveConfigOption`'s;\n // what this adds is the instance it runs for, read at the moment of the call.\n // `root` is assigned by the host after the factory has run, so it is passed\n // as it stands now rather than as it stood when this was built.\n const resolveConfigs = async (): Promise<ResolvedConfig[]> =>\n resolveConfigOption({\n config: options.config,\n configContext,\n discovery,\n log,\n root,\n })\n\n // Parse token files to watch\n const getWatchTargets = async (\n resolvedConfigs: ResolvedConfig[],\n ): Promise<{ paths: string[]; patterns: string[] }> => {\n const patterns = await watchPatternsOf(\n { log, root, watch: options.watch },\n resolvedConfigs,\n )\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, log), patterns }\n }\n\n // What `runBuilds` reads from this plugin instance, built once. `root` and\n // the overlay callback go in as functions rather than values, because both\n // are assigned after this runs — see `PluginInstance`.\n const instance: PluginInstance = {\n cache,\n compiledFingerprints,\n failOnError,\n generatedDestinations,\n log,\n notifyBuildOutcome: (error) => {\n notifyBuildOutcome?.(error)\n },\n onBuildEnd,\n onBuildError,\n onBuildStart,\n platformsOption,\n quiet,\n report,\n root: () => root,\n stdoutColour,\n verbosity,\n volume: atomicVolume,\n watch: options.watch,\n }\n\n // `runBuilds` for the first build of a process, with the compile shared\n // between every plugin instance that wants the same one.\n //\n // An instance arriving while a compile for the same key is running waits on\n // that compile instead of starting a second. It is the concurrent half that\n // needs this: an up-to-date check compares what is on disk against the\n // sources, and two instances that start together have nothing on disk to\n // compare against yet, so only a shared promise can tell them apart from\n // two genuinely separate builds.\n //\n // The entry is dropped as soon as the compile settles, so this coalesces\n // rather than caches — a later `buildStart` still compiles. Skipping one\n // whose output is already current is #212's up-to-date check, and belongs\n // with it rather than as a second mechanism here.\n //\n // A rejection reaches every waiter, which is the point: an instance that\n // waited on a failed compile must not carry on as though the tokens were\n // written. Whether that rejection is thrown at all is `failOnError`'s\n // decision, already made inside `runBuilds`.\n const compileOnceAcrossInstances = async (\n resolvedConfigs: ResolvedConfig[],\n ): Promise<void> => {\n const key = buildKey(root, resolvedConfigs)\n if (key === null) {\n await runBuilds(instance, resolvedConfigs)\n return\n }\n\n const running = compilesInFlight.get(key)\n if (running) {\n await running\n return\n }\n\n const compile = runBuilds(instance, resolvedConfigs)\n compilesInFlight.set(key, compile)\n\n try {\n await compile\n } finally {\n compilesInFlight.delete(key)\n }\n }\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 // Also set by `configureServer`: closes the `node_modules` watchers it\n // opened. Called from `buildEnd` rather than registered on the server's own\n // shutdown, because nothing on the server fires in every mode — see the\n // `vite` block's `buildEnd`.\n let closeOwnWatchers: (() => void) | undefined\n\n // Whether the discovered path has been announced. Once per plugin instance:\n // `resolveConfigs` runs on every build and rebuild, and a dev server would\n // otherwise repeat the line for the rest of the session.\n //\n // An object rather than a flag, because it is handed to\n // `resolveConfigOption` to set: that function serves every instance in the\n // process, so the record of what this one has said has to stay here.\n const discovery = { announced: false }\n\n // Resolved by `configResolved` so it can amend the watcher's ignore list, and\n // handed to `configureServer` rather than resolved again — one start-up, one\n // call of the consumer's `config` function.\n let startupResolved: ResolvedConfig[] | undefined\n\n // Also set by `configureServer`, and left undefined everywhere else: this is\n // how a compile outcome reaches Vite's error overlay. It is deliberately not\n // the same path as `failOnError`.\n //\n // `failOnError` decides whether the host stops; this decides whether the\n // browser is told. Under a dev server the default is not to stop, so the\n // failure is reported and swallowed — and that is exactly the case where the\n // page is left rendering the last good file with nothing to say it is stale.\n // Reading the outcome off whether `runBuilds` threw would therefore see\n // nothing at all on the only configuration that matters.\n let notifyBuildOutcome: ((error: Error | null) => void) | undefined\n\n // A rebuild, as the scheduler runs it: resolve, compile, and bring the dev\n // server's watch list up to date. A failure is rethrown once it has been\n // reported, so the scheduler can hand it to every trigger this covered.\n const rebuild = async (reason: string): Promise<void> => {\n let compiling = false\n\n try {\n const resolved = await resolveConfigs()\n if (resolved.length > 0) {\n compiling = true\n await runBuilds(instance, resolved, reason)\n compiling = false\n hasCompiled = true\n await refreshServerWatchList?.(resolved)\n }\n } catch (err) {\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. They reach the overlay for the same reason: from the\n // page's point of view the rebuild failed, whichever half of it did.\n if (!compiling) {\n log(`Rebuild failed: ${errorMessage(err)}`, 'error')\n notifyBuildOutcome?.(asError(err))\n }\n\n throw err\n }\n }\n\n // One rebuild per burst of watcher events, and never two at once — see\n // `createScheduler`. Created here, once per plugin instance: a debounce two\n // instances shared would run one instance's rebuild and drop the other's.\n const schedule = createScheduler({\n debounceMs: 50,\n isClosed: () => hostClosed,\n run: rebuild,\n })\n\n // Every host that runs a rollup-shaped watcher calls this on shutdown, and\n // all three get the same handler below. There is deliberately no webpack\n // equivalent here: it has no `closeWatcher`, its nearest thing is\n // `compiler.hooks.watchClose`, and nothing measured shows it exposed.\n //\n // It raises the flag and nothing else. A debounce timer armed before the\n // close is deliberately left to fire: the rebuild it runs is one the host\n // asked for while the project was still whole, and `rebuild` reports its\n // own failures. Cancelling it would be a guard no test could fail on, since a\n // trigger arriving after the close is declined by `schedule` instead.\n const closeWatcher = (): void => {\n hostClosed = true\n }\n\n // What both webpack-shaped hosts do with a compiler, written once.\n // rspack reimplements webpack's plugin API hook for hook, but ships its\n // own `Compiler` type and unplugin dispatches the two through separate\n // keys — so a function typed against either one rejects the other. Naming\n // the surface actually used is what lets one implementation serve both.\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 const adoptCompiler = (compiler: BundlerCompiler): void => {\n if (rootOption === undefined) {\n root = compiler.options.context ?? process.cwd()\n }\n\n // webpack's `buildStart` context carries no `meta`, so neither half of\n // the build context can come from there. `mode` is a webpack option, and\n // `watchMode` is only true once `watch()` has been called — which is\n // after this runs, so it is read per compile below rather than here.\n hostMode = compiler.options.mode\n\n // The compile happens in `beforeCompile`, which webpack awaits *before*\n // the compilation exists — so a message from it has nothing to attach to\n // yet and is held until one appears.\n //\n // Only failures are routed. `stats` carries warnings and errors and\n // nothing else, so the progress lines stay on the console rather than\n // being reported as warnings they are not.\n //\n // A warning rather than an error, for the same reason as on rollup: this\n // is the report, and `failOnError` decides separately whether the build\n // stops. Pushing to `compilation.errors` would fail a webpack build that\n // asked not to be failed.\n const pending: string[] = []\n host = {\n error: (message) => {\n pending.push(message)\n },\n }\n\n compiler.hooks.compilation.tap(\n 'unplugin-style-dictionary',\n (compilation) => {\n for (const message of pending.splice(0)) {\n const reported = new Error(message)\n reported.name = 'UnpluginStyleDictionaryWarning'\n compilation.warnings.push(reported)\n }\n },\n )\n\n // A `beforeCompile` that throws ends the run without ever creating a\n // compilation, and that is exactly the case that produced the message.\n // Left to the buffer it would be reported nowhere at all, so whatever is\n // still held when the run ends goes to the console after all.\n const drainToConsole = () => {\n for (const message of pending.splice(0)) {\n console.error(paint('31', message, stderrColour))\n }\n }\n compiler.hooks.failed.tap('unplugin-style-dictionary', drainToConsole)\n compiler.hooks.done.tap('unplugin-style-dictionary', drainToConsole)\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 isWatching = compiler.watchMode\n\n const resolved = await resolveConfigs()\n if (resolved.length === 0) return\n\n await compileOnceAcrossInstances(resolved)\n hasCompiled = true\n },\n )\n }\n\n return {\n async buildStart() {\n adoptHost(this)\n adoptWatchMode(this)\n\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 //\n // Skipped outright once the host has closed, because rollup discards the\n // result: with the task closed, `Task.run` returns before\n // `updateWatchedFiles`, so every path registered here goes nowhere. What\n // deriving it does still do is read each config file — with\n // its errors reported — and report an ENOENT for a project the host is\n // in the middle of tearing down. That report was the one thing this\n // block contributed after a close.\n if (!hostClosed) {\n const { paths } = await getWatchTargets(resolved)\n for (const file of paths) {\n this.addWatchFile(file)\n }\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 compileOnceAcrossInstances(resolved)\n hasCompiled = true\n },\n\n name: 'unplugin-style-dictionary',\n\n // `closeWatcher` is a rollup-shaped hook and `UnpluginOptions` declares no\n // top-level equivalent, so it is registered per target instead: rolldown\n // lists it among its input plugin hooks, and Vite's plugin type is\n // rollup's, which is what carries it to `vite build --watch`. Vite's dev\n // server runs no rollup watcher, so there it simply never fires.\n rolldown: { closeWatcher },\n\n rollup: { closeWatcher },\n\n // unplugin calls the matching key from inside `apply(compiler)` and\n // never both, so the two share one implementation rather than one\n // delegating to the other.\n rspack: adoptCompiler,\n\n vite: {\n // **`buildEnd` is the hook a dev server's close reliably reaches.** It\n // replaced `server.httpServer?.once('close', …)`, which middleware mode\n // — Express, Koa, most SSR — has no `httpServer` for, so the optional\n // chain registered nothing and every restart leaked the watchers.\n //\n // Measured on Vite 6.4.3, 7.3.6 and 8.3.0, in middleware mode and\n // listening alike: while a server runs only `buildStart` fires, and on\n // `server.close()` it is `buildEnd` once and `closeBundle` twice.\n // **`closeWatcher` fires in neither mode on any of them**, which is why\n // this is not the `closeWatcher` handler registered below.\n //\n // `buildEnd` also fires at the end of `vite build`, and on every rebuild\n // of `vite build --watch`. Neither runs `configureServer`, so there is\n // nothing to close there and this is a no-op.\n buildEnd() {\n closeOwnWatchers?.()\n },\n\n closeWatcher,\n\n async configResolved(config) {\n if (rootOption === undefined) root = config.root || process.cwd()\n\n // The only host that has both. `command` is what makes `'serve'`\n // reachable at all, since nothing else here serves.\n hostCommand = config.command\n hostMode = config.mode\n\n // Vite's own logger, so the plugin's lines obey `customLogger` and\n // `clearScreen` like every other line the dev server prints. It\n // colours and prefixes its own output, which is why nothing painted\n // reaches it.\n //\n // Ahead of the early return below, because `vite build` needs the\n // logger just as much and takes that return.\n host = {\n error: (message) => {\n config.logger.error(message)\n },\n info: (message) => {\n config.logger.info(message)\n },\n }\n\n // Nothing below concerns a build: only the dev server has a watcher,\n // and only its ignore list needs amending.\n if (config.command !== 'serve') return\n\n // Ahead of the resolution below, so the `config` function a consumer\n // wrote is told `watch: true` on this call as well as on every later\n // one. Setting it in `configureServer` alone was correct until this\n // hook started resolving configurations too.\n isWatching = true\n\n // **This is the last hook that can reach the ignore list.** Vite\n // builds the watcher from the resolved config, and `configureServer`\n // runs after it exists — `server.watcher` is a parameter there — so a\n // negation added then changes nothing. Measured on Vite 6.4.3, 7.3.6\n // and 8.3.0: amending it here reaches the watcher on all three, and\n // amending it in `configureServer` does not.\n //\n // The resolution is kept for `configureServer` to reuse rather than\n // discarded, because resolving is how a `config` function gets called\n // and doing it twice in one start-up would call the consumer's code an\n // extra time for nothing.\n try {\n startupResolved = await resolveConfigs()\n if (startupResolved.length === 0) return\n\n const { paths } = await getWatchTargets(startupResolved)\n const negations = nodeModulesNegations(paths)\n if (negations.length === 0) return\n\n // Appended to whatever the consumer asked for, not replacing it.\n const existing = config.server.watch?.ignored\n config.server.watch = {\n ...config.server.watch,\n ignored: [\n ...(Array.isArray(existing)\n ? existing\n : existing === undefined\n ? []\n : [existing]),\n ...negations,\n ],\n }\n } catch (err) {\n // A configuration that cannot be resolved is the build's problem to\n // report, and it will: `buildStart` resolves again and fails there\n // with the host watching. Throwing here would fail the dev server\n // before it started, for the sake of a watch-list refinement.\n log(\n `Could not read the configuration while preparing the watch list: ${errorMessage(err)}`,\n 'error',\n )\n startupResolved = undefined\n }\n },\n\n async configureServer(server: ViteDevServer) {\n // A dev server watches, by definition. Said here rather than left to\n // `adoptWatchMode` because this hook runs *before* `buildStart` —\n // `createServer` calls it, and `buildStart` waits for the plugin\n // container — so the first `config` function of the process would\n // otherwise be told `watch: false` while a dev server started up\n // around it.\n isWatching = true\n\n // `configResolved` has already resolved these, on its way to amending\n // the watcher's ignore list. Taken rather than copied, so a later\n // rebuild re-resolves as it always did.\n const resolved = startupResolved ?? (await resolveConfigs())\n startupResolved = undefined\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 // The `node_modules` half, which Vite's watcher cannot be made to\n // deliver on Windows — see `nodeModulesWatchDirectories`. Keyed by\n // directory so a configuration that changes can close the ones it no\n // longer needs rather than accumulating watchers for the session.\n const ownWatchers = new Map<string, fs.FSWatcher>()\n\n const watchNodeModules = (forPaths: string[]) => {\n const wanted = new Set(nodeModulesWatchDirectories(forPaths))\n\n for (const [directory, watcher] of ownWatchers) {\n if (wanted.has(directory)) continue\n watcher.close()\n ownWatchers.delete(directory)\n }\n\n for (const directory of wanted) {\n if (ownWatchers.has(directory)) continue\n\n try {\n const watcher = fs.watch(directory, (_event, filename) => {\n if (filename === null) return\n\n const changed = path.posix.join(directory, filename)\n if (\n !isWatchedSource(\n changed,\n targets.patterns,\n generatedDestinations,\n )\n )\n return\n\n void schedule(path.basename(changed)).catch(() => {})\n })\n\n // A watcher of ours must not be what keeps a process alive; the\n // dev server already is.\n watcher.unref()\n ownWatchers.set(directory, watcher)\n } catch {\n // A directory that cannot be watched is not a reason to fail a\n // dev server. Where the negation works — Linux, macOS — Vite's\n // own watcher is still delivering these events.\n }\n }\n }\n\n watchNodeModules(targets.paths)\n\n // Clearing the map is what makes this safe to call more than once.\n closeOwnWatchers = () => {\n for (const watcher of ownWatchers.values()) watcher.close()\n ownWatchers.clear()\n }\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 watchNodeModules(targets.paths)\n }\n\n if (errorOverlay) {\n // Whether the page is currently showing an overlay this plugin put\n // there. Only the clearing frame reads it: a success that follows a\n // success has no overlay to take down, and sending an update frame\n // for it would be traffic for nothing — and would spend the client's\n // one-time `isFirstUpdate`, which Vite uses to decide that an\n // overlay standing at the first update means a full reload.\n let overlayShowing = false\n\n notifyBuildOutcome = (error) => {\n if (error) {\n // Sent on every failure rather than only on the transition into\n // one. Vite's client replaces the overlay wholesale, so a repeat\n // is idempotent — and two different failures in a row must not\n // leave the first one's message on screen describing the second.\n overlayShowing = true\n server.hot.send({\n err: {\n message: error.message,\n plugin: 'unplugin-style-dictionary',\n stack: error.stack ?? '',\n },\n type: 'error',\n })\n return\n }\n\n if (!overlayShowing) return\n overlayShowing = false\n\n // Vite's protocol has no frame for \"take the overlay down\". The\n // client clears it when an update arrives, so an update carrying\n // nothing is the clear: it dismisses the overlay and then iterates\n // an empty list, reloading no page and touching no stylesheet.\n server.hot.send({ type: 'update', updates: [] })\n }\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, generatedDestinations))\n 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 adoptHost(this)\n adoptWatchMode(this)\n\n // Ahead of everything, including the flag below: a change reported\n // after the watcher closed earns no rebuild, so there is no re-entry\n // into `buildStart` for a flag to describe.\n if (hostClosed) return\n\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 (\n cachedPatterns &&\n !isWatchedSource(id, cachedPatterns, generatedDestinations)\n )\n 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, generatedDestinations)) 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, log)) {\n this.addWatchFile(file)\n }\n },\n\n webpack: adoptCompiler,\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 { UnpluginFactory } from 'unplugin'\nimport type { ViteDevServer } from 'vite'\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { createUnplugin } from 'unplugin'\n\nimport type { PluginInstance } from './compile.js'\nimport type { ResolvedConfig } from './config.js'\nimport type {\n StyleDictionaryConfigContext,\n UnpluginStyleDictionaryOptions,\n} from './types.js'\n\nimport { colourAllowed, paint } from './colour.js'\nimport { runBuilds } from './compile.js'\nimport { resolveConfigOption } from './config.js'\nimport { asError, errorMessage } from './errors.js'\nimport { expandPatterns, watchPatternsOf } from './patterns.js'\nimport { createScheduler } from './scheduler.js'\nimport { isWatchedSource } from './watch-filter.js'\n\nexport type * from './types.js'\n\n// Where the plugin's own lines go when a host offers somewhere better than the\n// console: Vite's `config.logger`, rollup's and rolldown's plugin context, or\n// webpack's `compilation`.\n//\n// **There is no `error` channel that merely reports.** Rollup's `this.error`\n// aborts the bundle — measured: a `buildStart` calling it ends the run with\n// `THREW: [plugin err-probe] fatal?` — so routing a failure report through it\n// would stop every build that reported one and silently override `failOnError`,\n// whose entire job is deciding that. A failure is therefore reported on the\n// host's warning channel, and whether the build stops stays `failOnError`'s\n// decision alone.\ninterface HostMessenger {\n error: (message: string) => void\n\n // Optional because not every host has somewhere for a progress line to go.\n // webpack's `stats` carries warnings and errors and nothing else, and\n // `Compiling design tokens...` is neither — so there it stays on the\n // console rather than being dressed up as a warning.\n info?: (message: string) => void\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// A host's message channel, narrowed by a predicate rather than asserted: what\n// a plugin context carries under `warn` is the host's business, and a cast\n// would only claim it is callable.\nfunction isMessageChannel(value: unknown): value is (message: string) => void {\n return typeof value === 'function'\n}\n\n// Vite builds its dev-server watcher with a fixed ignore list — `**/.git/**`,\n// `**/node_modules/**`, `**/test-results/**` and the cache directory — and\n// spreads the consumer's own `server.watch.ignored` entries in *after* them.\n// Entries are appended, never subtracted, so `server.watcher.add()` cannot\n// reach a path an earlier entry already covers.\n//\n// That makes a token package resolved through `node_modules` — the shape of\n// every workspace, `app/node_modules/@acme/tokens` symlinked to\n// `packages/tokens` — build correctly once and then never rebuild, with\n// nothing said about it. Measured on Vite 6.4.3, 7.3.6 and 8.3.0: zero watcher\n// events for an edit, while a token file outside the root but outside\n// `node_modules` rebuilt in the same run.\n//\n// A negation naming the file exactly is what un-ignores it, and is deliberately\n// the narrowest form that works. `!**/node_modules/**` would restore the whole\n// dependency tree to the watcher.\nfunction nodeModulesNegations(paths: string[]): string[] {\n const negations = new Set<string>()\n\n for (const file of paths) {\n const normalised = file.replace(/\\\\/g, '/')\n if (normalised.includes('/node_modules/')) negations.add(`!${normalised}`)\n }\n\n return Array.from(negations)\n}\n\n// The directories holding token files that resolve through `node_modules`.\n//\n// Vite's ignore list cannot be argued with on Windows. The negation below is\n// honoured on Linux and macOS, and there a token inside `node_modules` rebuilds\n// through the dev-server watcher like any other. On Windows it is not, and no\n// spelling of the negation changes that — measured on a `windows-latest`\n// runner: the file path, every ancestor directory, and the package subtree as\n// a globstar all leave the edit reaching no rebuild, while the same fixture\n// outside `node_modules` rebuilds. `server.watcher.add()` does not reach it\n// either, which is the same limit AGENTS.md already records for a path an\n// earlier ignore entry covers.\n//\n// A symlink is *not* what distinguishes them, which is worth stating because it\n// is the obvious suspect: a real directory inside `node_modules` fails exactly\n// as the symlinked one does, and a symlink outside it succeeds.\n//\n// So these directories get a watcher of the plugin's own, which Vite's ignore\n// list has no say over. It runs on every platform rather than behind a\n// `process.platform` check: one path that is exercised everywhere beats a\n// Windows-only branch that nothing else executes, and the scheduler already\n// collapses the duplicate trigger this produces where the negation also works.\nfunction nodeModulesWatchDirectories(paths: string[]): string[] {\n const directories = new Set<string>()\n\n for (const file of paths) {\n const normalised = file.replace(/\\\\/g, '/')\n if (!normalised.includes('/node_modules/')) continue\n\n // The directory rather than the file: `fs.watch` on a file stops reporting\n // once an editor replaces it by rename, which is what an atomic save does.\n directories.add(path.dirname(normalised))\n }\n\n return Array.from(directories)\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\n// Windows refuses a rename over a destination another process holds open, and\n// that is exactly the case the atomic write exists to serve: measured on a\n// `windows-latest` runner, the suite's own concurrent-reader case fails with\n// `EPERM: operation not permitted, rename`. So the feature inverts — the\n// compile fails rather than the read being protected.\n//\n// This **refutes** the reasoning that put the case in doubt. It was argued that\n// libuv opens files with `FILE_SHARE_DELETE`, so a concurrent reader would most\n// likely not block the rename. It blocks it.\n//\n// The blocking handle is transient — a reader, an indexer, a virus scanner —\n// so a short bounded backoff clears it. The bound matters as much as the retry:\n// a rename that genuinely cannot succeed has to fail rather than hang a dev\n// server, and the existing failure path already reports and lets `failOnError`\n// decide.\n//\n// Unreachable on Linux and macOS, where a rename over an open file succeeds.\nconst RENAME_RETRY_CODES = new Set(['EBUSY', 'EPERM'])\n\n// Doubling rather than a fixed interval, so the common case — a handle already\n// gone by the first retry — costs a millisecond rather than the whole budget.\n//\n// **The total is sized by what a dev server can tolerate waiting, not by how\n// long a handle usually persists.** An earlier version stopped at about 255ms,\n// which clears a transient hold and is not the situation that matters: a\n// consumer polling the generated file holds it for a large fraction of wall\n// time, and then no budget wins every race. What that produced was a rebuild\n// that failed roughly once in a hundred renames — which the suite's own\n// concurrent-reader case turns into a failure about once a run, because it\n// performs twenty of them. Intermittent, and on a platform where the whole\n// point of the atomic write is that a reader never sees a partial file.\n//\n// About two seconds is therefore the bound. A rebuild that takes a second is\n// something a dev server absorbs; one that fails is not. `write-file-atomic`\n// and `graceful-fs` both take this approach, the latter retrying for up to a\n// minute, so this is still the conservative end.\n//\n// The bound stays a bound: a rename that genuinely cannot succeed has to fail\n// rather than hang, and the unguarded attempt after the loop is what makes it.\nconst RENAME_RETRY_DELAYS_MS = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]\n\n// **The synchronous path gets a shorter budget, and the asymmetry is the\n// point.** `renameWithRetry` waits on a timer, so the event loop keeps serving\n// while it does; `renameWithRetrySync` waits on `Atomics.wait`, which blocks\n// everything — a dev server holding its main thread for two seconds is worse\n// than the failed rebuild the wait is trying to avoid.\n//\n// It is reached only through a Style Dictionary custom action's own\n// `vol.writeFileSync`, which is rare, and it keeps roughly the budget the async\n// path had before this change.\nconst RENAME_RETRY_DELAYS_SYNC_MS = [1, 2, 4, 8, 16, 32, 64, 128]\n\nfunction isRetryableRenameError(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'code' in error &&\n typeof error.code === 'string' &&\n RENAME_RETRY_CODES.has(error.code)\n )\n}\n\n// `Atomics.wait` rather than a spin on `Date.now()`, because the sync path has\n// no event loop to yield to and a busy loop would hold the CPU for the whole\n// backoff — on the one platform where the handle it is waiting for belongs to\n// another process.\nconst sleepSync = (ms: number): void => {\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)\n}\n\nasync function renameWithRetry(\n temporary: string,\n destination: string,\n): Promise<void> {\n for (const delay of RENAME_RETRY_DELAYS_MS) {\n try {\n await fs.promises.rename(temporary, destination)\n return\n } catch (err) {\n if (!isRetryableRenameError(err)) throw err\n await new Promise((resolve) => setTimeout(resolve, delay))\n }\n }\n\n // The last attempt is deliberately outside the loop and unguarded: the bound\n // is a bound, so whatever it throws here is what the caller sees.\n await fs.promises.rename(temporary, destination)\n}\n\nfunction renameWithRetrySync(temporary: string, destination: string): void {\n for (const delay of RENAME_RETRY_DELAYS_SYNC_MS) {\n try {\n fs.renameSync(temporary, destination)\n return\n } catch (err) {\n if (!isRetryableRenameError(err)) throw err\n sleepSync(delay)\n }\n }\n\n fs.renameSync(temporary, destination)\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 renameWithRetry(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 renameWithRetrySync(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// The fingerprints of configurations this process has compiled at least once.\n// It is what lets a configuration given as an object or a function be skipped\n// at all: such a configuration has no file to stat, so an edit to it inside\n// `vite.config.ts` moves no mtime and the filesystem cannot tell the two\n// apart. Having built it here, the plugin can — the fingerprint changes with\n// the configuration.\n//\n// Module scope rather than the factory's, for the same reason `compilesInFlight`\n// below is: the instances that would otherwise repeat the work are different\n// instances, so per-instance state cannot see them. A `vitest run` stands up\n// several, and a function configuration — the form the README recommends for\n// registering custom formats — would be the one form that never skipped.\n//\n// The fingerprint carries the root, so two projects in one process never share\n// one. A configuration given as a path needs none of this: its own file is one\n// of the sources the mtime comparison reads, so an edit to it is visible across\n// processes as well as within one.\nconst compiledFingerprints = new Set<string>()\n\n// A compile that is running right now, keyed by `buildKey`, so bundler\n// instances in one process wait on each other rather than each starting their\n// own.\n//\n// Generated token files are a side effect on the filesystem, not per-bundler\n// output, and one process routinely holds several instances of this plugin. A\n// single `vitest run` on a project with two test projects and browser mode\n// stands up five Vite servers — the root one, one per project, and one more\n// per project once its HTTP server listens — and every one of them runs\n// `buildStart`. `hasCompiled` cannot see any of that: it is closure state\n// inside the factory, so each instance has its own and each compiles.\n//\n// Module scope is the only place a shared answer can live, since the\n// instances know nothing about each other. It stays a claim about identical\n// work, never about identity: the key carries the root and the resolved\n// configurations, so one script building two packages shares nothing.\nconst compilesInFlight = new Map<string, Promise<void>>()\n\n// The slice of a webpack-shaped compiler this plugin touches, named rather\n// than imported. webpack and rspack each ship their own `Compiler` type, and\n// neither is assignable to the other, so a hook written against one cannot be\n// handed to the other's key. Both satisfy this structurally, which is what\n// makes `adoptCompiler` one function instead of two copies drifting apart.\ninterface BundlerCompiler {\n hooks: {\n beforeCompile: {\n tapPromise: (name: string, handler: () => Promise<void>) => void\n }\n compilation: {\n tap: (name: string, handler: (compilation: Compilation) => void) => void\n }\n done: { tap: (name: string, handler: () => void) => void }\n failed: { tap: (name: string, handler: () => void) => void }\n }\n options: { context?: string | undefined; mode?: string | undefined }\n watchMode: boolean\n}\n\n// Only the one array a report is pushed onto. webpack types it `WebpackError[]`\n// and rspack `Error[]`; both are arrays of something extending `Error`, so a\n// plain one is what this pushes on either.\ninterface Compilation {\n warnings: Error[]\n}\n\n// A stable identity for a set of resolved configurations, or `null` for one\n// that cannot have a stable identity at all.\n//\n// Functions are serialised by source rather than dropped, because a `format`\n// or `transform` written inline is exactly what distinguishes two otherwise\n// identical configurations — and `JSON.stringify` omits a function outright,\n// which would make two different builds look like one.\nfunction buildKey(root: string, resolved: ResolvedConfig[]): null | string {\n try {\n return JSON.stringify(\n [root, resolved.map((item) => item.file ?? item.config)],\n (_key, value: unknown) =>\n typeof value === 'function' ? `[fn]${String(value)}` : value,\n )\n } catch {\n // A configuration that will not serialise — a circular reference, a\n // BigInt — takes no shared identity rather than a wrong one, and compiles\n // exactly as it did before.\n return null\n }\n}\n\n// Not exported. It cannot be called in the form a reader would guess —\n// unplugin types the factory as `(options, meta)`, and `meta` is the\n// bundler-identifying `UnpluginContextMeta` a consumer would have to build by\n// hand — so publishing it offered a name that answered nothing. What a\n// consumer imports is the default export of the entry for their bundler.\nconst unpluginFactory: UnpluginFactory<\n undefined | UnpluginStyleDictionaryOptions,\n false\n> = (options = {}, meta) => {\n // webpack and rspack are the targets whose `buildStart` does not run before\n // the module graph is resolved: unplugin taps it on `make`, an\n // `AsyncParallelHook` that `EntryPlugin` taps too. The `webpack` and\n // `rspack` keys below compile on `beforeCompile` instead, which both await\n // before the compilation exists. rspack reimplements webpack's plugin API,\n // so everything this plugin does with a compiler is the same on either —\n // but unplugin dispatches them by separate keys, so the flag names both.\n const isWebpack = meta.framework === 'webpack' || meta.framework === 'rspack'\n const {\n cache = true,\n errorOverlay = true,\n failOnError = 'build',\n logLevel,\n onBuildEnd,\n onBuildError,\n onBuildStart,\n platforms: platformsOption,\n report = true,\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 // What the host is doing, for the function form of `config`. Populated where\n // each host knows the answer and read when that function is called — the\n // same shape as `root` and the message host above, and for the same reason:\n // `resolveConfigs` is reached from every hook that builds or watches, and\n // threading a context parameter through each of them would make every caller\n // restate what only the host can say.\n let hostCommand: 'build' | 'serve' = 'build'\n let hostMode: string | undefined\n let isWatching = false\n\n // `mode` is derived rather than invented where a host has no notion of one.\n // rollup and rolldown report nothing, and following `command` is the answer\n // Vite itself would give: its default mode is `development` serving and\n // `production` building.\n const configContext = (): StyleDictionaryConfigContext => ({\n command: hostCommand,\n mode: hostMode ?? (hostCommand === 'serve' ? 'development' : 'production'),\n watch: isWatching,\n })\n\n // Whether the host will keep rebuilding, as the plugin context reports it.\n // Read from `meta.watchMode`, which rollup, rolldown and Vite all carry and\n // webpack does not — there it comes off the compiler instead.\n const adoptWatchMode = (context: object): void => {\n const hookMeta: unknown = 'meta' in context ? context.meta : undefined\n if (typeof hookMeta !== 'object' || hookMeta === null) return\n\n const watching: unknown =\n 'watchMode' in hookMeta ? hookMeta.watchMode : undefined\n if (typeof watching === 'boolean') isWatching = watching\n }\n\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 // Whether the host has shut its watcher down. `await watcher.close()` is not\n // a promise that no build is in flight: rollup's `Watcher.close` clears the\n // pending build timeout, closes each task's file watcher and emits `close`,\n // and never awaits `run` — while `Task.run` checks `closed` only *after*\n // `rollupInternal` has resolved. So a build that has already entered\n // `rollupInternal` runs its `buildStart` hooks through to completion after\n // `close()` has returned to its caller, against a project that may be half\n // torn down by then. Measured on rollup 4.63.3 with no plugin of ours: a\n // `buildStart` reading a file 300ms after `close()` resolved gets ENOENT.\n //\n // `closeWatcher` is what makes that answerable. It runs synchronously inside\n // `close()`, and so before the in-flight hook resumes, which is the whole\n // reason a flag set there is worth setting. What it must not be is\n // `closeBundle`: that fires once per bundle — every `BUNDLE_END` a consumer\n // calls `result.close()` on — and would read as a shutdown on every rebuild.\n let hostClosed = false\n\n // Decided once, when the plugin is constructed, and held for its life. The\n // two streams are asked separately because they are redirected separately —\n // `build 2>err.log` leaves stdout a terminal and stderr a file.\n const stdoutColour = colourAllowed(process.stdout)\n const stderrColour = colourAllowed(process.stderr)\n\n // Where a message goes once a host has offered somewhere better than the\n // console. Set by `configResolved` under Vite, by the build hooks under\n // rollup and rolldown, and by the `webpack` block; left undefined when no\n // host has claimed it, which is every unit test binding its own context.\n let host: HostMessenger | undefined\n\n // Adopts a plugin context as the message host, if it has the channels — the\n // unit tests bind a context carrying `addWatchFile` and nothing else, and a\n // hook calling `this.warn` against that throws in a way that reads as a\n // plugin bug rather than as a missing stub.\n //\n // Only when nothing has claimed the host yet. Under Vite `configResolved`\n // has already installed the dev server's own logger, and `buildStart` runs\n // after it with a rollup-shaped context that would otherwise replace it.\n const adoptHost = (context: object): void => {\n if (host) return\n\n const warn: unknown = 'warn' in context ? context.warn : undefined\n if (!isMessageChannel(warn)) return\n\n const info: unknown = 'info' in context ? context.info : undefined\n\n host = {\n // `warn`, never `error`. Rollup's `this.error` aborts the bundle, so\n // reporting through it would stop every build that reported anything and\n // take the decision `failOnError` exists to make.\n error: (message) => {\n warn.call(context, message)\n },\n info: isMessageChannel(info)\n ? (message) => {\n info.call(context, message)\n }\n : undefined,\n }\n }\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 // The host renders and colours its own output, so nothing painted here\n // is handed to one — an escape inside a webpack `stats` entry survives\n // into `stats.toJson()` and into whatever reads it.\n if (host) {\n host.error(`${prefix} ${message}`)\n return\n }\n\n console.error(paint('31', `${prefix} ${message}`, stderrColour))\n return\n }\n\n if (quiet) return\n\n if (host?.info) {\n host.info(`${prefix} ${message}`)\n return\n }\n\n console.log(\n paint(\n type === 'success' ? '32' : '36',\n `${prefix} ${message}`,\n stdoutColour,\n ),\n )\n }\n\n // Resolve config file paths / objects. The work is `resolveConfigOption`'s;\n // what this adds is the instance it runs for, read at the moment of the call.\n // `root` is assigned by the host after the factory has run, so it is passed\n // as it stands now rather than as it stood when this was built.\n const resolveConfigs = async (): Promise<ResolvedConfig[]> =>\n resolveConfigOption({\n config: options.config,\n configContext,\n discovery,\n log,\n root,\n })\n\n // Parse token files to watch\n const getWatchTargets = async (\n resolvedConfigs: ResolvedConfig[],\n ): Promise<{ paths: string[]; patterns: string[] }> => {\n const patterns = await watchPatternsOf(\n { log, root, watch: options.watch },\n resolvedConfigs,\n )\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, log), patterns }\n }\n\n // What `runBuilds` reads from this plugin instance, built once. `root` and\n // the overlay callback go in as functions rather than values, because both\n // are assigned after this runs — see `PluginInstance`.\n const instance: PluginInstance = {\n cache,\n compiledFingerprints,\n failOnError,\n generatedDestinations,\n log,\n notifyBuildOutcome: (error) => {\n notifyBuildOutcome?.(error)\n },\n onBuildEnd,\n onBuildError,\n onBuildStart,\n platformsOption,\n quiet,\n report,\n root: () => root,\n stdoutColour,\n verbosity,\n volume: atomicVolume,\n watch: options.watch,\n }\n\n // `runBuilds` for the first build of a process, with the compile shared\n // between every plugin instance that wants the same one.\n //\n // An instance arriving while a compile for the same key is running waits on\n // that compile instead of starting a second. It is the concurrent half that\n // needs this: an up-to-date check compares what is on disk against the\n // sources, and two instances that start together have nothing on disk to\n // compare against yet, so only a shared promise can tell them apart from\n // two genuinely separate builds.\n //\n // The entry is dropped as soon as the compile settles, so this coalesces\n // rather than caches — a later `buildStart` still compiles. Skipping one\n // whose output is already current is #212's up-to-date check, and belongs\n // with it rather than as a second mechanism here.\n //\n // A rejection reaches every waiter, which is the point: an instance that\n // waited on a failed compile must not carry on as though the tokens were\n // written. Whether that rejection is thrown at all is `failOnError`'s\n // decision, already made inside `runBuilds`.\n const compileOnceAcrossInstances = async (\n resolvedConfigs: ResolvedConfig[],\n ): Promise<void> => {\n const key = buildKey(root, resolvedConfigs)\n if (key === null) {\n await runBuilds(instance, resolvedConfigs)\n return\n }\n\n const running = compilesInFlight.get(key)\n if (running) {\n await running\n return\n }\n\n const compile = runBuilds(instance, resolvedConfigs)\n compilesInFlight.set(key, compile)\n\n try {\n await compile\n } finally {\n compilesInFlight.delete(key)\n }\n }\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 // Also set by `configureServer`: closes the `node_modules` watchers it\n // opened. Called from `buildEnd` rather than registered on the server's own\n // shutdown, because nothing on the server fires in every mode — see the\n // `vite` block's `buildEnd`.\n let closeOwnWatchers: (() => void) | undefined\n\n // Whether the discovered path has been announced. Once per plugin instance:\n // `resolveConfigs` runs on every build and rebuild, and a dev server would\n // otherwise repeat the line for the rest of the session.\n //\n // An object rather than a flag, because it is handed to\n // `resolveConfigOption` to set: that function serves every instance in the\n // process, so the record of what this one has said has to stay here.\n const discovery = { announced: false }\n\n // Resolved by `configResolved` so it can amend the watcher's ignore list, and\n // handed to `configureServer` rather than resolved again — one start-up, one\n // call of the consumer's `config` function.\n let startupResolved: ResolvedConfig[] | undefined\n\n // Also set by `configureServer`, and left undefined everywhere else: this is\n // how a compile outcome reaches Vite's error overlay. It is deliberately not\n // the same path as `failOnError`.\n //\n // `failOnError` decides whether the host stops; this decides whether the\n // browser is told. Under a dev server the default is not to stop, so the\n // failure is reported and swallowed — and that is exactly the case where the\n // page is left rendering the last good file with nothing to say it is stale.\n // Reading the outcome off whether `runBuilds` threw would therefore see\n // nothing at all on the only configuration that matters.\n let notifyBuildOutcome: ((error: Error | null) => void) | undefined\n\n // A rebuild, as the scheduler runs it: resolve, compile, and bring the dev\n // server's watch list up to date. A failure is rethrown once it has been\n // reported, so the scheduler can hand it to every trigger this covered.\n const rebuild = async (reason: string): Promise<void> => {\n let compiling = false\n\n try {\n const resolved = await resolveConfigs()\n if (resolved.length > 0) {\n compiling = true\n await runBuilds(instance, resolved, reason)\n compiling = false\n hasCompiled = true\n await refreshServerWatchList?.(resolved)\n }\n } catch (err) {\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. They reach the overlay for the same reason: from the\n // page's point of view the rebuild failed, whichever half of it did.\n if (!compiling) {\n log(`Rebuild failed: ${errorMessage(err)}`, 'error')\n notifyBuildOutcome?.(asError(err))\n }\n\n throw err\n }\n }\n\n // One rebuild per burst of watcher events, and never two at once — see\n // `createScheduler`. Created here, once per plugin instance: a debounce two\n // instances shared would run one instance's rebuild and drop the other's.\n const schedule = createScheduler({\n debounceMs: 50,\n isClosed: () => hostClosed,\n run: rebuild,\n })\n\n // Every host that runs a rollup-shaped watcher calls this on shutdown, and\n // all three get the same handler below. There is deliberately no webpack\n // equivalent here: it has no `closeWatcher`, its nearest thing is\n // `compiler.hooks.watchClose`, and nothing measured shows it exposed.\n //\n // It raises the flag and nothing else. A debounce timer armed before the\n // close is deliberately left to fire: the rebuild it runs is one the host\n // asked for while the project was still whole, and `rebuild` reports its\n // own failures. Cancelling it would be a guard no test could fail on, since a\n // trigger arriving after the close is declined by `schedule` instead.\n const closeWatcher = (): void => {\n hostClosed = true\n }\n\n // What both webpack-shaped hosts do with a compiler, written once.\n // rspack reimplements webpack's plugin API hook for hook, but ships its\n // own `Compiler` type and unplugin dispatches the two through separate\n // keys — so a function typed against either one rejects the other. Naming\n // the surface actually used is what lets one implementation serve both.\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 const adoptCompiler = (compiler: BundlerCompiler): void => {\n if (rootOption === undefined) {\n root = compiler.options.context ?? process.cwd()\n }\n\n // webpack's `buildStart` context carries no `meta`, so neither half of\n // the build context can come from there. `mode` is a webpack option, and\n // `watchMode` is only true once `watch()` has been called — which is\n // after this runs, so it is read per compile below rather than here.\n hostMode = compiler.options.mode\n\n // The compile happens in `beforeCompile`, which webpack awaits *before*\n // the compilation exists — so a message from it has nothing to attach to\n // yet and is held until one appears.\n //\n // Only failures are routed. `stats` carries warnings and errors and\n // nothing else, so the progress lines stay on the console rather than\n // being reported as warnings they are not.\n //\n // A warning rather than an error, for the same reason as on rollup: this\n // is the report, and `failOnError` decides separately whether the build\n // stops. Pushing to `compilation.errors` would fail a webpack build that\n // asked not to be failed.\n const pending: string[] = []\n host = {\n error: (message) => {\n pending.push(message)\n },\n }\n\n compiler.hooks.compilation.tap(\n 'unplugin-style-dictionary',\n (compilation) => {\n for (const message of pending.splice(0)) {\n const reported = new Error(message)\n reported.name = 'UnpluginStyleDictionaryWarning'\n compilation.warnings.push(reported)\n }\n },\n )\n\n // A `beforeCompile` that throws ends the run without ever creating a\n // compilation, and that is exactly the case that produced the message.\n // Left to the buffer it would be reported nowhere at all, so whatever is\n // still held when the run ends goes to the console after all.\n const drainToConsole = () => {\n for (const message of pending.splice(0)) {\n console.error(paint('31', message, stderrColour))\n }\n }\n compiler.hooks.failed.tap('unplugin-style-dictionary', drainToConsole)\n compiler.hooks.done.tap('unplugin-style-dictionary', drainToConsole)\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 isWatching = compiler.watchMode\n\n const resolved = await resolveConfigs()\n if (resolved.length === 0) return\n\n await compileOnceAcrossInstances(resolved)\n hasCompiled = true\n },\n )\n }\n\n return {\n async buildStart() {\n adoptHost(this)\n adoptWatchMode(this)\n\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 //\n // Skipped outright once the host has closed, because rollup discards the\n // result: with the task closed, `Task.run` returns before\n // `updateWatchedFiles`, so every path registered here goes nowhere. What\n // deriving it does still do is read each config file — with\n // its errors reported — and report an ENOENT for a project the host is\n // in the middle of tearing down. That report was the one thing this\n // block contributed after a close.\n if (!hostClosed) {\n const { paths } = await getWatchTargets(resolved)\n for (const file of paths) {\n this.addWatchFile(file)\n }\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 compileOnceAcrossInstances(resolved)\n hasCompiled = true\n },\n\n name: 'unplugin-style-dictionary',\n\n // `closeWatcher` is a rollup-shaped hook and `UnpluginOptions` declares no\n // top-level equivalent, so it is registered per target instead: rolldown\n // lists it among its input plugin hooks, and Vite's plugin type is\n // rollup's, which is what carries it to `vite build --watch`. Vite's dev\n // server runs no rollup watcher, so there it simply never fires.\n rolldown: { closeWatcher },\n\n rollup: { closeWatcher },\n\n // unplugin calls the matching key from inside `apply(compiler)` and\n // never both, so the two share one implementation rather than one\n // delegating to the other.\n rspack: adoptCompiler,\n\n vite: {\n // **`buildEnd` is the hook a dev server's close reliably reaches.** It\n // replaced `server.httpServer?.once('close', …)`, which middleware mode\n // — Express, Koa, most SSR — has no `httpServer` for, so the optional\n // chain registered nothing and every restart leaked the watchers.\n //\n // Measured on Vite 6.4.3, 7.3.6 and 8.3.0, in middleware mode and\n // listening alike: while a server runs only `buildStart` fires, and on\n // `server.close()` it is `buildEnd` once and `closeBundle` twice.\n // **`closeWatcher` fires in neither mode on any of them**, which is why\n // this is not the `closeWatcher` handler registered below.\n //\n // `buildEnd` also fires at the end of `vite build`, and on every rebuild\n // of `vite build --watch`. Neither runs `configureServer`, so there is\n // nothing to close there and this is a no-op.\n buildEnd() {\n closeOwnWatchers?.()\n },\n\n closeWatcher,\n\n async configResolved(config) {\n if (rootOption === undefined) root = config.root || process.cwd()\n\n // The only host that has both. `command` is what makes `'serve'`\n // reachable at all, since nothing else here serves.\n hostCommand = config.command\n hostMode = config.mode\n\n // Vite's own logger, so the plugin's lines obey `customLogger` and\n // `clearScreen` like every other line the dev server prints. It\n // colours and prefixes its own output, which is why nothing painted\n // reaches it.\n //\n // Ahead of the early return below, because `vite build` needs the\n // logger just as much and takes that return.\n host = {\n error: (message) => {\n config.logger.error(message)\n },\n info: (message) => {\n config.logger.info(message)\n },\n }\n\n // Nothing below concerns a build: only the dev server has a watcher,\n // and only its ignore list needs amending.\n if (config.command !== 'serve') return\n\n // Ahead of the resolution below, so the `config` function a consumer\n // wrote is told `watch: true` on this call as well as on every later\n // one. Setting it in `configureServer` alone was correct until this\n // hook started resolving configurations too.\n isWatching = true\n\n // **This is the last hook that can reach the ignore list.** Vite\n // builds the watcher from the resolved config, and `configureServer`\n // runs after it exists — `server.watcher` is a parameter there — so a\n // negation added then changes nothing. Measured on Vite 6.4.3, 7.3.6\n // and 8.3.0: amending it here reaches the watcher on all three, and\n // amending it in `configureServer` does not.\n //\n // The resolution is kept for `configureServer` to reuse rather than\n // discarded, because resolving is how a `config` function gets called\n // and doing it twice in one start-up would call the consumer's code an\n // extra time for nothing.\n try {\n startupResolved = await resolveConfigs()\n if (startupResolved.length === 0) return\n\n const { paths } = await getWatchTargets(startupResolved)\n const negations = nodeModulesNegations(paths)\n if (negations.length === 0) return\n\n // Appended to whatever the consumer asked for, not replacing it.\n const existing = config.server.watch?.ignored\n config.server.watch = {\n ...config.server.watch,\n ignored: [\n ...(Array.isArray(existing)\n ? existing\n : existing === undefined\n ? []\n : [existing]),\n ...negations,\n ],\n }\n } catch (err) {\n // A configuration that cannot be resolved is the build's problem to\n // report, and it will: `buildStart` resolves again and fails there\n // with the host watching. Throwing here would fail the dev server\n // before it started, for the sake of a watch-list refinement.\n log(\n `Could not read the configuration while preparing the watch list: ${errorMessage(err)}`,\n 'error',\n )\n startupResolved = undefined\n }\n },\n\n async configureServer(server: ViteDevServer) {\n // A dev server watches, by definition. Said here rather than left to\n // `adoptWatchMode` because this hook runs *before* `buildStart` —\n // `createServer` calls it, and `buildStart` waits for the plugin\n // container — so the first `config` function of the process would\n // otherwise be told `watch: false` while a dev server started up\n // around it.\n isWatching = true\n\n // `configResolved` has already resolved these, on its way to amending\n // the watcher's ignore list. Taken rather than copied, so a later\n // rebuild re-resolves as it always did.\n const resolved = startupResolved ?? (await resolveConfigs())\n startupResolved = undefined\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 // The `node_modules` half, which Vite's watcher cannot be made to\n // deliver on Windows — see `nodeModulesWatchDirectories`. Keyed by\n // directory so a configuration that changes can close the ones it no\n // longer needs rather than accumulating watchers for the session.\n const ownWatchers = new Map<string, fs.FSWatcher>()\n\n const watchNodeModules = (forPaths: string[]) => {\n const wanted = new Set(nodeModulesWatchDirectories(forPaths))\n\n for (const [directory, watcher] of ownWatchers) {\n if (wanted.has(directory)) continue\n watcher.close()\n ownWatchers.delete(directory)\n }\n\n for (const directory of wanted) {\n if (ownWatchers.has(directory)) continue\n\n try {\n const watcher = fs.watch(directory, (_event, filename) => {\n if (filename === null) return\n\n const changed = path.posix.join(directory, filename)\n if (\n !isWatchedSource(\n changed,\n targets.patterns,\n generatedDestinations,\n )\n )\n return\n\n void schedule(path.basename(changed)).catch(() => {})\n })\n\n // A watcher of ours must not be what keeps a process alive; the\n // dev server already is.\n watcher.unref()\n ownWatchers.set(directory, watcher)\n } catch {\n // A directory that cannot be watched is not a reason to fail a\n // dev server. Where the negation works — Linux, macOS — Vite's\n // own watcher is still delivering these events.\n }\n }\n }\n\n watchNodeModules(targets.paths)\n\n // Clearing the map is what makes this safe to call more than once.\n closeOwnWatchers = () => {\n for (const watcher of ownWatchers.values()) watcher.close()\n ownWatchers.clear()\n }\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 watchNodeModules(targets.paths)\n }\n\n if (errorOverlay) {\n // Whether the page is currently showing an overlay this plugin put\n // there. Only the clearing frame reads it: a success that follows a\n // success has no overlay to take down, and sending an update frame\n // for it would be traffic for nothing — and would spend the client's\n // one-time `isFirstUpdate`, which Vite uses to decide that an\n // overlay standing at the first update means a full reload.\n let overlayShowing = false\n\n notifyBuildOutcome = (error) => {\n if (error) {\n // Sent on every failure rather than only on the transition into\n // one. Vite's client replaces the overlay wholesale, so a repeat\n // is idempotent — and two different failures in a row must not\n // leave the first one's message on screen describing the second.\n overlayShowing = true\n server.hot.send({\n err: {\n message: error.message,\n plugin: 'unplugin-style-dictionary',\n stack: error.stack ?? '',\n },\n type: 'error',\n })\n return\n }\n\n if (!overlayShowing) return\n overlayShowing = false\n\n // Vite's protocol has no frame for \"take the overlay down\". The\n // client clears it when an update arrives, so an update carrying\n // nothing is the clear: it dismisses the overlay and then iterates\n // an empty list, reloading no page and touching no stylesheet.\n server.hot.send({ type: 'update', updates: [] })\n }\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, generatedDestinations))\n 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 adoptHost(this)\n adoptWatchMode(this)\n\n // Ahead of everything, including the flag below: a change reported\n // after the watcher closed earns no rebuild, so there is no re-entry\n // into `buildStart` for a flag to describe.\n if (hostClosed) return\n\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 (\n cachedPatterns &&\n !isWatchedSource(id, cachedPatterns, generatedDestinations)\n )\n 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, generatedDestinations)) 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, log)) {\n this.addWatchFile(file)\n }\n },\n\n webpack: adoptCompiler,\n }\n}\n\nexport const unplugin = /* #__PURE__ */ createUnplugin(unpluginFactory)\n\nexport default unplugin\n"],"mappings":"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"}
package/dist/patterns.js CHANGED
@@ -36,7 +36,7 @@ async function patternsMatchingNothing(patterns) {
36
36
  function sourcePatternsOf(configObj) {
37
37
  const patterns = [];
38
38
  const add = (pattern) => {
39
- if (typeof pattern === "string") patterns.push((path.isAbsolute(pattern) ? pattern : path.resolve(process.cwd(), pattern)).replace(/\\/g, "/"));
39
+ if (typeof pattern === "string" && pattern !== "") patterns.push((path.isAbsolute(pattern) ? pattern : path.resolve(process.cwd(), pattern)).replace(/\\/g, "/"));
40
40
  };
41
41
  for (const value of [configObj.source, configObj.include]) if (Array.isArray(value)) value.forEach(add);
42
42
  else add(value);
@@ -47,22 +47,7 @@ async function watchPatternsOf({ log, root, watch }, resolvedConfigs) {
47
47
  for (const item of resolvedConfigs) {
48
48
  if (item.file) filesToWatch.add(item.file.replace(/\\/g, "/"));
49
49
  const configObj = await readConfigObject(item, log);
50
- if (configObj) {
51
- const addPattern = (pattern) => {
52
- if (typeof pattern === "string") {
53
- const normalized = (path.isAbsolute(pattern) ? pattern : path.resolve(process.cwd(), pattern)).replace(/\\/g, "/");
54
- filesToWatch.add(normalized);
55
- }
56
- };
57
- if (configObj.source) {
58
- if (Array.isArray(configObj.source)) configObj.source.forEach(addPattern);
59
- else addPattern(configObj.source);
60
- }
61
- if (configObj.include) {
62
- if (Array.isArray(configObj.include)) configObj.include.forEach(addPattern);
63
- else addPattern(configObj.include);
64
- }
65
- }
50
+ if (configObj) for (const pattern of sourcePatternsOf(configObj)) filesToWatch.add(pattern);
66
51
  }
67
52
  if (watch) {
68
53
  const extraWatches = Array.isArray(watch) ? watch : [watch];
@@ -1 +1 @@
1
- {"version":3,"file":"patterns.js","names":[],"sources":["../src/patterns.ts"],"sourcesContent":["// The `source` and `include` patterns a configuration reads, the watch list\n// derived from them, and the paths a watcher is actually handed for it.\n//\n// Nothing here holds state. What a plugin instance owns — its root, its\n// logger, its `watch` option — is handed in on each call.\n\nimport type { Config } from 'style-dictionary'\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { glob } from 'tinyglobby'\n\nimport type { Log, ResolvedConfig } from './config.js'\nimport type { UnpluginStyleDictionaryOptions } from './types.js'\n\nimport { readConfigObject } from './config.js'\nimport { errorMessage } from './errors.js'\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// 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.\nexport async function expandPatterns(\n patterns: string[],\n log: Log,\n): 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// Which of a configuration's own `source`/`include` patterns match no file on\n// disk. Only for diagnosis: it is the emptiness of the resolved token set that\n// decides whether a build fails, because only that catches every route to an\n// empty set. This names the pattern at fault, which the token count cannot, and\n// it reports a mistyped pattern in a configuration whose others still match —\n// where nothing fails at all and one platform quietly loses its tokens.\nexport async function patternsMatchingNothing(\n patterns: string[],\n): Promise<string[]> {\n const barren: string[] = []\n\n for (const pattern of patterns) {\n // A literal path is a `stat`, not a glob: `tinyglobby` treats a path with\n // no magic characters as a literal anyway, and this keeps the common case\n // off the filesystem walk.\n if (!GLOB_CHARACTERS.test(pattern)) {\n if (!fs.existsSync(pattern)) barren.push(pattern)\n continue\n }\n\n try {\n const matched = await glob([pattern], { absolute: true })\n if (matched.length === 0) barren.push(pattern)\n } catch {\n // A pattern that cannot even be globbed is the build's problem to\n // report; saying it twice, in a diagnostic, helps nobody.\n }\n }\n\n return barren\n}\n\n// The patterns one configuration reads, resolved the way the build resolves\n// them. The same `source`/`include` walk `watchPatternsOf` does, for one\n// item rather than the whole set — against the working directory, because\n// that is where Style Dictionary's own `combineJSON` globs them.\nexport function sourcePatternsOf(configObj: Config): string[] {\n const patterns: string[] = []\n\n const add = (pattern: unknown) => {\n if (typeof pattern === 'string') {\n patterns.push(\n (path.isAbsolute(pattern)\n ? pattern\n : path.resolve(process.cwd(), pattern)\n ).replace(/\\\\/g, '/'),\n )\n }\n }\n\n for (const value of [configObj.source, configObj.include]) {\n if (Array.isArray(value)) value.forEach(add)\n else add(value)\n }\n\n return patterns\n}\n\n// Parse token files to watch\n//\n// The patterns alone. Expanding them into the paths a watcher takes, and\n// remembering them for `watchChange` to filter against, is the caller's: the\n// second is state of one plugin instance, and this module holds none.\nexport async function watchPatternsOf(\n {\n log,\n root,\n watch,\n }: {\n log: Log\n root: string\n watch: UnpluginStyleDictionaryOptions['watch']\n },\n resolvedConfigs: ResolvedConfig[],\n): Promise<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, log)\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 (watch) {\n const extraWatches = Array.isArray(watch) ? watch : [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 return Array.from(filesToWatch)\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"],"mappings":"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"}
1
+ {"version":3,"file":"patterns.js","names":[],"sources":["../src/patterns.ts"],"sourcesContent":["// The `source` and `include` patterns a configuration reads, the watch list\n// derived from them, and the paths a watcher is actually handed for it.\n//\n// Nothing here holds state. What a plugin instance owns — its root, its\n// logger, its `watch` option — is handed in on each call.\n\nimport type { Config } from 'style-dictionary'\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { glob } from 'tinyglobby'\n\nimport type { Log, ResolvedConfig } from './config.js'\nimport type { UnpluginStyleDictionaryOptions } from './types.js'\n\nimport { readConfigObject } from './config.js'\nimport { errorMessage } from './errors.js'\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// 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.\nexport async function expandPatterns(\n patterns: string[],\n log: Log,\n): 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// Which of a configuration's own `source`/`include` patterns match no file on\n// disk. Only for diagnosis: it is the emptiness of the resolved token set that\n// decides whether a build fails, because only that catches every route to an\n// empty set. This names the pattern at fault, which the token count cannot, and\n// it reports a mistyped pattern in a configuration whose others still match —\n// where nothing fails at all and one platform quietly loses its tokens.\nexport async function patternsMatchingNothing(\n patterns: string[],\n): Promise<string[]> {\n const barren: string[] = []\n\n for (const pattern of patterns) {\n // A literal path is a `stat`, not a glob: `tinyglobby` treats a path with\n // no magic characters as a literal anyway, and this keeps the common case\n // off the filesystem walk.\n if (!GLOB_CHARACTERS.test(pattern)) {\n if (!fs.existsSync(pattern)) barren.push(pattern)\n continue\n }\n\n try {\n const matched = await glob([pattern], { absolute: true })\n if (matched.length === 0) barren.push(pattern)\n } catch {\n // A pattern that cannot even be globbed is the build's problem to\n // report; saying it twice, in a diagnostic, helps nobody.\n }\n }\n\n return barren\n}\n\n// The patterns one configuration reads, resolved the way the build resolves\n// them. The watch list, the up-to-date check and the empty-token-set message\n// all read a configuration's patterns through here, so they cannot disagree\n// about which files it reads.\n//\n// Against the working directory, because that is where Style Dictionary\n// resolves them: `combineJSON` globs each pattern with no `cwd` of its own.\n// Resolving against the configuration file's directory instead is how the\n// watch list came to name paths the build never reads — a configuration in a\n// subdirectory built correctly and watched nothing at all.\n//\n// An empty pattern is skipped, because Style Dictionary reads nothing from one.\n// Resolved, it would be the working directory itself, and the watch list used\n// to register exactly that for an empty entry in a `source` array.\nexport function sourcePatternsOf(configObj: Config): string[] {\n const patterns: string[] = []\n\n const add = (pattern: unknown) => {\n if (typeof pattern === 'string' && pattern !== '') {\n patterns.push(\n (path.isAbsolute(pattern)\n ? pattern\n : path.resolve(process.cwd(), pattern)\n ).replace(/\\\\/g, '/'),\n )\n }\n }\n\n for (const value of [configObj.source, configObj.include]) {\n if (Array.isArray(value)) value.forEach(add)\n else add(value)\n }\n\n return patterns\n}\n\n// Parse token files to watch\n//\n// The patterns alone. Expanding them into the paths a watcher takes, and\n// remembering them for `watchChange` to filter against, is the caller's: the\n// second is state of one plugin instance, and this module holds none.\nexport async function watchPatternsOf(\n {\n log,\n root,\n watch,\n }: {\n log: Log\n root: string\n watch: UnpluginStyleDictionaryOptions['watch']\n },\n resolvedConfigs: ResolvedConfig[],\n): Promise<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, log)\n\n if (configObj) {\n for (const pattern of sourcePatternsOf(configObj)) {\n filesToWatch.add(pattern)\n }\n }\n }\n\n // Add manually configured watch files\n if (watch) {\n const extraWatches = Array.isArray(watch) ? watch : [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 return Array.from(filesToWatch)\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"],"mappings":";;;;;;AAsBA,MAAM,kBAAkB;AASxB,eAAsB,eACpB,UACA,KACmB;CACnB,MAAM,wBAAQ,IAAI,IAAY;CAC9B,MAAM,QAAkB,CAAC;CAEzB,KAAK,MAAM,WAAW,UACpB,IAAI,gBAAgB,KAAK,OAAO,GAAG;EACjC,MAAM,KAAK,OAAO;EAKlB,MAAM,SAAS,eAAe,OAAO;EACrC,IAAI,UAAU,GAAG,WAAW,MAAM,GAAG,MAAM,IAAI,MAAM;CACvD,OACE,MAAM,IAAI,OAAO;CAIrB,IAAI,MAAM,SAAS,GACjB,IAAI;EAIF,KAAK,MAAM,SAAS,MAAM,KAAK,OAAO,EAAE,UAAU,KAAK,CAAC,GACtD,MAAM,IAAI,MAAM,QAAQ,OAAO,GAAG,CAAC;CAEvC,SAAS,KAAK;EACZ,IAAI,oCAAoC,aAAa,GAAG,KAAK,OAAO;CACtE;CAGF,OAAO,MAAM,KAAK,KAAK;AACzB;AAQA,eAAsB,wBACpB,UACmB;CACnB,MAAM,SAAmB,CAAC;CAE1B,KAAK,MAAM,WAAW,UAAU;EAI9B,IAAI,CAAC,gBAAgB,KAAK,OAAO,GAAG;GAClC,IAAI,CAAC,GAAG,WAAW,OAAO,GAAG,OAAO,KAAK,OAAO;GAChD;EACF;EAEA,IAAI;GAEF,KAAI,MADkB,KAAK,CAAC,OAAO,GAAG,EAAE,UAAU,KAAK,CAAC,EAAA,CAC5C,WAAW,GAAG,OAAO,KAAK,OAAO;EAC/C,QAAQ,CAGR;CACF;CAEA,OAAO;AACT;AAgBA,SAAgB,iBAAiB,WAA6B;CAC5D,MAAM,WAAqB,CAAC;CAE5B,MAAM,OAAO,YAAqB;EAChC,IAAI,OAAO,YAAY,YAAY,YAAY,IAC7C,SAAS,MACN,KAAK,WAAW,OAAO,IACpB,UACA,KAAK,QAAQ,QAAQ,IAAI,GAAG,OAAO,EAAA,CACrC,QAAQ,OAAO,GAAG,CACtB;CAEJ;CAEA,KAAK,MAAM,SAAS,CAAC,UAAU,QAAQ,UAAU,OAAO,GACtD,IAAI,MAAM,QAAQ,KAAK,GAAG,MAAM,QAAQ,GAAG;MACtC,IAAI,KAAK;CAGhB,OAAO;AACT;AAOA,eAAsB,gBACpB,EACE,KACA,MACA,SAMF,iBACmB;CACnB,MAAM,+BAAe,IAAI,IAAY;CAErC,KAAK,MAAM,QAAQ,iBAAiB;EAClC,IAAI,KAAK,MACP,aAAa,IAAI,KAAK,KAAK,QAAQ,OAAO,GAAG,CAAC;EAGhD,MAAM,YAAY,MAAM,iBAAiB,MAAM,GAAG;EAElD,IAAI,WACF,KAAK,MAAM,WAAW,iBAAiB,SAAS,GAC9C,aAAa,IAAI,OAAO;CAG9B;CAGA,IAAI,OAAO;EACT,MAAM,eAAe,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;EAC1D,KAAK,MAAM,WAAW,cAAc;GAClC,MAAM,kBAAkB,KAAK,WAAW,OAAO,IAC3C,UACA,KAAK,QAAQ,MAAM,OAAO;GAC9B,aAAa,IAAI,gBAAgB,QAAQ,OAAO,GAAG,CAAC;EACtD;CACF;CAEA,OAAO,MAAM,KAAK,YAAY;AAChC;AAOA,SAAS,eAAe,SAAyB;CAC/C,MAAM,WAAW,QAAQ,MAAM,GAAG;CAClC,MAAM,YAAY,SAAS,WAAW,YACpC,gBAAgB,KAAK,OAAO,CAC9B;CAEA,OAAO,cAAc,KACjB,KAAK,MAAM,QAAQ,OAAO,IAC1B,SAAS,MAAM,GAAG,SAAS,CAAC,CAAC,KAAK,GAAG;AAC3C"}
package/dist/types.d.ts CHANGED
@@ -314,7 +314,8 @@ export interface UnpluginStyleDictionaryOptions {
314
314
  */
315
315
  report?: boolean;
316
316
  /**
317
- * The directory a relative `config` path is looked up in.
317
+ * The directory a relative `config` path is looked up in, and a relative
318
+ * `watch` entry is resolved against.
318
319
  *
319
320
  * Defaults to the host's own root — Vite's `root`, webpack's `context` —
320
321
  * and to the working directory for rollup and rolldown, which offer none.
@@ -348,6 +349,13 @@ export interface UnpluginStyleDictionaryOptions {
348
349
  * pattern's own directory is registered alongside them, so a token file
349
350
  * created later is noticed too.
350
351
  *
352
+ * A relative entry is resolved against `root`, like a relative `config`: both
353
+ * are options of this plugin rather than paths inside a configuration. That
354
+ * is not the base `source` and `include` use — Style Dictionary reads those
355
+ * against the working directory — so where the host's root is somewhere
356
+ * else, name a file here relative to the root, and in `source` relative to
357
+ * where the build runs.
358
+ *
351
359
  * What a change to a watched file then triggers is the host's to decide —
352
360
  * see the interface documentation above.
353
361
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kanso-labs/unplugin-style-dictionary",
3
- "version": "0.10.6",
3
+ "version": "0.10.7",
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",
@@ -94,7 +94,7 @@
94
94
  "globals": "17.12.0",
95
95
  "husky": "9.1.7",
96
96
  "lint-staged": "17.5.1",
97
- "oxfmt": "0.68.0",
97
+ "oxfmt": "0.69.0",
98
98
  "oxlint": "1.83.0",
99
99
  "oxlint-tsgolint": "7.0.2002",
100
100
  "publint": "0.3.24",