@jk2908/cxx 0.2.4 → 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
- // src/index.ts
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
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
- // src/index.ts
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,26 +132,42 @@ 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
  }
97
144
 
98
- class TagDuplicateError extends Error {
145
+ class DuplicateTagError extends Error {
146
+ name = "DuplicateTagError";
99
147
  constructor(tag) {
100
148
  super(`Tag ${tag} has already been seen. Tags must be unique across your application`);
101
149
  }
102
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
+ }
103
158
  function collect(source, id, config) {
104
- const tags = new Map;
159
+ const configKey = JSON.stringify(config);
105
160
  const cached = collectCache.get(id);
106
- if (cached?.source === source)
161
+ if (cached?.source === source && cached.config === configKey)
107
162
  return cached;
108
- 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;
109
171
  const css = plainCss ?? taggedCss;
110
172
  const { code, exports = {} } = transform({
111
173
  minify: true,
@@ -118,49 +180,112 @@ function collect(source, id, config) {
118
180
  if (tag) {
119
181
  const suffix = typeof config.typeSuffix === "string" ? config.typeSuffix : config.typeSuffix === false ? "" : "Classes";
120
182
  const tagWithSuffix = pascalise(`${tag}${suffix}`);
183
+ if (!IDENTIFIER.test(tagWithSuffix))
184
+ throw new InvalidTagError(tag);
121
185
  if (tags.has(tagWithSuffix))
122
- throw new TagDuplicateError(tagWithSuffix);
186
+ throw new DuplicateTagError(tagWithSuffix);
123
187
  tags.set(tagWithSuffix, Object.keys(styles));
124
188
  }
125
- return [
126
- varOne && `const ${varOne} = \`${code}\``,
127
- varTwo && `const ${varTwo} = ${JSON.stringify(styles)}`,
128
- varThree && `const ${varThree} = \`${href}\``
129
- ].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(`
130
219
  `);
131
- });
132
- collectCache.set(id, { source, template, tags });
133
- return {
134
- template,
135
- tags
136
- };
137
220
  }
138
- async function writeTypes(tags) {
221
+ async function writeRenderedTypes(tags) {
139
222
  const filePath = path.join(GENERATED_DIR, "index.d.ts");
140
223
  if (!tags.size) {
141
- try {
142
- await fs.rm(filePath, { force: true });
143
- fileCache.delete(filePath);
144
- } catch (err) {
145
- if (!isENOENT(err))
146
- throw err;
147
- }
224
+ await fs.rm(filePath, { force: true });
225
+ fileCache.delete(filePath);
148
226
  return;
149
227
  }
150
228
  await fs.mkdir(GENERATED_DIR, { recursive: true });
151
- await maybeWrite(filePath, `${AUTOGEN_MSG}
152
-
153
- import '${PKG_NAME}'
154
-
155
- declare module '${PKG_NAME}' {
156
- ${tagsToType(tags)}
157
- }
158
- `);
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
+ });
159
283
  }
160
284
  function tagsToType(tags) {
161
- return [...tags].map(([tag, classes]) => `
162
- export type ${tag} = ${classes.length > 0 ? classes.toSorted((a, b) => a.length - b.length).map((c) => `'${c}'`).join(" | ") : "never"}
163
- `).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(`
164
289
  `);
165
290
  }
166
291
  function flattenTags(tagsByFile) {
@@ -168,19 +293,12 @@ function flattenTags(tagsByFile) {
168
293
  for (const fileTags of tagsByFile.values()) {
169
294
  for (const [name, classes] of fileTags) {
170
295
  if (tags.has(name))
171
- throw new TagDuplicateError(name);
296
+ throw new DuplicateTagError(name);
172
297
  tags.set(name, classes);
173
298
  }
174
299
  }
175
300
  return tags;
176
301
  }
177
- var cxx = Object.assign((_) => ["", {}, ""], {
178
- tag: (_) => (_strings) => [
179
- "",
180
- {},
181
- ""
182
- ]
183
- });
184
302
 
185
303
  // src/vite/index.ts
186
304
  import { realpathSync } from "node:fs";
@@ -195,20 +313,14 @@ var LEVELS = {
195
313
  error: 3,
196
314
  fatal: 4
197
315
  };
316
+ var defaultLevel = process.env["NODE_ENV"] === "production" ? "error" : "info";
198
317
 
199
318
  class Logger {
200
319
  name;
201
- static #defaultLevel = "info";
202
320
  #level;
203
321
  constructor(level, name) {
204
322
  this.name = name;
205
- this.#level = level;
206
- }
207
- static set defaultLevel(level) {
208
- Logger.#defaultLevel = level;
209
- }
210
- static get defaultLevel() {
211
- return Logger.#defaultLevel;
323
+ this.#level = level ?? defaultLevel;
212
324
  }
213
325
  static toError(err) {
214
326
  return err instanceof Error ? err : new Error(String(err), { cause: err });
@@ -231,7 +343,7 @@ ${err.stack}` : "");
231
343
  this.#level = level;
232
344
  }
233
345
  get level() {
234
- return this.#level ?? Logger.#defaultLevel;
346
+ return this.#level;
235
347
  }
236
348
  log(level, message, error) {
237
349
  if (LEVELS[level] < LEVELS[this.level])
@@ -279,15 +391,12 @@ var ACCEPTED_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx"]);
279
391
  function normaliseWatchPath(p) {
280
392
  return p.replace(/\\/g, "/");
281
393
  }
282
- function cxx2(pluginConfig = {}) {
394
+ function cxx(pluginConfig = {}) {
283
395
  const buildContext = {
284
396
  tagsByFile: new Map
285
397
  };
286
- const logger = new Logger(pluginConfig?.logger?.level ?? false ? "error" : "debug");
287
- const watchRoot = normaliseWatchPath(realpathSync.native(process.cwd()));
288
- const watchedRoots = [`${watchRoot}/`];
289
- let rebuildRunning = false;
290
- let rebuildQueued = false;
398
+ const logger = new Logger;
399
+ let watchRoot = normaliseWatchPath(process.cwd());
291
400
  function resolveWatchFile(filePath) {
292
401
  const absolutePath = path2.resolve(watchRoot, filePath);
293
402
  const parentPath = path2.dirname(absolutePath);
@@ -300,72 +409,115 @@ function cxx2(pluginConfig = {}) {
300
409
  }
301
410
  function watchedFile(filePath) {
302
411
  const resolvedPath = resolveWatchFile(filePath);
303
- 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;
304
416
  }
305
- const rebuild = debounce((event, filePath) => {
306
- function queue() {
307
- (async () => {
308
- if (rebuildRunning) {
309
- rebuildQueued = true;
310
- return;
311
- }
312
- rebuildRunning = true;
313
- do {
314
- rebuildQueued = false;
315
- try {
316
- const hadTags = buildContext.tagsByFile.has(file);
317
- let hasTags = false;
318
- if (event === "unlink") {
319
- buildContext.tagsByFile.delete(file);
320
- } else {
321
- const source = await fs2.readFile(file, "utf-8");
322
- const { tags } = collect(source, file, pluginConfig);
323
- hasTags = tags.size > 0;
324
- if (hasTags) {
325
- buildContext.tagsByFile.set(file, tags);
326
- } else {
327
- buildContext.tagsByFile.delete(file);
328
- }
329
- }
330
- if (!hadTags && !hasTags)
331
- continue;
332
- await writeTypes(flattenTags(buildContext.tagsByFile));
333
- logger.info(`updated types from ${file}`);
334
- } catch (err) {
335
- if (isENOENT(err)) {
336
- if (!buildContext.tagsByFile.has(file))
337
- continue;
338
- buildContext.tagsByFile.delete(file);
339
- await writeTypes(flattenTags(buildContext.tagsByFile));
340
- logger.info(`updated types from ${file}`);
341
- continue;
342
- }
343
- logger.error(`failed to update types from ${file}`, err);
344
- }
345
- } while (rebuildQueued);
346
- rebuildRunning = false;
347
- })();
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;
348
426
  }
349
- 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}`);
438
+ return;
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)
350
448
  return;
351
- const file = resolveWatchFile(filePath);
352
- queue();
353
- }, 75);
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
+ });
354
461
  return {
355
462
  name: "cxx",
356
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
+ },
357
484
  transform(code, id) {
358
- const { template, tags } = collect(code, id, pluginConfig);
359
- if (tags.size)
360
- 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
+ }
361
501
  return {
362
502
  code: template,
363
- map: null
503
+ map
364
504
  };
365
505
  },
366
506
  configureServer(server) {
367
507
  logger.info("Watching for changes...");
368
- 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
+ });
369
521
  },
370
522
  async writeBundle() {
371
523
  await writeTypes(flattenTags(buildContext.tagsByFile));
@@ -373,7 +525,7 @@ function cxx2(pluginConfig = {}) {
373
525
  };
374
526
  }
375
527
  export {
376
- cxx2 as default
528
+ cxx as default
377
529
  };
378
530
 
379
- //# debugId=1D46DAA8A3CB5F1264756E2164756E21
531
+ //# debugId=8BACD1ACA42BF59D64756E2164756E21