@kanso-labs/unplugin-style-dictionary 0.8.0 → 0.10.0

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/dist/index.js CHANGED
@@ -36,9 +36,48 @@ function isMessageChannel(value) {
36
36
  function isThenable(value) {
37
37
  return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
38
38
  }
39
+ function looksLikeConfig(value) {
40
+ if (typeof value !== "object" || value === null) return false;
41
+ return [
42
+ "include",
43
+ "platforms",
44
+ "source",
45
+ "tokens"
46
+ ].some((key) => key in value);
47
+ }
48
+ function nodeModulesNegations(paths) {
49
+ const negations = /* @__PURE__ */ new Set();
50
+ for (const file of paths) {
51
+ const normalised = file.replace(/\\/g, "/");
52
+ if (normalised.includes("/node_modules/")) negations.add(`!${normalised}`);
53
+ }
54
+ return Array.from(negations);
55
+ }
56
+ function nodeModulesWatchDirectories(paths) {
57
+ const directories = /* @__PURE__ */ new Set();
58
+ for (const file of paths) {
59
+ const normalised = file.replace(/\\/g, "/");
60
+ if (!normalised.includes("/node_modules/")) continue;
61
+ directories.add(path.dirname(normalised));
62
+ }
63
+ return Array.from(directories);
64
+ }
39
65
  function paint(code, value, allowed) {
40
66
  return allowed ? `\u001B[${code}m${value}\u001B[0m` : value;
41
67
  }
68
+ async function patternsMatchingNothing(patterns) {
69
+ const barren = [];
70
+ for (const pattern of patterns) {
71
+ if (!GLOB_CHARACTERS.test(pattern)) {
72
+ if (!fs.existsSync(pattern)) barren.push(pattern);
73
+ continue;
74
+ }
75
+ try {
76
+ if ((await glob([pattern], { absolute: true })).length === 0) barren.push(pattern);
77
+ } catch {}
78
+ }
79
+ return barren;
80
+ }
42
81
  function unwrapDefault(value) {
43
82
  return typeof value === "object" && value !== null && "default" in value ? value.default ?? value : value;
44
83
  }
@@ -62,6 +101,43 @@ function temporaryPathFor(destination) {
62
101
  const extension = path.extname(destination);
63
102
  return path.join(path.dirname(destination), `.${path.basename(destination, extension)}.${process.pid}.${temporaryFileCounter++}.tmp`);
64
103
  }
104
+ const RENAME_RETRY_CODES = /* @__PURE__ */ new Set(["EBUSY", "EPERM"]);
105
+ const RENAME_RETRY_DELAYS_MS = [
106
+ 1,
107
+ 2,
108
+ 4,
109
+ 8,
110
+ 16,
111
+ 32,
112
+ 64,
113
+ 128
114
+ ];
115
+ function isRetryableRenameError(error) {
116
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && RENAME_RETRY_CODES.has(error.code);
117
+ }
118
+ const sleepSync = (ms) => {
119
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
120
+ };
121
+ async function renameWithRetry(temporary, destination) {
122
+ for (const delay of RENAME_RETRY_DELAYS_MS) try {
123
+ await fs.promises.rename(temporary, destination);
124
+ return;
125
+ } catch (err) {
126
+ if (!isRetryableRenameError(err)) throw err;
127
+ await new Promise((resolve) => setTimeout(resolve, delay));
128
+ }
129
+ await fs.promises.rename(temporary, destination);
130
+ }
131
+ function renameWithRetrySync(temporary, destination) {
132
+ for (const delay of RENAME_RETRY_DELAYS_MS) try {
133
+ fs.renameSync(temporary, destination);
134
+ return;
135
+ } catch (err) {
136
+ if (!isRetryableRenameError(err)) throw err;
137
+ sleepSync(delay);
138
+ }
139
+ fs.renameSync(temporary, destination);
140
+ }
65
141
  const writeFileAtomic = async (file, data, options) => {
66
142
  if (typeof file !== "string") return fs.promises.writeFile(file, data, options);
67
143
  const temporary = temporaryPathFor(file);
@@ -71,7 +147,7 @@ const writeFileAtomic = async (file, data, options) => {
71
147
  discardTemporaryFile(temporary);
72
148
  return;
73
149
  }
74
- await fs.promises.rename(temporary, file);
150
+ await renameWithRetry(temporary, file);
75
151
  } catch (err) {
76
152
  discardTemporaryFile(temporary);
77
153
  throw err;
@@ -89,7 +165,7 @@ const writeFileSyncAtomic = (file, data, options) => {
89
165
  discardTemporaryFile(temporary);
90
166
  return;
91
167
  }
92
- fs.renameSync(temporary, file);
168
+ renameWithRetrySync(temporary, file);
93
169
  } catch (err) {
94
170
  discardTemporaryFile(temporary);
95
171
  throw err;
@@ -105,6 +181,9 @@ const IMPORTED_CONFIG_EXTENSIONS = [
105
181
  ".mjs",
106
182
  ".ts"
107
183
  ];
184
+ function describeConfig(item, index) {
185
+ return item.file ? `The configuration ${item.file}` : `The configuration at position ${index + 1}`;
186
+ }
108
187
  function isImportedConfig(file) {
109
188
  return IMPORTED_CONFIG_EXTENSIONS.some((extension) => file.endsWith(extension));
110
189
  }
@@ -139,11 +218,16 @@ function statOrNull(file) {
139
218
  }
140
219
  }
141
220
  const unpluginFactory = (options = {}, meta) => {
142
- const isWebpack = meta.framework === "webpack";
143
- const { cache = true, errorOverlay = true, failOnError = "build", logLevel, onBuildEnd, onBuildError, onBuildStart, report = true, root: rootOption, silent = false } = options;
221
+ const isWebpack = meta.framework === "webpack" || meta.framework === "rspack";
222
+ const { cache = true, errorOverlay = true, failOnError = "build", logLevel, onBuildEnd, onBuildError, onBuildStart, platforms: platformsOption, report = true, root: rootOption, silent = false } = options;
144
223
  const level = logLevel ?? (silent ? "silent" : void 0);
145
224
  const quiet = level === "silent" || level === "warn";
146
225
  const verbosity = level === void 0 ? void 0 : level === "verbose" ? "verbose" : level === "silent" ? "silent" : "default";
226
+ const platformsFor = (context) => {
227
+ if (platformsOption === void 0) return void 0;
228
+ if (Array.isArray(platformsOption)) return platformsOption;
229
+ return context === void 0 ? platformsOption.build : platformsOption.watch;
230
+ };
147
231
  const failsTheBuild = (context) => failOnError === true || (context === void 0 ? failOnError === "build" : failOnError === "serve");
148
232
  let hostCommand = "build";
149
233
  let hostMode;
@@ -164,6 +248,7 @@ const unpluginFactory = (options = {}, meta) => {
164
248
  let cachedPatterns;
165
249
  let watchRebuild = false;
166
250
  let hasCompiled = false;
251
+ let hostClosed = false;
167
252
  const expandPatterns = async (patterns) => {
168
253
  const paths = /* @__PURE__ */ new Set();
169
254
  const globs = [];
@@ -229,17 +314,33 @@ const unpluginFactory = (options = {}, meta) => {
229
314
  };
230
315
  const resolveConfigs = async () => {
231
316
  let rawConfig = options.config;
232
- if (!rawConfig) for (const file of [
233
- "sd.config.json",
234
- "config.json",
235
- "sd.config.js",
236
- "sd.config.mjs"
237
- ]) {
238
- const fullPath = path.resolve(root, file);
239
- if (fs.existsSync(fullPath)) {
317
+ if (rawConfig === false) return [];
318
+ if (!rawConfig) {
319
+ const defaults = [
320
+ "sd.config.json",
321
+ "config.json",
322
+ "sd.config.js",
323
+ "sd.config.mjs"
324
+ ];
325
+ const rejected = [];
326
+ for (const file of defaults) {
327
+ const fullPath = path.resolve(root, file);
328
+ if (!fs.existsSync(fullPath)) continue;
329
+ if (!looksLikeConfig(await readConfigObject({
330
+ config: fullPath,
331
+ file: fullPath
332
+ }, false))) {
333
+ rejected.push(file);
334
+ continue;
335
+ }
336
+ if (!announcedDiscovery) {
337
+ announcedDiscovery = true;
338
+ log(`Using the configuration it found at ${fullPath}`, "info");
339
+ }
240
340
  rawConfig = file;
241
341
  break;
242
342
  }
343
+ if (rejected.length > 0) log(`Ignored ${rejected.join(", ")} in ${root}: nothing there declares platforms, source, include or tokens, so it does not look like a Style Dictionary configuration. Name it with the config option if it is one, or set config to false to stop looking.`, "error");
243
344
  }
244
345
  if (!rawConfig) {
245
346
  log("No configuration specified and no default config file found. Style Dictionary will not compile.", "error");
@@ -324,9 +425,11 @@ const unpluginFactory = (options = {}, meta) => {
324
425
  return loaded;
325
426
  }
326
427
  };
327
- const declaredDestinations = (configObj) => {
428
+ const declaredDestinations = (configObj, only) => {
328
429
  const destinations = [];
329
- for (const platform of Object.values(configObj.platforms ?? {})) {
430
+ const entries = Object.entries(configObj.platforms ?? {});
431
+ const selected = only ? entries.filter(([name]) => only.includes(name)) : entries;
432
+ for (const [, platform] of selected) {
330
433
  const buildPath = platform.buildPath ?? "";
331
434
  const absoluteBuildPath = path.isAbsolute(buildPath) ? buildPath : path.resolve(root, buildPath);
332
435
  for (const file of platform.files ?? []) if (file.destination) destinations.push(path.isAbsolute(file.destination) ? file.destination : path.resolve(absoluteBuildPath, file.destination));
@@ -340,9 +443,9 @@ const unpluginFactory = (options = {}, meta) => {
340
443
  return null;
341
444
  }
342
445
  };
343
- const isUpToDate = async (item, configObj) => {
446
+ const isUpToDate = async (item, configObj, only) => {
344
447
  if (Object.values(configObj.platforms ?? {}).some((platform) => (platform.actions?.length ?? 0) > 0)) return false;
345
- const destinations = declaredDestinations(configObj);
448
+ const destinations = declaredDestinations(configObj, only);
346
449
  if (destinations.length === 0) return false;
347
450
  const extraWatches = options.watch ? Array.isArray(options.watch) ? options.watch : [options.watch] : [];
348
451
  const sources = await expandPatterns([...sourcePatternsOf(configObj), ...extraWatches.map((pattern) => (path.isAbsolute(pattern) ? pattern : path.resolve(root, pattern)).replace(/\\/g, "/"))]);
@@ -405,9 +508,10 @@ const unpluginFactory = (options = {}, meta) => {
405
508
  try {
406
509
  if (!context) log("Compiling design tokens...", "info");
407
510
  if (onBuildStart) callHook("onBuildStart", onBuildStart);
408
- for (const item of resolvedConfigs) {
511
+ for (const [index, item] of resolvedConfigs.entries()) {
409
512
  const declared = cache ? await readConfigObject(item, false) : null;
410
- if (declared && await isUpToDate(item, declared)) {
513
+ const selectedPlatforms = platformsFor(context);
514
+ if (declared && await isUpToDate(item, declared, selectedPlatforms)) {
411
515
  for (const destination of declaredDestinations(declared)) generatedFiles.add(destination);
412
516
  skipped++;
413
517
  continue;
@@ -417,8 +521,23 @@ const unpluginFactory = (options = {}, meta) => {
417
521
  mutateOriginal: true,
418
522
  verbosity
419
523
  });
524
+ if (sd.allTokens.length === 0) {
525
+ const asObject = await readConfigObject(item, false);
526
+ const barren = asObject ? await patternsMatchingNothing(sourcePatternsOf(asObject)) : [];
527
+ throw new Error([
528
+ `${describeConfig(item, index)} resolved no tokens, so its output would be emptied.`,
529
+ barren.length > 0 ? `These patterns matched no files: ${barren.join(", ")}` : `It declares no source or include patterns that matched anything.`,
530
+ `Nothing was written. Set failOnError to false to build anyway.`
531
+ ].join(" "));
532
+ }
420
533
  sd.volume = atomicVolume;
421
- await sd.buildAllPlatforms();
534
+ if (selectedPlatforms === void 0) await sd.buildAllPlatforms();
535
+ else {
536
+ const defined = Object.keys(sd.platforms);
537
+ const unknown = selectedPlatforms.filter((name) => !defined.includes(name));
538
+ if (unknown.length > 0) throw new Error(`${describeConfig(item, index)} does not define the platform(s) ${unknown.join(", ")}. It defines ${defined.join(", ")}.`);
539
+ for (const name of selectedPlatforms) await sd.buildPlatform(name);
540
+ }
422
541
  for (const platform of Object.values(sd.platforms)) {
423
542
  const buildPath = platform.buildPath ?? "";
424
543
  for (const file of platform.files ?? []) if (file.destination) {
@@ -487,6 +606,8 @@ const unpluginFactory = (options = {}, meta) => {
487
606
  let inFlight;
488
607
  let waiting = [];
489
608
  let refreshServerWatchList;
609
+ let announcedDiscovery = false;
610
+ let startupResolved;
490
611
  let notifyBuildOutcome;
491
612
  const drain = async () => {
492
613
  while (pendingReason !== void 0) {
@@ -516,6 +637,7 @@ const unpluginFactory = (options = {}, meta) => {
516
637
  }
517
638
  };
518
639
  const schedule = async (reason) => {
640
+ if (hostClosed) return;
519
641
  pendingReason = reason;
520
642
  const covered = new Promise((resolve, reject) => {
521
643
  waiting.push((failure) => {
@@ -531,14 +653,46 @@ const unpluginFactory = (options = {}, meta) => {
531
653
  debounceTimer.unref();
532
654
  return covered;
533
655
  };
656
+ const closeWatcher = () => {
657
+ hostClosed = true;
658
+ };
659
+ const adoptCompiler = (compiler) => {
660
+ if (rootOption === void 0) root = compiler.options.context ?? process.cwd();
661
+ hostMode = compiler.options.mode;
662
+ const pending = [];
663
+ host = { error: (message) => {
664
+ pending.push(message);
665
+ } };
666
+ compiler.hooks.compilation.tap("unplugin-style-dictionary", (compilation) => {
667
+ for (const message of pending.splice(0)) {
668
+ const reported = new Error(message);
669
+ reported.name = "UnpluginStyleDictionaryWarning";
670
+ compilation.warnings.push(reported);
671
+ }
672
+ });
673
+ const drainToConsole = () => {
674
+ for (const message of pending.splice(0)) console.error(paint("31", message, stderrColour));
675
+ };
676
+ compiler.hooks.failed.tap("unplugin-style-dictionary", drainToConsole);
677
+ compiler.hooks.done.tap("unplugin-style-dictionary", drainToConsole);
678
+ compiler.hooks.beforeCompile.tapPromise("unplugin-style-dictionary", async () => {
679
+ isWatching = compiler.watchMode;
680
+ const resolved = await resolveConfigs();
681
+ if (resolved.length === 0) return;
682
+ await compileOnceAcrossInstances(resolved);
683
+ hasCompiled = true;
684
+ });
685
+ };
534
686
  return {
535
687
  async buildStart() {
536
688
  adoptHost(this);
537
689
  adoptWatchMode(this);
538
690
  const resolved = await resolveConfigs();
539
691
  if (resolved.length === 0) return;
540
- const { paths } = await getWatchTargets(resolved);
541
- for (const file of paths) this.addWatchFile(file);
692
+ if (!hostClosed) {
693
+ const { paths } = await getWatchTargets(resolved);
694
+ for (const file of paths) this.addWatchFile(file);
695
+ }
542
696
  if (isWebpack) return;
543
697
  if (watchRebuild && hasCompiled) {
544
698
  watchRebuild = false;
@@ -548,8 +702,12 @@ const unpluginFactory = (options = {}, meta) => {
548
702
  hasCompiled = true;
549
703
  },
550
704
  name: "unplugin-style-dictionary",
705
+ rolldown: { closeWatcher },
706
+ rollup: { closeWatcher },
707
+ rspack: adoptCompiler,
551
708
  vite: {
552
- configResolved(config) {
709
+ closeWatcher,
710
+ async configResolved(config) {
553
711
  if (rootOption === void 0) root = config.root || process.cwd();
554
712
  hostCommand = config.command;
555
713
  hostMode = config.mode;
@@ -561,16 +719,62 @@ const unpluginFactory = (options = {}, meta) => {
561
719
  config.logger.info(message);
562
720
  }
563
721
  };
722
+ if (config.command !== "serve") return;
723
+ isWatching = true;
724
+ try {
725
+ startupResolved = await resolveConfigs();
726
+ if (startupResolved.length === 0) return;
727
+ const { paths } = await getWatchTargets(startupResolved);
728
+ const negations = nodeModulesNegations(paths);
729
+ if (negations.length === 0) return;
730
+ const existing = config.server.watch?.ignored;
731
+ config.server.watch = {
732
+ ...config.server.watch,
733
+ ignored: [...Array.isArray(existing) ? existing : existing === void 0 ? [] : [existing], ...negations]
734
+ };
735
+ } catch (err) {
736
+ log(`Could not read the configuration while preparing the watch list: ${errorMessage(err)}`, "error");
737
+ startupResolved = void 0;
738
+ }
564
739
  },
565
740
  async configureServer(server) {
566
741
  isWatching = true;
567
- const resolved = await resolveConfigs();
742
+ const resolved = startupResolved ?? await resolveConfigs();
743
+ startupResolved = void 0;
568
744
  if (resolved.length === 0) return;
569
745
  let targets = await getWatchTargets(resolved);
570
746
  server.watcher.add(targets.paths);
747
+ const ownWatchers = /* @__PURE__ */ new Map();
748
+ const watchNodeModules = (forPaths) => {
749
+ const wanted = new Set(nodeModulesWatchDirectories(forPaths));
750
+ for (const [directory, watcher] of ownWatchers) {
751
+ if (wanted.has(directory)) continue;
752
+ watcher.close();
753
+ ownWatchers.delete(directory);
754
+ }
755
+ for (const directory of wanted) {
756
+ if (ownWatchers.has(directory)) continue;
757
+ try {
758
+ const watcher = fs.watch(directory, (_event, filename) => {
759
+ if (filename === null) return;
760
+ const changed = path.posix.join(directory, filename);
761
+ if (!isWatchedSource(changed, targets.patterns)) return;
762
+ schedule(path.basename(changed)).catch(() => {});
763
+ });
764
+ watcher.unref();
765
+ ownWatchers.set(directory, watcher);
766
+ } catch {}
767
+ }
768
+ };
769
+ watchNodeModules(targets.paths);
770
+ server.httpServer?.once("close", () => {
771
+ for (const watcher of ownWatchers.values()) watcher.close();
772
+ ownWatchers.clear();
773
+ });
571
774
  refreshServerWatchList = async (rebuilt) => {
572
775
  targets = await getWatchTargets(rebuilt);
573
776
  server.watcher.add(targets.paths);
777
+ watchNodeModules(targets.paths);
574
778
  };
575
779
  if (errorOverlay) {
576
780
  let overlayShowing = false;
@@ -604,6 +808,7 @@ const unpluginFactory = (options = {}, meta) => {
604
808
  async watchChange(id) {
605
809
  adoptHost(this);
606
810
  adoptWatchMode(this);
811
+ if (hostClosed) return;
607
812
  watchRebuild = true;
608
813
  if (cachedPatterns && !isWatchedSource(id, cachedPatterns)) return;
609
814
  const resolved = await resolveConfigs();
@@ -613,33 +818,7 @@ const unpluginFactory = (options = {}, meta) => {
613
818
  if (!isWebpack) await schedule(path.basename(id));
614
819
  for (const file of await expandPatterns(patterns)) this.addWatchFile(file);
615
820
  },
616
- webpack(compiler) {
617
- if (rootOption === void 0) root = compiler.options.context ?? process.cwd();
618
- hostMode = compiler.options.mode;
619
- const pending = [];
620
- host = { error: (message) => {
621
- pending.push(message);
622
- } };
623
- compiler.hooks.compilation.tap("unplugin-style-dictionary", (compilation) => {
624
- for (const message of pending.splice(0)) {
625
- const reported = new Error(message);
626
- reported.name = "UnpluginStyleDictionaryWarning";
627
- compilation.warnings.push(reported);
628
- }
629
- });
630
- const drainToConsole = () => {
631
- for (const message of pending.splice(0)) console.error(paint("31", message, stderrColour));
632
- };
633
- compiler.hooks.failed.tap("unplugin-style-dictionary", drainToConsole);
634
- compiler.hooks.done.tap("unplugin-style-dictionary", drainToConsole);
635
- compiler.hooks.beforeCompile.tapPromise("unplugin-style-dictionary", async () => {
636
- isWatching = compiler.watchMode;
637
- const resolved = await resolveConfigs();
638
- if (resolved.length === 0) return;
639
- await compileOnceAcrossInstances(resolved);
640
- hasCompiled = true;
641
- });
642
- }
821
+ webpack: adoptCompiler
643
822
  };
644
823
  };
645
824
  const unplugin = /* #__PURE__ */ createUnplugin(unpluginFactory);