@jk2908/cxx 0.3.3 → 0.3.4

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.
@@ -14,6 +14,86 @@ var PKG_NAME = `@jk2908/${NAME}`;
14
14
  var GENERATED_DIR = `.${NAME}`;
15
15
  var AUTOGEN_MSG = `// auto-generated by ${NAME}`;
16
16
 
17
+ // src/logger.ts
18
+ var LEVELS = {
19
+ debug: 0,
20
+ info: 1,
21
+ warn: 2,
22
+ error: 3,
23
+ fatal: 4
24
+ };
25
+ var defaultLevel = process.env["NODE_ENV"] === "production" ? "error" : "info";
26
+
27
+ class Logger {
28
+ name;
29
+ #level = defaultLevel;
30
+ constructor(name) {
31
+ this.name = name;
32
+ }
33
+ static toError(err) {
34
+ return err instanceof Error ? err : new Error(String(err), { cause: err });
35
+ }
36
+ static print(err) {
37
+ if (err instanceof Error) {
38
+ return err.message + (err.stack ? `
39
+ ${err.stack}` : "");
40
+ }
41
+ if (typeof err === "object" && err !== null) {
42
+ try {
43
+ return JSON.stringify(err, null, 2);
44
+ } catch {
45
+ return String(err);
46
+ }
47
+ }
48
+ return String(err);
49
+ }
50
+ set level(level) {
51
+ this.#level = level;
52
+ }
53
+ get level() {
54
+ return this.#level;
55
+ }
56
+ log(level, message, error) {
57
+ if (LEVELS[level] < LEVELS[this.level])
58
+ return;
59
+ const entry = {
60
+ ts: Date.now(),
61
+ level,
62
+ message
63
+ };
64
+ if (level === "error" || level === "fatal") {
65
+ entry.error = error ? Logger.toError(error) : new Error(message);
66
+ }
67
+ const line = `${this.name ? `[${this.name}]` : ""} [${entry.ts}] [${level.toUpperCase()}] ${message}`;
68
+ const extra = entry.error ? `
69
+ ${Logger.print(entry.error)}` : "";
70
+ if (level === "warn") {
71
+ console.warn(line, extra);
72
+ return;
73
+ }
74
+ if (level === "error" || level === "fatal") {
75
+ console.error(line, extra);
76
+ return;
77
+ }
78
+ console.log(line, extra);
79
+ }
80
+ debug(...messages) {
81
+ this.log("debug", messages.join(" "));
82
+ }
83
+ info(...messages) {
84
+ this.log("info", messages.join(" "));
85
+ }
86
+ warn(...messages) {
87
+ this.log("warn", messages.join(" "));
88
+ }
89
+ error(message, error) {
90
+ this.log("error", message, error === undefined ? undefined : Logger.toError(error));
91
+ }
92
+ fatal(message, error) {
93
+ this.log("fatal", message, error === undefined ? undefined : Logger.toError(error));
94
+ }
95
+ }
96
+
17
97
  // src/lru.ts
18
98
  var CACHE_MAX_SIZE = 100;
19
99
 
@@ -56,54 +136,9 @@ function capitalise(str) {
56
136
  function pascalise(str) {
57
137
  return str.split(/[ _-]+/).map(capitalise).join("");
58
138
  }
59
- function createBatcher(wait, flush) {
60
- const pending = new Map;
61
- let timer = null;
62
- let current = null;
63
- function drain() {
64
- if (current)
65
- return current;
66
- const run = (async () => {
67
- while (pending.size > 0) {
68
- const batch = new Map(pending);
69
- pending.clear();
70
- await flush(batch);
71
- }
72
- })();
73
- current = run;
74
- run.then(() => {
75
- if (current === run)
76
- current = null;
77
- }, () => {
78
- if (current === run)
79
- current = null;
80
- });
81
- return run;
82
- }
83
- function schedule() {
84
- if (timer)
85
- clearTimeout(timer);
86
- timer = setTimeout(() => {
87
- timer = null;
88
- drain().catch(() => {});
89
- }, wait);
90
- }
91
- return {
92
- add(key, value) {
93
- pending.set(key, value);
94
- schedule();
95
- },
96
- flush() {
97
- if (timer) {
98
- clearTimeout(timer);
99
- timer = null;
100
- }
101
- return drain();
102
- }
103
- };
104
- }
105
139
 
106
140
  // src/build.ts
141
+ var logger = new Logger(NAME);
107
142
  var decoder = new TextDecoder;
108
143
  var encoder = new TextEncoder;
109
144
  var collectCache = new LruCache;
@@ -244,29 +279,21 @@ async function writeRenderedTypes(tags) {
244
279
  var PARTIAL_DIR = path.join(GENERATED_DIR, "tags");
245
280
  var REBUILD_WAIT = 100;
246
281
  var rebuildTimer = null;
247
- var rebuildError = null;
248
282
  function partialPath(filePath) {
249
283
  return path.join(PARTIAL_DIR, `${createHash("sha1").update(filePath).digest("hex")}.json`);
250
284
  }
251
- async function partialDirMtime() {
252
- try {
253
- return (await fs.stat(PARTIAL_DIR)).mtimeMs;
254
- } catch (err) {
255
- if (isENOENT(err))
256
- return 0;
257
- throw err;
258
- }
259
- }
260
285
  async function readPartials() {
261
286
  let entries;
262
287
  try {
263
288
  entries = await fs.readdir(PARTIAL_DIR);
264
289
  } catch (err) {
265
290
  if (isENOENT(err))
266
- return new Map;
291
+ return { tags: new Map, duplicates: [] };
267
292
  throw err;
268
293
  }
269
294
  const tags = new Map;
295
+ const fileByTag = new Map;
296
+ const duplicates = [];
270
297
  for (const entry of entries) {
271
298
  if (!entry.endsWith(".json"))
272
299
  continue;
@@ -277,19 +304,28 @@ async function readPartials() {
277
304
  } catch {
278
305
  continue;
279
306
  }
307
+ let source;
280
308
  try {
281
- await fs.access(parsed.file);
309
+ source = await fs.readFile(parsed.file, "utf-8");
282
310
  } catch {
283
311
  await fs.rm(partial, { force: true });
284
312
  continue;
285
313
  }
314
+ if (!source.includes("cxx")) {
315
+ await fs.rm(partial, { force: true });
316
+ continue;
317
+ }
286
318
  for (const [name, classes] of Object.entries(parsed.tags)) {
287
- if (tags.has(name))
288
- throw new DuplicateTagError(name);
319
+ const existing = fileByTag.get(name);
320
+ if (existing) {
321
+ duplicates.push({ tag: name, files: [existing, parsed.file] });
322
+ continue;
323
+ }
324
+ fileByTag.set(name, parsed.file);
289
325
  tags.set(name, classes);
290
326
  }
291
327
  }
292
- return tags;
328
+ return { tags, duplicates };
293
329
  }
294
330
  function writeTypesForFile(filePath, tags) {
295
331
  return enqueueWrite(async () => {
@@ -300,8 +336,8 @@ function writeTypesForFile(filePath, tags) {
300
336
  } else {
301
337
  await writeFileAtomic(partial, JSON.stringify({ file: filePath, tags: Object.fromEntries(tags) }));
302
338
  }
303
- }).then(() => {
304
- scheduleRebuild();
339
+ }).then(() => scheduleRebuild()).catch((err) => {
340
+ logger.error(`failed to write types for ${filePath}`, err);
305
341
  });
306
342
  }
307
343
  function scheduleRebuild() {
@@ -310,7 +346,7 @@ function scheduleRebuild() {
310
346
  rebuildTimer = setTimeout(() => {
311
347
  rebuildTimer = null;
312
348
  flushTypes().catch((err) => {
313
- rebuildError = err;
349
+ logger.error("failed to generate types", err);
314
350
  });
315
351
  }, REBUILD_WAIT);
316
352
  }
@@ -320,23 +356,64 @@ function flushTypes() {
320
356
  rebuildTimer = null;
321
357
  }
322
358
  return enqueueWrite(async () => {
323
- let before = await partialDirMtime();
324
- for (let attempt = 0;attempt < 3; attempt++) {
325
- await writeRenderedTypes(await readPartials());
326
- const after = await partialDirMtime();
327
- if (after === before)
328
- return;
329
- before = after;
359
+ const { tags, duplicates } = await readPartials();
360
+ await writeRenderedTypes(tags);
361
+ for (const duplicate of duplicates) {
362
+ logger.error(`Tag "${duplicate.tag}" is defined in multiple files: ${duplicate.files.join(", ")}`);
330
363
  }
331
364
  });
332
365
  }
333
- function takeRebuildError() {
334
- const err = rebuildError;
335
- rebuildError = null;
336
- return err;
366
+ var SOURCE_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx"]);
367
+ var SKIP_DIRS = new Set(["node_modules", "dist", "build", "coverage", "out"]);
368
+ async function runWithConcurrency(items, concurrency, fn) {
369
+ let cursor = 0;
370
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
371
+ while (cursor < items.length) {
372
+ const index = cursor++;
373
+ await fn(items[index]);
374
+ }
375
+ });
376
+ await Promise.all(workers);
377
+ }
378
+ async function refreshFile(file, config) {
379
+ let source;
380
+ try {
381
+ source = await fs.readFile(file, "utf-8");
382
+ } catch (err) {
383
+ if (!isENOENT(err))
384
+ throw err;
385
+ await writeTypesForFile(file, new Map);
386
+ return;
387
+ }
388
+ const tags = source.includes("cxx") ? collect(source, file, config).tags : new Map;
389
+ await writeTypesForFile(file, tags);
390
+ }
391
+ async function seedTypes(root, config) {
392
+ async function scan(dir) {
393
+ let entries;
394
+ try {
395
+ entries = await fs.readdir(dir, { withFileTypes: true });
396
+ } catch {
397
+ return;
398
+ }
399
+ await runWithConcurrency(entries, 8, async (entry) => {
400
+ if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name))
401
+ return;
402
+ const full = path.join(dir, entry.name);
403
+ if (entry.isDirectory()) {
404
+ await scan(full);
405
+ return;
406
+ }
407
+ if (!SOURCE_EXTENSIONS.has(path.extname(entry.name)))
408
+ return;
409
+ await refreshFile(full, config);
410
+ });
411
+ }
412
+ await scan(root);
413
+ await flushTypes();
337
414
  }
338
415
  function tagsToType(tags) {
339
- return [...tags].toSorted(([a], [b]) => a.localeCompare(b)).map(([tag, classes]) => {
416
+ return [...tags].toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([tag, classes]) => {
340
417
  const union = classes.length > 0 ? classes.toSorted((a, b) => a.length - b.length).map((c) => `'${c}'`).join(" | ") : "never";
341
418
  return ` export type ${tag} = ${union}`;
342
419
  }).join(`
@@ -345,170 +422,60 @@ function tagsToType(tags) {
345
422
 
346
423
  // src/vite/index.ts
347
424
  import { realpathSync } from "node:fs";
348
- import fs2 from "node:fs/promises";
349
425
  import path2 from "node:path";
350
-
351
- // src/logger.ts
352
- var LEVELS = {
353
- debug: 0,
354
- info: 1,
355
- warn: 2,
356
- error: 3,
357
- fatal: 4
358
- };
359
- var defaultLevel = process.env["NODE_ENV"] === "production" ? "error" : "info";
360
-
361
- class Logger {
362
- name;
363
- #level;
364
- constructor(level, name) {
365
- this.name = name;
366
- this.#level = level ?? defaultLevel;
367
- }
368
- static toError(err) {
369
- return err instanceof Error ? err : new Error(String(err), { cause: err });
370
- }
371
- static print(err) {
372
- if (err instanceof Error) {
373
- return err.message + (err.stack ? `
374
- ${err.stack}` : "");
375
- }
376
- if (typeof err === "object" && err !== null) {
377
- try {
378
- return JSON.stringify(err, null, 2);
379
- } catch {
380
- return String(err);
381
- }
382
- }
383
- return String(err);
384
- }
385
- set level(level) {
386
- this.#level = level;
387
- }
388
- get level() {
389
- return this.#level;
390
- }
391
- log(level, message, error) {
392
- if (LEVELS[level] < LEVELS[this.level])
393
- return;
394
- const entry = {
395
- ts: Date.now(),
396
- level,
397
- message
398
- };
399
- if (level === "error" || level === "fatal") {
400
- entry.error = error ? Logger.toError(error) : new Error(message);
401
- }
402
- const line = `${this.name ? `[${this.name}]` : ""} [${entry.ts}] [${level.toUpperCase()}] ${message}`;
403
- const extra = entry.error ? `
404
- ${Logger.print(entry.error)}` : "";
405
- if (level === "warn") {
406
- console.warn(line, extra);
407
- return;
408
- }
409
- if (level === "error" || level === "fatal") {
410
- console.error(line, extra);
411
- return;
412
- }
413
- console.log(line, extra);
414
- }
415
- debug(...messages) {
416
- this.log("debug", messages.join(" "));
417
- }
418
- info(...messages) {
419
- this.log("info", messages.join(" "));
420
- }
421
- warn(...messages) {
422
- this.log("warn", messages.join(" "));
423
- }
424
- error(message, error) {
425
- this.log("error", message, error === undefined ? undefined : Logger.toError(error));
426
- }
427
- fatal(message, error) {
428
- this.log("fatal", message, error === undefined ? undefined : Logger.toError(error));
429
- }
430
- }
431
-
432
- // src/vite/index.ts
433
- var ACCEPTED_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx"]);
434
426
  function normaliseWatchPath(p) {
435
427
  return p.replace(/\\/g, "/");
436
428
  }
437
- function globToRegExp(glob) {
438
- const pattern = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*(\/|$)/g, ".*$1").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
439
- return new RegExp(`^${pattern}$`);
440
- }
441
429
  function cxx(pluginConfig = {}) {
442
- const logger = new Logger;
430
+ const logger2 = new Logger;
443
431
  let watchRoot = normaliseWatchPath(process.cwd());
444
- const ignored = (pluginConfig.watch?.ignore ?? []).map(globToRegExp);
432
+ let seeded = false;
433
+ const realpaths = new Map;
445
434
  function resolveWatchFile(filePath) {
446
435
  const absolutePath = path2.resolve(watchRoot, filePath);
447
436
  const parentPath = path2.dirname(absolutePath);
448
- try {
449
- const resolvedParentPath = normaliseWatchPath(realpathSync.native(parentPath));
450
- return normaliseWatchPath(path2.join(resolvedParentPath, path2.basename(absolutePath)));
451
- } catch {
452
- return normaliseWatchPath(absolutePath);
437
+ let resolvedParentPath = realpaths.get(parentPath);
438
+ if (resolvedParentPath === undefined) {
439
+ try {
440
+ resolvedParentPath = normaliseWatchPath(realpathSync.native(parentPath));
441
+ } catch {
442
+ resolvedParentPath = normaliseWatchPath(parentPath);
443
+ }
444
+ realpaths.set(parentPath, resolvedParentPath);
453
445
  }
446
+ return normaliseWatchPath(path2.join(resolvedParentPath, path2.basename(absolutePath)));
454
447
  }
455
448
  function watchedFile(filePath) {
456
449
  const resolvedPath = resolveWatchFile(filePath);
457
- if (resolvedPath.startsWith(`${watchRoot}/`) && !resolvedPath.includes(`/${GENERATED_DIR}/`) && ACCEPTED_EXTENSIONS.has(path2.extname(resolvedPath)) && !ignored.some((regexp) => regexp.test(resolvedPath))) {
450
+ if (resolvedPath.startsWith(`${watchRoot}/`) && !resolvedPath.includes(`/${GENERATED_DIR}/`) && SOURCE_EXTENSIONS.has(path2.extname(resolvedPath))) {
458
451
  return resolvedPath;
459
452
  }
460
453
  return null;
461
454
  }
462
- async function recordFile(file) {
463
- let source;
464
- try {
465
- source = await fs2.readFile(file, "utf-8");
466
- } catch (err) {
467
- if (!isENOENT(err))
468
- throw err;
455
+ async function handleEvent(event, file) {
456
+ if (event === "unlink") {
469
457
  await writeTypesForFile(file, new Map);
470
- return;
458
+ } else {
459
+ await refreshFile(file, pluginConfig);
471
460
  }
472
- const tags = source.includes("cxx") ? collect(source, file, pluginConfig).tags : new Map;
473
- await writeTypesForFile(file, tags);
461
+ logger2.info(`updated types from ${file}`);
474
462
  }
475
- async function scan(dir) {
476
- let entries;
477
- try {
478
- entries = await fs2.readdir(dir, { withFileTypes: true });
479
- } catch {
480
- return;
481
- }
482
- await Promise.all(entries.map(async (entry) => {
483
- if (entry.name.startsWith(".") || entry.name === "node_modules")
484
- return;
485
- const full = path2.join(dir, entry.name);
486
- if (entry.isDirectory()) {
487
- await scan(full);
463
+ function onWatch(event) {
464
+ return (p) => {
465
+ const file = watchedFile(p);
466
+ if (!file)
488
467
  return;
489
- }
490
- if (!ACCEPTED_EXTENSIONS.has(path2.extname(entry.name)))
491
- return;
492
- const resolved = resolveWatchFile(full);
493
- if (ignored.some((regexp) => regexp.test(resolved)))
494
- return;
495
- await recordFile(resolved);
496
- }));
468
+ handleEvent(event, file).catch((err) => {
469
+ logger2.error(`failed to update types from ${file}`, err);
470
+ });
471
+ };
472
+ }
473
+ async function seed() {
474
+ if (seeded)
475
+ return;
476
+ seeded = true;
477
+ await seedTypes(watchRoot, pluginConfig);
497
478
  }
498
- const queue = createBatcher(75, async (batch) => {
499
- for (const [file, event] of batch) {
500
- try {
501
- if (event === "unlink") {
502
- await writeTypesForFile(file, new Map);
503
- } else {
504
- await recordFile(file);
505
- }
506
- logger.info(`updated types from ${file}`);
507
- } catch (err) {
508
- logger.error(`failed to update types from ${file}`, err);
509
- }
510
- }
511
- });
512
479
  return {
513
480
  name: "cxx",
514
481
  enforce: "pre",
@@ -519,6 +486,10 @@ function cxx(pluginConfig = {}) {
519
486
  } catch {
520
487
  watchRoot = normaliseWatchPath(root);
521
488
  }
489
+ realpaths.clear();
490
+ },
491
+ async buildStart() {
492
+ await seed();
522
493
  },
523
494
  transform(code, id) {
524
495
  if (id.startsWith("\x00") || id.includes("/node_modules/"))
@@ -526,7 +497,7 @@ function cxx(pluginConfig = {}) {
526
497
  if (!code.includes("cxx"))
527
498
  return null;
528
499
  const cleanId = id.split("?")[0];
529
- if (!ACCEPTED_EXTENSIONS.has(path2.extname(cleanId)))
500
+ if (!SOURCE_EXTENSIONS.has(path2.extname(cleanId)))
530
501
  return null;
531
502
  const { template, tags, map } = collect(code, cleanId, pluginConfig);
532
503
  if (template === code)
@@ -538,25 +509,8 @@ function cxx(pluginConfig = {}) {
538
509
  };
539
510
  },
540
511
  configureServer(server) {
541
- logger.info("Watching for changes...");
542
- server.watcher.on("add", (p) => {
543
- const file = watchedFile(p);
544
- if (file)
545
- queue.add(file, "add");
546
- }).on("change", (p) => {
547
- const file = watchedFile(p);
548
- if (file)
549
- queue.add(file, "change");
550
- }).on("unlink", (p) => {
551
- const file = watchedFile(p);
552
- if (file)
553
- queue.add(file, "unlink");
554
- });
555
- scan(watchRoot).then(() => flushTypes()).then(() => {
556
- const err = takeRebuildError();
557
- if (err)
558
- logger.error("failed to generate types", err);
559
- }).catch((err) => logger.error("failed to scan for cxx templates", err));
512
+ logger2.info("Watching for changes...");
513
+ server.watcher.on("add", onWatch("add")).on("change", onWatch("change")).on("unlink", onWatch("unlink"));
560
514
  },
561
515
  async writeBundle() {
562
516
  await flushTypes();
@@ -567,4 +521,4 @@ export {
567
521
  cxx as default
568
522
  };
569
523
 
570
- //# debugId=7C0503EFBFB9E21F64756E2164756E21
524
+ //# debugId=294541A31A244C8364756E2164756E21