@zealakit/css-doctor-next 0.0.0-stage → 1.0.0

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.
package/dist/index.cjs ADDED
@@ -0,0 +1,1318 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ findCssDoctorConfig: () => findCssDoctorConfig,
34
+ loadCssDoctorConfig: () => loadCssDoctorConfig,
35
+ withCssDoctor: () => withCssDoctor
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/config.ts
40
+ var import_node_fs = require("fs");
41
+ var import_node_path = require("path");
42
+ var import_jiti = require("jiti");
43
+ var import_meta = {};
44
+ var CONFIG_FILENAMES = [
45
+ "cssd.config.ts",
46
+ "cssd.config.mts",
47
+ "cssd.config.js",
48
+ "cssd.config.mjs"
49
+ ];
50
+ function findCssDoctorConfig(cwd = process.cwd()) {
51
+ let directory = (0, import_node_path.resolve)(cwd);
52
+ while (true) {
53
+ for (const filename of CONFIG_FILENAMES) {
54
+ const candidate = (0, import_node_path.resolve)(directory, filename);
55
+ if ((0, import_node_fs.existsSync)(candidate)) {
56
+ return candidate;
57
+ }
58
+ }
59
+ const parent = (0, import_node_path.dirname)(directory);
60
+ if (parent === directory) {
61
+ return null;
62
+ }
63
+ directory = parent;
64
+ }
65
+ }
66
+ function loadCssDoctorConfig(options = {}) {
67
+ const cwd = (0, import_node_path.resolve)(options.cwd ?? process.cwd());
68
+ const file = options.configFile ? (0, import_node_path.resolve)(cwd, options.configFile) : findCssDoctorConfig(cwd);
69
+ if (!file) {
70
+ return { options: {} };
71
+ }
72
+ if (!(0, import_node_fs.existsSync)(file)) {
73
+ return { options: {} };
74
+ }
75
+ try {
76
+ if ((0, import_node_fs.readFileSync)(file, "utf8").trim().length === 0) {
77
+ return { options: {}, file };
78
+ }
79
+ const jiti = (0, import_jiti.createJiti)(import_meta.url, {
80
+ interopDefault: true,
81
+ fsCache: false,
82
+ moduleCache: false
83
+ });
84
+ const loaded = jiti(file);
85
+ return {
86
+ options: normalizeConfig(loaded),
87
+ file
88
+ };
89
+ } catch (error) {
90
+ return {
91
+ options: {},
92
+ file,
93
+ error: `failed to load ${file}: ` + (error instanceof Error ? error.message : String(error))
94
+ };
95
+ }
96
+ }
97
+ function normalizeConfig(value) {
98
+ let candidate = value;
99
+ if (isRecord(candidate) && "default" in candidate) {
100
+ candidate = candidate.default;
101
+ }
102
+ if (isRecord(candidate) && "cssDoctor" in candidate) {
103
+ candidate = candidate.cssDoctor;
104
+ }
105
+ return isRecord(candidate) ? candidate : {};
106
+ }
107
+ function isRecord(value) {
108
+ return typeof value === "object" && value !== null && !Array.isArray(value);
109
+ }
110
+
111
+ // src/webpack-plugin.ts
112
+ var import_node_crypto = require("crypto");
113
+ var import_node_fs2 = require("fs");
114
+ var import_promises = require("fs/promises");
115
+ var import_node_path2 = require("path");
116
+ var import_node_perf_hooks = require("perf_hooks");
117
+ var import_css_doctor_binding = require("@zealakit/css-doctor-binding");
118
+
119
+ // src/reporter.ts
120
+ var import_picocolors = __toESM(require("picocolors"), 1);
121
+ var MATRIX_SELECTOR_MAX = 36;
122
+ function formatDiagnostics(diagnostics, options = {}) {
123
+ const colors = import_picocolors.default.createColors(
124
+ options.color ?? import_picocolors.default.isColorSupported
125
+ );
126
+ const badge = colors.bgCyan(colors.black("[css-doctor]"));
127
+ const lines = [];
128
+ lines.push(
129
+ `${badge} Next.js ${diagnostics.pages.length} page${diagnostics.pages.length === 1 ? "" : "s"} ${colors.dim("\xB7")} ${diagnostics.analyzedCssAssets}/${diagnostics.totalCssAssets} CSS assets ${colors.dim("\xB7")} ${formatBytes(
130
+ diagnostics.totalSizeBeforeBytes
131
+ )} \u2192 ${formatBytes(diagnostics.totalSizeAfterBytes)} common ${colors.dim("\xB7")} ${formatDuration(diagnostics.durationMs)} ${colors.dim("\xB7")} ${diagnostics.minify ? colors.green("Minified: YES") : colors.yellow("Minified: NO")}`
132
+ );
133
+ for (const file of diagnostics.files) {
134
+ lines.push(...formatFile(file, colors));
135
+ }
136
+ if (diagnostics.splitApplied) {
137
+ lines.push("");
138
+ lines.push(...formatMatrix(diagnostics, colors));
139
+ }
140
+ if (diagnostics.emittedPageChunks.length > 0) {
141
+ lines.push("");
142
+ lines.push(
143
+ ` ${colors.bold("Emitted page chunks")} ${colors.dim(
144
+ `(${diagnostics.emittedPageChunks.length})`
145
+ )}`
146
+ );
147
+ for (const chunk of diagnostics.emittedPageChunks) {
148
+ lines.push(
149
+ ` ${colors.magenta(chunk.fileName)} ${colors.dim(
150
+ formatBytes(chunk.sizeBytes)
151
+ )}`
152
+ );
153
+ }
154
+ }
155
+ if (diagnostics.sizeBudget.enabled) {
156
+ lines.push("");
157
+ lines.push(...formatSizeBudget(diagnostics, colors));
158
+ }
159
+ if (diagnostics.deadCodeCount > 0) {
160
+ lines.push("");
161
+ lines.push(
162
+ ` ${colors.yellow("\u26A0")} ${colors.bold("Dead code eliminated:")} ${diagnostics.deadCodeCount} selector occurrence${diagnostics.deadCodeCount === 1 ? "" : "s"}`
163
+ );
164
+ }
165
+ if (diagnostics.errors.length > 0) {
166
+ lines.push("");
167
+ lines.push(` ${colors.red(colors.bold("Errors"))}`);
168
+ for (const error of diagnostics.errors) {
169
+ lines.push(` ${colors.red("\u2716")} ${error}`);
170
+ }
171
+ }
172
+ return lines.join("\n");
173
+ }
174
+ function printDiagnostics(diagnostics) {
175
+ console.log(formatDiagnostics(diagnostics));
176
+ }
177
+ function formatFile(file, colors) {
178
+ const failed = file.error !== void 0;
179
+ const hasWarnings = file.warnings.length > 0;
180
+ const icon = failed ? colors.red("\u2716") : hasWarnings ? colors.yellow("\u26A0") : colors.green("\u2713");
181
+ const size = `${formatBytes(file.sizeBeforeBytes)} \u2192 ${formatBytes(
182
+ file.sizeAfterBytes
183
+ )}`;
184
+ const selectors = failed ? "" : ` \xB7 ${file.selectorCount} selectors`;
185
+ const pages = file.associatedPages.length > 0 ? ` \xB7 pages: ${file.associatedPages.join(", ")}` : "";
186
+ const lines = [
187
+ ` ${icon} ${colors.bold(file.fileName)} ${colors.dim(size)}${selectors}${colors.dim(pages)}`
188
+ ];
189
+ if (file.error) {
190
+ lines.push(` ${colors.red(file.error)}`);
191
+ return lines;
192
+ }
193
+ if (file.split) {
194
+ lines.push(
195
+ ` ${colors.dim("split")} common ${formatBytes(
196
+ file.split.commonSizeBytes
197
+ )} ${colors.dim("\xB7")} page-specific ${formatBytes(
198
+ file.split.pageSpecificSizeBytes
199
+ )} ${colors.dim("\xB7")} dead ${file.split.deadCodeCount}`
200
+ );
201
+ }
202
+ for (const warning of file.warnings) {
203
+ lines.push(` ${formatWarning(warning, colors)}`);
204
+ }
205
+ return lines;
206
+ }
207
+ function formatMatrix(diagnostics, colors) {
208
+ const pageTotal = diagnostics.pages.length;
209
+ const rows = diagnostics.usageMatrix;
210
+ const header = [
211
+ "Selector".padEnd(MATRIX_SELECTOR_MAX),
212
+ "Total Used".padStart(10),
213
+ "Page Usage".padStart(10),
214
+ "Coverage %".padStart(10),
215
+ "Assigned Chunk"
216
+ ].join(" ");
217
+ const lines = [
218
+ ` ${colors.bold("Usage Matrix")} ${colors.dim(
219
+ `(${rows.length} selectors \xB7 ${pageTotal} pages)`
220
+ )}`,
221
+ ` ${colors.dim(header)}`
222
+ ];
223
+ for (const row of rows) {
224
+ const selector = truncate(row.selector, MATRIX_SELECTOR_MAX).padEnd(
225
+ MATRIX_SELECTOR_MAX
226
+ );
227
+ const totalUsed = String(row.totalOccurrences).padStart(10);
228
+ const pageUsage = `${row.pageCount}/${pageTotal}`.padStart(10);
229
+ const coverage = `${(row.coverageRatio * 100).toFixed(1)}%`.padStart(10);
230
+ const chunk = formatChunk(row.targetChunk, colors);
231
+ lines.push(
232
+ ` ${colors.cyan(selector)} ${totalUsed} ${pageUsage} ${colorCoverage(
233
+ coverage,
234
+ row.coverageRatio,
235
+ diagnostics.minPageRatio,
236
+ colors
237
+ )} ${chunk}`
238
+ );
239
+ }
240
+ return lines;
241
+ }
242
+ function formatSizeBudget(diagnostics, colors) {
243
+ const budget = diagnostics.sizeBudget;
244
+ const lines = [
245
+ ` ${colors.bold("Size budget")} ${colors.dim(
246
+ `(max ${formatBytes(budget.maxChunkSize)} \xB7 variables ${budget.isolateVariables ? "isolated" : "inline"})`
247
+ )}`
248
+ ];
249
+ const chunks = diagnostics.emittedCssChunks;
250
+ if (chunks.length === 0) {
251
+ lines.push(` ${colors.dim("No CSS chunks were emitted.")}`);
252
+ return lines;
253
+ }
254
+ for (const chunk of chunks) {
255
+ const markers = [];
256
+ if (chunk.variables) {
257
+ markers.push("vars");
258
+ }
259
+ if (chunk.part !== void 0) {
260
+ markers.push(`part ${chunk.part}`);
261
+ }
262
+ if (chunk.embedded) {
263
+ markers.push("embedded");
264
+ }
265
+ if (chunk.splitDueToSize) {
266
+ markers.push(colors.yellow("split"));
267
+ }
268
+ if (chunk.oversized) {
269
+ markers.push(colors.red("oversized"));
270
+ }
271
+ lines.push(
272
+ ` ${colors.magenta(chunk.fileName)} ${colors.dim(
273
+ formatBytes(chunk.sizeBytes)
274
+ )}${markers.length > 0 ? ` ${colors.dim("\xB7")} ${markers.join(" \xB7 ")}` : ""}`
275
+ );
276
+ }
277
+ if (budget.splitChunks === 0) {
278
+ lines.push(
279
+ ` ${colors.dim("No chunk exceeded the configured size budget.")}`
280
+ );
281
+ }
282
+ return lines;
283
+ }
284
+ function formatWarning(warning, colors) {
285
+ const location = warning.line !== void 0 ? colors.dim(
286
+ `${warning.line}${warning.column !== void 0 ? `:${warning.column}` : ""} `
287
+ ) : "";
288
+ return `${colors.yellow("!")} ${location}${warning.message}`;
289
+ }
290
+ function colorCoverage(coverage, ratio, minPageRatio, colors) {
291
+ if (ratio === 0) {
292
+ return colors.red(coverage);
293
+ }
294
+ return ratio >= minPageRatio ? colors.green(coverage) : colors.yellow(coverage);
295
+ }
296
+ function formatChunk(chunk, colors) {
297
+ if (chunk === "common") {
298
+ return colors.cyan(chunk);
299
+ }
300
+ if (chunk === "dead") {
301
+ return colors.red(chunk);
302
+ }
303
+ return colors.magenta(chunk);
304
+ }
305
+ function truncate(value, maxLength) {
306
+ return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}\u2026`;
307
+ }
308
+ function formatBytes(bytes) {
309
+ if (bytes < 1024) {
310
+ return `${bytes} B`;
311
+ }
312
+ if (bytes < 1024 * 1024) {
313
+ return `${(bytes / 1024).toFixed(1)} KB`;
314
+ }
315
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
316
+ }
317
+ function formatDuration(milliseconds) {
318
+ if (!Number.isFinite(milliseconds)) {
319
+ return "\u2014";
320
+ }
321
+ return milliseconds < 1 ? `${milliseconds.toFixed(2)} ms` : `${milliseconds.toFixed(1)} ms`;
322
+ }
323
+
324
+ // src/webpack-plugin.ts
325
+ var PLUGIN_NAME = "CssDoctorWebpackPlugin";
326
+ var DEFAULT_MIN_PAGE_RATIO = 0.5;
327
+ var DEFAULT_MAX_CHUNK_SIZE = 60 * 1024;
328
+ var LARGE_CSS_ASSET_BYTES = 500 * 1024;
329
+ var CssDoctorWebpackPlugin = class {
330
+ options;
331
+ pageCoverage;
332
+ sizeBudget;
333
+ minify;
334
+ include;
335
+ exclude;
336
+ configurationErrors = [];
337
+ constructor(options = {}) {
338
+ this.options = options;
339
+ const ratio = options.pageCoverage?.minPageRatio ?? options.minPageRatio ?? DEFAULT_MIN_PAGE_RATIO;
340
+ const maxChunkSize = options.sizeBudget?.maxChunkSize ?? DEFAULT_MAX_CHUNK_SIZE;
341
+ this.pageCoverage = {
342
+ enabled: options.pageCoverage?.enabled ?? true,
343
+ minPageRatio: Number.isFinite(ratio) && ratio >= 0 && ratio <= 1 ? ratio : DEFAULT_MIN_PAGE_RATIO,
344
+ forceCommon: options.pageCoverage?.forceCommon ?? options.forceCommonSelectors ?? []
345
+ };
346
+ if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1) {
347
+ this.configurationErrors.push(
348
+ "minPageRatio must be a number between 0 and 1; using 0.5"
349
+ );
350
+ }
351
+ this.sizeBudget = {
352
+ enabled: options.sizeBudget?.enabled ?? true,
353
+ maxChunkSize: Number.isInteger(maxChunkSize) && maxChunkSize > 0 ? maxChunkSize : DEFAULT_MAX_CHUNK_SIZE,
354
+ isolateVariables: options.sizeBudget?.isolateVariables ?? true
355
+ };
356
+ if (options.sizeBudget?.enabled !== false && (!Number.isInteger(maxChunkSize) || maxChunkSize <= 0)) {
357
+ this.configurationErrors.push(
358
+ "maxChunkSize must be a positive integer; using 61440"
359
+ );
360
+ }
361
+ this.minify = options.minify ?? true;
362
+ this.include = options.include ?? [];
363
+ this.exclude = options.exclude ?? [];
364
+ }
365
+ apply(compiler) {
366
+ compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
367
+ compilation.hooks.processAssets.tapPromise(
368
+ {
369
+ name: PLUGIN_NAME,
370
+ stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE
371
+ },
372
+ async () => {
373
+ try {
374
+ await this.process(compiler, compilation);
375
+ } catch (error) {
376
+ compilation.getLogger(PLUGIN_NAME).warn(
377
+ `[css-doctor] diagnostics failed without interrupting the build: ${describeError(
378
+ error
379
+ )}`
380
+ );
381
+ }
382
+ }
383
+ );
384
+ });
385
+ }
386
+ async process(compiler, compilation) {
387
+ const startedAt = import_node_perf_hooks.performance.now();
388
+ const logger = compilation.getLogger(PLUGIN_NAME);
389
+ const pagesResult = await collectPages(compilation, this.options.pageUsage);
390
+ const pageNames = pagesResult.pages.map((page) => page.name);
391
+ const pageByName = new Map(
392
+ pagesResult.pages.map((page) => [page.name, page])
393
+ );
394
+ const allPages = new Set(pageNames);
395
+ const pageUsage = {};
396
+ const pageUsageCounts = {};
397
+ for (const page of pagesResult.pages) {
398
+ const classes = [...page.classes].sort();
399
+ pageUsage[page.name] = classes;
400
+ pageUsageCounts[page.name] = classes.length;
401
+ }
402
+ const pageCoverageApplied = this.pageCoverage.enabled && pageNames.length > 0;
403
+ const diagnostics = {
404
+ plugin: "css-doctor-next",
405
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
406
+ durationMs: 0,
407
+ minPageRatio: this.pageCoverage.minPageRatio,
408
+ minify: this.minify,
409
+ pageCoverage: {
410
+ enabled: this.pageCoverage.enabled,
411
+ minPageRatio: this.pageCoverage.minPageRatio
412
+ },
413
+ sizeBudget: {
414
+ enabled: this.sizeBudget.enabled,
415
+ maxChunkSize: this.sizeBudget.maxChunkSize,
416
+ isolateVariables: this.sizeBudget.isolateVariables,
417
+ splitChunks: 0,
418
+ oversizedChunks: 0
419
+ },
420
+ pages: pageNames,
421
+ pageUsage: pageUsageCounts,
422
+ splitApplied: pageCoverageApplied,
423
+ totalCssAssets: 0,
424
+ analyzedCssAssets: 0,
425
+ failedCssAssets: 0,
426
+ skippedCssAssets: 0,
427
+ totalSizeBeforeBytes: 0,
428
+ totalSizeAfterBytes: 0,
429
+ totalSelectors: 0,
430
+ totalRules: 0,
431
+ commonCssBytes: 0,
432
+ pageSpecificCssBytes: 0,
433
+ emittedPageChunks: [],
434
+ emittedCssChunks: [],
435
+ usageMatrix: [],
436
+ deadCodeCount: 0,
437
+ files: [],
438
+ errors: []
439
+ };
440
+ diagnostics.errors.push(...this.configurationErrors);
441
+ const matrix = /* @__PURE__ */ new Map();
442
+ const pageFiles = /* @__PURE__ */ new Map();
443
+ const usedAssetNames = new Set(
444
+ compilation.getAssets().map((asset) => asset.name)
445
+ );
446
+ const RawSource = compiler.webpack.sources.RawSource;
447
+ const emitCssChunk = (chunk) => {
448
+ const hash = (0, import_node_crypto.createHash)("sha256").update(chunk.css).digest("hex").slice(0, 8);
449
+ const safeName = chunk.name.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "chunk";
450
+ let fileName = `static/css/${safeName}.${hash}.css`;
451
+ let index = 2;
452
+ while (usedAssetNames.has(fileName)) {
453
+ fileName = `static/css/${safeName}.${hash}-${index}.css`;
454
+ index += 1;
455
+ }
456
+ compilation.emitAsset(fileName, new RawSource(chunk.css));
457
+ usedAssetNames.add(fileName);
458
+ return fileName;
459
+ };
460
+ for (const asset of compilation.getAssets()) {
461
+ if (!asset.name.endsWith(".css")) {
462
+ continue;
463
+ }
464
+ diagnostics.totalCssAssets += 1;
465
+ if (this.shouldSkipAsset(
466
+ compilation,
467
+ asset.name,
468
+ sourceToString(asset.source.source())
469
+ )) {
470
+ diagnostics.skippedCssAssets += 1;
471
+ continue;
472
+ }
473
+ const associatedPages = [
474
+ ...pagesResult.pagesByCss.get(asset.name) ?? allPages
475
+ ].sort();
476
+ const file = analyzeAsset(asset.name, asset.source.source());
477
+ file.associatedPages = associatedPages;
478
+ if (file.error) {
479
+ diagnostics.failedCssAssets += 1;
480
+ diagnostics.errors.push(`${asset.name}: ${file.error}`);
481
+ diagnostics.files.push(file);
482
+ logger.warn(`[css-doctor] ${asset.name}: ${file.error}`);
483
+ continue;
484
+ }
485
+ const originalCss = sourceToString(asset.source.source());
486
+ try {
487
+ const pipeline = (0, import_css_doctor_binding.optimizeCss)({
488
+ cssContent: originalCss,
489
+ pageUsage,
490
+ pageCoverage: {
491
+ enabled: this.pageCoverage.enabled,
492
+ minPageRatio: this.pageCoverage.minPageRatio,
493
+ forceCommon: this.pageCoverage.forceCommon
494
+ },
495
+ sizeBudget: {
496
+ enabled: this.sizeBudget.enabled,
497
+ maxChunkSize: this.sizeBudget.maxChunkSize,
498
+ isolateVariables: this.sizeBudget.isolateVariables
499
+ },
500
+ minify: this.minify
501
+ });
502
+ const commonChunks = pipeline.chunks.filter(
503
+ (chunk) => chunk.page === void 0 && chunk.sourceChunk === "common"
504
+ );
505
+ const pageChunks = pipeline.chunks.filter(
506
+ (chunk) => chunk.page !== void 0
507
+ );
508
+ const commonWasSplit = commonChunks.length > 1 || commonChunks.some((chunk) => chunk.splitDueToSize);
509
+ const embeddedCommon = commonWasSplit ? void 0 : commonChunks[0];
510
+ const commonFileNames = [];
511
+ let commonSizeBytes = 0;
512
+ for (const chunk of commonChunks) {
513
+ const embedded = chunk === embeddedCommon;
514
+ const fileName = embedded ? asset.name : emitCssChunk(chunk);
515
+ commonSizeBytes += chunk.sizeBytes;
516
+ commonFileNames.push(fileName);
517
+ diagnostics.emittedCssChunks.push({
518
+ fileName,
519
+ sourceChunk: chunk.sourceChunk,
520
+ kind: chunk.kind,
521
+ page: chunk.page,
522
+ variables: chunk.variables,
523
+ part: chunk.part,
524
+ sizeBytes: chunk.sizeBytes,
525
+ oversized: chunk.oversized,
526
+ splitDueToSize: chunk.splitDueToSize,
527
+ embedded
528
+ });
529
+ if (chunk.splitDueToSize) {
530
+ diagnostics.sizeBudget.splitChunks += 1;
531
+ }
532
+ if (chunk.oversized) {
533
+ diagnostics.sizeBudget.oversizedChunks += 1;
534
+ }
535
+ }
536
+ if (commonWasSplit) {
537
+ const imports = commonFileNames.map((fileName) => {
538
+ const relativePath = import_node_path2.posix.relative(
539
+ import_node_path2.posix.dirname(asset.name),
540
+ fileName
541
+ );
542
+ return `@import "./${relativePath}";`;
543
+ }).join("\n");
544
+ compilation.updateAsset(asset.name, new RawSource(imports));
545
+ } else {
546
+ compilation.updateAsset(
547
+ asset.name,
548
+ new RawSource(embeddedCommon?.css ?? "")
549
+ );
550
+ }
551
+ let pageSpecificSizeBytes = 0;
552
+ for (const chunk of pageChunks) {
553
+ const page = chunk.page;
554
+ if (!page) {
555
+ continue;
556
+ }
557
+ const fileName = emitCssChunk(chunk);
558
+ pageSpecificSizeBytes += chunk.sizeBytes;
559
+ const files = pageFiles.get(page) ?? [];
560
+ files.push(fileName);
561
+ pageFiles.set(page, files);
562
+ diagnostics.emittedPageChunks.push({
563
+ page,
564
+ fileName,
565
+ sizeBytes: chunk.sizeBytes
566
+ });
567
+ diagnostics.emittedCssChunks.push({
568
+ fileName,
569
+ sourceChunk: chunk.sourceChunk,
570
+ kind: chunk.kind,
571
+ page,
572
+ variables: chunk.variables,
573
+ part: chunk.part,
574
+ sizeBytes: chunk.sizeBytes,
575
+ oversized: chunk.oversized,
576
+ splitDueToSize: chunk.splitDueToSize,
577
+ embedded: false
578
+ });
579
+ if (chunk.splitDueToSize) {
580
+ diagnostics.sizeBudget.splitChunks += 1;
581
+ }
582
+ if (chunk.oversized) {
583
+ diagnostics.sizeBudget.oversizedChunks += 1;
584
+ }
585
+ }
586
+ file.sizeAfterBytes = commonWasSplit ? commonSizeBytes : Buffer.byteLength(embeddedCommon?.css ?? "", "utf8");
587
+ file.split = {
588
+ associatedPages,
589
+ commonSizeBytes,
590
+ pageSpecificSizeBytes,
591
+ deadCodeCount: pipeline.deadCodeCount
592
+ };
593
+ for (const stat of pipeline.stats) {
594
+ const previous = matrix.get(stat.selector);
595
+ if (!previous || stat.totalOccurrences > previous.totalOccurrences) {
596
+ matrix.set(stat.selector, stat);
597
+ }
598
+ }
599
+ diagnostics.deadCodeCount += pipeline.deadCodeCount;
600
+ diagnostics.commonCssBytes += commonSizeBytes;
601
+ diagnostics.pageSpecificCssBytes += pageSpecificSizeBytes;
602
+ } catch (error) {
603
+ const message = `failed to optimize CSS: ${describeError(error)}`;
604
+ diagnostics.errors.push(`${asset.name}: ${message}`);
605
+ logger.warn(`[css-doctor] ${asset.name}: ${message}`);
606
+ }
607
+ diagnostics.analyzedCssAssets += 1;
608
+ diagnostics.totalSizeBeforeBytes += file.sizeBeforeBytes;
609
+ diagnostics.totalSizeAfterBytes += file.sizeAfterBytes;
610
+ diagnostics.totalSelectors += file.selectorCount;
611
+ diagnostics.totalRules += file.ruleCount;
612
+ diagnostics.files.push(file);
613
+ }
614
+ if (pageFiles.size > 0) {
615
+ injectPageJsLoaders(compilation, pageFiles, pageByName, RawSource);
616
+ }
617
+ diagnostics.usageMatrix = [...matrix.values()].sort(
618
+ (left, right) => left.selector.localeCompare(right.selector)
619
+ );
620
+ diagnostics.durationMs = roundMs(import_node_perf_hooks.performance.now() - startedAt);
621
+ if (this.options.onDiagnostics) {
622
+ try {
623
+ await this.options.onDiagnostics(diagnostics);
624
+ } catch (error) {
625
+ const message = `onDiagnostics callback failed: ${describeError(
626
+ error
627
+ )}`;
628
+ diagnostics.errors.push(message);
629
+ logger.warn(`[css-doctor] ${message}`);
630
+ }
631
+ }
632
+ if (this.options.report !== false && (diagnostics.totalCssAssets > 0 || diagnostics.errors.length > 0)) {
633
+ try {
634
+ printDiagnostics(diagnostics);
635
+ } catch (error) {
636
+ logger.warn(`[css-doctor] reporter failed: ${describeError(error)}`);
637
+ }
638
+ }
639
+ }
640
+ shouldSkipAsset(compilation, assetName, source) {
641
+ if (isCssModuleAsset(compilation, assetName, source)) {
642
+ return true;
643
+ }
644
+ if (this.include.length > 0 && !matchesPatterns(assetName, this.include)) {
645
+ return true;
646
+ }
647
+ return this.exclude.length > 0 && matchesPatterns(assetName, this.exclude);
648
+ }
649
+ };
650
+ function injectPageJsLoaders(compilation, cssByPage, pageByName, RawSource) {
651
+ const publicPath = typeof compilation.options.output.publicPath === "string" && compilation.options.output.publicPath.length > 0 ? compilation.options.output.publicPath : "/_next/";
652
+ const normalizedBase = publicPath.endsWith("/") ? publicPath : `${publicPath}/`;
653
+ for (const [page, files] of cssByPage) {
654
+ const jsFiles = pageByName.get(page)?.jsFiles;
655
+ if (!jsFiles || files.length === 0) {
656
+ continue;
657
+ }
658
+ const hrefs = files.map((file) => `${normalizedBase}${file}`);
659
+ const snippet = `(()=>{const __cssDoctorPageStyles=${JSON.stringify(hrefs)};if(typeof document!=="undefined"){for(const __cssDoctorPageHref of __cssDoctorPageStyles){if(!Array.from(document.querySelectorAll("link[data-css-doctor-page]")).some((link)=>link.dataset.cssDoctorPage===__cssDoctorPageHref)){const __cssDoctorPageLink=document.createElement("link");__cssDoctorPageLink.rel="stylesheet";__cssDoctorPageLink.href=__cssDoctorPageHref;__cssDoctorPageLink.dataset.cssDoctorPage=__cssDoctorPageHref;document.head.append(__cssDoctorPageLink);}}}})();
660
+ `;
661
+ for (const jsFile of jsFiles) {
662
+ const asset = compilation.getAsset(jsFile);
663
+ if (!asset) {
664
+ continue;
665
+ }
666
+ const source = sourceToString(asset.source.source());
667
+ compilation.updateAsset(jsFile, new RawSource(snippet + source));
668
+ }
669
+ }
670
+ }
671
+ function matchesPatterns(value, patterns) {
672
+ return patterns.some(
673
+ (pattern) => typeof pattern === "string" ? value.includes(pattern) : pattern.test(value)
674
+ );
675
+ }
676
+ function isCssModuleAsset(compilation, assetName, source) {
677
+ if (/\.module\.(css|scss|sass|less|styl|stylus)$/i.test(assetName)) {
678
+ return true;
679
+ }
680
+ if (/\.[A-Za-z0-9_-]+_[A-Za-z0-9_-]+__[A-Za-z0-9_-]+/.test(source)) {
681
+ return true;
682
+ }
683
+ for (const chunk of compilation.chunks) {
684
+ if (!chunk.files.has(assetName) && !chunk.auxiliaryFiles.has(assetName)) {
685
+ continue;
686
+ }
687
+ for (const module2 of compilation.chunkGraph.getChunkModulesIterable(
688
+ chunk
689
+ )) {
690
+ if (moduleHasCssModule(module2)) {
691
+ return true;
692
+ }
693
+ }
694
+ }
695
+ return false;
696
+ }
697
+ function moduleHasCssModule(module2) {
698
+ const nested = module2.modules;
699
+ if (nested) {
700
+ for (const child of nested) {
701
+ if (moduleHasCssModule(child)) {
702
+ return true;
703
+ }
704
+ }
705
+ }
706
+ const resource = module2.resource;
707
+ return typeof resource === "string" && /\.module\.(css|scss|sass|less|styl|stylus)(\?|$)/i.test(resource);
708
+ }
709
+ function analyzeAsset(fileName, source) {
710
+ try {
711
+ const css = sourceToString(source);
712
+ const sizeBeforeBytes = Buffer.byteLength(css, "utf8");
713
+ const analysis = (0, import_css_doctor_binding.analyzeCss)(css);
714
+ const warnings = collectSyntaxWarnings(analysis);
715
+ if (sizeBeforeBytes >= LARGE_CSS_ASSET_BYTES) {
716
+ warnings.push({
717
+ kind: "large-asset",
718
+ message: `CSS asset is larger than ${formatBytes2(
719
+ LARGE_CSS_ASSET_BYTES
720
+ )}; consider route-level code splitting.`
721
+ });
722
+ }
723
+ return {
724
+ fileName,
725
+ sizeBeforeBytes,
726
+ sizeAfterBytes: sizeBeforeBytes,
727
+ selectorCount: analysis.stats.selectorCount,
728
+ ruleCount: analysis.stats.ruleCount,
729
+ associatedPages: [],
730
+ warnings,
731
+ analysis
732
+ };
733
+ } catch (error) {
734
+ return {
735
+ fileName,
736
+ sizeBeforeBytes: 0,
737
+ sizeAfterBytes: 0,
738
+ selectorCount: 0,
739
+ ruleCount: 0,
740
+ associatedPages: [],
741
+ warnings: [],
742
+ error: describeError(error)
743
+ };
744
+ }
745
+ }
746
+ async function collectPages(compilation, directPageUsage) {
747
+ const entries = [...compilation.entrypoints.values()];
748
+ const pageEntries = [];
749
+ const sharedEntries = [];
750
+ const fallbackEntries = [];
751
+ for (const entry of entries) {
752
+ const name = entryName(entry);
753
+ if (!name) {
754
+ continue;
755
+ }
756
+ if (isPageEntry(name)) {
757
+ const route = routeNameForEntry(name) ?? name;
758
+ pageEntries.push({ entry, name: route, sourceName: name });
759
+ } else if (isSharedEntry(name)) {
760
+ sharedEntries.push(entry);
761
+ } else if (!isInternalEntry(name)) {
762
+ fallbackEntries.push({
763
+ entry,
764
+ name: routeNameForEntry(name) ?? name,
765
+ sourceName: name
766
+ });
767
+ }
768
+ }
769
+ const selectedPages = pageEntries.length > 0 ? pageEntries : fallbackEntries;
770
+ const direct = directPageUsage ? Object.keys(directPageUsage).length > 0 : false;
771
+ const pages = /* @__PURE__ */ new Map();
772
+ const pagesByCss = /* @__PURE__ */ new Map();
773
+ if (direct && directPageUsage) {
774
+ for (const [rawName, classes] of Object.entries(directPageUsage)) {
775
+ const name = rawName.trim();
776
+ if (!name) {
777
+ continue;
778
+ }
779
+ pages.set(name, {
780
+ name,
781
+ classes: new Set(classes.map((className) => className.toLowerCase())),
782
+ cssFiles: /* @__PURE__ */ new Set(),
783
+ jsFiles: /* @__PURE__ */ new Set()
784
+ });
785
+ }
786
+ } else {
787
+ const sourceCache = /* @__PURE__ */ new Map();
788
+ const sharedClasses = /* @__PURE__ */ new Set();
789
+ const projectRoot = process.cwd();
790
+ for (const entry of sharedEntries) {
791
+ for (const className of await collectEntryClasses(
792
+ compilation,
793
+ entry,
794
+ sourceCache
795
+ )) {
796
+ sharedClasses.add(className);
797
+ }
798
+ const name = entryName(entry);
799
+ if (name) {
800
+ for (const className of await collectNameSourceClasses(
801
+ name,
802
+ projectRoot,
803
+ sourceCache
804
+ )) {
805
+ sharedClasses.add(className);
806
+ }
807
+ }
808
+ }
809
+ for (const { entry, name, sourceName } of selectedPages) {
810
+ const page = pages.get(name) ?? {
811
+ name,
812
+ classes: /* @__PURE__ */ new Set(),
813
+ cssFiles: /* @__PURE__ */ new Set(),
814
+ jsFiles: collectEntryJsFiles(entry)
815
+ };
816
+ for (const jsFile of collectEntryJsFiles(entry)) {
817
+ page.jsFiles.add(jsFile);
818
+ }
819
+ for (const className of sharedClasses) {
820
+ page.classes.add(className);
821
+ }
822
+ for (const className of await collectEntryClasses(
823
+ compilation,
824
+ entry,
825
+ sourceCache
826
+ )) {
827
+ page.classes.add(className);
828
+ }
829
+ for (const className of await collectPageSourceClasses(
830
+ sourceName,
831
+ projectRoot,
832
+ sourceCache
833
+ )) {
834
+ page.classes.add(className);
835
+ }
836
+ pages.set(name, page);
837
+ }
838
+ }
839
+ const targetPages = [...pages.keys()];
840
+ for (const { entry, name } of selectedPages) {
841
+ const cssFiles = collectEntryCssFiles(entry);
842
+ if (direct) {
843
+ const directPage = pages.get(name);
844
+ if (directPage) {
845
+ for (const cssFile of cssFiles) {
846
+ directPage.cssFiles.add(cssFile);
847
+ }
848
+ }
849
+ } else {
850
+ const page = pages.get(name);
851
+ if (page) {
852
+ for (const cssFile of cssFiles) {
853
+ page.cssFiles.add(cssFile);
854
+ }
855
+ }
856
+ }
857
+ }
858
+ const sharedCssFiles = /* @__PURE__ */ new Set();
859
+ for (const entry of sharedEntries) {
860
+ for (const cssFile of collectEntryCssFiles(entry)) {
861
+ sharedCssFiles.add(cssFile);
862
+ }
863
+ }
864
+ for (const page of pages.values()) {
865
+ for (const cssFile of page.cssFiles) {
866
+ addCssOwner(pagesByCss, cssFile, page.name);
867
+ }
868
+ }
869
+ for (const cssFile of sharedCssFiles) {
870
+ for (const page of targetPages) {
871
+ addCssOwner(pagesByCss, cssFile, page);
872
+ }
873
+ }
874
+ return {
875
+ pages: [...pages.values()],
876
+ pagesByCss
877
+ };
878
+ }
879
+ async function collectPageSourceClasses(entryName2, projectRoot, sourceCache) {
880
+ const classes = /* @__PURE__ */ new Set();
881
+ const visited = /* @__PURE__ */ new Set();
882
+ const normalized = normalizeSlashes(entryName2);
883
+ const candidates = [];
884
+ if (normalized.startsWith("app/")) {
885
+ const path = normalized.slice(4);
886
+ if (path === "page" || path.endsWith("/page")) {
887
+ const pagePath = path === "page" ? "app/page" : `app/${path}`;
888
+ candidates.push((0, import_node_path2.resolve)(projectRoot, pagePath));
889
+ if (path === "_not-found/page") {
890
+ candidates.push((0, import_node_path2.resolve)(projectRoot, "app/not-found"));
891
+ }
892
+ const directories = path.split("/").slice(0, -1).filter(Boolean);
893
+ const layoutPaths = ["app"];
894
+ for (let index = 0; index < directories.length; index += 1) {
895
+ layoutPaths.push(`app/${directories.slice(0, index + 1).join("/")}`);
896
+ }
897
+ for (const layoutPath of layoutPaths) {
898
+ candidates.push((0, import_node_path2.resolve)(projectRoot, `${layoutPath}/layout`));
899
+ }
900
+ } else {
901
+ candidates.push((0, import_node_path2.resolve)(projectRoot, normalized));
902
+ }
903
+ } else if (normalized.startsWith("pages/")) {
904
+ candidates.push((0, import_node_path2.resolve)(projectRoot, normalized));
905
+ candidates.push((0, import_node_path2.resolve)(projectRoot, "pages/_app"));
906
+ } else {
907
+ candidates.push((0, import_node_path2.resolve)(projectRoot, normalized));
908
+ }
909
+ for (const candidate of candidates) {
910
+ const file = findSourceFile(candidate);
911
+ if (file) {
912
+ await scanSourceFile(file, classes, projectRoot, sourceCache, visited);
913
+ }
914
+ }
915
+ return classes;
916
+ }
917
+ async function collectNameSourceClasses(entryName2, projectRoot, sourceCache) {
918
+ const classes = /* @__PURE__ */ new Set();
919
+ const visited = /* @__PURE__ */ new Set();
920
+ const candidate = (0, import_node_path2.resolve)(projectRoot, normalizeSlashes(entryName2));
921
+ const file = findSourceFile(candidate);
922
+ if (file) {
923
+ await scanSourceFile(file, classes, projectRoot, sourceCache, visited);
924
+ }
925
+ return classes;
926
+ }
927
+ async function scanSourceFile(filePath, classes, projectRoot, sourceCache, visited) {
928
+ const absolutePath = (0, import_node_path2.resolve)(filePath);
929
+ if (visited.has(absolutePath)) {
930
+ return;
931
+ }
932
+ visited.add(absolutePath);
933
+ let pending = sourceCache.get(absolutePath);
934
+ if (!pending) {
935
+ pending = (0, import_promises.readFile)(absolutePath, "utf8").catch(() => void 0);
936
+ sourceCache.set(absolutePath, pending);
937
+ }
938
+ const source = await pending;
939
+ if (!source) {
940
+ return;
941
+ }
942
+ scanClasses(source, classes);
943
+ for (const specifier of extractImportSpecifiers(source)) {
944
+ const resolved = resolveImportSpecifier(
945
+ specifier,
946
+ absolutePath,
947
+ projectRoot
948
+ );
949
+ if (resolved && !visited.has(resolved)) {
950
+ await scanSourceFile(
951
+ resolved,
952
+ classes,
953
+ projectRoot,
954
+ sourceCache,
955
+ visited
956
+ );
957
+ }
958
+ }
959
+ }
960
+ var SOURCE_EXTENSIONS = [
961
+ ".tsx",
962
+ ".ts",
963
+ ".jsx",
964
+ ".js",
965
+ ".mjs",
966
+ ".cjs",
967
+ ".mts",
968
+ ".cts",
969
+ ".mdx"
970
+ ];
971
+ function findSourceFile(basePath) {
972
+ if (isStyleFile(basePath)) {
973
+ return null;
974
+ }
975
+ if ((0, import_node_path2.extname)(basePath) && (0, import_node_fs2.existsSync)(basePath)) {
976
+ return basePath;
977
+ }
978
+ for (const extension of SOURCE_EXTENSIONS) {
979
+ const candidate = `${basePath}${extension}`;
980
+ if ((0, import_node_fs2.existsSync)(candidate)) {
981
+ return candidate;
982
+ }
983
+ }
984
+ for (const extension of SOURCE_EXTENSIONS) {
985
+ const candidate = (0, import_node_path2.resolve)(basePath, `index${extension}`);
986
+ if ((0, import_node_fs2.existsSync)(candidate)) {
987
+ return candidate;
988
+ }
989
+ }
990
+ return null;
991
+ }
992
+ function extractImportSpecifiers(source) {
993
+ const specifiers = /* @__PURE__ */ new Set();
994
+ const patterns = [
995
+ /(?:import|export)\s+(?:type\s+)?(?:[^'"]*?\s+from\s+)?['"]([^'"]+)['"]/g,
996
+ /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
997
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g
998
+ ];
999
+ for (const pattern of patterns) {
1000
+ for (const match of source.matchAll(pattern)) {
1001
+ const specifier = match[1];
1002
+ if (specifier) {
1003
+ specifiers.add(specifier);
1004
+ }
1005
+ }
1006
+ }
1007
+ return [...specifiers];
1008
+ }
1009
+ function resolveImportSpecifier(specifier, fromFile, projectRoot) {
1010
+ let basePath;
1011
+ if (specifier.startsWith("@/")) {
1012
+ basePath = (0, import_node_path2.resolve)(projectRoot, specifier.slice(2));
1013
+ } else if (specifier.startsWith("./") || specifier.startsWith("../")) {
1014
+ basePath = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(fromFile), specifier);
1015
+ } else {
1016
+ return null;
1017
+ }
1018
+ if (isStyleFile(basePath)) {
1019
+ return null;
1020
+ }
1021
+ return findSourceFile(basePath);
1022
+ }
1023
+ async function collectEntryClasses(compilation, entry, sourceCache) {
1024
+ const classes = /* @__PURE__ */ new Set();
1025
+ const visitedModules = /* @__PURE__ */ new Set();
1026
+ for (const chunk of entry.chunks) {
1027
+ for (const module2 of compilation.chunkGraph.getChunkModulesIterable(
1028
+ chunk
1029
+ )) {
1030
+ await scanModule(module2, classes, sourceCache, visitedModules);
1031
+ }
1032
+ }
1033
+ return classes;
1034
+ }
1035
+ async function scanModule(module2, classes, sourceCache, visited) {
1036
+ if (visited.has(module2)) {
1037
+ return;
1038
+ }
1039
+ visited.add(module2);
1040
+ const nested = module2.modules;
1041
+ if (nested) {
1042
+ for (const child of nested) {
1043
+ await scanModule(child, classes, sourceCache, visited);
1044
+ }
1045
+ }
1046
+ const resource = module2.resource;
1047
+ if (typeof resource !== "string" || isStyleFile(resource)) {
1048
+ return;
1049
+ }
1050
+ const source = readModuleSource(module2);
1051
+ if (source) {
1052
+ scanClasses(source, classes);
1053
+ return;
1054
+ }
1055
+ if (!isScannableSource(resource)) {
1056
+ return;
1057
+ }
1058
+ const filePath = resource.split("?")[0] ?? resource;
1059
+ if (!filePath) {
1060
+ return;
1061
+ }
1062
+ let pending = sourceCache.get(filePath);
1063
+ if (!pending) {
1064
+ pending = (0, import_promises.readFile)(filePath, "utf8").catch(() => void 0);
1065
+ sourceCache.set(filePath, pending);
1066
+ }
1067
+ const fresh = await pending;
1068
+ if (fresh) {
1069
+ scanClasses(fresh, classes);
1070
+ }
1071
+ }
1072
+ function readModuleSource(module2) {
1073
+ try {
1074
+ const source = module2.originalSource();
1075
+ if (!source) {
1076
+ return void 0;
1077
+ }
1078
+ return sourceToString(source.source());
1079
+ } catch {
1080
+ return void 0;
1081
+ }
1082
+ }
1083
+ function collectEntryJsFiles(entry) {
1084
+ const files = /* @__PURE__ */ new Set();
1085
+ for (const file of entry.getFiles()) {
1086
+ if (file.endsWith(".js")) {
1087
+ files.add(file);
1088
+ }
1089
+ }
1090
+ return files;
1091
+ }
1092
+ function collectEntryCssFiles(entry) {
1093
+ const cssFiles = /* @__PURE__ */ new Set();
1094
+ for (const file of entry.getFiles()) {
1095
+ if (file.endsWith(".css")) {
1096
+ cssFiles.add(file);
1097
+ }
1098
+ }
1099
+ for (const chunk of entry.chunks) {
1100
+ for (const file of chunk.files) {
1101
+ if (file.endsWith(".css")) {
1102
+ cssFiles.add(file);
1103
+ }
1104
+ }
1105
+ for (const file of chunk.auxiliaryFiles) {
1106
+ if (file.endsWith(".css")) {
1107
+ cssFiles.add(file);
1108
+ }
1109
+ }
1110
+ }
1111
+ return cssFiles;
1112
+ }
1113
+ function entryName(entry) {
1114
+ if (entry.name) {
1115
+ return entry.name;
1116
+ }
1117
+ const chunk = entry.getEntrypointChunk();
1118
+ return chunk.name ?? void 0;
1119
+ }
1120
+ function isPageEntry(name) {
1121
+ const normalized = normalizeSlashes(name);
1122
+ if (normalized.startsWith("app/")) {
1123
+ const path = normalized.slice(4);
1124
+ return path === "page" || path.endsWith("/page");
1125
+ }
1126
+ if (normalized.startsWith("pages/")) {
1127
+ const path = normalized.slice(6);
1128
+ return path.length > 0 && !path.split("/").some((segment) => segment.startsWith("_"));
1129
+ }
1130
+ return false;
1131
+ }
1132
+ function isSharedEntry(name) {
1133
+ const normalized = normalizeSlashes(name);
1134
+ return normalized.startsWith("app/") && (normalized === "app/layout" || normalized.endsWith("/layout")) || normalized === "pages/_app" || normalized === "pages/_document";
1135
+ }
1136
+ function isInternalEntry(name) {
1137
+ const normalized = normalizeSlashes(name);
1138
+ const base = normalized.split("/").pop() ?? normalized;
1139
+ return base === "main" || base === "main-app" || base === "polyfills" || base === "webpack" || base === "app-pages-internals" || base === "middleware" || base === "instrumentation";
1140
+ }
1141
+ function routeNameForEntry(name) {
1142
+ const normalized = normalizeSlashes(name).replace(/^\.\//, "");
1143
+ if (normalized.startsWith("app/")) {
1144
+ let path = normalized.slice(4);
1145
+ if (path === "page") {
1146
+ return "/";
1147
+ }
1148
+ if (path.endsWith("/page")) {
1149
+ path = path.slice(0, -"/page".length);
1150
+ }
1151
+ if (!path || path.endsWith("layout")) {
1152
+ return null;
1153
+ }
1154
+ return `/${path}`;
1155
+ }
1156
+ if (normalized.startsWith("pages/")) {
1157
+ let path = normalized.slice(6);
1158
+ if (!path || path.startsWith("_")) {
1159
+ return null;
1160
+ }
1161
+ path = path.replace(/\/?index$/, "");
1162
+ return path.length > 0 ? `/${path}` : "/";
1163
+ }
1164
+ return normalized.length > 0 ? normalized : null;
1165
+ }
1166
+ function normalizeSlashes(value) {
1167
+ return value.replace(/\\/g, "/").replace(/^\/+/, "");
1168
+ }
1169
+ function isStyleFile(resource) {
1170
+ return /\.(css|scss|sass|less|styl|stylus)(\?|$)/i.test(resource);
1171
+ }
1172
+ function isScannableSource(resource) {
1173
+ return /\.(js|jsx|ts|tsx|mjs|cjs|mts|cts|html?|vue|svelte|mdx)(\?|$)/i.test(
1174
+ resource
1175
+ );
1176
+ }
1177
+ function scanClasses(source, classes) {
1178
+ const patterns = [
1179
+ /(?:class|className)\s*=\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`)/g,
1180
+ /(?:^|[,{;\s])(?:class|className)\s*:\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`)/g,
1181
+ /classList\.(?:add|remove|toggle|contains)\(\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`)/g
1182
+ ];
1183
+ for (const pattern of patterns) {
1184
+ for (const match of source.matchAll(pattern)) {
1185
+ const value = match[1] ?? match[2] ?? match[3];
1186
+ if (!value) {
1187
+ continue;
1188
+ }
1189
+ for (const token of value.split(/\s+/)) {
1190
+ if (/^-?[_a-zA-Z][\w-]*$/.test(token)) {
1191
+ classes.add(token.toLowerCase());
1192
+ }
1193
+ }
1194
+ }
1195
+ }
1196
+ }
1197
+ function addCssOwner(pagesByCss, cssFile, page) {
1198
+ const owners = pagesByCss.get(cssFile) ?? /* @__PURE__ */ new Set();
1199
+ owners.add(page);
1200
+ pagesByCss.set(cssFile, owners);
1201
+ }
1202
+ function collectSyntaxWarnings(analysis) {
1203
+ return analysis.warnings.map((warning) => ({
1204
+ kind: "syntax",
1205
+ message: warning.message,
1206
+ ...warning.location ? {
1207
+ line: warning.location.line,
1208
+ column: warning.location.column
1209
+ } : {}
1210
+ }));
1211
+ }
1212
+ function sourceToString(source) {
1213
+ return typeof source === "string" ? source : Buffer.from(source).toString("utf8");
1214
+ }
1215
+ function formatBytes2(bytes) {
1216
+ if (bytes < 1024) {
1217
+ return `${bytes} B`;
1218
+ }
1219
+ if (bytes < 1024 * 1024) {
1220
+ return `${(bytes / 1024).toFixed(1)} KB`;
1221
+ }
1222
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1223
+ }
1224
+ function roundMs(value) {
1225
+ return Math.round(value * 1e3) / 1e3;
1226
+ }
1227
+ function describeError(error) {
1228
+ return error instanceof Error ? error.message : String(error);
1229
+ }
1230
+
1231
+ // src/index.ts
1232
+ function withCssDoctor(nextConfig = {}, options = {}) {
1233
+ const pluginOptions = resolvePluginOptions(options);
1234
+ const originalWebpack = nextConfig.webpack;
1235
+ const webpack = (config, context) => {
1236
+ const result = originalWebpack ? originalWebpack(config, context) : config;
1237
+ if (isPromiseLike(result)) {
1238
+ return result.then(
1239
+ (resolvedConfig) => enableCssDoctor(resolvedConfig, context, pluginOptions)
1240
+ );
1241
+ }
1242
+ return enableCssDoctor(result, context, pluginOptions);
1243
+ };
1244
+ return {
1245
+ ...nextConfig,
1246
+ webpack
1247
+ };
1248
+ }
1249
+ function enableCssDoctor(config, context, options) {
1250
+ if (!isRecord2(config)) {
1251
+ return config;
1252
+ }
1253
+ if (context.dev === true || context.isServer === true) {
1254
+ return config;
1255
+ }
1256
+ const webpackConfig = config;
1257
+ const plugins = Array.isArray(webpackConfig.plugins) ? [...webpackConfig.plugins] : [];
1258
+ const alreadyInstalled = plugins.some(
1259
+ (plugin) => plugin instanceof CssDoctorWebpackPlugin
1260
+ );
1261
+ if (!alreadyInstalled) {
1262
+ plugins.push(new CssDoctorWebpackPlugin(options));
1263
+ }
1264
+ webpackConfig.plugins = plugins;
1265
+ if (options.minify === false) {
1266
+ disableNextCssMinifier(webpackConfig);
1267
+ }
1268
+ return webpackConfig;
1269
+ }
1270
+ function disableNextCssMinifier(config) {
1271
+ const optimization = config.optimization;
1272
+ if (!optimization || !Array.isArray(optimization.minimizer)) {
1273
+ return;
1274
+ }
1275
+ optimization.minimizer = optimization.minimizer.filter((plugin) => {
1276
+ const source = typeof plugin === "function" ? Function.prototype.toString.call(plugin) : String(plugin);
1277
+ return !source.includes("CssMinimizerPlugin") && !source.includes("css-minimizer-plugin");
1278
+ });
1279
+ }
1280
+ function resolvePluginOptions(options) {
1281
+ const { configFile, ...inlineOptions } = options;
1282
+ const hasInlineOptions = Object.keys(inlineOptions).length > 0;
1283
+ let loadedOptions = {};
1284
+ let configError;
1285
+ if (configFile) {
1286
+ const loaded = loadCssDoctorConfig({ configFile });
1287
+ loadedOptions = loaded.options;
1288
+ configError = loaded.error;
1289
+ } else if (!hasInlineOptions) {
1290
+ const loaded = loadCssDoctorConfig();
1291
+ loadedOptions = loaded.options;
1292
+ configError = loaded.error;
1293
+ }
1294
+ if (configError) {
1295
+ console.warn(
1296
+ `[css-doctor] ${configError}. Continuing with inline/default options.`
1297
+ );
1298
+ }
1299
+ const merged = {
1300
+ ...loadedOptions,
1301
+ ...inlineOptions
1302
+ };
1303
+ const { configFile: _ignored, ...pluginOptions } = merged;
1304
+ return pluginOptions;
1305
+ }
1306
+ function isPromiseLike(value) {
1307
+ return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
1308
+ }
1309
+ function isRecord2(value) {
1310
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1311
+ }
1312
+ // Annotate the CommonJS export names for ESM import in node:
1313
+ 0 && (module.exports = {
1314
+ findCssDoctorConfig,
1315
+ loadCssDoctorConfig,
1316
+ withCssDoctor
1317
+ });
1318
+ //# sourceMappingURL=index.cjs.map