@kanso-labs/unplugin-style-dictionary 0.10.13 → 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,10 +1,10 @@
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";
@@ -46,25 +46,93 @@ function nodeModulesWatchDirectories(paths) {
46
46
  return Array.from(directories);
47
47
  }
48
48
  let temporaryFileCounter = 0;
49
- async function rendersWhatIsAlreadyThere(temporary, destination) {
49
+ const sweptTemporaryPrefixes = /* @__PURE__ */ new Set();
50
+ const ABANDONED_TEMPORARY_AGE_MS = 36e5;
51
+ async function alreadyHolds(destination, rendered) {
50
52
  try {
51
- const [existing, rendered] = await Promise.all([fs.promises.readFile(destination), fs.promises.readFile(temporary)]);
52
- return existing.equals(rendered);
53
+ return (await fs.promises.readFile(destination)).equals(rendered);
53
54
  } catch {
54
55
  return false;
55
56
  }
56
57
  }
57
- function rendersWhatIsAlreadyThereSync(temporary, destination) {
58
+ function alreadyHoldsSync(destination, rendered) {
58
59
  try {
59
- return fs.readFileSync(destination).equals(fs.readFileSync(temporary));
60
+ return fs.readFileSync(destination).equals(rendered);
60
61
  } catch {
61
62
  return false;
62
63
  }
63
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
+ }
64
127
  function temporaryPathFor(destination) {
65
128
  const extension = path.extname(destination);
66
129
  return path.join(path.dirname(destination), `.${path.basename(destination, extension)}.${process.pid}.${temporaryFileCounter++}.tmp`);
67
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
+ }
68
136
  const RENAME_RETRY_CODES = /* @__PURE__ */ new Set(["EBUSY", "EPERM"]);
69
137
  const RENAME_RETRY_DELAYS_MS = [
70
138
  1,
@@ -117,14 +185,19 @@ function renameWithRetrySync(temporary, destination) {
117
185
  }
118
186
  const writeFileAtomic = async (file, data, options) => {
119
187
  if (typeof file !== "string") return fs.promises.writeFile(file, data, options);
120
- 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);
121
193
  try {
122
194
  await fs.promises.writeFile(temporary, data, options);
123
- if (await rendersWhatIsAlreadyThere(temporary, file)) {
195
+ if (!bytes && await alreadyHolds(target, await fs.promises.readFile(temporary))) {
124
196
  discardTemporaryFile(temporary);
125
197
  return;
126
198
  }
127
- await renameWithRetry(temporary, file);
199
+ keepExistingMode(temporary, target);
200
+ await renameWithRetry(temporary, target);
128
201
  } catch (err) {
129
202
  discardTemporaryFile(temporary);
130
203
  throw err;
@@ -135,33 +208,122 @@ const writeFileSyncAtomic = (file, data, options) => {
135
208
  fs.writeFileSync(file, data, options);
136
209
  return;
137
210
  }
138
- 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);
139
216
  try {
140
217
  fs.writeFileSync(temporary, data, options);
141
- if (rendersWhatIsAlreadyThereSync(temporary, file)) {
142
- discardTemporaryFile(temporary);
143
- return;
144
- }
145
- 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);
146
251
  } catch (err) {
147
252
  discardTemporaryFile(temporary);
148
253
  throw err;
149
254
  }
150
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
+ }
151
286
  const atomicVolume = Object.create(fs, {
152
- 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
+ }) },
153
293
  writeFileSync: { value: writeFileSyncAtomic }
154
294
  });
155
295
  const destinationRecords = /* @__PURE__ */ new Map();
156
296
  const persistedRecordFiles = /* @__PURE__ */ new Map();
157
297
  const compilesInFlight = /* @__PURE__ */ new Map();
158
- function buildKey(root, resolved) {
298
+ function buildKey(root, resolved, settings) {
159
299
  try {
160
- 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);
161
305
  } catch {
162
306
  return null;
163
307
  }
164
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
+ }
165
327
  const unpluginFactory = (options = {}, meta) => {
166
328
  const isWebpack = meta.framework === "webpack" || meta.framework === "rspack";
167
329
  const { cache = true, errorOverlay = true, failOnError = "build", logLevel, onBuildEnd, onBuildError, onBuildStart, platforms: platformsOption, report = true, root: rootOption, silent = false } = options;
@@ -185,6 +347,26 @@ const unpluginFactory = (options = {}, meta) => {
185
347
  let root = rootOption ? path.resolve(process.cwd(), rootOption) : process.cwd();
186
348
  const generatedDestinations = /* @__PURE__ */ new Set();
187
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();
188
370
  let watchRebuild = false;
189
371
  let hasCompiled = false;
190
372
  let hostClosed = false;
@@ -222,13 +404,26 @@ const unpluginFactory = (options = {}, meta) => {
222
404
  }
223
405
  console.log(paint(type === "success" ? "32" : "36", `${prefix} ${message}`, stdoutColour));
224
406
  };
225
- const resolveConfigs = async () => resolveConfigOption({
226
- config: options.config,
227
- configContext,
228
- discovery,
229
- log,
230
- root
231
- });
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
+ };
232
427
  const getWatchTargets = async (resolvedConfigs) => {
233
428
  const patterns = await watchPatternsOf({
234
429
  log,
@@ -247,6 +442,7 @@ const unpluginFactory = (options = {}, meta) => {
247
442
  failOnError,
248
443
  generatedDestinations,
249
444
  log,
445
+ logTable,
250
446
  notifyBuildOutcome: (error) => {
251
447
  notifyBuildOutcome?.(error);
252
448
  },
@@ -258,13 +454,20 @@ const unpluginFactory = (options = {}, meta) => {
258
454
  quiet,
259
455
  report,
260
456
  root: () => root,
261
- stdoutColour,
262
457
  verbosity,
263
458
  volume: atomicVolume,
264
459
  watch: options.watch
265
460
  };
266
461
  const compileOnceAcrossInstances = async (resolvedConfigs) => {
267
- 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
+ });
268
471
  if (key === null) {
269
472
  await runBuilds(instance, resolvedConfigs);
270
473
  return;
@@ -321,25 +524,55 @@ const unpluginFactory = (options = {}, meta) => {
321
524
  host = { error: (message) => {
322
525
  pending.push(message);
323
526
  } };
527
+ let heldFailure;
324
528
  compiler.hooks.compilation.tap("unplugin-style-dictionary", (compilation) => {
325
529
  for (const message of pending.splice(0)) {
326
530
  const reported = new Error(message);
327
531
  reported.name = "UnpluginStyleDictionaryWarning";
328
532
  compilation.warnings.push(reported);
329
533
  }
534
+ if (heldFailure) {
535
+ compilation.errors.push(heldFailure);
536
+ heldFailure = void 0;
537
+ }
330
538
  });
331
539
  const drainToConsole = () => {
332
540
  for (const message of pending.splice(0)) console.error(paint("31", message, stderrColour));
333
541
  };
334
542
  compiler.hooks.failed.tap("unplugin-style-dictionary", drainToConsole);
335
543
  compiler.hooks.done.tap("unplugin-style-dictionary", drainToConsole);
336
- compiler.hooks.beforeCompile.tapPromise("unplugin-style-dictionary", async () => {
544
+ const compileTokens = async () => {
337
545
  isWatching = compiler.watchMode;
338
546
  const resolved = await resolveConfigs();
339
547
  if (resolved.length === 0) return;
340
548
  await compileOnceAcrossInstances(resolved);
341
549
  hasCompiled = true;
342
- });
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);
343
576
  };
344
577
  return {
345
578
  async buildStart() {
@@ -349,7 +582,7 @@ const unpluginFactory = (options = {}, meta) => {
349
582
  if (resolved.length === 0) return;
350
583
  if (!hostClosed) {
351
584
  const { paths } = await getWatchTargets(resolved);
352
- for (const file of paths) this.addWatchFile(file);
585
+ addWatchFiles(this, paths);
353
586
  }
354
587
  if (isWebpack) return;
355
588
  const reentry = watchRebuild && hasCompiled && hostCommand !== "serve";
@@ -482,18 +715,19 @@ const unpluginFactory = (options = {}, meta) => {
482
715
  });
483
716
  }
484
717
  },
485
- async watchChange(id) {
718
+ async watchChange(changed) {
486
719
  adoptHost(this);
487
720
  adoptWatchMode(this);
488
721
  if (hostClosed) return;
489
722
  watchRebuild = true;
723
+ const id = registeredSpellingOf(changed, linkedPaths);
490
724
  if (cachedPatterns && !isWatchedSource(id, cachedPatterns, generatedDestinations)) return;
491
725
  const resolved = await resolveConfigs();
492
726
  if (resolved.length === 0) return;
493
727
  const { patterns } = await getWatchTargets(resolved);
494
728
  if (!isWatchedSource(id, patterns, generatedDestinations)) return;
495
729
  if (!isWebpack) await schedule(path.basename(id));
496
- for (const file of await expandPatterns(patterns, log)) this.addWatchFile(file);
730
+ addWatchFiles(this, await expandPatterns(patterns, log));
497
731
  },
498
732
  webpack: adoptCompiler
499
733
  };