@kanso-labs/unplugin-style-dictionary 0.10.12 → 0.10.14

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
@@ -1,14 +1,25 @@
1
1
  import { colourAllowed, paint } from "./colour.js";
2
2
  import { asError, errorMessage } from "./errors.js";
3
3
  import { resolveConfigOption } from "./config.js";
4
- import { expandPatterns, watchPatternsOf } from "./patterns.js";
4
+ import { expandPatterns, realpathOf, watchOptionPatterns, watchPatternsOf } from "./patterns.js";
5
5
  import { runBuilds } from "./compile.js";
6
6
  import { createScheduler } from "./scheduler.js";
7
- import { isWatchedSource } from "./watch-filter.js";
7
+ import { isWatchedSource, registeredSpellingOf } from "./watch-filter.js";
8
8
  import fs from "node:fs";
9
9
  import path from "node:path";
10
10
  import { createUnplugin } from "unplugin";
11
11
  //#region src/index.ts
12
+ function appendChokidarIgnored(watch, negations) {
13
+ if (negations.length === 0) return;
14
+ const chokidar = Reflect.get(watch, "chokidar");
15
+ const options = typeof chokidar === "object" && chokidar !== null ? chokidar : {};
16
+ const existing = Reflect.get(options, "ignored");
17
+ const previous = Array.isArray(existing) ? existing : existing === void 0 ? [] : [existing];
18
+ Reflect.set(watch, "chokidar", {
19
+ ...options,
20
+ ignored: [...previous, ...negations]
21
+ });
22
+ }
12
23
  function discardTemporaryFile(temporary) {
13
24
  try {
14
25
  fs.rmSync(temporary, { force: true });
@@ -35,25 +46,93 @@ function nodeModulesWatchDirectories(paths) {
35
46
  return Array.from(directories);
36
47
  }
37
48
  let temporaryFileCounter = 0;
38
- async function rendersWhatIsAlreadyThere(temporary, destination) {
49
+ const sweptTemporaryPrefixes = /* @__PURE__ */ new Set();
50
+ const ABANDONED_TEMPORARY_AGE_MS = 36e5;
51
+ async function alreadyHolds(destination, rendered) {
39
52
  try {
40
- const [existing, rendered] = await Promise.all([fs.promises.readFile(destination), fs.promises.readFile(temporary)]);
41
- return existing.equals(rendered);
53
+ return (await fs.promises.readFile(destination)).equals(rendered);
42
54
  } catch {
43
55
  return false;
44
56
  }
45
57
  }
46
- function rendersWhatIsAlreadyThereSync(temporary, destination) {
58
+ function alreadyHoldsSync(destination, rendered) {
47
59
  try {
48
- return fs.readFileSync(destination).equals(fs.readFileSync(temporary));
60
+ return fs.readFileSync(destination).equals(rendered);
49
61
  } catch {
50
62
  return false;
51
63
  }
52
64
  }
65
+ function atomicTargetOf(destination) {
66
+ try {
67
+ return fs.realpathSync(destination);
68
+ } catch {
69
+ try {
70
+ return path.resolve(path.dirname(destination), fs.readlinkSync(destination));
71
+ } catch {
72
+ return destination;
73
+ }
74
+ }
75
+ }
76
+ function bytesToWrite(data, options) {
77
+ if (ArrayBuffer.isView(data)) return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
78
+ if (typeof data !== "string") return void 0;
79
+ const encoding = typeof options === "string" ? options : options?.encoding;
80
+ return Buffer.from(data, encoding ?? "utf8");
81
+ }
82
+ function isOlderThan(file, ageMs) {
83
+ try {
84
+ return Date.now() - fs.statSync(file).mtimeMs > ageMs;
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+ function isRunning(pid) {
90
+ try {
91
+ process.kill(pid, 0);
92
+ return true;
93
+ } catch (err) {
94
+ return !(typeof err === "object" && err !== null && "code" in err && err.code === "ESRCH");
95
+ }
96
+ }
97
+ function keepExistingMode(temporary, target) {
98
+ let existing;
99
+ try {
100
+ existing = fs.statSync(target);
101
+ } catch {
102
+ return;
103
+ }
104
+ fs.chmodSync(temporary, existing.mode & 4095);
105
+ if (process.getuid?.() === 0) fs.chownSync(temporary, existing.uid, existing.gid);
106
+ }
107
+ function sweepAbandonedTemporaries(destination) {
108
+ const directory = path.dirname(destination);
109
+ const stem = path.basename(destination, path.extname(destination));
110
+ const prefix = path.join(directory, `.${stem}.`);
111
+ if (sweptTemporaryPrefixes.has(prefix)) return;
112
+ sweptTemporaryPrefixes.add(prefix);
113
+ let names;
114
+ try {
115
+ names = fs.readdirSync(directory);
116
+ } catch {
117
+ return;
118
+ }
119
+ for (const name of names) {
120
+ const writer = temporaryWriterOf(name, stem);
121
+ if (writer === void 0) continue;
122
+ const file = path.join(directory, name);
123
+ if (isRunning(writer) && !isOlderThan(file, ABANDONED_TEMPORARY_AGE_MS)) continue;
124
+ discardTemporaryFile(file);
125
+ }
126
+ }
53
127
  function temporaryPathFor(destination) {
54
128
  const extension = path.extname(destination);
55
129
  return path.join(path.dirname(destination), `.${path.basename(destination, extension)}.${process.pid}.${temporaryFileCounter++}.tmp`);
56
130
  }
131
+ function temporaryWriterOf(name, stem) {
132
+ if (!name.startsWith(`.${stem}.`) || !name.endsWith(".tmp")) return void 0;
133
+ const numbers = /^(\d+)\.\d+$/.exec(name.slice(stem.length + 2, -4));
134
+ return numbers ? Number(numbers[1]) : void 0;
135
+ }
57
136
  const RENAME_RETRY_CODES = /* @__PURE__ */ new Set(["EBUSY", "EPERM"]);
58
137
  const RENAME_RETRY_DELAYS_MS = [
59
138
  1,
@@ -106,14 +185,19 @@ function renameWithRetrySync(temporary, destination) {
106
185
  }
107
186
  const writeFileAtomic = async (file, data, options) => {
108
187
  if (typeof file !== "string") return fs.promises.writeFile(file, data, options);
109
- const temporary = temporaryPathFor(file);
188
+ const target = atomicTargetOf(file);
189
+ sweepAbandonedTemporaries(target);
190
+ const bytes = bytesToWrite(data, options);
191
+ if (bytes && await alreadyHolds(target, bytes)) return;
192
+ const temporary = temporaryPathFor(target);
110
193
  try {
111
194
  await fs.promises.writeFile(temporary, data, options);
112
- if (await rendersWhatIsAlreadyThere(temporary, file)) {
195
+ if (!bytes && await alreadyHolds(target, await fs.promises.readFile(temporary))) {
113
196
  discardTemporaryFile(temporary);
114
197
  return;
115
198
  }
116
- await renameWithRetry(temporary, file);
199
+ keepExistingMode(temporary, target);
200
+ await renameWithRetry(temporary, target);
117
201
  } catch (err) {
118
202
  discardTemporaryFile(temporary);
119
203
  throw err;
@@ -124,32 +208,122 @@ const writeFileSyncAtomic = (file, data, options) => {
124
208
  fs.writeFileSync(file, data, options);
125
209
  return;
126
210
  }
127
- const temporary = temporaryPathFor(file);
211
+ const target = atomicTargetOf(file);
212
+ sweepAbandonedTemporaries(target);
213
+ const bytes = bytesToWrite(data, options);
214
+ if (bytes && alreadyHoldsSync(target, bytes)) return;
215
+ const temporary = temporaryPathFor(target);
128
216
  try {
129
217
  fs.writeFileSync(temporary, data, options);
130
- if (rendersWhatIsAlreadyThereSync(temporary, file)) {
131
- discardTemporaryFile(temporary);
132
- return;
133
- }
134
- renameWithRetrySync(temporary, file);
218
+ keepExistingMode(temporary, target);
219
+ renameWithRetrySync(temporary, target);
220
+ } catch (err) {
221
+ discardTemporaryFile(temporary);
222
+ throw err;
223
+ }
224
+ };
225
+ const copyFileAtomic = async (source, destination, mode) => {
226
+ if (typeof destination !== "string" || excludesExisting(mode)) return fs.promises.copyFile(source, destination, mode);
227
+ const target = atomicTargetOf(destination);
228
+ sweepAbandonedTemporaries(target);
229
+ if (await alreadyHolds(target, await fs.promises.readFile(source))) return;
230
+ const temporary = temporaryPathFor(target);
231
+ try {
232
+ await fs.promises.copyFile(source, temporary, mode);
233
+ await renameWithRetry(temporary, target);
234
+ } catch (err) {
235
+ discardTemporaryFile(temporary);
236
+ throw err;
237
+ }
238
+ };
239
+ const copyFileSyncAtomic = (source, destination, mode) => {
240
+ if (typeof destination !== "string" || excludesExisting(mode)) {
241
+ fs.copyFileSync(source, destination, mode);
242
+ return;
243
+ }
244
+ const target = atomicTargetOf(destination);
245
+ sweepAbandonedTemporaries(target);
246
+ if (alreadyHoldsSync(target, fs.readFileSync(source))) return;
247
+ const temporary = temporaryPathFor(target);
248
+ try {
249
+ fs.copyFileSync(source, temporary, mode);
250
+ renameWithRetrySync(temporary, target);
135
251
  } catch (err) {
136
252
  discardTemporaryFile(temporary);
137
253
  throw err;
138
254
  }
139
255
  };
256
+ function excludesExisting(mode) {
257
+ return mode !== void 0 && (mode & fs.constants.COPYFILE_EXCL) !== 0;
258
+ }
259
+ const cpAtomic = async (source, destination, options) => {
260
+ if (typeof source !== "string" || typeof destination !== "string" || !walksCopy(options) || isWithin(destination, source)) return fs.promises.cp(source, destination, options);
261
+ await fs.promises.mkdir(path.dirname(path.resolve(destination)), { recursive: true });
262
+ await copyTreeAtomic(source, destination, options);
263
+ };
264
+ async function copyTreeAtomic(source, destination, options) {
265
+ const stat = await fs.promises.lstat(source);
266
+ const existing = await fs.promises.lstat(destination).catch(() => void 0);
267
+ if (stat.isFile() && !existing?.isDirectory()) {
268
+ await copyFileAtomic(source, destination, options?.mode);
269
+ return;
270
+ }
271
+ if (!stat.isDirectory() || !options?.recursive || existing && !existing.isDirectory()) {
272
+ await fs.promises.cp(source, destination, options);
273
+ return;
274
+ }
275
+ const created = await fs.promises.mkdir(destination, { recursive: true });
276
+ for (const entry of await fs.promises.readdir(source)) await copyTreeAtomic(path.join(source, entry), path.join(destination, entry), options);
277
+ if (created !== void 0) await fs.promises.chmod(destination, stat.mode);
278
+ }
279
+ function isWithin(candidate, directory) {
280
+ const relative = path.relative(path.resolve(directory), path.resolve(candidate));
281
+ return !(relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative));
282
+ }
283
+ function walksCopy(options) {
284
+ return Object.entries(options ?? {}).every(([key, value]) => value === void 0 || key === "mode" || key === "recursive" || key === "force" && value === true);
285
+ }
140
286
  const atomicVolume = Object.create(fs, {
141
- promises: { value: Object.create(fs.promises, { writeFile: { value: writeFileAtomic } }) },
287
+ copyFileSync: { value: copyFileSyncAtomic },
288
+ promises: { value: Object.create(fs.promises, {
289
+ copyFile: { value: copyFileAtomic },
290
+ cp: { value: cpAtomic },
291
+ writeFile: { value: writeFileAtomic }
292
+ }) },
142
293
  writeFileSync: { value: writeFileSyncAtomic }
143
294
  });
144
- const compiledFingerprints = /* @__PURE__ */ new Set();
295
+ const destinationRecords = /* @__PURE__ */ new Map();
296
+ const persistedRecordFiles = /* @__PURE__ */ new Map();
145
297
  const compilesInFlight = /* @__PURE__ */ new Map();
146
- function buildKey(root, resolved) {
298
+ function buildKey(root, resolved, settings) {
147
299
  try {
148
- return JSON.stringify([root, resolved.map((item) => item.file ?? item.config)], (_key, value) => typeof value === "function" ? `[fn]${String(value)}` : value);
300
+ return JSON.stringify([
301
+ root,
302
+ resolved.map((item) => item.file ?? item.config),
303
+ settings
304
+ ], (_key, value) => typeof value === "function" ? `[fn]${String(value)}` : value);
149
305
  } catch {
150
306
  return null;
151
307
  }
152
308
  }
309
+ function contextDependenciesOf(context) {
310
+ const native = context.getNativeBuildContext?.();
311
+ const compilation = typeof native === "object" && native !== null && "compilation" in native ? native.compilation : void 0;
312
+ const dependencies = typeof compilation === "object" && compilation !== null && "contextDependencies" in compilation ? compilation.contextDependencies : void 0;
313
+ if (typeof dependencies !== "object" || dependencies === null || !("add" in dependencies)) return;
314
+ const add = dependencies.add;
315
+ if (typeof add !== "function") return void 0;
316
+ return { add: (entry) => {
317
+ Reflect.apply(add, dependencies, [entry]);
318
+ } };
319
+ }
320
+ function isDirectory(file) {
321
+ try {
322
+ return fs.statSync(file).isDirectory();
323
+ } catch {
324
+ return false;
325
+ }
326
+ }
153
327
  const unpluginFactory = (options = {}, meta) => {
154
328
  const isWebpack = meta.framework === "webpack" || meta.framework === "rspack";
155
329
  const { cache = true, errorOverlay = true, failOnError = "build", logLevel, onBuildEnd, onBuildError, onBuildStart, platforms: platformsOption, report = true, root: rootOption, silent = false } = options;
@@ -173,6 +347,26 @@ const unpluginFactory = (options = {}, meta) => {
173
347
  let root = rootOption ? path.resolve(process.cwd(), rootOption) : process.cwd();
174
348
  const generatedDestinations = /* @__PURE__ */ new Set();
175
349
  let cachedPatterns;
350
+ const linkedPaths = /* @__PURE__ */ new Map();
351
+ const addWatchFiles = (context, paths) => {
352
+ const contextDependencies = isWebpack ? contextDependenciesOf(context) : void 0;
353
+ const register = (file) => {
354
+ if (contextDependencies && isDirectory(file)) {
355
+ contextDependencies.add(path.resolve(file));
356
+ watchedDirectories.add(file);
357
+ return;
358
+ }
359
+ context.addWatchFile(file);
360
+ };
361
+ for (const file of paths) {
362
+ register(file);
363
+ const real = realpathOf(file);
364
+ if (real === void 0 || real === file) continue;
365
+ linkedPaths.set(real, file);
366
+ register(real);
367
+ }
368
+ };
369
+ const watchedDirectories = /* @__PURE__ */ new Set();
176
370
  let watchRebuild = false;
177
371
  let hasCompiled = false;
178
372
  let hostClosed = false;
@@ -210,13 +404,26 @@ const unpluginFactory = (options = {}, meta) => {
210
404
  }
211
405
  console.log(paint(type === "success" ? "32" : "36", `${prefix} ${message}`, stdoutColour));
212
406
  };
213
- const resolveConfigs = async () => resolveConfigOption({
214
- config: options.config,
215
- configContext,
216
- discovery,
217
- log,
218
- root
219
- });
407
+ const logTable = (render) => {
408
+ if (host?.info) {
409
+ for (const line of render(false)) host.info(line);
410
+ return;
411
+ }
412
+ for (const line of render(stdoutColour)) console.log(line);
413
+ };
414
+ const resolveConfigs = async () => {
415
+ const resolved = await resolveConfigOption({
416
+ config: options.config,
417
+ configContext,
418
+ discovery,
419
+ log,
420
+ root
421
+ });
422
+ if (!options.watch) return resolved;
423
+ const watched = (await expandPatterns(watchOptionPatterns(root, options.watch), log)).filter((file) => !isDirectory(file));
424
+ for (const item of resolved) if (item.file) item.watched = watched;
425
+ return resolved;
426
+ };
220
427
  const getWatchTargets = async (resolvedConfigs) => {
221
428
  const patterns = await watchPatternsOf({
222
429
  log,
@@ -231,27 +438,36 @@ const unpluginFactory = (options = {}, meta) => {
231
438
  };
232
439
  const instance = {
233
440
  cache,
234
- compiledFingerprints,
441
+ destinationRecords,
235
442
  failOnError,
236
443
  generatedDestinations,
237
444
  log,
445
+ logTable,
238
446
  notifyBuildOutcome: (error) => {
239
447
  notifyBuildOutcome?.(error);
240
448
  },
241
449
  onBuildEnd,
242
450
  onBuildError,
243
451
  onBuildStart,
452
+ persistedRecordFiles,
244
453
  platformsOption,
245
454
  quiet,
246
455
  report,
247
456
  root: () => root,
248
- stdoutColour,
249
457
  verbosity,
250
458
  volume: atomicVolume,
251
459
  watch: options.watch
252
460
  };
253
461
  const compileOnceAcrossInstances = async (resolvedConfigs) => {
254
- const key = buildKey(root, resolvedConfigs);
462
+ const key = buildKey(root, resolvedConfigs, {
463
+ cache,
464
+ failOnError,
465
+ onBuildEnd,
466
+ onBuildError,
467
+ onBuildStart,
468
+ platforms: platformsOption,
469
+ watch: options.watch
470
+ });
255
471
  if (key === null) {
256
472
  await runBuilds(instance, resolvedConfigs);
257
473
  return;
@@ -308,25 +524,55 @@ const unpluginFactory = (options = {}, meta) => {
308
524
  host = { error: (message) => {
309
525
  pending.push(message);
310
526
  } };
527
+ let heldFailure;
311
528
  compiler.hooks.compilation.tap("unplugin-style-dictionary", (compilation) => {
312
529
  for (const message of pending.splice(0)) {
313
530
  const reported = new Error(message);
314
531
  reported.name = "UnpluginStyleDictionaryWarning";
315
532
  compilation.warnings.push(reported);
316
533
  }
534
+ if (heldFailure) {
535
+ compilation.errors.push(heldFailure);
536
+ heldFailure = void 0;
537
+ }
317
538
  });
318
539
  const drainToConsole = () => {
319
540
  for (const message of pending.splice(0)) console.error(paint("31", message, stderrColour));
320
541
  };
321
542
  compiler.hooks.failed.tap("unplugin-style-dictionary", drainToConsole);
322
543
  compiler.hooks.done.tap("unplugin-style-dictionary", drainToConsole);
323
- compiler.hooks.beforeCompile.tapPromise("unplugin-style-dictionary", async () => {
544
+ const compileTokens = async () => {
324
545
  isWatching = compiler.watchMode;
325
546
  const resolved = await resolveConfigs();
326
547
  if (resolved.length === 0) return;
327
548
  await compileOnceAcrossInstances(resolved);
328
549
  hasCompiled = true;
329
- });
550
+ };
551
+ const changedSource = () => {
552
+ const changed = [...compiler.modifiedFiles ?? [], ...compiler.removedFiles ?? []];
553
+ for (const file of changed) {
554
+ const id = registeredSpellingOf(file, linkedPaths);
555
+ if (!cachedPatterns || watchedDirectories.has(id.replace(/\\/g, "/")) || isWatchedSource(id, cachedPatterns, generatedDestinations)) return id;
556
+ }
557
+ };
558
+ const rebuildTokens = async () => {
559
+ if (!hasCompiled || compiler.modifiedFiles === void 0) {
560
+ await compileTokens();
561
+ return;
562
+ }
563
+ isWatching = compiler.watchMode;
564
+ const changed = changedSource();
565
+ if (changed === void 0) return;
566
+ const resolved = await resolveConfigs();
567
+ if (resolved.length === 0) return;
568
+ try {
569
+ await runBuilds(instance, resolved, path.basename(changed));
570
+ } catch (err) {
571
+ heldFailure = asError(err);
572
+ }
573
+ };
574
+ compiler.hooks.run.tapPromise("unplugin-style-dictionary", compileTokens);
575
+ compiler.hooks.watchRun.tapPromise("unplugin-style-dictionary", rebuildTokens);
330
576
  };
331
577
  return {
332
578
  async buildStart() {
@@ -336,13 +582,12 @@ const unpluginFactory = (options = {}, meta) => {
336
582
  if (resolved.length === 0) return;
337
583
  if (!hostClosed) {
338
584
  const { paths } = await getWatchTargets(resolved);
339
- for (const file of paths) this.addWatchFile(file);
585
+ addWatchFiles(this, paths);
340
586
  }
341
587
  if (isWebpack) return;
342
- if (watchRebuild && hasCompiled) {
343
- watchRebuild = false;
344
- return;
345
- }
588
+ const reentry = watchRebuild && hasCompiled && hostCommand !== "serve";
589
+ watchRebuild = false;
590
+ if (reentry) return;
346
591
  await compileOnceAcrossInstances(resolved);
347
592
  hasCompiled = true;
348
593
  },
@@ -367,11 +612,26 @@ const unpluginFactory = (options = {}, meta) => {
367
612
  config.logger.info(message);
368
613
  }
369
614
  };
370
- if (config.command !== "serve") return;
615
+ if (config.command !== "serve") {
616
+ if (config.build.watch) {
617
+ isWatching = true;
618
+ try {
619
+ const resolved = await resolveConfigs();
620
+ if (resolved.length > 0) {
621
+ const { paths } = await getWatchTargets(resolved);
622
+ appendChokidarIgnored(config.build.watch, nodeModulesNegations(paths));
623
+ }
624
+ } catch (err) {
625
+ log(`Could not read the configuration while preparing the watch list: ${errorMessage(err)}`, "error");
626
+ }
627
+ }
628
+ return;
629
+ }
371
630
  isWatching = true;
372
631
  try {
373
632
  startupResolved = await resolveConfigs();
374
633
  if (startupResolved.length === 0) return;
634
+ if (config.server.watch === null) return;
375
635
  const { paths } = await getWatchTargets(startupResolved);
376
636
  const negations = nodeModulesNegations(paths);
377
637
  if (negations.length === 0) return;
@@ -393,7 +653,9 @@ const unpluginFactory = (options = {}, meta) => {
393
653
  let targets = await getWatchTargets(resolved);
394
654
  server.watcher.add(targets.paths);
395
655
  const ownWatchers = /* @__PURE__ */ new Map();
656
+ const watcherDisabled = server.config.server.watch === null;
396
657
  const watchNodeModules = (forPaths) => {
658
+ if (watcherDisabled) return;
397
659
  const wanted = new Set(nodeModulesWatchDirectories(forPaths));
398
660
  for (const [directory, watcher] of ownWatchers) {
399
661
  if (wanted.has(directory)) continue;
@@ -453,18 +715,19 @@ const unpluginFactory = (options = {}, meta) => {
453
715
  });
454
716
  }
455
717
  },
456
- async watchChange(id) {
718
+ async watchChange(changed) {
457
719
  adoptHost(this);
458
720
  adoptWatchMode(this);
459
721
  if (hostClosed) return;
460
722
  watchRebuild = true;
723
+ const id = registeredSpellingOf(changed, linkedPaths);
461
724
  if (cachedPatterns && !isWatchedSource(id, cachedPatterns, generatedDestinations)) return;
462
725
  const resolved = await resolveConfigs();
463
726
  if (resolved.length === 0) return;
464
727
  const { patterns } = await getWatchTargets(resolved);
465
728
  if (!isWatchedSource(id, patterns, generatedDestinations)) return;
466
729
  if (!isWebpack) await schedule(path.basename(id));
467
- for (const file of await expandPatterns(patterns, log)) this.addWatchFile(file);
730
+ addWatchFiles(this, await expandPatterns(patterns, log));
468
731
  },
469
732
  webpack: adoptCompiler
470
733
  };