@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,111 +291,179 @@ 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/next/index.ts
180
347
  import fs2 from "node:fs/promises";
181
348
  import path2 from "node:path";
182
349
  import { fileURLToPath } from "node:url";
183
- import watcher from "@parcel/watcher";
184
350
  var nextDir = path2.dirname(fileURLToPath(import.meta.url));
185
351
  var loader = path2.join(nextDir, "cxx-loader.cjs");
186
- var tagsByFile = new Map;
187
352
  var watchedFiles = new Map;
188
- var subscriptionsByDir = new Map;
189
- async function rebuildFile(filePath, event) {
190
- const watchedFile = watchedFiles.get(filePath);
191
- if (!watchedFile)
353
+ var subscription = null;
354
+ var subscriptionPromise = null;
355
+ var cleanupRegistered = false;
356
+ var watcherLogger = null;
357
+ var DEFAULT_IGNORE = [
358
+ "**/node_modules/**",
359
+ "**/.next/**",
360
+ `**/${GENERATED_DIR}/**`,
361
+ "**/.git/**"
362
+ ];
363
+ async function onEvent(event, filePath) {
364
+ const watched = watchedFiles.get(filePath);
365
+ if (!watched)
192
366
  return;
193
- const { pluginConfig, logger } = watchedFile;
194
- const hadTags = tagsByFile.has(filePath);
367
+ const { pluginConfig, logger } = watched;
195
368
  if (event === "delete") {
196
- tagsByFile.delete(filePath);
197
- if (!hadTags)
198
- return;
199
- await writeTypes(flattenTags(tagsByFile));
369
+ await writeTypesForFile(filePath, new Map);
200
370
  logger.info(`updated types from ${filePath}`);
201
371
  return;
202
372
  }
373
+ let source;
203
374
  try {
204
- const source = await fs2.readFile(filePath, "utf-8");
205
- const { tags } = collect(source, filePath, pluginConfig);
206
- const hasTags = tags.size > 0;
207
- if (hasTags) {
208
- tagsByFile.set(filePath, tags);
209
- } else {
210
- tagsByFile.delete(filePath);
211
- }
212
- if (!hadTags && !hasTags)
213
- return;
214
- await writeTypes(flattenTags(tagsByFile));
215
- logger.info(`updated types from ${filePath}`);
375
+ source = await fs2.readFile(filePath, "utf-8");
216
376
  } catch (err) {
217
377
  if (!isENOENT(err))
218
378
  throw err;
219
- if (!hadTags)
220
- return;
221
- tagsByFile.delete(filePath);
222
- await writeTypes(flattenTags(tagsByFile));
379
+ await writeTypesForFile(filePath, new Map);
223
380
  logger.info(`updated types from ${filePath}`);
381
+ return;
382
+ }
383
+ const tags = source.includes("cxx") ? collect(source, filePath, pluginConfig).tags : new Map;
384
+ await writeTypesForFile(filePath, tags);
385
+ logger.info(`updated types from ${filePath}`);
386
+ }
387
+ function ensureWatcher(pluginConfig, logger) {
388
+ watcherLogger = logger;
389
+ if (subscriptionPromise)
390
+ return;
391
+ const root = pluginConfig.watch?.root ?? process.cwd();
392
+ const ignore = [...DEFAULT_IGNORE, ...pluginConfig.watch?.ignore ?? []];
393
+ subscriptionPromise = (async () => {
394
+ const { subscribe } = await import("@parcel/watcher");
395
+ subscription = await subscribe(root, (watchErr, events) => {
396
+ if (watchErr) {
397
+ watcherLogger?.error(`failed to watch ${root}`, watchErr);
398
+ return;
399
+ }
400
+ for (const event of events) {
401
+ const filePath = path2.resolve(event.path);
402
+ if (!watchedFiles.has(filePath))
403
+ continue;
404
+ onEvent(event.type, filePath).catch((err) => {
405
+ watcherLogger?.error(`failed to update types from ${filePath}`, err);
406
+ });
407
+ }
408
+ }, { ignore });
409
+ registerCleanup();
410
+ })().catch((err) => {
411
+ watcherLogger?.error("failed to start cxx watcher", err);
412
+ subscriptionPromise = null;
413
+ });
414
+ }
415
+ async function cleanup() {
416
+ const active = subscription;
417
+ subscription = null;
418
+ subscriptionPromise = null;
419
+ watchedFiles.clear();
420
+ if (active)
421
+ await active.unsubscribe();
422
+ }
423
+ async function shutdown(signal) {
424
+ try {
425
+ await cleanup();
426
+ } finally {
427
+ process.kill(process.pid, signal);
224
428
  }
225
429
  }
430
+ function registerCleanup() {
431
+ if (cleanupRegistered)
432
+ return;
433
+ cleanupRegistered = true;
434
+ process.once("SIGINT", () => void shutdown("SIGINT"));
435
+ process.once("SIGTERM", () => void shutdown("SIGTERM"));
436
+ }
226
437
  function processFile(source, filePath, pluginConfig, logger) {
227
438
  const resolvedFilePath = path2.resolve(filePath);
228
- const directoryPath = path2.dirname(resolvedFilePath);
229
- let subscription = subscriptionsByDir.get(directoryPath);
230
- if (true) {
231
- watchedFiles.set(resolvedFilePath, { pluginConfig, logger });
232
- if (!subscription) {
233
- subscription = watcher.subscribe(directoryPath, async (err, events) => {
234
- if (err) {
235
- logger.error(`failed to update types from ${directoryPath}`, err);
236
- return;
237
- }
238
- for (const event of events) {
239
- const eventFilePath = path2.resolve(event.path);
240
- if (!watchedFiles.has(eventFilePath))
241
- continue;
242
- try {
243
- await rebuildFile(eventFilePath, event.type);
244
- } catch (watchErr) {
245
- logger.error(`failed to update types from ${eventFilePath}`, watchErr);
246
- }
247
- }
248
- });
249
- subscriptionsByDir.set(directoryPath, subscription);
250
- }
439
+ if (!source.includes("cxx") || /[\\/]node_modules[\\/]/.test(resolvedFilePath)) {
440
+ return { template: source, tags: new Map, map: null };
251
441
  }
252
- const result = collect(source, filePath, pluginConfig);
253
- if (result.tags.size > 0) {
254
- tagsByFile.set(filePath, result.tags);
255
- } else {
256
- tagsByFile.delete(filePath);
442
+ const priorError = takeRebuildError();
443
+ if (priorError)
444
+ throw priorError;
445
+ if (process.env["NODE_ENV"] !== "production") {
446
+ watchedFiles.set(resolvedFilePath, { pluginConfig, logger });
447
+ ensureWatcher(pluginConfig, logger);
257
448
  }
258
- writeTypes(flattenTags(tagsByFile));
449
+ const result = collect(source, resolvedFilePath, pluginConfig);
450
+ writeTypesForFile(resolvedFilePath, result.tags);
259
451
  return result;
260
452
  }
261
- async function cleanup() {
262
- await Promise.all([...subscriptionsByDir].map(async ([directoryPath, subPromise]) => {
263
- subscriptionsByDir.delete(directoryPath);
264
- await (await subPromise).unsubscribe();
265
- }));
266
- subscriptionsByDir.clear();
267
- }
268
- process.once("SIGINT", () => {
269
- cleanup().finally(() => process.exit(130));
270
- });
271
- process.once("SIGTERM", () => {
272
- cleanup().finally(() => process.exit(143));
273
- });
274
- async function withCxx(nextConfig = {}, pluginConfig = {}) {
275
- const TURBOPACK_GLOB = "*.{tsx,jsx,ts,js}";
453
+ var TURBOPACK_GLOB = "*.{tsx,jsx,ts,js}";
454
+ function withCxx(nextConfig = {}, pluginConfig = {}) {
455
+ const watchRoot = pluginConfig.watch?.root ?? nextConfig.turbopack?.root ?? process.cwd();
276
456
  const loaderItem = {
277
457
  loader,
278
458
  options: {
279
- pluginConfig
459
+ pluginConfig: {
460
+ ...pluginConfig,
461
+ watch: { ...pluginConfig.watch, root: watchRoot }
462
+ }
280
463
  }
281
464
  };
282
465
  const currentRule = nextConfig.turbopack?.rules?.[TURBOPACK_GLOB];
466
+ const userWebpack = nextConfig.webpack;
283
467
  return {
284
468
  ...nextConfig,
285
469
  turbopack: {
@@ -291,12 +475,24 @@ async function withCxx(nextConfig = {}, pluginConfig = {}) {
291
475
  loaders: [...currentRule.loaders ?? [], loaderItem]
292
476
  } : { loaders: [loaderItem] }
293
477
  }
478
+ },
479
+ webpack(config, context) {
480
+ const next = userWebpack ? userWebpack(config, context) : config;
481
+ next.module ??= {};
482
+ next.module.rules ??= [];
483
+ next.module.rules.push({
484
+ test: /\.[jt]sx?$/,
485
+ exclude: /node_modules/,
486
+ enforce: "pre",
487
+ use: [loaderItem]
488
+ });
489
+ return next;
294
490
  }
295
491
  };
296
492
  }
297
493
  export {
298
- withCxx,
299
- processFile
494
+ processFile,
495
+ withCxx
300
496
  };
301
497
 
302
- //# debugId=B9F61A8763996E6064756E2164756E21
498
+ //# debugId=89C0BE13A5C5F77E64756E2164756E21