@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,124 +293,130 @@ 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/next/index.ts
186
304
  import fs2 from "node:fs/promises";
187
305
  import path2 from "node:path";
188
306
  import { fileURLToPath } from "node:url";
189
- import watcher from "@parcel/watcher";
190
307
  var nextDir = path2.dirname(fileURLToPath(import.meta.url));
191
308
  var loader = path2.join(nextDir, "cxx-loader.cjs");
192
- var tagsByFile = new Map;
193
309
  var watchedFiles = new Map;
194
- var subscriptionsByDir = new Map;
195
- async function rebuildFile(filePath, event) {
196
- const watchedFile = watchedFiles.get(filePath);
197
- if (!watchedFile)
310
+ var subscription = null;
311
+ var subscriptionPromise = null;
312
+ var cleanupRegistered = false;
313
+ var watcherLogger = null;
314
+ var DEFAULT_IGNORE = [
315
+ "**/node_modules/**",
316
+ "**/.next/**",
317
+ `**/${GENERATED_DIR}/**`,
318
+ "**/.git/**"
319
+ ];
320
+ async function onEvent(event, filePath) {
321
+ const watched = watchedFiles.get(filePath);
322
+ if (!watched)
198
323
  return;
199
- const { pluginConfig, logger } = watchedFile;
200
- const hadTags = tagsByFile.has(filePath);
324
+ const { pluginConfig, logger } = watched;
201
325
  if (event === "delete") {
202
- tagsByFile.delete(filePath);
203
- if (!hadTags)
204
- return;
205
- await writeTypes(flattenTags(tagsByFile));
326
+ await writeTypesForFile(filePath, new Map);
206
327
  logger.info(`updated types from ${filePath}`);
207
328
  return;
208
329
  }
330
+ let source;
209
331
  try {
210
- const source = await fs2.readFile(filePath, "utf-8");
211
- const { tags } = collect(source, filePath, pluginConfig);
212
- const hasTags = tags.size > 0;
213
- if (hasTags) {
214
- tagsByFile.set(filePath, tags);
215
- } else {
216
- tagsByFile.delete(filePath);
217
- }
218
- if (!hadTags && !hasTags)
219
- return;
220
- await writeTypes(flattenTags(tagsByFile));
221
- logger.info(`updated types from ${filePath}`);
332
+ source = await fs2.readFile(filePath, "utf-8");
222
333
  } catch (err) {
223
334
  if (!isENOENT(err))
224
335
  throw err;
225
- if (!hadTags)
226
- return;
227
- tagsByFile.delete(filePath);
228
- await writeTypes(flattenTags(tagsByFile));
336
+ await writeTypesForFile(filePath, new Map);
229
337
  logger.info(`updated types from ${filePath}`);
338
+ return;
339
+ }
340
+ const { tags } = collect(source, filePath, pluginConfig);
341
+ await writeTypesForFile(filePath, tags);
342
+ logger.info(`updated types from ${filePath}`);
343
+ }
344
+ function ensureWatcher(pluginConfig, logger) {
345
+ watcherLogger = logger;
346
+ if (subscriptionPromise)
347
+ return;
348
+ const root = pluginConfig.watch?.root ?? process.cwd();
349
+ const ignore = [...DEFAULT_IGNORE, ...pluginConfig.watch?.ignore ?? []];
350
+ subscriptionPromise = (async () => {
351
+ const { subscribe } = await import("@parcel/watcher");
352
+ subscription = await subscribe(root, (watchErr, events) => {
353
+ if (watchErr) {
354
+ watcherLogger?.error(`failed to watch ${root}`, watchErr);
355
+ return;
356
+ }
357
+ for (const event of events) {
358
+ const filePath = path2.resolve(event.path);
359
+ if (!watchedFiles.has(filePath))
360
+ continue;
361
+ onEvent(event.type, filePath).catch((err) => {
362
+ watcherLogger?.error(`failed to update types from ${filePath}`, err);
363
+ });
364
+ }
365
+ }, { ignore });
366
+ registerCleanup();
367
+ })().catch((err) => {
368
+ watcherLogger?.error("failed to start cxx watcher", err);
369
+ subscriptionPromise = null;
370
+ });
371
+ }
372
+ async function cleanup() {
373
+ const active = subscription;
374
+ subscription = null;
375
+ subscriptionPromise = null;
376
+ watchedFiles.clear();
377
+ if (active)
378
+ await active.unsubscribe();
379
+ }
380
+ async function shutdown(signal) {
381
+ try {
382
+ await cleanup();
383
+ } finally {
384
+ process.kill(process.pid, signal);
230
385
  }
231
386
  }
387
+ function registerCleanup() {
388
+ if (cleanupRegistered)
389
+ return;
390
+ cleanupRegistered = true;
391
+ process.once("SIGINT", () => void shutdown("SIGINT"));
392
+ process.once("SIGTERM", () => void shutdown("SIGTERM"));
393
+ }
232
394
  function processFile(source, filePath, pluginConfig, logger) {
233
395
  const resolvedFilePath = path2.resolve(filePath);
234
- const directoryPath = path2.dirname(resolvedFilePath);
235
- let subscription = subscriptionsByDir.get(directoryPath);
236
- if (true) {
396
+ if (process.env["NODE_ENV"] !== "production") {
237
397
  watchedFiles.set(resolvedFilePath, { pluginConfig, logger });
238
- if (!subscription) {
239
- subscription = watcher.subscribe(directoryPath, async (err, events) => {
240
- if (err) {
241
- logger.error(`failed to update types from ${directoryPath}`, err);
242
- return;
243
- }
244
- for (const event of events) {
245
- const eventFilePath = path2.resolve(event.path);
246
- if (!watchedFiles.has(eventFilePath))
247
- continue;
248
- try {
249
- await rebuildFile(eventFilePath, event.type);
250
- } catch (watchErr) {
251
- logger.error(`failed to update types from ${eventFilePath}`, watchErr);
252
- }
253
- }
254
- });
255
- subscriptionsByDir.set(directoryPath, subscription);
256
- }
257
- }
258
- const result = collect(source, filePath, pluginConfig);
259
- if (result.tags.size > 0) {
260
- tagsByFile.set(filePath, result.tags);
261
- } else {
262
- tagsByFile.delete(filePath);
398
+ ensureWatcher(pluginConfig, logger);
263
399
  }
264
- writeTypes(flattenTags(tagsByFile));
400
+ const result = collect(source, resolvedFilePath, pluginConfig);
401
+ writeTypesForFile(resolvedFilePath, result.tags).catch((err) => {
402
+ logger.error(`failed to write types for ${resolvedFilePath}`, err);
403
+ });
265
404
  return result;
266
405
  }
267
- async function cleanup() {
268
- await Promise.all([...subscriptionsByDir].map(async ([directoryPath, subPromise]) => {
269
- subscriptionsByDir.delete(directoryPath);
270
- await (await subPromise).unsubscribe();
271
- }));
272
- subscriptionsByDir.clear();
273
- }
274
- process.once("SIGINT", () => {
275
- cleanup().finally(() => process.exit(130));
276
- });
277
- process.once("SIGTERM", () => {
278
- cleanup().finally(() => process.exit(143));
279
- });
280
- async function withCxx(nextConfig = {}, pluginConfig = {}) {
281
- const TURBOPACK_GLOB = "*.{tsx,jsx,ts,js}";
406
+ var TURBOPACK_GLOB = "*.{tsx,jsx,ts,js}";
407
+ function withCxx(nextConfig = {}, pluginConfig = {}) {
408
+ const watchRoot = pluginConfig.watch?.root ?? nextConfig.turbopack?.root ?? process.cwd();
282
409
  const loaderItem = {
283
410
  loader,
284
411
  options: {
285
- pluginConfig
412
+ pluginConfig: {
413
+ ...pluginConfig,
414
+ watch: { ...pluginConfig.watch, root: watchRoot }
415
+ }
286
416
  }
287
417
  };
288
418
  const currentRule = nextConfig.turbopack?.rules?.[TURBOPACK_GLOB];
419
+ const userWebpack = nextConfig.webpack;
289
420
  return {
290
421
  ...nextConfig,
291
422
  turbopack: {
@@ -297,12 +428,24 @@ async function withCxx(nextConfig = {}, pluginConfig = {}) {
297
428
  loaders: [...currentRule.loaders ?? [], loaderItem]
298
429
  } : { loaders: [loaderItem] }
299
430
  }
431
+ },
432
+ webpack(config, context) {
433
+ const next = userWebpack ? userWebpack(config, context) : config;
434
+ next.module ??= {};
435
+ next.module.rules ??= [];
436
+ next.module.rules.push({
437
+ test: /\.[jt]sx?$/,
438
+ exclude: /node_modules/,
439
+ enforce: "pre",
440
+ use: [loaderItem]
441
+ });
442
+ return next;
300
443
  }
301
444
  };
302
445
  }
303
446
  export {
304
- withCxx,
305
- processFile
447
+ processFile,
448
+ withCxx
306
449
  };
307
450
 
308
- //# debugId=9A17D8885429CB9164756E2164756E21
451
+ //# debugId=A4FD970DD20B088B64756E2164756E21