@kanso-labs/unplugin-style-dictionary 0.10.5 → 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 +9 -1
- package/dist/colour.js +16 -0
- package/dist/colour.js.map +1 -0
- package/dist/compile.js +114 -0
- package/dist/compile.js.map +1 -0
- package/dist/config.js +113 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.js +11 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.js +75 -440
- package/dist/index.js.map +1 -1
- package/dist/patterns.js +69 -0
- package/dist/patterns.js.map +1 -0
- package/dist/scheduler.js +44 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/size-report.js +38 -0
- package/dist/size-report.js.map +1 -0
- package/dist/types.d.ts +9 -1
- package/dist/up-to-date.js +61 -0
- package/dist/up-to-date.js.map +1 -0
- package/dist/watch-filter.js +4 -1
- package/dist/watch-filter.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { asError } from "./errors.js";
|
|
2
|
+
//#region src/scheduler.ts
|
|
3
|
+
function createScheduler({ debounceMs, isClosed, run }) {
|
|
4
|
+
let debounceTimer;
|
|
5
|
+
let pendingReason;
|
|
6
|
+
let inFlight;
|
|
7
|
+
let waiting = [];
|
|
8
|
+
const drain = async () => {
|
|
9
|
+
while (pendingReason !== void 0) {
|
|
10
|
+
const reason = pendingReason;
|
|
11
|
+
pendingReason = void 0;
|
|
12
|
+
const resolvers = waiting;
|
|
13
|
+
waiting = [];
|
|
14
|
+
let failure;
|
|
15
|
+
try {
|
|
16
|
+
await run(reason);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
failure = { error: err };
|
|
19
|
+
}
|
|
20
|
+
for (const settle of resolvers) settle(failure);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
return async (reason) => {
|
|
24
|
+
if (isClosed()) return;
|
|
25
|
+
pendingReason = reason;
|
|
26
|
+
const covered = new Promise((resolve, reject) => {
|
|
27
|
+
waiting.push((failure) => {
|
|
28
|
+
if (failure) reject(asError(failure.error));
|
|
29
|
+
else resolve();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
33
|
+
debounceTimer = setTimeout(() => {
|
|
34
|
+
debounceTimer = void 0;
|
|
35
|
+
inFlight = (inFlight ?? Promise.resolve()).then(drain);
|
|
36
|
+
}, debounceMs);
|
|
37
|
+
debounceTimer.unref();
|
|
38
|
+
return covered;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { createScheduler };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","names":[],"sources":["../src/scheduler.ts"],"sourcesContent":["// One rebuild per burst of watcher events, and never two at once.\n//\n// Two things went wrong without this. A single token edit under Vite's dev\n// server reached both the `configureServer` listener and `watchChange` —\n// Vite 6, 7 and 8 all invoke plugin `watchChange` while serving — and each\n// started its own build, so one write produced two. And nothing serialised\n// them: a four-file change started one build per file, all overlapping.\n// `runBuilds` builds its configurations one after another precisely so two\n// instances never write the same destination at once, and concurrent calls\n// to it reintroduced that one level up.\n//\n// The trailing debounce collapses the burst; the in-flight chain means a\n// trigger arriving mid-build queues exactly one follow-up rather than\n// starting a second build beside it.\n//\n// **Every plugin instance gets a scheduler of its own**, so the state below\n// lives inside `createScheduler` rather than at module scope. One process\n// routinely holds several instances, and a debounce they shared would collapse\n// two instances' triggers into one rebuild — run by only one of them, with the\n// other's output left stale and its caller told it was covered.\n\nimport { asError } from './errors.js'\n\n// Returns `schedule`, which resolves once a rebuild covering its trigger has\n// finished, and rejects with what that rebuild threw.\n//\n// `run` is the rebuild itself, and a failure is whatever it throws. `isClosed`\n// is asked on every trigger, because the host closes its watcher after the\n// scheduler exists.\nexport function createScheduler({\n debounceMs,\n isClosed,\n run,\n}: {\n debounceMs: number\n isClosed: () => boolean\n run: (reason: string) => Promise<void>\n}): (reason: string) => Promise<void> {\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n let pendingReason: string | undefined\n let inFlight: Promise<void> | undefined\n let waiting: Array<(failure?: { error: unknown }) => void> = []\n\n const drain = async (): Promise<void> => {\n // A loop rather than a single pass: anything scheduled while the build\n // below is running is picked up here instead of starting a second one.\n while (pendingReason !== undefined) {\n const reason = pendingReason\n pendingReason = undefined\n\n // Captured before the await, so a trigger arriving mid-build waits for\n // the next pass rather than being told this one covered it.\n const resolvers = waiting\n waiting = []\n\n let failure: undefined | { error: unknown }\n\n try {\n await run(reason)\n } catch (err) {\n failure = { error: err }\n }\n\n // Handed on to whatever awaited this rebuild, which is `watchChange`\n // and so the host under a watching bundler. Vite's dev-server listener\n // has no build to fail and catches it.\n for (const settle of resolvers) settle(failure)\n }\n }\n\n // Resolves once a rebuild covering this trigger has finished.\n return async (reason: string): Promise<void> => {\n // Nothing consumes a rebuild once the host has closed its watcher. This is\n // where a close actually lands: `watchChange` reaches here only after\n // resolving configurations and deriving a watch list, so a `closeWatcher`\n // arriving mid-hook finds no timer armed yet and nothing else to stop it.\n //\n // Resolving rather than rejecting, because the trigger was handled — by\n // being declined — and the caller awaiting it is a host on its way out.\n if (isClosed()) return\n\n pendingReason = reason\n\n const covered = new Promise<void>((resolve, reject) => {\n waiting.push((failure) => {\n if (failure) reject(asError(failure.error))\n else resolve()\n })\n })\n\n if (debounceTimer) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n debounceTimer = undefined\n inFlight = (inFlight ?? Promise.resolve()).then(drain)\n }, debounceMs)\n\n // A pending rebuild must not be what keeps a process alive; whatever is\n // watching already is.\n debounceTimer.unref()\n\n return covered\n }\n}\n"],"mappings":";;AA6BA,SAAgB,gBAAgB,EAC9B,YACA,UACA,OAKoC;CACpC,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI,UAAyD,CAAC;CAE9D,MAAM,QAAQ,YAA2B;EAGvC,OAAO,kBAAkB,KAAA,GAAW;GAClC,MAAM,SAAS;GACf,gBAAgB,KAAA;GAIhB,MAAM,YAAY;GAClB,UAAU,CAAC;GAEX,IAAI;GAEJ,IAAI;IACF,MAAM,IAAI,MAAM;GAClB,SAAS,KAAK;IACZ,UAAU,EAAE,OAAO,IAAI;GACzB;GAKA,KAAK,MAAM,UAAU,WAAW,OAAO,OAAO;EAChD;CACF;CAGA,OAAO,OAAO,WAAkC;EAQ9C,IAAI,SAAS,GAAG;EAEhB,gBAAgB;EAEhB,MAAM,UAAU,IAAI,SAAe,SAAS,WAAW;GACrD,QAAQ,MAAM,YAAY;IACxB,IAAI,SAAS,OAAO,QAAQ,QAAQ,KAAK,CAAC;SACrC,QAAQ;GACf,CAAC;EACH,CAAC;EAED,IAAI,eAAe,aAAa,aAAa;EAC7C,gBAAgB,iBAAiB;GAC/B,gBAAgB,KAAA;GAChB,YAAY,YAAY,QAAQ,QAAQ,EAAA,CAAG,KAAK,KAAK;EACvD,GAAG,UAAU;EAIb,cAAc,MAAM;EAEpB,OAAO;CACT;AACF"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { paint } from "./colour.js";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import zlib from "node:zlib";
|
|
5
|
+
//#region src/size-report.ts
|
|
6
|
+
function reportSizes({ root, stdoutColour }, generatedFiles) {
|
|
7
|
+
const fileInfos = [];
|
|
8
|
+
for (const filePath of generatedFiles) if (fs.existsSync(filePath)) {
|
|
9
|
+
const displayPath = path.relative(root, filePath).replace(/\\/g, "/");
|
|
10
|
+
const dir = path.dirname(displayPath);
|
|
11
|
+
const base = path.basename(displayPath);
|
|
12
|
+
const coloredPath = dir === "." ? paint("32", base, stdoutColour) : paint("90", `${dir}/`, stdoutColour) + paint("32", base, stdoutColour);
|
|
13
|
+
try {
|
|
14
|
+
const sizeStr = `${(fs.statSync(filePath).size / 1024).toFixed(2)} kB`;
|
|
15
|
+
const content = fs.readFileSync(filePath);
|
|
16
|
+
const gzipSizeStr = `${(zlib.gzipSync(content).length / 1024).toFixed(2)} kB`;
|
|
17
|
+
fileInfos.push({
|
|
18
|
+
coloredPath,
|
|
19
|
+
gzipSizeStr,
|
|
20
|
+
relativeDisplayPath: displayPath,
|
|
21
|
+
sizeStr
|
|
22
|
+
});
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
if (fileInfos.length > 0) {
|
|
26
|
+
const longestPathLength = Math.max(...fileInfos.map((f) => f.relativeDisplayPath.length), 0);
|
|
27
|
+
const longestSizeLength = Math.max(...fileInfos.map((f) => f.sizeStr.length), 0);
|
|
28
|
+
for (const info of fileInfos) {
|
|
29
|
+
const pathPadding = " ".repeat(Math.max(2, longestPathLength - info.relativeDisplayPath.length + 2));
|
|
30
|
+
const sizePadded = info.sizeStr.padStart(longestSizeLength);
|
|
31
|
+
console.log(info.coloredPath + pathPadding + paint("90", `${sizePadded} │ gzip: ${info.gzipSizeStr}`, stdoutColour));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { reportSizes };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=size-report.js.map
|
|
@@ -0,0 +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"}
|
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
|
*/
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { expandPatterns, sourcePatternsOf } from "./patterns.js";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
//#region src/up-to-date.ts
|
|
5
|
+
function configFingerprint(root, item) {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.stringify([root, item.file ?? item.config], (_key, value) => typeof value === "function" ? `[fn]${String(value)}` : value);
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function declaredDestinations(configObj, only) {
|
|
13
|
+
const destinations = [];
|
|
14
|
+
const entries = Object.entries(configObj.platforms ?? {});
|
|
15
|
+
const selected = only ? entries.filter(([name]) => only.includes(name)) : entries;
|
|
16
|
+
for (const [, platform] of selected) for (const file of platform.files ?? []) if (file.destination) destinations.push(writtenDestination(platform.buildPath, file.destination));
|
|
17
|
+
return destinations;
|
|
18
|
+
}
|
|
19
|
+
async function isUpToDate({ compiledFingerprints, log, root, watch }, item, configObj, only) {
|
|
20
|
+
if (Object.values(configObj.platforms ?? {}).some((platform) => (platform.actions?.length ?? 0) > 0)) return false;
|
|
21
|
+
const destinations = declaredDestinations(configObj, only);
|
|
22
|
+
if (destinations.length === 0) return false;
|
|
23
|
+
const extraWatches = watch ? Array.isArray(watch) ? watch : [watch] : [];
|
|
24
|
+
const sources = await expandPatterns([...sourcePatternsOf(configObj), ...extraWatches.map((pattern) => (path.isAbsolute(pattern) ? pattern : path.resolve(root, pattern)).replace(/\\/g, "/"))], log);
|
|
25
|
+
if (item.file) sources.push(item.file.replace(/\\/g, "/"));
|
|
26
|
+
if (sources.length === 0) return false;
|
|
27
|
+
let newestSource = -Infinity;
|
|
28
|
+
let sawFile = false;
|
|
29
|
+
for (const source of sources) {
|
|
30
|
+
const stats = statOrNull(source);
|
|
31
|
+
if (!stats) return false;
|
|
32
|
+
if (stats.isDirectory()) continue;
|
|
33
|
+
sawFile = true;
|
|
34
|
+
newestSource = Math.max(newestSource, stats.mtimeMs);
|
|
35
|
+
}
|
|
36
|
+
if (!sawFile) return false;
|
|
37
|
+
let oldestDestination = Infinity;
|
|
38
|
+
for (const destination of destinations) {
|
|
39
|
+
const stats = statOrNull(destination);
|
|
40
|
+
if (!stats) return false;
|
|
41
|
+
oldestDestination = Math.min(oldestDestination, stats.mtimeMs);
|
|
42
|
+
}
|
|
43
|
+
if (oldestDestination <= newestSource) return false;
|
|
44
|
+
if (item.file) return true;
|
|
45
|
+
const fingerprint = configFingerprint(root, item);
|
|
46
|
+
return fingerprint !== null && compiledFingerprints.has(fingerprint);
|
|
47
|
+
}
|
|
48
|
+
function writtenDestination(buildPath, destination) {
|
|
49
|
+
return path.resolve(process.cwd(), buildPath ? path.join(buildPath, destination) : destination);
|
|
50
|
+
}
|
|
51
|
+
function statOrNull(file) {
|
|
52
|
+
try {
|
|
53
|
+
return fs.statSync(file);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { configFingerprint, declaredDestinations, isUpToDate, writtenDestination };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=up-to-date.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"up-to-date.js","names":[],"sources":["../src/up-to-date.ts"],"sourcesContent":["// Whether a configuration's compile can be skipped, and the destinations that\n// question is asked about.\n//\n// Nothing here holds state — not even the fingerprints of what this process\n// has compiled, which are process-wide and live in `index.ts` beside\n// `compilesInFlight`. What a plugin instance owns is handed in on each call.\n\nimport type { Config } from 'style-dictionary'\n\nimport fs from 'node:fs'\nimport path from 'node:path'\n\nimport type { Log, ResolvedConfig } from './config.js'\nimport type { UnpluginStyleDictionaryOptions } from './types.js'\n\nimport { expandPatterns, sourcePatternsOf } from './patterns.js'\n\n// A stable identity for one resolved configuration, or `null` where it\n// cannot have one. Functions are serialised by source rather than dropped,\n// because an inline `format` or `transform` is exactly the edit a\n// fingerprint has to notice, and `JSON.stringify` omits a function outright.\nexport function configFingerprint(\n root: string,\n item: ResolvedConfig,\n): null | string {\n try {\n return JSON.stringify(\n [root, item.file ?? item.config],\n (_key, value: unknown) =>\n typeof value === 'function' ? `[fn]${String(value)}` : value,\n )\n } catch {\n // Circular, or holding a BigInt. It takes no identity rather than a\n // wrong one, so it compiles every time exactly as it did before.\n return null\n }\n}\n\n// Every absolute destination a configuration declares, read off the\n// configuration itself rather than off an extended Style Dictionary\n// instance. Reading it here is the whole point: constructing the instance\n// is what the skip exists to avoid.\n//\n// Each one is named by `writtenDestination`, which `runBuilds` asks as well,\n// so the up-to-date check and the record of what was built name the same\n// files — the ones Style Dictionary actually wrote.\n//\n// `only` narrows this to named platforms, and exactly one caller wants that:\n// the up-to-date check, which asks whether the work *this* compile would do\n// is already done. Everywhere else the answer has to cover every declared\n// platform, because a file an unselected platform wrote earlier is still the\n// plugin's own output and has to stay out of the watch list.\nexport function declaredDestinations(\n configObj: Config,\n only?: string[],\n): string[] {\n const destinations: string[] = []\n\n const entries = Object.entries(configObj.platforms ?? {})\n const selected = only\n ? entries.filter(([name]) => only.includes(name))\n : entries\n\n for (const [, platform] of selected) {\n for (const file of platform.files ?? []) {\n if (file.destination) {\n destinations.push(\n writtenDestination(platform.buildPath, file.destination),\n )\n }\n }\n }\n\n return destinations\n}\n\n// Whether every file a configuration declares is already newer than every\n// file it reads, so its compile can be skipped.\n//\n// Conservative in every direction it can be: anything it cannot establish —\n// a destination that is missing, a source it cannot stat, a configuration\n// declaring no destinations at all — is a reason to build rather than to\n// skip.\nexport async function isUpToDate(\n {\n compiledFingerprints,\n log,\n root,\n watch,\n }: {\n compiledFingerprints: ReadonlySet<string>\n log: Log\n root: string\n watch: UnpluginStyleDictionaryOptions['watch']\n },\n item: ResolvedConfig,\n configObj: Config,\n only?: string[],\n): Promise<boolean> {\n // An action writes what no `destination` names, so there is nothing for\n // the comparison below to check and skipping would leave its work undone.\n const hasActions = Object.values(configObj.platforms ?? {}).some(\n (platform) => (platform.actions?.length ?? 0) > 0,\n )\n if (hasActions) return false\n\n const destinations = declaredDestinations(configObj, only)\n if (destinations.length === 0) return false\n\n // `options.watch` belongs in here as much as `source` does. A consumer\n // names an extra file because something in the build reads it — a custom\n // format's own data file, most obviously — and leaving it out let a change\n // to it be skipped over while the watcher dutifully reported it.\n const extraWatches = watch ? (Array.isArray(watch) ? watch : [watch]) : []\n\n const sources = await expandPatterns(\n [\n ...sourcePatternsOf(configObj),\n ...extraWatches.map((pattern) =>\n (path.isAbsolute(pattern)\n ? pattern\n : path.resolve(root, pattern)\n ).replace(/\\\\/g, '/'),\n ),\n ],\n log,\n )\n if (item.file) sources.push(item.file.replace(/\\\\/g, '/'))\n\n if (sources.length === 0) return false\n\n let newestSource = -Infinity\n let sawFile = false\n\n for (const source of sources) {\n const stats = statOrNull(source)\n if (!stats) return false\n\n // Directories are in this list on purpose — `expandPatterns` registers\n // each pattern's static parent so a token file created later is\n // noticed — but their mtime cannot be read as an input signal here. A\n // directory's mtime moves whenever an entry is added or renamed inside\n // it, and the atomic write renames every generated file into place, so\n // a `buildPath` inside a watched directory made the build itself the\n // newest thing the comparison could see. Nothing was ever up to date.\n if (stats.isDirectory()) continue\n\n sawFile = true\n newestSource = Math.max(newestSource, stats.mtimeMs)\n }\n\n // Every pattern expanded to directories alone, so nothing was actually\n // read. Style Dictionary would build an empty dictionary from that, and a\n // skip would present the empty result as current.\n if (!sawFile) return false\n\n let oldestDestination = Infinity\n for (const destination of destinations) {\n const stats = statOrNull(destination)\n if (!stats) return false\n oldestDestination = Math.min(oldestDestination, stats.mtimeMs)\n }\n\n if (oldestDestination <= newestSource) return false\n\n // A configuration given as a path has its own file among the sources\n // above, so an edit to it has already been accounted for and the skip\n // holds across processes.\n if (item.file) return true\n\n // One given as an object or a function has not. Only this process knows\n // what it looked like when those destinations were written, so the skip\n // holds only against a fingerprint recorded here.\n const fingerprint = configFingerprint(root, item)\n\n return fingerprint !== null && compiledFingerprints.has(fingerprint)\n}\n\n// Where Style Dictionary writes one file, as an absolute path.\n//\n// It joins the destination onto the platform's `buildPath` when there is one\n// and writes the result relative to the working directory. **Joins, not\n// resolves**: an absolute destination lands under the build path rather than\n// replacing it. Measured with `buildPath: 'gen/'` and an absolute destination,\n// Style Dictionary wrote `gen/<that path>`, while the plugin had named the\n// absolute path on its own — a file that was never written.\n//\n// `root` has no say. It is where a relative `config` path is looked up and\n// nothing more, and reading `buildPath` against it named files that did not\n// exist whenever the two differed (#362). Style Dictionary joins with\n// `path-unified/posix`; the platform's own `join` names the same file, and\n// resolving the result is what normalises the separators either way.\nexport function writtenDestination(\n buildPath: string | undefined,\n destination: string,\n): string {\n return path.resolve(\n process.cwd(),\n buildPath ? path.join(buildPath, destination) : destination,\n )\n}\n\n// `fs.statSync` without the throw. A file that is missing, or that cannot be\n// read, is the same answer to every caller here: nothing to compare against.\nfunction statOrNull(file: string): fs.Stats | null {\n try {\n return fs.statSync(file)\n } catch {\n return null\n }\n}\n"],"mappings":";;;;AAqBA,SAAgB,kBACd,MACA,MACe;CACf,IAAI;EACF,OAAO,KAAK,UACV,CAAC,MAAM,KAAK,QAAQ,KAAK,MAAM,IAC9B,MAAM,UACL,OAAO,UAAU,aAAa,OAAO,OAAO,KAAK,MAAM,KAC3D;CACF,QAAQ;EAGN,OAAO;CACT;AACF;AAgBA,SAAgB,qBACd,WACA,MACU;CACV,MAAM,eAAyB,CAAC;CAEhC,MAAM,UAAU,OAAO,QAAQ,UAAU,aAAa,CAAC,CAAC;CACxD,MAAM,WAAW,OACb,QAAQ,QAAQ,CAAC,UAAU,KAAK,SAAS,IAAI,CAAC,IAC9C;CAEJ,KAAK,MAAM,GAAG,aAAa,UACzB,KAAK,MAAM,QAAQ,SAAS,SAAS,CAAC,GACpC,IAAI,KAAK,aACP,aAAa,KACX,mBAAmB,SAAS,WAAW,KAAK,WAAW,CACzD;CAKN,OAAO;AACT;AASA,eAAsB,WACpB,EACE,sBACA,KACA,MACA,SAOF,MACA,WACA,MACkB;CAMlB,IAHmB,OAAO,OAAO,UAAU,aAAa,CAAC,CAAC,CAAC,CAAC,MACzD,cAAc,SAAS,SAAS,UAAU,KAAK,CAErC,GAAG,OAAO;CAEvB,MAAM,eAAe,qBAAqB,WAAW,IAAI;CACzD,IAAI,aAAa,WAAW,GAAG,OAAO;CAMtC,MAAM,eAAe,QAAS,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,IAAK,CAAC;CAEzE,MAAM,UAAU,MAAM,eACpB,CACE,GAAG,iBAAiB,SAAS,GAC7B,GAAG,aAAa,KAAK,aAClB,KAAK,WAAW,OAAO,IACpB,UACA,KAAK,QAAQ,MAAM,OAAO,EAAA,CAC5B,QAAQ,OAAO,GAAG,CACtB,CACF,GACA,GACF;CACA,IAAI,KAAK,MAAM,QAAQ,KAAK,KAAK,KAAK,QAAQ,OAAO,GAAG,CAAC;CAEzD,IAAI,QAAQ,WAAW,GAAG,OAAO;CAEjC,IAAI,eAAe;CACnB,IAAI,UAAU;CAEd,KAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,QAAQ,WAAW,MAAM;EAC/B,IAAI,CAAC,OAAO,OAAO;EASnB,IAAI,MAAM,YAAY,GAAG;EAEzB,UAAU;EACV,eAAe,KAAK,IAAI,cAAc,MAAM,OAAO;CACrD;CAKA,IAAI,CAAC,SAAS,OAAO;CAErB,IAAI,oBAAoB;CACxB,KAAK,MAAM,eAAe,cAAc;EACtC,MAAM,QAAQ,WAAW,WAAW;EACpC,IAAI,CAAC,OAAO,OAAO;EACnB,oBAAoB,KAAK,IAAI,mBAAmB,MAAM,OAAO;CAC/D;CAEA,IAAI,qBAAqB,cAAc,OAAO;CAK9C,IAAI,KAAK,MAAM,OAAO;CAKtB,MAAM,cAAc,kBAAkB,MAAM,IAAI;CAEhD,OAAO,gBAAgB,QAAQ,qBAAqB,IAAI,WAAW;AACrE;AAgBA,SAAgB,mBACd,WACA,aACQ;CACR,OAAO,KAAK,QACV,QAAQ,IAAI,GACZ,YAAY,KAAK,KAAK,WAAW,WAAW,IAAI,WAClD;AACF;AAIA,SAAS,WAAW,MAA+B;CACjD,IAAI;EACF,OAAO,GAAG,SAAS,IAAI;CACzB,QAAQ;EACN,OAAO;CACT;AACF"}
|
package/dist/watch-filter.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import picomatch from "picomatch";
|
|
2
2
|
//#region src/watch-filter.ts
|
|
3
|
+
function isWatchedSource(file, patterns, generatedDestinations) {
|
|
4
|
+
return !generatedDestinations.has(file.replace(/\\/g, "/")) && matchesWatchedFile(file, patterns);
|
|
5
|
+
}
|
|
3
6
|
function matchesWatchedFile(file, patterns) {
|
|
4
7
|
const normalizedFile = file.replace(/\\/g, "/");
|
|
5
8
|
return patterns.some((pattern) => {
|
|
@@ -8,6 +11,6 @@ function matchesWatchedFile(file, patterns) {
|
|
|
8
11
|
});
|
|
9
12
|
}
|
|
10
13
|
//#endregion
|
|
11
|
-
export { matchesWatchedFile };
|
|
14
|
+
export { isWatchedSource, matchesWatchedFile };
|
|
12
15
|
|
|
13
16
|
//# sourceMappingURL=watch-filter.js.map
|
package/dist/watch-filter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watch-filter.js","names":[],"sources":["../src/watch-filter.ts"],"sourcesContent":["import picomatch from 'picomatch'\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// `
|
|
1
|
+
{"version":3,"file":"watch-filter.js","names":[],"sources":["../src/watch-filter.ts"],"sourcesContent":["import picomatch from 'picomatch'\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.\nexport function isWatchedSource(\n file: string,\n patterns: string[],\n generatedDestinations: ReadonlySet<string>,\n): boolean {\n return (\n !generatedDestinations.has(file.replace(/\\\\/g, '/')) &&\n matchesWatchedFile(file, patterns)\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.\nexport function matchesWatchedFile(file: string, patterns: string[]): boolean {\n const normalizedFile = file.replace(/\\\\/g, '/')\n\n return patterns.some((pattern) => {\n const normalizedPattern = pattern.replace(/\\\\/g, '/')\n\n // A config file reaches this function as its own literal path, which is\n // both the common case and the one shape that is not a glob at all.\n return (\n normalizedPattern === normalizedFile ||\n picomatch.isMatch(normalizedFile, normalizedPattern)\n )\n })\n}\n"],"mappings":";;AASA,SAAgB,gBACd,MACA,UACA,uBACS;CACT,OACE,CAAC,sBAAsB,IAAI,KAAK,QAAQ,OAAO,GAAG,CAAC,KACnD,mBAAmB,MAAM,QAAQ;AAErC;AAkCA,SAAgB,mBAAmB,MAAc,UAA6B;CAC5E,MAAM,iBAAiB,KAAK,QAAQ,OAAO,GAAG;CAE9C,OAAO,SAAS,MAAM,YAAY;EAChC,MAAM,oBAAoB,QAAQ,QAAQ,OAAO,GAAG;EAIpD,OACE,sBAAsB,kBACtB,UAAU,QAAQ,gBAAgB,iBAAiB;CAEvD,CAAC;AACH"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kanso-labs/unplugin-style-dictionary",
|
|
3
|
-
"version": "0.10.
|
|
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.
|
|
97
|
+
"oxfmt": "0.69.0",
|
|
98
98
|
"oxlint": "1.83.0",
|
|
99
99
|
"oxlint-tsgolint": "7.0.2002",
|
|
100
100
|
"publint": "0.3.24",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"rolldown": "1.2.9",
|
|
103
103
|
"rollup": "4.63.4",
|
|
104
104
|
"semver": "7.8.5",
|
|
105
|
-
"style-dictionary": "5.5.
|
|
105
|
+
"style-dictionary": "5.5.5",
|
|
106
106
|
"tsdown": "0.23.0",
|
|
107
107
|
"typescript": "6.0.3",
|
|
108
108
|
"typescript-eslint": "8.70.0",
|