@jk2908/cxx 0.2.6 → 0.3.3

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.
@@ -1,8 +1,12 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
1
4
  // src/build.ts
2
5
  import { createHash } from "node:crypto";
3
6
  import fs from "node:fs/promises";
4
7
  import path from "node:path";
5
8
  import { transform } from "lightningcss";
9
+ import MagicString from "magic-string";
6
10
 
7
11
  // src/config.ts
8
12
  var NAME = "cxx";
@@ -52,45 +56,93 @@ function capitalise(str) {
52
56
  function pascalise(str) {
53
57
  return str.split(/[ _-]+/).map(capitalise).join("");
54
58
  }
55
- function debounce(fn, wait) {
56
- let timeoutId = null;
57
- return (...args) => {
58
- if (timeoutId) {
59
- clearTimeout(timeoutId);
60
- }
61
- timeoutId = setTimeout(() => {
62
- fn.apply(null, args);
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(() => {});
63
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
+ }
64
103
  };
65
104
  }
66
105
 
67
106
  // src/build.ts
107
+ var decoder = new TextDecoder;
108
+ var encoder = new TextEncoder;
68
109
  var collectCache = new LruCache;
69
- var fileCache = new LruCache;
110
+ var writeChain = Promise.resolve();
70
111
  function isENOENT(err) {
71
112
  return err instanceof Error && "code" in err && err.code === "ENOENT";
72
113
  }
73
- async function maybeWrite(filePath, content) {
74
- const cached = fileCache.get(filePath);
75
- if (cached !== content) {
76
- await fs.writeFile(filePath, content);
77
- fileCache.set(filePath, content);
78
- return true;
114
+ async function writeFileAtomic(filePath, content) {
115
+ const tmp = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
116
+ await fs.writeFile(tmp, content);
117
+ try {
118
+ await fs.rename(tmp, filePath);
119
+ } catch (err) {
120
+ await fs.rm(tmp, { force: true });
121
+ throw err;
79
122
  }
123
+ }
124
+ function enqueueWrite(task) {
125
+ const run = writeChain.then(task, task);
126
+ writeChain = run.then(() => {
127
+ return;
128
+ }, () => {
129
+ return;
130
+ });
131
+ return run;
132
+ }
133
+ async function maybeWrite(filePath, content) {
80
134
  try {
81
- if (await fs.readFile(filePath, "utf-8") === content) {
82
- fileCache.set(filePath, content);
135
+ if (await fs.readFile(filePath, "utf-8") === content)
83
136
  return false;
84
- }
85
137
  } catch (err) {
86
138
  if (!isENOENT(err))
87
139
  throw err;
88
140
  }
89
- await fs.writeFile(filePath, content);
90
- fileCache.set(filePath, content);
141
+ await writeFileAtomic(filePath, content);
91
142
  return true;
92
143
  }
93
- var regex = /(?:const|var|let)\s*\[(\w+|)\s*,?\s*(\w+|)\s*,?\s*(\w+|)\s*\]\s*=\s*cxx(?:\s*`([\s\S]*?)`|\s*\.\s*tag(?:\s*<[^>]+>)?\s*\(\s*(['"])(.*?)\5\s*\)\s*`([\s\S]*?)`)/gm;
144
+ var regex = /((?:export\s+)?)(?:const|var|let)\s*\[(\w*)\s*,?\s*(\w*)\s*,?\s*(\w*)\s*\]\s*=\s*cxx(?:\s*`([\s\S]*?)`|\s*\.\s*tag(?:\s*<[^>]+>)?\s*\(\s*(['"])(.*?)\6\s*\)\s*`([\s\S]*?)`)/gm;
145
+ var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
94
146
  function hashCss(code) {
95
147
  return `cxx-${createHash("sha256").update(code).digest("base64url").slice(0, 12)}`;
96
148
  }
@@ -101,73 +153,137 @@ class DuplicateTagError extends Error {
101
153
  super(`Tag ${tag} has already been seen. Tags must be unique across your application`);
102
154
  }
103
155
  }
156
+
157
+ class InvalidTagError extends Error {
158
+ name = "InvalidTagError";
159
+ constructor(tag) {
160
+ super(`Tag "${tag}" does not map to a valid TypeScript identifier. ` + `Use letters, numbers, spaces, "_" or "-"`);
161
+ }
162
+ }
104
163
  function collect(source, id, config) {
105
- const tags = new Map;
164
+ const configKey = JSON.stringify(config);
106
165
  const cached = collectCache.get(id);
107
- if (cached?.source === source)
166
+ if (cached?.source === source && cached.config === configKey)
108
167
  return cached;
109
- const template = source.replace(regex, (_match, varOne, varTwo, varThree, plainCss, _quote, tag, taggedCss) => {
168
+ const tags = new Map;
169
+ const magic = new MagicString(source);
170
+ let matched = false;
171
+ regex.lastIndex = 0;
172
+ let match;
173
+ while (match = regex.exec(source)) {
174
+ matched = true;
175
+ const [, exp, varOne, varTwo, varThree, plainCss, , tag, taggedCss] = match;
110
176
  const css = plainCss ?? taggedCss;
111
- const { code, exports = {} } = transform({
112
- minify: true,
113
- cssModules: true,
114
- filename: id,
115
- code: new TextEncoder().encode(css)
116
- });
177
+ const matchIndex = match.index;
178
+ const { code, exports: classExports = {} } = (() => {
179
+ try {
180
+ return transform({
181
+ minify: true,
182
+ cssModules: true,
183
+ filename: id,
184
+ code: encoder.encode(css)
185
+ });
186
+ } catch (err) {
187
+ const line = source.slice(0, matchIndex).split(`
188
+ `).length;
189
+ throw new Error(`Failed to compile cxx CSS at ${id}:${line}`, { cause: err });
190
+ }
191
+ })();
117
192
  const href = hashCss(code);
118
- const styles = Object.fromEntries(Object.entries(exports).map(([k, v]) => [k, v.name]));
193
+ const styles = Object.fromEntries(Object.entries(classExports).map(([k, v]) => [k, v.name]));
119
194
  if (tag) {
120
195
  const suffix = typeof config.typeSuffix === "string" ? config.typeSuffix : config.typeSuffix === false ? "" : "Classes";
121
196
  const tagWithSuffix = pascalise(`${tag}${suffix}`);
197
+ if (!IDENTIFIER.test(tagWithSuffix))
198
+ throw new InvalidTagError(tag);
122
199
  if (tags.has(tagWithSuffix))
123
200
  throw new DuplicateTagError(tagWithSuffix);
124
201
  tags.set(tagWithSuffix, Object.keys(styles));
125
202
  }
126
- return [
127
- varOne && `const ${varOne} = \`${code}\``,
128
- varTwo && `const ${varTwo} = ${JSON.stringify(styles)}`,
129
- varThree && `const ${varThree} = \`${href}\``
130
- ].filter(Boolean).join(`
203
+ const statements = [];
204
+ if (varOne) {
205
+ statements.push(`${exp}const ${varOne} = ${JSON.stringify(decoder.decode(code))}`);
206
+ }
207
+ if (varTwo) {
208
+ statements.push(`${exp}const ${varTwo} = ${JSON.stringify(styles)}`);
209
+ }
210
+ if (varThree) {
211
+ statements.push(`${exp}const ${varThree} = ${JSON.stringify(href)}`);
212
+ }
213
+ magic.overwrite(matchIndex, matchIndex + match[0].length, statements.join(`
214
+ `));
215
+ }
216
+ const template = magic.toString();
217
+ const map = matched ? magic.generateMap({ source: id, includeContent: true, hires: true }) : null;
218
+ const result = { source, config: configKey, template, tags, map };
219
+ collectCache.set(id, result);
220
+ return result;
221
+ }
222
+ function renderTypes(tags) {
223
+ return [
224
+ AUTOGEN_MSG,
225
+ "",
226
+ `import '${PKG_NAME}'`,
227
+ "",
228
+ `declare module '${PKG_NAME}' {`,
229
+ tagsToType(tags),
230
+ "}",
231
+ ""
232
+ ].join(`
131
233
  `);
132
- });
133
- collectCache.set(id, { source, template, tags });
134
- return {
135
- template,
136
- tags
137
- };
138
234
  }
139
- async function writeTypes(tags) {
235
+ async function writeRenderedTypes(tags) {
140
236
  const filePath = path.join(GENERATED_DIR, "index.d.ts");
141
237
  if (!tags.size) {
142
- try {
143
- await fs.rm(filePath, { force: true });
144
- fileCache.delete(filePath);
145
- } catch (err) {
146
- if (!isENOENT(err))
147
- throw err;
148
- }
238
+ await fs.rm(filePath, { force: true });
149
239
  return;
150
240
  }
151
241
  await fs.mkdir(GENERATED_DIR, { recursive: true });
152
- await maybeWrite(filePath, `${AUTOGEN_MSG}
153
-
154
- import '${PKG_NAME}'
155
-
156
- declare module '${PKG_NAME}' {
157
- ${tagsToType(tags)}
158
- }
159
- `);
242
+ await maybeWrite(filePath, renderTypes(tags));
160
243
  }
161
- function tagsToType(tags) {
162
- return [...tags].map(([tag, classes]) => `
163
- export type ${tag} = ${classes.length > 0 ? classes.toSorted((a, b) => a.length - b.length).map((c) => `'${c}'`).join(" | ") : "never"}
164
- `).join(`
165
- `);
244
+ var PARTIAL_DIR = path.join(GENERATED_DIR, "tags");
245
+ var REBUILD_WAIT = 100;
246
+ var rebuildTimer = null;
247
+ var rebuildError = null;
248
+ function partialPath(filePath) {
249
+ return path.join(PARTIAL_DIR, `${createHash("sha1").update(filePath).digest("hex")}.json`);
250
+ }
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
+ }
166
259
  }
167
- function flattenTags(tagsByFile) {
260
+ async function readPartials() {
261
+ let entries;
262
+ try {
263
+ entries = await fs.readdir(PARTIAL_DIR);
264
+ } catch (err) {
265
+ if (isENOENT(err))
266
+ return new Map;
267
+ throw err;
268
+ }
168
269
  const tags = new Map;
169
- for (const fileTags of tagsByFile.values()) {
170
- for (const [name, classes] of fileTags) {
270
+ for (const entry of entries) {
271
+ if (!entry.endsWith(".json"))
272
+ continue;
273
+ const partial = path.join(PARTIAL_DIR, entry);
274
+ let parsed;
275
+ try {
276
+ parsed = JSON.parse(await fs.readFile(partial, "utf-8"));
277
+ } catch {
278
+ continue;
279
+ }
280
+ try {
281
+ await fs.access(parsed.file);
282
+ } catch {
283
+ await fs.rm(partial, { force: true });
284
+ continue;
285
+ }
286
+ for (const [name, classes] of Object.entries(parsed.tags)) {
171
287
  if (tags.has(name))
172
288
  throw new DuplicateTagError(name);
173
289
  tags.set(name, classes);
@@ -175,6 +291,57 @@ function flattenTags(tagsByFile) {
175
291
  }
176
292
  return tags;
177
293
  }
294
+ function writeTypesForFile(filePath, tags) {
295
+ return enqueueWrite(async () => {
296
+ await fs.mkdir(PARTIAL_DIR, { recursive: true });
297
+ const partial = partialPath(filePath);
298
+ if (!tags.size) {
299
+ await fs.rm(partial, { force: true });
300
+ } else {
301
+ await writeFileAtomic(partial, JSON.stringify({ file: filePath, tags: Object.fromEntries(tags) }));
302
+ }
303
+ }).then(() => {
304
+ scheduleRebuild();
305
+ });
306
+ }
307
+ function scheduleRebuild() {
308
+ if (rebuildTimer)
309
+ clearTimeout(rebuildTimer);
310
+ rebuildTimer = setTimeout(() => {
311
+ rebuildTimer = null;
312
+ flushTypes().catch((err) => {
313
+ rebuildError = err;
314
+ });
315
+ }, REBUILD_WAIT);
316
+ }
317
+ function flushTypes() {
318
+ if (rebuildTimer) {
319
+ clearTimeout(rebuildTimer);
320
+ rebuildTimer = null;
321
+ }
322
+ 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;
330
+ }
331
+ });
332
+ }
333
+ function takeRebuildError() {
334
+ const err = rebuildError;
335
+ rebuildError = null;
336
+ return err;
337
+ }
338
+ function tagsToType(tags) {
339
+ return [...tags].toSorted(([a], [b]) => a.localeCompare(b)).map(([tag, classes]) => {
340
+ const union = classes.length > 0 ? classes.toSorted((a, b) => a.length - b.length).map((c) => `'${c}'`).join(" | ") : "never";
341
+ return ` export type ${tag} = ${union}`;
342
+ }).join(`
343
+ `);
344
+ }
178
345
 
179
346
  // src/vite/index.ts
180
347
  import { realpathSync } from "node:fs";
@@ -189,20 +356,14 @@ var LEVELS = {
189
356
  error: 3,
190
357
  fatal: 4
191
358
  };
359
+ var defaultLevel = process.env["NODE_ENV"] === "production" ? "error" : "info";
192
360
 
193
361
  class Logger {
194
362
  name;
195
- static #defaultLevel = "info";
196
363
  #level;
197
364
  constructor(level, name) {
198
365
  this.name = name;
199
- this.#level = level;
200
- }
201
- static set defaultLevel(level) {
202
- Logger.#defaultLevel = level;
203
- }
204
- static get defaultLevel() {
205
- return Logger.#defaultLevel;
366
+ this.#level = level ?? defaultLevel;
206
367
  }
207
368
  static toError(err) {
208
369
  return err instanceof Error ? err : new Error(String(err), { cause: err });
@@ -225,7 +386,7 @@ ${err.stack}` : "");
225
386
  this.#level = level;
226
387
  }
227
388
  get level() {
228
- return this.#level ?? Logger.#defaultLevel;
389
+ return this.#level;
229
390
  }
230
391
  log(level, message, error) {
231
392
  if (LEVELS[level] < LEVELS[this.level])
@@ -273,15 +434,14 @@ var ACCEPTED_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx"]);
273
434
  function normaliseWatchPath(p) {
274
435
  return p.replace(/\\/g, "/");
275
436
  }
437
+ function globToRegExp(glob) {
438
+ const pattern = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*(\/|$)/g, ".*$1").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
439
+ return new RegExp(`^${pattern}$`);
440
+ }
276
441
  function cxx(pluginConfig = {}) {
277
- const buildContext = {
278
- tagsByFile: new Map
279
- };
280
- const logger = new Logger(pluginConfig?.logger?.level ?? false ? "error" : "debug");
281
- const watchRoot = normaliseWatchPath(realpathSync.native(process.cwd()));
282
- const watchedRoots = [`${watchRoot}/`];
283
- let rebuildRunning = false;
284
- let rebuildQueued = false;
442
+ const logger = new Logger;
443
+ let watchRoot = normaliseWatchPath(process.cwd());
444
+ const ignored = (pluginConfig.watch?.ignore ?? []).map(globToRegExp);
285
445
  function resolveWatchFile(filePath) {
286
446
  const absolutePath = path2.resolve(watchRoot, filePath);
287
447
  const parentPath = path2.dirname(absolutePath);
@@ -294,75 +454,112 @@ function cxx(pluginConfig = {}) {
294
454
  }
295
455
  function watchedFile(filePath) {
296
456
  const resolvedPath = resolveWatchFile(filePath);
297
- return watchedRoots.some((root) => resolvedPath.startsWith(root)) && !resolvedPath.includes(`/${GENERATED_DIR}/`) && ACCEPTED_EXTENSIONS.has(path2.extname(resolvedPath));
298
- }
299
- const rebuild = debounce((event, filePath) => {
300
- function queue() {
301
- (async () => {
302
- if (rebuildRunning) {
303
- rebuildQueued = true;
304
- return;
305
- }
306
- rebuildRunning = true;
307
- do {
308
- rebuildQueued = false;
309
- try {
310
- const hadTags = buildContext.tagsByFile.has(file);
311
- let hasTags = false;
312
- if (event === "unlink") {
313
- buildContext.tagsByFile.delete(file);
314
- } else {
315
- const source = await fs2.readFile(file, "utf-8");
316
- const { tags } = collect(source, file, pluginConfig);
317
- hasTags = tags.size > 0;
318
- if (hasTags) {
319
- buildContext.tagsByFile.set(file, tags);
320
- } else {
321
- buildContext.tagsByFile.delete(file);
322
- }
323
- }
324
- if (!hadTags && !hasTags)
325
- continue;
326
- await writeTypes(flattenTags(buildContext.tagsByFile));
327
- logger.info(`updated types from ${file}`);
328
- } catch (err) {
329
- if (isENOENT(err)) {
330
- if (!buildContext.tagsByFile.has(file))
331
- continue;
332
- buildContext.tagsByFile.delete(file);
333
- await writeTypes(flattenTags(buildContext.tagsByFile));
334
- logger.info(`updated types from ${file}`);
335
- continue;
336
- }
337
- logger.error(`failed to update types from ${file}`, err);
338
- }
339
- } while (rebuildQueued);
340
- rebuildRunning = false;
341
- })();
457
+ if (resolvedPath.startsWith(`${watchRoot}/`) && !resolvedPath.includes(`/${GENERATED_DIR}/`) && ACCEPTED_EXTENSIONS.has(path2.extname(resolvedPath)) && !ignored.some((regexp) => regexp.test(resolvedPath))) {
458
+ return resolvedPath;
342
459
  }
343
- if (!watchedFile(filePath))
460
+ return null;
461
+ }
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;
469
+ await writeTypesForFile(file, new Map);
470
+ return;
471
+ }
472
+ const tags = source.includes("cxx") ? collect(source, file, pluginConfig).tags : new Map;
473
+ await writeTypesForFile(file, tags);
474
+ }
475
+ async function scan(dir) {
476
+ let entries;
477
+ try {
478
+ entries = await fs2.readdir(dir, { withFileTypes: true });
479
+ } catch {
344
480
  return;
345
- const file = resolveWatchFile(filePath);
346
- queue();
347
- }, 75);
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);
488
+ 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
+ }));
497
+ }
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
+ });
348
512
  return {
349
513
  name: "cxx",
350
514
  enforce: "pre",
515
+ configResolved(config) {
516
+ const root = pluginConfig.watch?.root ?? config.root;
517
+ try {
518
+ watchRoot = normaliseWatchPath(realpathSync.native(root));
519
+ } catch {
520
+ watchRoot = normaliseWatchPath(root);
521
+ }
522
+ },
351
523
  transform(code, id) {
352
- const { template, tags } = collect(code, id, pluginConfig);
353
- if (tags.size)
354
- buildContext.tagsByFile.set(id, tags);
524
+ if (id.startsWith("\x00") || id.includes("/node_modules/"))
525
+ return null;
526
+ if (!code.includes("cxx"))
527
+ return null;
528
+ const cleanId = id.split("?")[0];
529
+ if (!ACCEPTED_EXTENSIONS.has(path2.extname(cleanId)))
530
+ return null;
531
+ const { template, tags, map } = collect(code, cleanId, pluginConfig);
532
+ if (template === code)
533
+ return null;
534
+ writeTypesForFile(resolveWatchFile(cleanId), tags);
355
535
  return {
356
536
  code: template,
357
- map: null
537
+ map
358
538
  };
359
539
  },
360
540
  configureServer(server) {
361
541
  logger.info("Watching for changes...");
362
- server.watcher.on("add", (p) => rebuild("add", p)).on("change", (p) => rebuild("change", p)).on("unlink", (p) => rebuild("unlink", p));
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));
363
560
  },
364
561
  async writeBundle() {
365
- await writeTypes(flattenTags(buildContext.tagsByFile));
562
+ await flushTypes();
366
563
  }
367
564
  };
368
565
  }
@@ -370,4 +567,4 @@ export {
370
567
  cxx as default
371
568
  };
372
569
 
373
- //# debugId=99F5EDE4479120ED64756E2164756E21
570
+ //# debugId=7C0503EFBFB9E21F64756E2164756E21