@jk2908/cxx 0.2.6 → 0.3.1

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,31 +56,73 @@ 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);
59
+ function createBatcher(wait, flush) {
60
+ const pending = new Map;
61
+ let timer = null;
62
+ let running = false;
63
+ async function drain() {
64
+ if (running)
65
+ return;
66
+ running = true;
67
+ try {
68
+ while (pending.size > 0) {
69
+ const batch = new Map(pending);
70
+ pending.clear();
71
+ await flush(batch);
72
+ }
73
+ } finally {
74
+ running = false;
60
75
  }
61
- timeoutId = setTimeout(() => {
62
- fn.apply(null, args);
76
+ }
77
+ function schedule() {
78
+ if (timer)
79
+ clearTimeout(timer);
80
+ timer = setTimeout(() => {
81
+ timer = null;
82
+ drain();
63
83
  }, wait);
84
+ }
85
+ return {
86
+ add(key, value) {
87
+ pending.set(key, value);
88
+ schedule();
89
+ },
90
+ flush() {
91
+ if (timer) {
92
+ clearTimeout(timer);
93
+ timer = null;
94
+ }
95
+ return drain();
96
+ }
64
97
  };
65
98
  }
66
99
 
67
100
  // src/build.ts
101
+ var decoder = new TextDecoder;
68
102
  var collectCache = new LruCache;
69
103
  var fileCache = new LruCache;
104
+ var writeChain = Promise.resolve();
70
105
  function isENOENT(err) {
71
106
  return err instanceof Error && "code" in err && err.code === "ENOENT";
72
107
  }
108
+ async function writeFileAtomic(filePath, content) {
109
+ const tmp = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
110
+ await fs.writeFile(tmp, content);
111
+ await fs.rename(tmp, filePath);
112
+ }
113
+ function enqueueWrite(task) {
114
+ const run = writeChain.then(task, task);
115
+ writeChain = run.then(() => {
116
+ return;
117
+ }, () => {
118
+ return;
119
+ });
120
+ return run;
121
+ }
73
122
  async function maybeWrite(filePath, content) {
74
123
  const cached = fileCache.get(filePath);
75
- if (cached !== content) {
76
- await fs.writeFile(filePath, content);
77
- fileCache.set(filePath, content);
78
- return true;
79
- }
124
+ if (cached === content)
125
+ return false;
80
126
  try {
81
127
  if (await fs.readFile(filePath, "utf-8") === content) {
82
128
  fileCache.set(filePath, content);
@@ -86,11 +132,12 @@ async function maybeWrite(filePath, content) {
86
132
  if (!isENOENT(err))
87
133
  throw err;
88
134
  }
89
- await fs.writeFile(filePath, content);
135
+ await writeFileAtomic(filePath, content);
90
136
  fileCache.set(filePath, content);
91
137
  return true;
92
138
  }
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;
139
+ 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;
140
+ var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
94
141
  function hashCss(code) {
95
142
  return `cxx-${createHash("sha256").update(code).digest("base64url").slice(0, 12)}`;
96
143
  }
@@ -101,12 +148,26 @@ class DuplicateTagError extends Error {
101
148
  super(`Tag ${tag} has already been seen. Tags must be unique across your application`);
102
149
  }
103
150
  }
151
+
152
+ class InvalidTagError extends Error {
153
+ name = "InvalidTagError";
154
+ constructor(tag) {
155
+ super(`Tag "${tag}" does not map to a valid TypeScript identifier. ` + `Use letters, numbers, spaces, "_" or "-"`);
156
+ }
157
+ }
104
158
  function collect(source, id, config) {
105
- const tags = new Map;
159
+ const configKey = JSON.stringify(config);
106
160
  const cached = collectCache.get(id);
107
- if (cached?.source === source)
161
+ if (cached?.source === source && cached.config === configKey)
108
162
  return cached;
109
- const template = source.replace(regex, (_match, varOne, varTwo, varThree, plainCss, _quote, tag, taggedCss) => {
163
+ const tags = new Map;
164
+ const magic = new MagicString(source);
165
+ let matched = false;
166
+ regex.lastIndex = 0;
167
+ let match;
168
+ while (match = regex.exec(source)) {
169
+ matched = true;
170
+ const [, exp, varOne, varTwo, varThree, plainCss, , tag, taggedCss] = match;
110
171
  const css = plainCss ?? taggedCss;
111
172
  const { code, exports = {} } = transform({
112
173
  minify: true,
@@ -119,49 +180,112 @@ function collect(source, id, config) {
119
180
  if (tag) {
120
181
  const suffix = typeof config.typeSuffix === "string" ? config.typeSuffix : config.typeSuffix === false ? "" : "Classes";
121
182
  const tagWithSuffix = pascalise(`${tag}${suffix}`);
183
+ if (!IDENTIFIER.test(tagWithSuffix))
184
+ throw new InvalidTagError(tag);
122
185
  if (tags.has(tagWithSuffix))
123
186
  throw new DuplicateTagError(tagWithSuffix);
124
187
  tags.set(tagWithSuffix, Object.keys(styles));
125
188
  }
126
- return [
127
- varOne && `const ${varOne} = \`${code}\``,
128
- varTwo && `const ${varTwo} = ${JSON.stringify(styles)}`,
129
- varThree && `const ${varThree} = \`${href}\``
130
- ].filter(Boolean).join(`
189
+ const statements = [];
190
+ if (varOne) {
191
+ statements.push(`${exp}const ${varOne} = ${JSON.stringify(decoder.decode(code))}`);
192
+ }
193
+ if (varTwo) {
194
+ statements.push(`${exp}const ${varTwo} = ${JSON.stringify(styles)}`);
195
+ }
196
+ if (varThree) {
197
+ statements.push(`${exp}const ${varThree} = ${JSON.stringify(href)}`);
198
+ }
199
+ magic.overwrite(match.index, match.index + match[0].length, statements.join(`
200
+ `));
201
+ }
202
+ const template = magic.toString();
203
+ const map = matched ? magic.generateMap({ source: id, includeContent: true, hires: true }) : null;
204
+ const result = { source, config: configKey, template, tags, map };
205
+ collectCache.set(id, result);
206
+ return result;
207
+ }
208
+ function renderTypes(tags) {
209
+ return [
210
+ AUTOGEN_MSG,
211
+ "",
212
+ `import '${PKG_NAME}'`,
213
+ "",
214
+ `declare module '${PKG_NAME}' {`,
215
+ tagsToType(tags),
216
+ "}",
217
+ ""
218
+ ].join(`
131
219
  `);
132
- });
133
- collectCache.set(id, { source, template, tags });
134
- return {
135
- template,
136
- tags
137
- };
138
220
  }
139
- async function writeTypes(tags) {
221
+ async function writeRenderedTypes(tags) {
140
222
  const filePath = path.join(GENERATED_DIR, "index.d.ts");
141
223
  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
- }
224
+ await fs.rm(filePath, { force: true });
225
+ fileCache.delete(filePath);
149
226
  return;
150
227
  }
151
228
  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
- `);
229
+ await maybeWrite(filePath, renderTypes(tags));
230
+ }
231
+ function writeTypes(tags) {
232
+ return enqueueWrite(() => writeRenderedTypes(tags));
233
+ }
234
+ var PARTIAL_DIR = path.join(GENERATED_DIR, "tags");
235
+ function partialPath(filePath) {
236
+ return path.join(PARTIAL_DIR, `${createHash("sha1").update(filePath).digest("hex")}.json`);
237
+ }
238
+ async function readPartials() {
239
+ let entries;
240
+ try {
241
+ entries = await fs.readdir(PARTIAL_DIR);
242
+ } catch (err) {
243
+ if (isENOENT(err))
244
+ return new Map;
245
+ throw err;
246
+ }
247
+ const tags = new Map;
248
+ for (const entry of entries) {
249
+ if (!entry.endsWith(".json"))
250
+ continue;
251
+ const partial = path.join(PARTIAL_DIR, entry);
252
+ let parsed;
253
+ try {
254
+ parsed = JSON.parse(await fs.readFile(partial, "utf-8"));
255
+ } catch {
256
+ continue;
257
+ }
258
+ try {
259
+ await fs.access(parsed.file);
260
+ } catch {
261
+ await fs.rm(partial, { force: true });
262
+ continue;
263
+ }
264
+ for (const [name, classes] of Object.entries(parsed.tags)) {
265
+ if (tags.has(name))
266
+ throw new DuplicateTagError(name);
267
+ tags.set(name, classes);
268
+ }
269
+ }
270
+ return tags;
271
+ }
272
+ function writeTypesForFile(filePath, tags) {
273
+ return enqueueWrite(async () => {
274
+ await fs.mkdir(PARTIAL_DIR, { recursive: true });
275
+ const partial = partialPath(filePath);
276
+ if (!tags.size) {
277
+ await fs.rm(partial, { force: true });
278
+ } else {
279
+ await writeFileAtomic(partial, JSON.stringify({ file: filePath, tags: Object.fromEntries(tags) }));
280
+ }
281
+ await writeRenderedTypes(await readPartials());
282
+ });
160
283
  }
161
284
  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(`
285
+ return [...tags].map(([tag, classes]) => {
286
+ const union = classes.length > 0 ? classes.toSorted((a, b) => a.length - b.length).map((c) => `'${c}'`).join(" | ") : "never";
287
+ return ` export type ${tag} = ${union}`;
288
+ }).join(`
165
289
  `);
166
290
  }
167
291
  function flattenTags(tagsByFile) {
@@ -189,20 +313,14 @@ var LEVELS = {
189
313
  error: 3,
190
314
  fatal: 4
191
315
  };
316
+ var defaultLevel = process.env["NODE_ENV"] === "production" ? "error" : "info";
192
317
 
193
318
  class Logger {
194
319
  name;
195
- static #defaultLevel = "info";
196
320
  #level;
197
321
  constructor(level, name) {
198
322
  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;
323
+ this.#level = level ?? defaultLevel;
206
324
  }
207
325
  static toError(err) {
208
326
  return err instanceof Error ? err : new Error(String(err), { cause: err });
@@ -225,7 +343,7 @@ ${err.stack}` : "");
225
343
  this.#level = level;
226
344
  }
227
345
  get level() {
228
- return this.#level ?? Logger.#defaultLevel;
346
+ return this.#level;
229
347
  }
230
348
  log(level, message, error) {
231
349
  if (LEVELS[level] < LEVELS[this.level])
@@ -277,11 +395,8 @@ function cxx(pluginConfig = {}) {
277
395
  const buildContext = {
278
396
  tagsByFile: new Map
279
397
  };
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;
398
+ const logger = new Logger;
399
+ let watchRoot = normaliseWatchPath(process.cwd());
285
400
  function resolveWatchFile(filePath) {
286
401
  const absolutePath = path2.resolve(watchRoot, filePath);
287
402
  const parentPath = path2.dirname(absolutePath);
@@ -294,72 +409,115 @@ function cxx(pluginConfig = {}) {
294
409
  }
295
410
  function watchedFile(filePath) {
296
411
  const resolvedPath = resolveWatchFile(filePath);
297
- return watchedRoots.some((root) => resolvedPath.startsWith(root)) && !resolvedPath.includes(`/${GENERATED_DIR}/`) && ACCEPTED_EXTENSIONS.has(path2.extname(resolvedPath));
412
+ if (resolvedPath.startsWith(`${watchRoot}/`) && !resolvedPath.includes(`/${GENERATED_DIR}/`) && ACCEPTED_EXTENSIONS.has(path2.extname(resolvedPath))) {
413
+ return resolvedPath;
414
+ }
415
+ return null;
298
416
  }
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
- })();
417
+ async function onEvent(event, file) {
418
+ const hadTags = buildContext.tagsByFile.has(file);
419
+ if (event === "unlink") {
420
+ if (!hadTags)
421
+ return;
422
+ buildContext.tagsByFile.delete(file);
423
+ await writeTypes(flattenTags(buildContext.tagsByFile));
424
+ logger.info(`updated types from ${file}`);
425
+ return;
342
426
  }
343
- if (!watchedFile(filePath))
427
+ let source;
428
+ try {
429
+ source = await fs2.readFile(file, "utf-8");
430
+ } catch (err) {
431
+ if (!isENOENT(err))
432
+ throw err;
433
+ if (!hadTags)
434
+ return;
435
+ buildContext.tagsByFile.delete(file);
436
+ await writeTypes(flattenTags(buildContext.tagsByFile));
437
+ logger.info(`updated types from ${file}`);
344
438
  return;
345
- const file = resolveWatchFile(filePath);
346
- queue();
347
- }, 75);
439
+ }
440
+ const { tags } = collect(source, file, pluginConfig);
441
+ const hasTags = tags.size > 0;
442
+ if (hasTags) {
443
+ buildContext.tagsByFile.set(file, tags);
444
+ } else {
445
+ buildContext.tagsByFile.delete(file);
446
+ }
447
+ if (!hadTags && !hasTags)
448
+ return;
449
+ await writeTypes(flattenTags(buildContext.tagsByFile));
450
+ logger.info(`updated types from ${file}`);
451
+ }
452
+ const queue = createBatcher(75, async (batch) => {
453
+ for (const [file, event] of batch) {
454
+ try {
455
+ await onEvent(event, file);
456
+ } catch (err) {
457
+ logger.error(`failed to update types from ${file}`, err);
458
+ }
459
+ }
460
+ });
348
461
  return {
349
462
  name: "cxx",
350
463
  enforce: "pre",
464
+ config() {
465
+ const ignore = pluginConfig.watch?.ignore;
466
+ if (!ignore?.length)
467
+ return null;
468
+ return {
469
+ server: {
470
+ watch: {
471
+ ignored: ignore
472
+ }
473
+ }
474
+ };
475
+ },
476
+ configResolved(config) {
477
+ const root = pluginConfig.watch?.root ?? config.root;
478
+ try {
479
+ watchRoot = normaliseWatchPath(realpathSync.native(root));
480
+ } catch {
481
+ watchRoot = normaliseWatchPath(root);
482
+ }
483
+ },
351
484
  transform(code, id) {
352
- const { template, tags } = collect(code, id, pluginConfig);
353
- if (tags.size)
354
- buildContext.tagsByFile.set(id, tags);
485
+ if (id.startsWith("\x00") || id.includes("/node_modules/"))
486
+ return null;
487
+ if (!code.includes("cxx"))
488
+ return null;
489
+ const cleanId = id.split("?")[0];
490
+ if (!ACCEPTED_EXTENSIONS.has(path2.extname(cleanId)))
491
+ return null;
492
+ const { template, tags, map } = collect(code, cleanId, pluginConfig);
493
+ if (template === code)
494
+ return null;
495
+ const resolved = resolveWatchFile(cleanId);
496
+ if (tags.size) {
497
+ buildContext.tagsByFile.set(resolved, tags);
498
+ } else {
499
+ buildContext.tagsByFile.delete(resolved);
500
+ }
355
501
  return {
356
502
  code: template,
357
- map: null
503
+ map
358
504
  };
359
505
  },
360
506
  configureServer(server) {
361
507
  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));
508
+ server.watcher.on("add", (p) => {
509
+ const file = watchedFile(p);
510
+ if (file)
511
+ queue.add(file, "add");
512
+ }).on("change", (p) => {
513
+ const file = watchedFile(p);
514
+ if (file)
515
+ queue.add(file, "change");
516
+ }).on("unlink", (p) => {
517
+ const file = watchedFile(p);
518
+ if (file)
519
+ queue.add(file, "unlink");
520
+ });
363
521
  },
364
522
  async writeBundle() {
365
523
  await writeTypes(flattenTags(buildContext.tagsByFile));
@@ -370,4 +528,4 @@ export {
370
528
  cxx as default
371
529
  };
372
530
 
373
- //# debugId=99F5EDE4479120ED64756E2164756E21
531
+ //# debugId=8BACD1ACA42BF59D64756E2164756E21