@kanso-labs/unplugin-style-dictionary 0.10.13 → 0.10.15

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.
@@ -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 { escapePath, 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.\n//\n// That is why it is only ever asked of what a consumer wrote. The plugin\n// makes a relative pattern absolute by prefixing a directory of its own\n// choosing, and that directory's characters are no part of the pattern.\nconst GLOB_CHARACTERS = /[!*?[\\]{}]/\n\n// The patterns a watch list is made of, split by what each one is. The split\n// is decided once, from the pattern as the consumer wrote it, and never again\n// from the absolute string: that string carries the project's own path, and a\n// project in `Dropbox (Personal)` or `app [v2]` would read as a glob.\n//\n// `literals` are absolute paths, matched by equality and registered as they\n// are. `globs` are absolute patterns in picomatch's syntax, in which the part\n// the plugin prefixed is escaped.\nexport interface WatchPatterns {\n globs: string[]\n literals: string[]\n}\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 { globs, literals }: WatchPatterns,\n log: Log,\n): Promise<string[]> {\n const paths = new Set<string>(literals)\n\n for (const pattern of globs) {\n // Watching the directory as well as its current contents. chokidar\n // reports a creation inside a watched directory, which is the only way a\n // token file added later is ever noticed.\n const parent = staticParentOf(pattern)\n if (parent && fs.existsSync(parent)) paths.add(parent)\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 globs,\n literals,\n}: WatchPatterns): Promise<string[]> {\n // A literal path is a `stat`, not a glob, which keeps the common case off\n // the filesystem walk.\n const barren = literals.filter((literal) => !fs.existsSync(literal))\n\n for (const pattern of globs) {\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): WatchPatterns {\n const patterns: WatchPatterns = { globs: [], literals: [] }\n\n const add = (pattern: unknown) => {\n if (typeof pattern === 'string' && pattern !== '') {\n addAnchored(patterns, process.cwd(), pattern)\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// The `watch` option's entries, resolved the way #369 settled: a relative one\n// against `root`, since it is an option of this plugin rather than a path\n// inside a configuration. The watch list and the up-to-date check both read\n// them through here.\nexport function watchOptionPatterns(\n root: string,\n watch: UnpluginStyleDictionaryOptions['watch'],\n): WatchPatterns {\n const patterns: WatchPatterns = { globs: [], literals: [] }\n\n for (const pattern of watch ? (Array.isArray(watch) ? watch : [watch]) : []) {\n addAnchored(patterns, root, pattern)\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<WatchPatterns> {\n const globs = new Set<string>()\n const literals = new Set<string>()\n\n const add = (patterns: WatchPatterns) => {\n for (const pattern of patterns.globs) globs.add(pattern)\n for (const literal of patterns.literals) literals.add(literal)\n }\n\n for (const item of resolvedConfigs) {\n // A path, whatever characters its directories happen to contain.\n if (item.file) literals.add(item.file.replace(/\\\\/g, '/'))\n\n const configObj = await readConfigObject(item, log)\n if (configObj) add(sourcePatternsOf(configObj))\n }\n\n add(watchOptionPatterns(root, watch))\n\n return { globs: Array.from(globs), literals: Array.from(literals) }\n}\n\n// One pattern as a consumer wrote it, made absolute against `base` and filed\n// under what it is.\n//\n// Whether it is a glob is read off what they wrote, before anything is\n// prefixed, and of a relative glob only the prefix is escaped. The rest is\n// theirs, glob characters included, exactly as `GLOB_CHARACTERS` says. The\n// prefix is `base` joined with the pattern's leading static segments,\n// resolved together so a `./` or `../` still settles against `base`.\n//\n// An absolute glob is taken as written. Every character in it is the\n// consumer's, and escaping any would be guessing which they meant as syntax.\nfunction addAnchored(into: WatchPatterns, base: string, written: string): void {\n const pattern = written.replace(/\\\\/g, '/')\n\n if (!GLOB_CHARACTERS.test(pattern)) {\n into.literals.push(path.resolve(base, pattern).replace(/\\\\/g, '/'))\n return\n }\n\n if (path.isAbsolute(pattern)) {\n into.globs.push(pattern)\n return\n }\n\n const segments = pattern.split('/')\n const firstGlob = segments.findIndex((segment) =>\n GLOB_CHARACTERS.test(segment),\n )\n const prefix = path\n .resolve(base, segments.slice(0, firstGlob).join('/'))\n .replace(/\\\\/g, '/')\n\n into.globs.push(\n path.posix.join(escapePath(prefix), segments.slice(firstGlob).join('/')),\n )\n}\n\n// The leading run of a pattern that contains no glob character —\n// `/p/tokens` for `/p/tokens/**/*.json`. Registering it alongside the files\n// that match today is what makes a token file created tomorrow visible:\n// watching only the current matches can never see a path that did not exist\n// when the watcher was built.\n//\n// An escaped character is not a glob character, so the prefix `addAnchored`\n// escaped never ends the run, and the result is unescaped: it is a path on\n// disk, handed to `fs.existsSync` and to the watcher. Before the prefix was\n// escaped, a project in `app [v2]` cut the run at its own directory, and the\n// directory holding every project beside it was registered instead.\nfunction staticParentOf(pattern: string): string {\n const segments = pattern.split('/')\n const firstGlob = segments.findIndex((segment) =>\n GLOB_CHARACTERS.test(segment.replace(/\\\\./g, '')),\n )\n\n const parent =\n firstGlob === -1\n ? path.posix.dirname(pattern)\n : segments.slice(0, firstGlob).join('/')\n\n return parent.replace(/\\\\(.)/g, '$1')\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 { escapePath, 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.\n//\n// That is why it is only ever asked of what a consumer wrote. The plugin\n// makes a relative pattern absolute by prefixing a directory of its own\n// choosing, and that directory's characters are no part of the pattern.\nconst GLOB_CHARACTERS = /[!*?[\\]{}]/\n\n// The patterns a watch list is made of, split by what each one is. The split\n// is decided once, from the pattern as the consumer wrote it, and never again\n// from the absolute string: that string carries the project's own path, and a\n// project in `Dropbox (Personal)` or `app [v2]` would read as a glob.\n//\n// `literals` are absolute paths, matched by equality and registered as they\n// are. `globs` are absolute patterns in picomatch's syntax, in which the part\n// the plugin prefixed is escaped.\nexport interface WatchPatterns {\n globs: string[]\n literals: string[]\n}\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 { globs, literals }: WatchPatterns,\n log: Log,\n): Promise<string[]> {\n const paths = new Set<string>(literals)\n\n for (const pattern of globs) {\n // Watching the directory as well as its current contents. chokidar\n // reports a creation inside a watched directory, which is the only way a\n // token file added later is ever noticed.\n const parent = staticParentOf(pattern)\n if (parent && fs.existsSync(parent)) paths.add(parent)\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 globs,\n literals,\n}: WatchPatterns): Promise<string[]> {\n // A literal path is a `stat`, not a glob, which keeps the common case off\n // the filesystem walk.\n const barren = literals.filter((literal) => !fs.existsSync(literal))\n\n for (const pattern of globs) {\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// Where `file` is once every symbolic link on its path is followed, spelled\n// with forward slashes like every other path handed to a watcher here.\n// `undefined` when nothing is there, which a literal is allowed to be: it is\n// registered whether or not it exists yet.\nexport function realpathOf(file: string): string | undefined {\n try {\n return fs.realpathSync(file).replace(/\\\\/g, '/')\n } catch {\n return undefined\n }\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): WatchPatterns {\n const patterns: WatchPatterns = { globs: [], literals: [] }\n\n const add = (pattern: unknown) => {\n if (typeof pattern === 'string' && pattern !== '') {\n addAnchored(patterns, process.cwd(), pattern)\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// The `watch` option's entries, resolved the way #369 settled: a relative one\n// against `root`, since it is an option of this plugin rather than a path\n// inside a configuration. The watch list and the up-to-date check both read\n// them through here.\nexport function watchOptionPatterns(\n root: string,\n watch: UnpluginStyleDictionaryOptions['watch'],\n): WatchPatterns {\n const patterns: WatchPatterns = { globs: [], literals: [] }\n\n for (const pattern of watch ? (Array.isArray(watch) ? watch : [watch]) : []) {\n addAnchored(patterns, root, pattern)\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<WatchPatterns> {\n const globs = new Set<string>()\n const literals = new Set<string>()\n\n const add = (patterns: WatchPatterns) => {\n for (const pattern of patterns.globs) globs.add(pattern)\n for (const literal of patterns.literals) literals.add(literal)\n }\n\n for (const item of resolvedConfigs) {\n // A path, whatever characters its directories happen to contain.\n if (item.file) literals.add(item.file.replace(/\\\\/g, '/'))\n\n const configObj = await readConfigObject(item, log)\n if (configObj) add(sourcePatternsOf(configObj))\n }\n\n add(watchOptionPatterns(root, watch))\n\n return { globs: Array.from(globs), literals: Array.from(literals) }\n}\n\n// One pattern as a consumer wrote it, made absolute against `base` and filed\n// under what it is.\n//\n// Whether it is a glob is read off what they wrote, before anything is\n// prefixed, and of a relative glob only the prefix is escaped. The rest is\n// theirs, glob characters included, exactly as `GLOB_CHARACTERS` says. The\n// prefix is `base` joined with the pattern's leading static segments,\n// resolved together so a `./` or `../` still settles against `base`.\n//\n// An absolute glob is taken as written. Every character in it is the\n// consumer's, and escaping any would be guessing which they meant as syntax.\nfunction addAnchored(into: WatchPatterns, base: string, written: string): void {\n const pattern = written.replace(/\\\\/g, '/')\n\n if (!GLOB_CHARACTERS.test(pattern)) {\n into.literals.push(path.resolve(base, pattern).replace(/\\\\/g, '/'))\n return\n }\n\n if (path.isAbsolute(pattern)) {\n into.globs.push(pattern)\n return\n }\n\n const segments = pattern.split('/')\n const firstGlob = segments.findIndex((segment) =>\n GLOB_CHARACTERS.test(segment),\n )\n const prefix = path\n .resolve(base, segments.slice(0, firstGlob).join('/'))\n .replace(/\\\\/g, '/')\n\n into.globs.push(\n path.posix.join(escapePath(prefix), segments.slice(firstGlob).join('/')),\n )\n}\n\n// The leading run of a pattern that contains no glob character —\n// `/p/tokens` for `/p/tokens/**/*.json`. Registering it alongside the files\n// that match today is what makes a token file created tomorrow visible:\n// watching only the current matches can never see a path that did not exist\n// when the watcher was built.\n//\n// An escaped character is not a glob character, so the prefix `addAnchored`\n// escaped never ends the run, and the result is unescaped: it is a path on\n// disk, handed to `fs.existsSync` and to the watcher. Before the prefix was\n// escaped, a project in `app [v2]` cut the run at its own directory, and the\n// directory holding every project beside it was registered instead.\nfunction staticParentOf(pattern: string): string {\n const segments = pattern.split('/')\n const firstGlob = segments.findIndex((segment) =>\n GLOB_CHARACTERS.test(segment.replace(/\\\\./g, '')),\n )\n\n const parent =\n firstGlob === -1\n ? path.posix.dirname(pattern)\n : segments.slice(0, firstGlob).join('/')\n\n return parent.replace(/\\\\(.)/g, '$1')\n}\n"],"mappings":"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"}
@@ -3,13 +3,13 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import zlib from "node:zlib";
5
5
  //#region src/size-report.ts
6
- function reportSizes({ root, stdoutColour }, generatedFiles) {
6
+ function sizeTable({ colour, root }, generatedFiles) {
7
7
  const fileInfos = [];
8
8
  for (const filePath of generatedFiles) if (fs.existsSync(filePath)) {
9
9
  const displayPath = path.relative(root, filePath).replace(/\\/g, "/");
10
10
  const dir = path.dirname(displayPath);
11
11
  const base = path.basename(displayPath);
12
- const coloredPath = dir === "." ? paint("32", base, stdoutColour) : paint("90", `${dir}/`, stdoutColour) + paint("32", base, stdoutColour);
12
+ const coloredPath = dir === "." ? paint("32", base, colour) : paint("90", `${dir}/`, colour) + paint("32", base, colour);
13
13
  try {
14
14
  const sizeStr = `${(fs.statSync(filePath).size / 1024).toFixed(2)} kB`;
15
15
  const content = fs.readFileSync(filePath);
@@ -22,17 +22,19 @@ function reportSizes({ root, stdoutColour }, generatedFiles) {
22
22
  });
23
23
  } catch {}
24
24
  }
25
+ const lines = [];
25
26
  if (fileInfos.length > 0) {
26
27
  const longestPathLength = Math.max(...fileInfos.map((f) => f.relativeDisplayPath.length), 0);
27
28
  const longestSizeLength = Math.max(...fileInfos.map((f) => f.sizeStr.length), 0);
28
29
  for (const info of fileInfos) {
29
30
  const pathPadding = " ".repeat(Math.max(2, longestPathLength - info.relativeDisplayPath.length + 2));
30
31
  const sizePadded = info.sizeStr.padStart(longestSizeLength);
31
- console.log(info.coloredPath + pathPadding + paint("90", `${sizePadded} │ gzip: ${info.gzipSizeStr}`, stdoutColour));
32
+ lines.push(info.coloredPath + pathPadding + paint("90", `${sizePadded} │ gzip: ${info.gzipSizeStr}`, colour));
32
33
  }
33
34
  }
35
+ return lines;
34
36
  }
35
37
  //#endregion
36
- export { reportSizes };
38
+ export { sizeTable };
37
39
 
38
40
  //# sourceMappingURL=size-report.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"size-report.js","names":[],"sources":["../src/size-report.ts"],"sourcesContent":["import fs from 'node:fs'\nimport path from 'node:path'\nimport zlib from 'node:zlib'\n\nimport { paint } from './colour.js'\n\n// The size-and-gzip table, in a function of its own so that the compile\n// `try` in `runBuilds` can stop before it. Everything here is presentation\n// over files Style Dictionary has already finished writing, so a throw from\n// it is a reporting bug and nothing more.\n//\n// `root` and whether stdout takes colour belong to the plugin instance, so\n// they are handed in rather than held.\nexport function reportSizes(\n { root, stdoutColour }: { root: string; stdoutColour: boolean },\n generatedFiles: Set<string>,\n): void {\n const fileInfos: Array<{\n coloredPath: string\n gzipSizeStr: string\n relativeDisplayPath: string\n sizeStr: string\n }> = []\n\n for (const filePath of generatedFiles) {\n if (fs.existsSync(filePath)) {\n const displayPath = path.relative(root, filePath).replace(/\\\\/g, '/')\n const dir = path.dirname(displayPath)\n const base = path.basename(displayPath)\n // The table goes to stdout, so it follows stdout's decision — which\n // is not always stderr's, since the two are redirected separately.\n const coloredPath =\n dir === '.'\n ? paint('32', base, stdoutColour)\n : paint('90', `${dir}/`, stdoutColour) +\n paint('32', base, stdoutColour)\n\n try {\n const stats = fs.statSync(filePath)\n const bytes = stats.size\n const sizeStr = `${(bytes / 1024).toFixed(2)} kB`\n\n const content = fs.readFileSync(filePath)\n const gzipBytes = zlib.gzipSync(content).length\n const gzipSizeStr = `${(gzipBytes / 1024).toFixed(2)} kB`\n\n fileInfos.push({\n coloredPath,\n gzipSizeStr,\n relativeDisplayPath: displayPath,\n sizeStr,\n })\n } catch {\n // One unreadable destination costs its row rather than the table.\n // Deliberately narrower than the caller's `catch`: it covers the\n // three filesystem and gzip calls above and not the arithmetic\n // below, so a padding bug is reported rather than quietly printing\n // short.\n }\n }\n }\n\n if (fileInfos.length > 0) {\n const longestPathLength = Math.max(\n ...fileInfos.map((f) => f.relativeDisplayPath.length),\n 0,\n )\n const longestSizeLength = Math.max(\n ...fileInfos.map((f) => f.sizeStr.length),\n 0,\n )\n\n for (const info of fileInfos) {\n const pathPadding = ' '.repeat(\n Math.max(2, longestPathLength - info.relativeDisplayPath.length + 2),\n )\n const sizePadded = info.sizeStr.padStart(longestSizeLength)\n console.log(\n info.coloredPath +\n pathPadding +\n paint(\n '90',\n `${sizePadded} │ gzip: ${info.gzipSizeStr}`,\n stdoutColour,\n ),\n )\n }\n }\n}\n"],"mappings":";;;;;AAaA,SAAgB,YACd,EAAE,MAAM,gBACR,gBACM;CACN,MAAM,YAKD,CAAC;CAEN,KAAK,MAAM,YAAY,gBACrB,IAAI,GAAG,WAAW,QAAQ,GAAG;EAC3B,MAAM,cAAc,KAAK,SAAS,MAAM,QAAQ,CAAC,CAAC,QAAQ,OAAO,GAAG;EACpE,MAAM,MAAM,KAAK,QAAQ,WAAW;EACpC,MAAM,OAAO,KAAK,SAAS,WAAW;EAGtC,MAAM,cACJ,QAAQ,MACJ,MAAM,MAAM,MAAM,YAAY,IAC9B,MAAM,MAAM,GAAG,IAAI,IAAI,YAAY,IACnC,MAAM,MAAM,MAAM,YAAY;EAEpC,IAAI;GAGF,MAAM,UAAU,IAFF,GAAG,SAAS,QACR,CAAC,CAAC,OACQ,KAAA,CAAM,QAAQ,CAAC,EAAE;GAE7C,MAAM,UAAU,GAAG,aAAa,QAAQ;GAExC,MAAM,cAAc,IADF,KAAK,SAAS,OAAO,CAAC,CAAC,SACL,KAAA,CAAM,QAAQ,CAAC,EAAE;GAErD,UAAU,KAAK;IACb;IACA;IACA,qBAAqB;IACrB;GACF,CAAC;EACH,QAAQ,CAMR;CACF;CAGF,IAAI,UAAU,SAAS,GAAG;EACxB,MAAM,oBAAoB,KAAK,IAC7B,GAAG,UAAU,KAAK,MAAM,EAAE,oBAAoB,MAAM,GACpD,CACF;EACA,MAAM,oBAAoB,KAAK,IAC7B,GAAG,UAAU,KAAK,MAAM,EAAE,QAAQ,MAAM,GACxC,CACF;EAEA,KAAK,MAAM,QAAQ,WAAW;GAC5B,MAAM,cAAc,IAAI,OACtB,KAAK,IAAI,GAAG,oBAAoB,KAAK,oBAAoB,SAAS,CAAC,CACrE;GACA,MAAM,aAAa,KAAK,QAAQ,SAAS,iBAAiB;GAC1D,QAAQ,IACN,KAAK,cACH,cACA,MACE,MACA,GAAG,WAAW,WAAW,KAAK,eAC9B,YACF,CACJ;EACF;CACF;AACF"}
1
+ {"version":3,"file":"size-report.js","names":[],"sources":["../src/size-report.ts"],"sourcesContent":["import fs from 'node:fs'\nimport path from 'node:path'\nimport zlib from 'node:zlib'\n\nimport { paint } from './colour.js'\n\n// The size-and-gzip table's lines, in a function of its own so that the\n// compile `try` in `runBuilds` can stop before it. Everything here is\n// presentation over files Style Dictionary has already finished writing, so a\n// throw from it is a reporting bug and nothing more.\n//\n// It returns the lines rather than printing them, because where they go is\n// the instance's to decide: a host's `info` channel where one exists, stdout\n// otherwise. `colour` says whether to paint them, which only stdout ever asks\n// for — a host renders its own output.\nexport function sizeTable(\n { colour, root }: { colour: boolean; root: string },\n generatedFiles: Set<string>,\n): string[] {\n const fileInfos: Array<{\n coloredPath: string\n gzipSizeStr: string\n relativeDisplayPath: string\n sizeStr: string\n }> = []\n\n for (const filePath of generatedFiles) {\n if (fs.existsSync(filePath)) {\n const displayPath = path.relative(root, filePath).replace(/\\\\/g, '/')\n const dir = path.dirname(displayPath)\n const base = path.basename(displayPath)\n const coloredPath =\n dir === '.'\n ? paint('32', base, colour)\n : paint('90', `${dir}/`, colour) + paint('32', base, colour)\n\n try {\n const stats = fs.statSync(filePath)\n const bytes = stats.size\n const sizeStr = `${(bytes / 1024).toFixed(2)} kB`\n\n const content = fs.readFileSync(filePath)\n const gzipBytes = zlib.gzipSync(content).length\n const gzipSizeStr = `${(gzipBytes / 1024).toFixed(2)} kB`\n\n fileInfos.push({\n coloredPath,\n gzipSizeStr,\n relativeDisplayPath: displayPath,\n sizeStr,\n })\n } catch {\n // One unreadable destination costs its row rather than the table.\n // Deliberately narrower than the caller's `catch`: it covers the\n // three filesystem and gzip calls above and not the arithmetic\n // below, so a padding bug is reported rather than quietly printing\n // short.\n }\n }\n }\n\n const lines: string[] = []\n\n if (fileInfos.length > 0) {\n const longestPathLength = Math.max(\n ...fileInfos.map((f) => f.relativeDisplayPath.length),\n 0,\n )\n const longestSizeLength = Math.max(\n ...fileInfos.map((f) => f.sizeStr.length),\n 0,\n )\n\n for (const info of fileInfos) {\n const pathPadding = ' '.repeat(\n Math.max(2, longestPathLength - info.relativeDisplayPath.length + 2),\n )\n const sizePadded = info.sizeStr.padStart(longestSizeLength)\n lines.push(\n info.coloredPath +\n pathPadding +\n paint('90', `${sizePadded} │ gzip: ${info.gzipSizeStr}`, colour),\n )\n }\n }\n\n return lines\n}\n"],"mappings":";;;;;AAeA,SAAgB,UACd,EAAE,QAAQ,QACV,gBACU;CACV,MAAM,YAKD,CAAC;CAEN,KAAK,MAAM,YAAY,gBACrB,IAAI,GAAG,WAAW,QAAQ,GAAG;EAC3B,MAAM,cAAc,KAAK,SAAS,MAAM,QAAQ,CAAC,CAAC,QAAQ,OAAO,GAAG;EACpE,MAAM,MAAM,KAAK,QAAQ,WAAW;EACpC,MAAM,OAAO,KAAK,SAAS,WAAW;EACtC,MAAM,cACJ,QAAQ,MACJ,MAAM,MAAM,MAAM,MAAM,IACxB,MAAM,MAAM,GAAG,IAAI,IAAI,MAAM,IAAI,MAAM,MAAM,MAAM,MAAM;EAE/D,IAAI;GAGF,MAAM,UAAU,IAFF,GAAG,SAAS,QACR,CAAC,CAAC,OACQ,KAAA,CAAM,QAAQ,CAAC,EAAE;GAE7C,MAAM,UAAU,GAAG,aAAa,QAAQ;GAExC,MAAM,cAAc,IADF,KAAK,SAAS,OAAO,CAAC,CAAC,SACL,KAAA,CAAM,QAAQ,CAAC,EAAE;GAErD,UAAU,KAAK;IACb;IACA;IACA,qBAAqB;IACrB;GACF,CAAC;EACH,QAAQ,CAMR;CACF;CAGF,MAAM,QAAkB,CAAC;CAEzB,IAAI,UAAU,SAAS,GAAG;EACxB,MAAM,oBAAoB,KAAK,IAC7B,GAAG,UAAU,KAAK,MAAM,EAAE,oBAAoB,MAAM,GACpD,CACF;EACA,MAAM,oBAAoB,KAAK,IAC7B,GAAG,UAAU,KAAK,MAAM,EAAE,QAAQ,MAAM,GACxC,CACF;EAEA,KAAK,MAAM,QAAQ,WAAW;GAC5B,MAAM,cAAc,IAAI,OACtB,KAAK,IAAI,GAAG,oBAAoB,KAAK,oBAAoB,SAAS,CAAC,CACrE;GACA,MAAM,aAAa,KAAK,QAAQ,SAAS,iBAAiB;GAC1D,MAAM,KACJ,KAAK,cACH,cACA,MAAM,MAAM,GAAG,WAAW,WAAW,KAAK,eAAe,MAAM,CACnE;EACF;CACF;CAEA,OAAO;AACT"}
package/dist/types.d.ts CHANGED
@@ -11,14 +11,21 @@ import { Config } from "style-dictionary";
11
11
  * build (e.g. `tsdown`/`rolldown build` without `--watch`) only builds once, in
12
12
  * `buildStart`.
13
13
  *
14
- * Everything the plugin says goes through the host rather than to the console:
15
- * Vite's `config.logger`, the plugin context under rollup and rolldown, and
16
- * `compilation.warnings` under webpack, which is what puts a failed compile in
17
- * `stats.toJson()`. A failure is reported on the warning channel and never the
18
- * error one — rollup's `this.error` aborts the bundle, and that decision is
19
- * `failOnError`'s alone. Where no host offers a channel the console is used,
20
- * with colour gated on `NO_COLOR`, `FORCE_COLOR` and whether the stream is a
21
- * terminal.
14
+ * What the plugin says goes through the host wherever the host has a channel
15
+ * for it: Vite's `config.logger`, the plugin context under rollup and
16
+ * rolldown, and `compilation.warnings` under webpack and rspack, which is what
17
+ * puts a failed compile in `stats.toJson()`. The progress lines and the size
18
+ * table take the host's `info` channel, and webpack and rspack have none, so
19
+ * there both go to the console. A failure is reported on the warning channel
20
+ * and never the error one — rollup's `this.error` aborts the bundle, and that
21
+ * decision is `failOnError`'s alone. Where no host offers a channel the console
22
+ * is used, with colour gated on `NO_COLOR`, `FORCE_COLOR` and whether the
23
+ * stream is a terminal.
24
+ *
25
+ * Style Dictionary's own lines are not the plugin's. The platform name and the
26
+ * `✔︎` for each file it wrote are its own `console` calls, so they reach the
27
+ * console under every host, whatever the host's log level. `logLevel: 'silent'`
28
+ * quiets them, as does `log.verbosity: 'silent'` in the configuration.
22
29
  *
23
30
  * The three `onBuild*` hooks are called synchronously and their return value
24
31
  * is not awaited, so a build never waits for one. A hook may still be written
@@ -147,14 +154,16 @@ export interface UnpluginStyleDictionaryOptions {
147
154
  * first one that *looks like a Style Dictionary configuration* wins — it has
148
155
  * to declare at least one of `platforms`, `source`, `include` or `tokens` —
149
156
  * and the path it picked is announced, so which file a build used is
150
- * answerable from the console. A candidate that fails that check is reported
151
- * and skipped rather than adopted, because `config.json` is an extremely
152
- * common name for something else entirely.
157
+ * answerable from the console. A candidate that fails that check is skipped
158
+ * rather than adopted, because `config.json` is an extremely common name for
159
+ * something else entirely. It is named in that announcement when a later
160
+ * candidate is adopted, and reported as an error when none is.
153
161
  *
154
162
  * **`false` turns discovery off.** Two of the four names are modules rather
155
163
  * than data, and reading a module means running it: a `sd.config.js` in the
156
- * root is imported, freshly, on every watch event. Validation cannot prevent
157
- * that, because the check can only look at what the import returned — so a
164
+ * root is imported when it is first read, and again whenever it or a file
165
+ * named in `watch` changes. Validation cannot prevent that, because the
166
+ * check can only look at what the import returned — so a
158
167
  * project that names its configuration explicitly, or has none, should say
159
168
  * `config: false` rather than rely on there being nothing to find.
160
169
  */
@@ -201,6 +210,10 @@ export interface UnpluginStyleDictionaryOptions {
201
210
  *
202
211
  * Reporting happens either way, and is not suppressed by `silent`.
203
212
  *
213
+ * Any other value throws a `TypeError` when the plugin is created — the
214
+ * string `'true'` from an environment variable included, which used to
215
+ * behave as `false`.
216
+ *
204
217
  * @default 'build'
205
218
  */
206
219
  failOnError?: 'build' | 'serve' | boolean;
@@ -220,7 +233,9 @@ export interface UnpluginStyleDictionaryOptions {
220
233
  * about rather than pointing at its own `--verbose` flag.
221
234
  *
222
235
  * A compile that fails is reported at every level, so there is no
223
- * `'error'`: `'silent'` is the quietest and still reports a failure.
236
+ * `'error'`: `'silent'` is the quietest and still reports a failure. Any
237
+ * other value, `'error'` included, throws a `TypeError` when the plugin is
238
+ * created.
224
239
  *
225
240
  * This option governs what the plugin says, not where it goes. The messages
226
241
  * are handed to the host — Vite's `config.logger`, the rollup and rolldown
@@ -255,6 +270,10 @@ export interface UnpluginStyleDictionaryOptions {
255
270
  /**
256
271
  * Called when a build fails, with whatever was thrown.
257
272
  *
273
+ * A failure inside Style Dictionary arrives wrapped in an `Error` whose
274
+ * message names the configuration it came from, with Style Dictionary's own
275
+ * error as its `cause`. One the plugin raises itself already names it.
276
+ *
258
277
  * It fires whatever `failOnError` is set to, and before that option decides
259
278
  * whether to rethrow — the two answer different questions, and under a dev
260
279
  * server the default is not to throw at all.
@@ -311,6 +330,11 @@ export interface UnpluginStyleDictionaryOptions {
311
330
  * stale output for the rest. Scope the watch half rather than the build half
312
331
  * unless that is what you want.
313
332
  *
333
+ * Any other shape throws a `TypeError` when the plugin is created: a single
334
+ * string, or an object with a key other than `build` and `watch` — `serve`
335
+ * is the easy slip, since `failOnError` says `'serve'`. Both used to build
336
+ * every platform.
337
+ *
314
338
  * @default undefined, which builds every platform
315
339
  */
316
340
  platforms?: string[] | {
@@ -380,6 +404,11 @@ export interface UnpluginStyleDictionaryOptions {
380
404
  * else, name a file here relative to the root, and in `source` relative to
381
405
  * where the build runs.
382
406
  *
407
+ * A configuration module is imported again when a file named here changes,
408
+ * so a data file it reads when it loads is picked up. A module that a
409
+ * configuration or a token module itself imports is evaluated once per
410
+ * process, though: naming it here buys a rebuild, not a re-read.
411
+ *
383
412
  * What a change to a watched file then triggers is the host's to decide —
384
413
  * see the interface documentation above.
385
414
  */
@@ -1,14 +1,29 @@
1
+ import path from "node:path";
1
2
  import picomatch from "picomatch";
2
3
  //#region src/watch-filter.ts
3
- function isWatchedSource(file, { globs, literals }, generatedDestinations) {
4
+ function compileWatchPatterns({ globs, literals }) {
5
+ const named = /* @__PURE__ */ new Set([...globs, ...literals]);
6
+ const matchesGlob = globs.length > 0 ? picomatch(globs) : () => false;
7
+ return (normalizedFile) => named.has(normalizedFile) || matchesGlob(normalizedFile);
8
+ }
9
+ function isWatchedSource(file, matches, generatedDestinations) {
4
10
  const normalizedFile = file.replace(/\\/g, "/");
5
- return !generatedDestinations.has(normalizedFile) && (literals.includes(normalizedFile) || matchesWatchedFile(normalizedFile, globs));
11
+ return !generatedDestinations.has(normalizedFile) && matches(normalizedFile);
6
12
  }
7
13
  function matchesWatchedFile(file, patterns) {
8
14
  const normalizedFile = file.replace(/\\/g, "/");
9
15
  return patterns.some((pattern) => pattern === normalizedFile || picomatch.isMatch(normalizedFile, pattern));
10
16
  }
17
+ function registeredSpellingOf(file, linkedPaths) {
18
+ if (linkedPaths.size === 0) return file;
19
+ const normalizedFile = file.replace(/\\/g, "/");
20
+ for (let real = normalizedFile;; real = path.posix.dirname(real)) {
21
+ const registered = linkedPaths.get(real);
22
+ if (registered !== void 0) return registered + normalizedFile.slice(real.length);
23
+ if (path.posix.dirname(real) === real) return file;
24
+ }
25
+ }
11
26
  //#endregion
12
- export { isWatchedSource, matchesWatchedFile };
27
+ export { compileWatchPatterns, isWatchedSource, matchesWatchedFile, registeredSpellingOf };
13
28
 
14
29
  //# sourceMappingURL=watch-filter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"watch-filter.js","names":[],"sources":["../src/watch-filter.ts"],"sourcesContent":["import picomatch from 'picomatch'\n\nimport type { WatchPatterns } from './patterns.js'\n\n// Whether a changed file is a token or config source rather than something\n// this plugin just wrote. Both watch entry points ask through here, so\n// neither can react to its own output.\n//\n// `generatedDestinations` is the plugin instance's own record of what it\n// wrote, so it is handed in rather than held: one module serves every\n// instance in the process, and each one's output is its own.\n//\n// A literal is a path, so it is compared as one. A config file reaches here\n// that way, and so does a token file named without a glob, and neither may be\n// read as a pattern: the project's own directory can hold characters a glob\n// treats as syntax.\nexport function isWatchedSource(\n file: string,\n { globs, literals }: WatchPatterns,\n generatedDestinations: ReadonlySet<string>,\n): boolean {\n const normalizedFile = file.replace(/\\\\/g, '/')\n\n return (\n !generatedDestinations.has(normalizedFile) &&\n (literals.includes(normalizedFile) ||\n matchesWatchedFile(normalizedFile, globs))\n )\n}\n\n// Whether `file` matches one of the resolved config/token watch patterns.\n// Shared by the Vite-specific `configureServer` watcher and the universal\n// `watchChange` hook — both need it, and both must skip files that don't\n// match: without this filter, `watchChange` reacts to *any* changed\n// module-graph file, including this plugin's own generated output (since\n// consuming code imports it). Every regenerate is itself a \"change\", which\n// without filtering re-triggers a rebuild forever.\n//\n// Matching a pattern is only half of that, and this function is only the half\n// it can answer. A `buildPath` inside a `source` directory is a supported\n// layout, and under any correct matcher its output matches the very glob that\n// produced it — so the caller also subtracts what the last build wrote. See\n// `isWatchedSource` above, and `generatedDestinations` in the factory.\n//\n// The patterns are Style Dictionary's own `source` and `include` globs, so the\n// filter has to admit exactly what the build reads — which is why the matching\n// is a real globber's rather than hand-rolled. The version this replaces was\n// wrong in both directions at once: it stripped `/**` out of a pattern and\n// prefix-matched the remainder, so `tokens/**/*.json` matched nothing sitting\n// directly in `tokens/` and `tokens/**` matched a `tokens-backup/` sibling,\n// while its regex branch mapped every `*` to `.*` — crossing `/` — and tested\n// it unanchored, so generated output under a watched directory matched its own\n// source glob and rebuilt forever.\n//\n// picomatch rather than `path.matchesGlob`, which would need no dependency at\n// all: that function is documented experimental, and on Node 20 — the floor\n// `engines` declares — it prints `ExperimentalWarning: glob is an experimental\n// feature and might change at any time` into the consumer's build output. The\n// dependency is free in practice, since `unplugin` depends on the same\n// picomatch and is already installed wherever this plugin is. Its `dot: false`\n// default is deliberate: it is what glob, and so Style Dictionary, reads\n// sources with, so a dotfile is invisible to the filter and to the build alike.\n//\n// **The patterns are used exactly as given.** A backslash in one is an escape:\n// the part of a pattern the plugin prefixed is escaped, and every producer\n// already writes separators as `/`. Rewriting backslashes to slashes here\n// turned each escape into a separator, so a project in `Dropbox (Personal)`\n// matched none of its own token files.\nexport function matchesWatchedFile(file: string, patterns: string[]): boolean {\n const normalizedFile = file.replace(/\\\\/g, '/')\n\n return patterns.some(\n (pattern) =>\n // An absolute path a consumer wrote with a glob character in it is filed\n // as a glob, and still names itself.\n pattern === normalizedFile || picomatch.isMatch(normalizedFile, pattern),\n )\n}\n"],"mappings":";;AAgBA,SAAgB,gBACd,MACA,EAAE,OAAO,YACT,uBACS;CACT,MAAM,iBAAiB,KAAK,QAAQ,OAAO,GAAG;CAE9C,OACE,CAAC,sBAAsB,IAAI,cAAc,MACxC,SAAS,SAAS,cAAc,KAC/B,mBAAmB,gBAAgB,KAAK;AAE9C;AAwCA,SAAgB,mBAAmB,MAAc,UAA6B;CAC5E,MAAM,iBAAiB,KAAK,QAAQ,OAAO,GAAG;CAE9C,OAAO,SAAS,MACb,YAGC,YAAY,kBAAkB,UAAU,QAAQ,gBAAgB,OAAO,CAC3E;AACF"}
1
+ {"version":3,"file":"watch-filter.js","names":[],"sources":["../src/watch-filter.ts"],"sourcesContent":["import path from 'node:path'\nimport picomatch from 'picomatch'\n\nimport type { WatchPatterns } from './patterns.js'\n\n// Whether a forward-slashed path is one a watch list names.\nexport type WatchMatcher = (normalizedFile: string) => boolean\n\n// One watch list compiled into a matcher, once, where the list is derived.\n// picomatch compiles a pattern on every `isMatch`, with no cache, so asking\n// per event compiled every pattern again for every file a watcher reported —\n// under a dev server, each unrelated save went through the filter twice. The\n// matcher is the factory's to hold, beside the list it came from: one module\n// serves every plugin instance, so a cache here would be state in a module\n// that holds none.\n//\n// A literal is a path, so it is compared as one. A config file reaches here\n// that way, and so does a token file named without a glob, and neither may be\n// read as a pattern: the project's own directory can hold characters a glob\n// treats as syntax. The globs are compiled exactly as given — see\n// `matchesWatchedFile` below — and an absolute path a consumer wrote with a\n// glob character in it is filed as a glob and still names itself.\nexport function compileWatchPatterns({\n globs,\n literals,\n}: WatchPatterns): WatchMatcher {\n const named = new Set([...globs, ...literals])\n const matchesGlob = globs.length > 0 ? picomatch(globs) : () => false\n\n return (normalizedFile) =>\n named.has(normalizedFile) || matchesGlob(normalizedFile)\n}\n\n// Whether a changed file is a token or config source rather than something\n// this plugin just wrote. Both watch entry points ask through here, so\n// neither can react to its own output.\n//\n// `generatedDestinations` is the plugin instance's own record of what it\n// wrote, so it is handed in rather than held: one module serves every\n// instance in the process, and each one's output is its own.\n//\n// It takes the list compiled rather than the list: see `compileWatchPatterns`.\nexport function isWatchedSource(\n file: string,\n matches: WatchMatcher,\n generatedDestinations: ReadonlySet<string>,\n): boolean {\n const normalizedFile = file.replace(/\\\\/g, '/')\n\n return !generatedDestinations.has(normalizedFile) && matches(normalizedFile)\n}\n\n// Whether `file` matches one of the resolved config/token watch patterns,\n// compiled for this one question. Every event goes through\n// `compileWatchPatterns` instead; this stays for a one-off answer.\n// Shared by the Vite-specific `configureServer` watcher and the universal\n// `watchChange` hook — both need it, and both must skip files that don't\n// match: without this filter, `watchChange` reacts to *any* changed\n// module-graph file, including this plugin's own generated output (since\n// consuming code imports it). Every regenerate is itself a \"change\", which\n// without filtering re-triggers a rebuild forever.\n//\n// Matching a pattern is only half of that, and this function is only the half\n// it can answer. A `buildPath` inside a `source` directory is a supported\n// layout, and under any correct matcher its output matches the very glob that\n// produced it — so the caller also subtracts what the last build wrote. See\n// `isWatchedSource` above, and `generatedDestinations` in the factory.\n//\n// The patterns are Style Dictionary's own `source` and `include` globs, so the\n// filter has to admit exactly what the build reads — which is why the matching\n// is a real globber's rather than hand-rolled. The version this replaces was\n// wrong in both directions at once: it stripped `/**` out of a pattern and\n// prefix-matched the remainder, so `tokens/**/*.json` matched nothing sitting\n// directly in `tokens/` and `tokens/**` matched a `tokens-backup/` sibling,\n// while its regex branch mapped every `*` to `.*` — crossing `/` — and tested\n// it unanchored, so generated output under a watched directory matched its own\n// source glob and rebuilt forever.\n//\n// picomatch rather than `path.matchesGlob`, which would need no dependency at\n// all: that function is documented experimental, and on Node 20 — the floor\n// `engines` declares — it prints `ExperimentalWarning: glob is an experimental\n// feature and might change at any time` into the consumer's build output. The\n// dependency is free in practice, since `unplugin` depends on the same\n// picomatch and is already installed wherever this plugin is. Its `dot: false`\n// default is deliberate: it is what glob, and so Style Dictionary, reads\n// sources with, so a dotfile is invisible to the filter and to the build alike.\n//\n// **The patterns are used exactly as given.** A backslash in one is an escape:\n// the part of a pattern the plugin prefixed is escaped, and every producer\n// already writes separators as `/`. Rewriting backslashes to slashes here\n// turned each escape into a separator, so a project in `Dropbox (Personal)`\n// matched none of its own token files.\nexport function matchesWatchedFile(file: string, patterns: string[]): boolean {\n const normalizedFile = file.replace(/\\\\/g, '/')\n\n return patterns.some(\n (pattern) =>\n // An absolute path a consumer wrote with a glob character in it is filed\n // as a glob, and still names itself.\n pattern === normalizedFile || picomatch.isMatch(normalizedFile, pattern),\n )\n}\n\n// The spelling the watch patterns use for `file`, which a host watching a\n// registered realpath reports by where a link leads rather than by the link.\n// `linkedPaths` maps each registered path that runs through a link from its\n// realpath back to the spelling it was registered under, and is the plugin\n// instance's, so it is handed in rather than held.\n//\n// The lookup walks up from `file` rather than asking about it alone, because\n// a pattern's static parent directory is registered so that a token file\n// created in it later is noticed — and that file arrives spelled under the\n// directory's realpath, which only the directory is a key for.\nexport function registeredSpellingOf(\n file: string,\n linkedPaths: ReadonlyMap<string, string>,\n): string {\n if (linkedPaths.size === 0) return file\n\n const normalizedFile = file.replace(/\\\\/g, '/')\n\n for (let real = normalizedFile; ; real = path.posix.dirname(real)) {\n const registered = linkedPaths.get(real)\n if (registered !== undefined) {\n return registered + normalizedFile.slice(real.length)\n }\n\n if (path.posix.dirname(real) === real) return file\n }\n}\n"],"mappings":";;;AAsBA,SAAgB,qBAAqB,EACnC,OACA,YAC8B;CAC9B,MAAM,wBAAQ,IAAI,IAAI,CAAC,GAAG,OAAO,GAAG,QAAQ,CAAC;CAC7C,MAAM,cAAc,MAAM,SAAS,IAAI,UAAU,KAAK,UAAU;CAEhE,QAAQ,mBACN,MAAM,IAAI,cAAc,KAAK,YAAY,cAAc;AAC3D;AAWA,SAAgB,gBACd,MACA,SACA,uBACS;CACT,MAAM,iBAAiB,KAAK,QAAQ,OAAO,GAAG;CAE9C,OAAO,CAAC,sBAAsB,IAAI,cAAc,KAAK,QAAQ,cAAc;AAC7E;AA0CA,SAAgB,mBAAmB,MAAc,UAA6B;CAC5E,MAAM,iBAAiB,KAAK,QAAQ,OAAO,GAAG;CAE9C,OAAO,SAAS,MACb,YAGC,YAAY,kBAAkB,UAAU,QAAQ,gBAAgB,OAAO,CAC3E;AACF;AAYA,SAAgB,qBACd,MACA,aACQ;CACR,IAAI,YAAY,SAAS,GAAG,OAAO;CAEnC,MAAM,iBAAiB,KAAK,QAAQ,OAAO,GAAG;CAE9C,KAAK,IAAI,OAAO,iBAAkB,OAAO,KAAK,MAAM,QAAQ,IAAI,GAAG;EACjE,MAAM,aAAa,YAAY,IAAI,IAAI;EACvC,IAAI,eAAe,KAAA,GACjB,OAAO,aAAa,eAAe,MAAM,KAAK,MAAM;EAGtD,IAAI,KAAK,MAAM,QAAQ,IAAI,MAAM,MAAM,OAAO;CAChD;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kanso-labs/unplugin-style-dictionary",
3
- "version": "0.10.13",
3
+ "version": "0.10.15",
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",
@@ -83,19 +83,19 @@
83
83
  "@commitlint/cli": "21.2.3",
84
84
  "@commitlint/config-conventional": "21.2.3",
85
85
  "@eslint/js": "10.0.1",
86
- "@rspack/core": "2.2.7",
86
+ "@rspack/core": "2.2.8",
87
87
  "@types/node": "26.6.3",
88
88
  "@types/picomatch": "4.0.3",
89
89
  "@types/semver": "7.8.0",
90
90
  "@vitest/coverage-v8": "5.0.2",
91
91
  "eslint": "10.11.0",
92
- "eslint-plugin-oxlint": "1.85.0",
92
+ "eslint-plugin-oxlint": "1.86.0",
93
93
  "eslint-plugin-perfectionist": "5.12.1",
94
94
  "globals": "17.12.0",
95
95
  "husky": "9.1.7",
96
96
  "lint-staged": "17.6.0",
97
- "oxfmt": "0.70.0",
98
- "oxlint": "1.85.0",
97
+ "oxfmt": "0.71.0",
98
+ "oxlint": "1.86.0",
99
99
  "oxlint-tsgolint": "7.0.2003",
100
100
  "publint": "0.3.24",
101
101
  "rimraf": "6.1.3",
@@ -105,7 +105,7 @@
105
105
  "style-dictionary": "5.5.5",
106
106
  "tsdown": "0.23.0",
107
107
  "typescript": "6.0.3",
108
- "typescript-eslint": "8.70.1",
108
+ "typescript-eslint": "8.71.0",
109
109
  "vite": "8.3.1",
110
110
  "vitest": "5.0.2",
111
111
  "webpack": "5.111.1"