@cleaver/breakcheck-core 0.1.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.
Files changed (59) hide show
  1. package/README.md +152 -0
  2. package/dist/__tests__/compare/index.test.js +322 -0
  3. package/dist/__tests__/compare/index.test.js.map +1 -0
  4. package/dist/__tests__/rules/RulesDsl.test.js +190 -0
  5. package/dist/__tests__/rules/RulesDsl.test.js.map +1 -0
  6. package/dist/__tests__/rules/RulesEngine.test.js +142 -0
  7. package/dist/__tests__/rules/RulesEngine.test.js.map +1 -0
  8. package/dist/__tests__/snapshot/index.test.js +238 -0
  9. package/dist/__tests__/snapshot/index.test.js.map +1 -0
  10. package/dist/__tests__/view/index.test.js +102 -0
  11. package/dist/__tests__/view/index.test.js.map +1 -0
  12. package/dist/api/index.js +69 -0
  13. package/dist/api/index.js.map +1 -0
  14. package/dist/core/compare/classes/ComparisonRepository.js +77 -0
  15. package/dist/core/compare/classes/ComparisonRepository.js.map +1 -0
  16. package/dist/core/compare/index.js +81 -0
  17. package/dist/core/compare/index.js.map +1 -0
  18. package/dist/core/crawler/implementations/cheerio.js +46 -0
  19. package/dist/core/crawler/implementations/cheerio.js.map +1 -0
  20. package/dist/core/crawler/implementations/playwright.js +44 -0
  21. package/dist/core/crawler/implementations/playwright.js.map +1 -0
  22. package/dist/core/crawler/index.js +61 -0
  23. package/dist/core/crawler/index.js.map +1 -0
  24. package/dist/core/rules/RulesDsl.js +251 -0
  25. package/dist/core/rules/RulesDsl.js.map +1 -0
  26. package/dist/core/rules/RulesEngine.js +91 -0
  27. package/dist/core/rules/RulesEngine.js.map +1 -0
  28. package/dist/core/snapshot/classes/LoadedSnapshot.js +37 -0
  29. package/dist/core/snapshot/classes/LoadedSnapshot.js.map +1 -0
  30. package/dist/core/snapshot/classes/SnapshotRepository.js +192 -0
  31. package/dist/core/snapshot/classes/SnapshotRepository.js.map +1 -0
  32. package/dist/core/snapshot/index.js +67 -0
  33. package/dist/core/snapshot/index.js.map +1 -0
  34. package/dist/core/view/index.handlers.js +70 -0
  35. package/dist/core/view/index.handlers.js.map +1 -0
  36. package/dist/core/view/index.js +50 -0
  37. package/dist/core/view/index.js.map +1 -0
  38. package/dist/index.d.ts +14 -0
  39. package/dist/index.js +13 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/lib/logger.js +26 -0
  42. package/dist/lib/logger.js.map +1 -0
  43. package/dist/lib/root.js +25 -0
  44. package/dist/lib/root.js.map +1 -0
  45. package/dist/public/css/diff2html.min.css +1 -0
  46. package/dist/public/js/diff2html.min.js +1 -0
  47. package/dist/types/api.js +3 -0
  48. package/dist/types/api.js.map +1 -0
  49. package/dist/types/compare.js +3 -0
  50. package/dist/types/compare.js.map +1 -0
  51. package/dist/types/crawler.js +2 -0
  52. package/dist/types/crawler.js.map +1 -0
  53. package/dist/types/rules.js +6 -0
  54. package/dist/types/rules.js.map +1 -0
  55. package/dist/types/snapshot.js +3 -0
  56. package/dist/types/snapshot.js.map +1 -0
  57. package/dist/views/diff.ejs +59 -0
  58. package/dist/views/index.ejs +83 -0
  59. package/package.json +41 -0
@@ -0,0 +1,91 @@
1
+ import * as cheerio from "cheerio";
2
+ import { logger } from "../../lib/logger";
3
+ import { processRulesDsl } from "./RulesDsl";
4
+ export class RulesEngine {
5
+ ruleset;
6
+ constructor(ruleset) {
7
+ this.ruleset = ruleset;
8
+ }
9
+ static async create(rulesetOrName) {
10
+ let ruleset;
11
+ if (typeof rulesetOrName === "string") {
12
+ try {
13
+ ruleset = await processRulesDsl(rulesetOrName);
14
+ }
15
+ catch (error) {
16
+ if (error instanceof Error) {
17
+ logger.error({ error, rulesetName: rulesetOrName }, "Failed to process ruleset DSL");
18
+ process.exit(1);
19
+ }
20
+ throw error;
21
+ }
22
+ }
23
+ else {
24
+ ruleset = rulesetOrName;
25
+ }
26
+ return new RulesEngine(ruleset);
27
+ }
28
+ process(html) {
29
+ const $ = cheerio.load(html);
30
+ // Apply each rule in sequence
31
+ for (const rule of this.ruleset.rules) {
32
+ const elements = $(rule.selector);
33
+ elements.each((_, element) => {
34
+ if (element.type === "tag") {
35
+ for (const action of rule.actions) {
36
+ this.applyAction($, element, action);
37
+ }
38
+ }
39
+ });
40
+ }
41
+ return $.html();
42
+ }
43
+ applyAction($, element, action) {
44
+ const $element = $(element);
45
+ // Check content regex if specified
46
+ if (action.modifiers?.content_regex) {
47
+ const content = $element.text();
48
+ const regex = new RegExp(action.modifiers.content_regex);
49
+ if (!regex.test(content)) {
50
+ return;
51
+ }
52
+ }
53
+ switch (action.action) {
54
+ case "exclude":
55
+ $element.remove();
56
+ break;
57
+ case "include":
58
+ // No action needed for include
59
+ break;
60
+ case "remove_attr":
61
+ if (action.modifiers && "attr" in action.modifiers) {
62
+ $element.removeAttr(action.modifiers.attr);
63
+ }
64
+ break;
65
+ case "rewrite_attr":
66
+ if (action.modifiers &&
67
+ "attr" in action.modifiers &&
68
+ "regex" in action.modifiers &&
69
+ "replace" in action.modifiers) {
70
+ const currentValue = $element.attr(action.modifiers.attr);
71
+ if (currentValue) {
72
+ const regex = new RegExp(action.modifiers.regex);
73
+ const newValue = currentValue.replace(regex, action.modifiers.replace);
74
+ $element.attr(action.modifiers.attr, newValue);
75
+ }
76
+ }
77
+ break;
78
+ case "rewrite_content":
79
+ if (action.modifiers &&
80
+ "regex" in action.modifiers &&
81
+ "replace" in action.modifiers) {
82
+ const content = $element.text();
83
+ const regex = new RegExp(action.modifiers.regex);
84
+ const newContent = content.replace(regex, action.modifiers.replace);
85
+ $element.text(newContent);
86
+ }
87
+ break;
88
+ }
89
+ }
90
+ }
91
+ //# sourceMappingURL=RulesEngine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RulesEngine.js","sourceRoot":"","sources":["../../../src/core/rules/RulesEngine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ import { promisify } from "util";
4
+ import * as zlib from "zlib";
5
+ import { logger } from "../../../lib/logger";
6
+ const gunzip = promisify(zlib.gunzip);
7
+ export class LoadedSnapshot {
8
+ metadata;
9
+ index;
10
+ pagesDir;
11
+ constructor(metadata, index, pagesDir) {
12
+ this.metadata = metadata;
13
+ this.index = index;
14
+ this.pagesDir = pagesDir;
15
+ }
16
+ /**
17
+ * Loads a single page snapshot on demand by URL
18
+ */
19
+ async getPage(url) {
20
+ const info = this.index.urls[url];
21
+ if (!info)
22
+ return null;
23
+ const filePath = path.join(this.pagesDir, info.filename);
24
+ try {
25
+ const compressed = await fs.readFile(filePath);
26
+ const decompressed = await gunzip(compressed);
27
+ const page = JSON.parse(decompressed.toString());
28
+ return page;
29
+ }
30
+ catch (err) {
31
+ // Could be file not found, decompression, or parse error
32
+ logger.error({ err, url, filePath }, "Failed to load page snapshot");
33
+ return null;
34
+ }
35
+ }
36
+ }
37
+ //# sourceMappingURL=LoadedSnapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LoadedSnapshot.js","sourceRoot":"","sources":["../../../../src/core/snapshot/classes/LoadedSnapshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAI7C,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAEtC,MAAM,OAAO,cAAc;IACT,QAAQ,CAAmB;IAC3B,KAAK,CAAgB;IACpB,QAAQ,CAAS;IAElC,YACE,QAA0B,EAC1B,KAAoB,EACpB,QAAgB;QAEhB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,OAAO,CAAC,GAAW;QACvB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QACvB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzD,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAC/C,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;YAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC,CAAC;YACjD,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,yDAAyD;YACzD,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,8BAA8B,CAAC,CAAC;YACrE,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,192 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ import { promisify } from "util";
4
+ import * as zlib from "zlib";
5
+ import { findRootDir } from "../../../lib/root";
6
+ import { LoadedSnapshot } from "./LoadedSnapshot";
7
+ const gzip = promisify(zlib.gzip);
8
+ export class SnapshotRepository {
9
+ snapshotsDir;
10
+ constructor(snapshotsDir) {
11
+ this.snapshotsDir = snapshotsDir;
12
+ }
13
+ /**
14
+ * Creates a new SnapshotRepository instance using the project root directory
15
+ * @returns Promise<SnapshotRepository>
16
+ */
17
+ static async create() {
18
+ const rootDir = await findRootDir();
19
+ const snapshotsDir = path.join(rootDir, "snapshots");
20
+ return new SnapshotRepository(snapshotsDir);
21
+ }
22
+ /**
23
+ * Creates a new SnapshotRepository instance with a custom snapshots directory
24
+ * @param snapshotsDir Custom directory path for snapshots
25
+ * @returns Promise<SnapshotRepository>
26
+ */
27
+ static async createWithCustomDir(snapshotsDir) {
28
+ return new SnapshotRepository(snapshotsDir);
29
+ }
30
+ /**
31
+ * Normalizes a URL by removing scheme, hostname, and port
32
+ * @param url The URL to normalize
33
+ * @returns The normalized URL path starting with "/"
34
+ */
35
+ normalizeUrl(url) {
36
+ try {
37
+ const urlObj = new URL(url);
38
+ return urlObj.pathname + urlObj.search + urlObj.hash;
39
+ }
40
+ catch {
41
+ // If URL parsing fails, return the original URL
42
+ return url;
43
+ }
44
+ }
45
+ /**
46
+ * Save a snapshot to disk (streaming/iterative)
47
+ * Returns the number of non-error pages saved
48
+ */
49
+ async saveSnapshot(name, data) {
50
+ await fs.mkdir(this.snapshotsDir, { recursive: true });
51
+ const snapshotDir = path.join(this.snapshotsDir, name);
52
+ try {
53
+ await fs.rm(snapshotDir, { recursive: true, force: true });
54
+ }
55
+ catch (error) {
56
+ // Ignore errors if directory doesn't exist
57
+ }
58
+ await fs.mkdir(snapshotDir, { recursive: true });
59
+ const metadataPath = path.join(snapshotDir, "metadata.json");
60
+ await fs.writeFile(metadataPath, JSON.stringify(data.metadata, null, 2));
61
+ const pagesDir = path.join(snapshotDir, "pages");
62
+ await fs.mkdir(pagesDir, { recursive: true });
63
+ // Create index
64
+ const index = {
65
+ urls: {},
66
+ metadata: {
67
+ baseUrl: data.metadata.baseUrl,
68
+ timestamp: data.metadata.timestamp,
69
+ totalPages: 0,
70
+ },
71
+ };
72
+ let pageCount = 0;
73
+ await data.dataset.forEach(async (item) => {
74
+ if (item.type === "error")
75
+ return;
76
+ // Save each page as a compressed JSON file
77
+ const page = item;
78
+ const normalizedUrl = this.normalizeUrl(page.url);
79
+ // Create a copy of the page with normalized URL
80
+ const normalizedPage = {
81
+ ...page,
82
+ url: normalizedUrl,
83
+ };
84
+ const pageData = JSON.stringify(normalizedPage);
85
+ const compressed = await gzip(pageData);
86
+ const filename = Buffer.from(normalizedUrl).toString("base64url") + ".json.gz";
87
+ await fs.writeFile(path.join(pagesDir, filename), compressed);
88
+ // Add to index
89
+ index.urls[normalizedUrl] = {
90
+ filename,
91
+ statusCode: page.statusCode,
92
+ finalUrl: page.finalUrl !== page.url ? page.finalUrl : undefined,
93
+ pageTitle: page.title,
94
+ };
95
+ pageCount++;
96
+ });
97
+ // Save index
98
+ index.metadata.totalPages = pageCount;
99
+ const indexPath = path.join(snapshotDir, "index.json");
100
+ await fs.writeFile(indexPath, JSON.stringify(index, null, 2));
101
+ return pageCount;
102
+ }
103
+ /**
104
+ * Load a snapshot from disk
105
+ */
106
+ async loadSnapshot(name) {
107
+ const snapshotDir = path.join(this.snapshotsDir, name);
108
+ // Load metadata
109
+ const metadataPath = path.join(snapshotDir, "metadata.json");
110
+ const metadataContent = await fs.readFile(metadataPath, "utf-8");
111
+ const metadata = JSON.parse(metadataContent);
112
+ // Load index
113
+ const indexPath = path.join(snapshotDir, "index.json");
114
+ const indexContent = await fs.readFile(indexPath, "utf-8");
115
+ const index = JSON.parse(indexContent);
116
+ // Prepare pages directory path
117
+ const pagesDir = path.join(snapshotDir, "pages");
118
+ // Return a LoadedSnapshot instance for on-demand page loading
119
+ return new LoadedSnapshot(metadata, index, pagesDir);
120
+ }
121
+ /**
122
+ * Generate a URL list file from a snapshot
123
+ * @param name Snapshot name
124
+ * @param outputPath Optional path to save the URL list. If not provided, saves in the snapshot directory
125
+ * @param filter Optional function to filter URLs
126
+ */
127
+ async generateUrlList(name, outputPath, filter) {
128
+ const snapshotDir = path.join(this.snapshotsDir, name);
129
+ // Load index
130
+ const indexPath = path.join(snapshotDir, "index.json");
131
+ const indexContent = await fs.readFile(indexPath, "utf-8");
132
+ const index = JSON.parse(indexContent);
133
+ // Filter and collect URLs
134
+ const urls = Object.entries(index.urls)
135
+ .filter(([url, info]) => !filter || filter(url, info.statusCode))
136
+ .map(([url]) => url);
137
+ // Generate URL list content
138
+ const content = urls.join("\n");
139
+ // Determine output path
140
+ const finalOutputPath = outputPath || path.join(snapshotDir, "urls.txt");
141
+ // Save URL list
142
+ await fs.writeFile(finalOutputPath, content);
143
+ return finalOutputPath;
144
+ }
145
+ /**
146
+ * List all available snapshots with their details
147
+ */
148
+ async listSnapshots() {
149
+ try {
150
+ const entries = await fs.readdir(this.snapshotsDir, {
151
+ withFileTypes: true,
152
+ });
153
+ const snapshots = entries.filter((entry) => entry.isDirectory());
154
+ const summaries = [];
155
+ for (const snapshot of snapshots) {
156
+ try {
157
+ // Load metadata
158
+ const metadataPath = path.join(this.snapshotsDir, snapshot.name, "metadata.json");
159
+ const metadataContent = await fs.readFile(metadataPath, "utf-8");
160
+ const metadata = JSON.parse(metadataContent);
161
+ // Load index for page count
162
+ const indexPath = path.join(this.snapshotsDir, snapshot.name, "index.json");
163
+ const indexContent = await fs.readFile(indexPath, "utf-8");
164
+ const index = JSON.parse(indexContent);
165
+ summaries.push({
166
+ name: snapshot.name,
167
+ date: metadata.timestamp,
168
+ pageCount: index.metadata.totalPages,
169
+ errorCount: 0,
170
+ });
171
+ }
172
+ catch (error) {
173
+ // If we can't read the metadata or index, just include the name
174
+ summaries.push({
175
+ name: snapshot.name,
176
+ date: "Unknown",
177
+ pageCount: 0,
178
+ errorCount: 0,
179
+ });
180
+ }
181
+ }
182
+ return summaries;
183
+ }
184
+ catch (error) {
185
+ if (error.code === "ENOENT") {
186
+ return [];
187
+ }
188
+ throw error;
189
+ }
190
+ }
191
+ }
192
+ //# sourceMappingURL=SnapshotRepository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SnapshotRepository.js","sourceRoot":"","sources":["../../../../src/core/snapshot/classes/SnapshotRepository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAOhD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAElC,MAAM,OAAO,kBAAkB;IACZ,YAAY,CAAS;IAEtC,YAAoB,YAAoB;QACtC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACnC,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,MAAM;QACjB,MAAM,OAAO,GAAG,MAAM,WAAW,EAAE,CAAC;QACpC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;QACrD,OAAO,IAAI,kBAAkB,CAAC,YAAY,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAC9B,YAAoB;QAEpB,OAAO,IAAI,kBAAkB,CAAC,YAAY,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACK,YAAY,CAAC,GAAW;QAC9B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;YAC5B,OAAO,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,gDAAgD;YAChD,OAAO,GAAG,CAAC;QACb,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,YAAY,CAAC,IAAY,EAAE,IAAkB;QACjD,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAEvD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;QAEvD,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,EAAE,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,2CAA2C;QAC7C,CAAC;QAED,MAAM,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAEjD,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC;QAC7D,MAAM,EAAE,CAAC,SAAS,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QAEzE,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QACjD,MAAM,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAE9C,eAAe;QACf,MAAM,KAAK,GAAkB;YAC3B,IAAI,EAAE,EAAE;YACR,QAAQ,EAAE;gBACR,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,OAAO;gBAC9B,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,SAAS;gBAClC,UAAU,EAAE,CAAC;aACd;SACF,CAAC;QAEF,IAAI,SAAS,GAAG,CAAC,CAAC;QAClB,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,IAAS,EAAE,EAAE;YAC7C,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;gBAAE,OAAO;YAClC,2CAA2C;YAC3C,MAAM,IAAI,GAAG,IAAoB,CAAC;YAClC,MAAM,aAAa,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAElD,gDAAgD;YAChD,MAAM,cAAc,GAAG;gBACrB,GAAG,IAAI;gBACP,GAAG,EAAE,aAAa;aACnB,CAAC;YAEF,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;YAChD,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;YACxC,MAAM,QAAQ,GACZ,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,UAAU,CAAC;YAChE,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,UAAU,CAAC,CAAC;YAE9D,eAAe;YACf,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG;gBAC1B,QAAQ;gBACR,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;gBAChE,SAAS,EAAE,IAAI,CAAC,KAAK;aACtB,CAAC;YACF,SAAS,EAAE,CAAC;QACd,CAAC,CAAC,CAAC;QAEH,aAAa;QACb,KAAK,CAAC,QAAQ,CAAC,UAAU,GAAG,SAAS,CAAC;QACtC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;QACvD,MAAM,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QAE9D,OAAO,SAAS,CAAC;IACnB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,YAAY,CAAC,IAAY;QAC7B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;QAEvD,gBAAgB;QAChB,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC;QAC7D,MAAM,eAAe,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QACjE,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;QAE7C,aAAa;QACb,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;QACvD,MAAM,YAAY,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAC3D,MAAM,KAAK,GAAkB,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QAEtD,+BAA+B;QAC/B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAEjD,8DAA8D;QAC9D,OAAO,IAAI,cAAc,CAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;IACvD,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,eAAe,CACnB,IAAY,EACZ,UAAmB,EACnB,MAAqD;QAErD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;QAEvD,aAAa;QACb,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;QACvD,MAAM,YAAY,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAC3D,MAAM,KAAK,GAAkB,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QAEtD,0BAA0B;QAC1B,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC;aACpC,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,IAAI,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;aAChE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;QAEvB,4BAA4B;QAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEhC,wBAAwB;QACxB,MAAM,eAAe,GAAG,UAAU,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;QAEzE,gBAAgB;QAChB,MAAM,EAAE,CAAC,SAAS,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;QAE7C,OAAO,eAAe,CAAC;IACzB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,aAAa;QACjB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE;gBAClD,aAAa,EAAE,IAAI;aACpB,CAAC,CAAC;YAEH,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;YACjE,MAAM,SAAS,GAAsB,EAAE,CAAC;YAExC,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;gBACjC,IAAI,CAAC;oBACH,gBAAgB;oBAChB,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAC5B,IAAI,CAAC,YAAY,EACjB,QAAQ,CAAC,IAAI,EACb,eAAe,CAChB,CAAC;oBACF,MAAM,eAAe,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;oBACjE,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;oBAE7C,4BAA4B;oBAC5B,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CACzB,IAAI,CAAC,YAAY,EACjB,QAAQ,CAAC,IAAI,EACb,YAAY,CACb,CAAC;oBACF,MAAM,YAAY,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;oBAC3D,MAAM,KAAK,GAAkB,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;oBAEtD,SAAS,CAAC,IAAI,CAAC;wBACb,IAAI,EAAE,QAAQ,CAAC,IAAI;wBACnB,IAAI,EAAE,QAAQ,CAAC,SAAS;wBACxB,SAAS,EAAE,KAAK,CAAC,QAAQ,CAAC,UAAU;wBACpC,UAAU,EAAE,CAAC;qBACd,CAAC,CAAC;gBACL,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,gEAAgE;oBAChE,SAAS,CAAC,IAAI,CAAC;wBACb,IAAI,EAAE,QAAQ,CAAC,IAAI;wBACnB,IAAI,EAAE,SAAS;wBACf,SAAS,EAAE,CAAC;wBACZ,UAAU,EAAE,CAAC;qBACd,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;YAED,OAAO,SAAS,CAAC;QACnB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACvD,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,67 @@
1
+ import { Dataset } from "crawlee";
2
+ import { logger } from "../../lib/logger";
3
+ import { BreakcheckCrawler } from "../crawler";
4
+ export { LoadedSnapshot } from "./classes/LoadedSnapshot";
5
+ export { SnapshotRepository } from "./classes/SnapshotRepository";
6
+ /**
7
+ * Creates a snapshot of a website based on the provided configuration.
8
+ * Orchestrates calls to Crawler and Snapshot Manager.
9
+ */
10
+ export async function createSnapshot(config, snapshotRepository) {
11
+ const startTime = Date.now();
12
+ const errors = [];
13
+ try {
14
+ // Validate input config
15
+ if (!config.baseUrl) {
16
+ logger.error({ config }, "Snapshot creation failed: baseUrl is required");
17
+ throw new Error("baseUrl is required");
18
+ }
19
+ if (!config.name) {
20
+ logger.error({ config }, "Snapshot creation failed: name is required");
21
+ throw new Error("name is required");
22
+ }
23
+ const crawler = new BreakcheckCrawler(config.crawlSettings);
24
+ const { datasetName, errors: crawlErrors } = await crawler.crawl();
25
+ errors.push(...crawlErrors);
26
+ const dataset = await Dataset.open(datasetName);
27
+ const pageCount = await snapshotRepository.saveSnapshot(config.name, {
28
+ dataset,
29
+ metadata: {
30
+ baseUrl: config.baseUrl,
31
+ timestamp: new Date().toISOString(),
32
+ crawlSettings: config.crawlSettings,
33
+ },
34
+ });
35
+ let urlListPath;
36
+ if (config.urlListPath) {
37
+ urlListPath = await snapshotRepository.generateUrlList(config.name, config.urlListPath);
38
+ }
39
+ const duration = Date.now() - startTime;
40
+ return {
41
+ status: "success",
42
+ snapshotId: config.name,
43
+ timestamp: new Date().toISOString(),
44
+ baseUrl: config.baseUrl,
45
+ pageCount,
46
+ errors: errors,
47
+ metadata: {
48
+ crawlSettings: config.crawlSettings,
49
+ durationMs: duration,
50
+ },
51
+ urlListPath,
52
+ };
53
+ }
54
+ catch (error) {
55
+ errors.push({
56
+ url: config.baseUrl,
57
+ message: error instanceof Error ? error.message : "Unknown error occurred",
58
+ });
59
+ return {
60
+ status: "failed",
61
+ snapshotId: config.name,
62
+ message: error instanceof Error ? error.message : "Unknown error occurred",
63
+ errors,
64
+ };
65
+ }
66
+ }
67
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/snapshot/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { gunzip } from "zlib";
4
+ import { logger } from "../../lib/logger";
5
+ export const createIndexHandler = (comparisonDir) => {
6
+ return async (req, res) => {
7
+ try {
8
+ const indexPath = path.join(comparisonDir, "index.json");
9
+ const fileData = await fs.readFile(indexPath, "utf-8");
10
+ const indexData = JSON.parse(fileData);
11
+ const comparisonName = path.basename(comparisonDir);
12
+ const pages = Object.entries(indexData.urls).map(([url, page]) => ({
13
+ url,
14
+ ...page,
15
+ }));
16
+ const pagesWithDifferences = pages.filter((p) => p.hasDifferences);
17
+ const unchangedPages = pages.filter((p) => !p.hasDifferences);
18
+ res.render("index", {
19
+ comparisonName,
20
+ pagesWithDifferences,
21
+ unchangedPages,
22
+ });
23
+ }
24
+ catch (error) {
25
+ logger.error({ err: error }, "Error reading index file");
26
+ res.status(500).send("Error reading comparison data");
27
+ }
28
+ };
29
+ };
30
+ export const createDiffHandler = (comparisonDir) => {
31
+ return async (req, res) => {
32
+ try {
33
+ const pageUrl = req.query.page;
34
+ if (!pageUrl) {
35
+ res.status(400).send("Page URL is required");
36
+ return;
37
+ }
38
+ const indexPath = path.join(comparisonDir, "index.json");
39
+ const fileData = await fs.readFile(indexPath, "utf-8");
40
+ const indexData = JSON.parse(fileData);
41
+ const comparisonName = path.basename(comparisonDir);
42
+ const page = indexData.urls[pageUrl];
43
+ if (!page) {
44
+ res.status(404).send("Page not found");
45
+ return;
46
+ }
47
+ const diffPath = path.join(comparisonDir, "diffs", page.filename);
48
+ const compressedData = await fs.readFile(diffPath);
49
+ const decompressedData = await new Promise((resolve, reject) => {
50
+ gunzip(compressedData, (err, data) => {
51
+ if (err)
52
+ reject(err);
53
+ else
54
+ resolve(data);
55
+ });
56
+ });
57
+ const diffData = JSON.parse(decompressedData.toString());
58
+ res.render("diff", {
59
+ comparisonName,
60
+ pageUrl,
61
+ diffData,
62
+ });
63
+ }
64
+ catch (error) {
65
+ logger.error({ err: error }, "Error reading diff file");
66
+ res.status(500).send("Error reading diff data");
67
+ }
68
+ };
69
+ };
70
+ //# sourceMappingURL=index.handlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.handlers.js","sourceRoot":"","sources":["../../../src/core/view/index.handlers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import express from "express";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { logger } from "../../lib/logger";
5
+ import { findRootDir } from "../../lib/root";
6
+ import { createDiffHandler, createIndexHandler } from "./index.handlers";
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ export async function startViewServer(comparisonName, port = 8080) {
10
+ const app = express();
11
+ const rootDir = await findRootDir();
12
+ const comparisonDir = path.join(rootDir, "comparisons", comparisonName);
13
+ app.set("view engine", "ejs");
14
+ app.set("views", path.join(__dirname, "..", "..", "views"));
15
+ app.get("/", createIndexHandler(comparisonDir));
16
+ app.get("/diff", createDiffHandler(comparisonDir));
17
+ app.use(express.static(path.join(__dirname, "..", "..", "public")));
18
+ return new Promise((resolve, reject) => {
19
+ const server = app.listen(port, () => {
20
+ // Once listening, we no longer need the startup error handler
21
+ server.removeListener('error', startupErrorHandler);
22
+ logger.info(`🌐 View server started at http://localhost:${port}`);
23
+ resolve(server);
24
+ });
25
+ // Define a specific error handler for startup
26
+ const startupErrorHandler = (err) => {
27
+ if (err.code === 'EADDRINUSE') {
28
+ reject(new Error(`Port ${port} is already in use.`));
29
+ }
30
+ else {
31
+ reject(err);
32
+ }
33
+ };
34
+ server.on('error', startupErrorHandler);
35
+ });
36
+ }
37
+ export async function startCliViewServer(comparisonName, port) {
38
+ const server = await startViewServer(comparisonName, port);
39
+ logger.info("Press Ctrl+C to stop the server");
40
+ const shutdown = () => {
41
+ logger.info("\nGracefully shutting down. Please wait...");
42
+ server.close(() => {
43
+ logger.info("✅ Server has been shut down.");
44
+ process.exit(0);
45
+ });
46
+ };
47
+ process.on("SIGINT", shutdown);
48
+ process.on("SIGTERM", shutdown);
49
+ }
50
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/view/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ export * from "./api/index";
2
+ export { createSnapshotFromConfig, listSnapshots, runComparison } from "./api/index";
3
+ export * from "./core/view";
4
+ export { startCliViewServer } from "./core/view";
5
+ export * from "./lib/logger";
6
+ export { createLogger, logger } from "./lib/logger";
7
+ export * from "./types/api";
8
+ export type { ComparisonConfig, SnapshotConfig, SnapshotResult } from "./types/api";
9
+ export * from "./types/compare";
10
+ export * from "./types/crawler";
11
+ export type { CrawlError } from "./types/crawler";
12
+ export * from "./types/rules";
13
+ export * from "./types/snapshot";
14
+ export type { SnapshotSummary } from "./types/snapshot";
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ export * from "./api/index";
2
+ // Explicit exports for CLI and other consumers
3
+ export { createSnapshotFromConfig, listSnapshots, runComparison } from "./api/index";
4
+ export * from "./core/view";
5
+ export { startCliViewServer } from "./core/view";
6
+ export * from "./lib/logger";
7
+ export { createLogger, logger } from "./lib/logger";
8
+ export * from "./types/api";
9
+ export * from "./types/compare";
10
+ export * from "./types/crawler";
11
+ export * from "./types/rules";
12
+ export * from "./types/snapshot";
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,+CAA+C;AAC/C,OAAO,EACH,wBAAwB,EACxB,aAAa,EACb,aAAa,EAChB,MAAM,aAAa,CAAC;AACrB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACpD,cAAc,aAAa,CAAC;AAM5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAEhC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC"}
@@ -0,0 +1,26 @@
1
+ import pino from "pino";
2
+ // Determine the log level from the environment variable, defaulting to 'info'
3
+ const logLevel = process.env.LOG_LEVEL || "info";
4
+ // Create a logger with the specified configuration
5
+ export function createLogger(config = {}) {
6
+ const { useJsonLogs = false, level = logLevel } = config;
7
+ // Configure transport based on useJsonLogs flag
8
+ const transport = useJsonLogs
9
+ ? undefined // Use standard JSON logging
10
+ : {
11
+ target: "pino-pretty",
12
+ options: {
13
+ colorize: true,
14
+ translateTime: "SYS:standard",
15
+ ignore: "pid,hostname",
16
+ },
17
+ };
18
+ return pino({
19
+ level,
20
+ transport,
21
+ });
22
+ }
23
+ // Create the default shared logger instance (for backward compatibility)
24
+ // This will use pretty logging by default for CLI, but can be overridden
25
+ export const logger = createLogger({ useJsonLogs: false });
26
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAKxB,8EAA8E;AAC9E,MAAM,QAAQ,GAAc,OAAO,CAAC,GAAG,CAAC,SAAsB,IAAI,MAAM,CAAC;AAQzE,mDAAmD;AACnD,MAAM,UAAU,YAAY,CAAC,SAAuB,EAAE;IACpD,MAAM,EAAE,WAAW,GAAG,KAAK,EAAE,KAAK,GAAG,QAAQ,EAAE,GAAG,MAAM,CAAC;IAEzD,gDAAgD;IAChD,MAAM,SAAS,GAAG,WAAW;QAC3B,CAAC,CAAC,SAAS,CAAC,4BAA4B;QACxC,CAAC,CAAC;YACE,MAAM,EAAE,aAAa;YACrB,OAAO,EAAE;gBACP,QAAQ,EAAE,IAAI;gBACd,aAAa,EAAE,cAAc;gBAC7B,MAAM,EAAE,cAAc;aACvB;SACF,CAAC;IAEN,OAAO,IAAI,CAAC;QACV,KAAK;QACL,SAAS;KACV,CAAC,CAAC;AACL,CAAC;AAED,yEAAyE;AACzE,yEAAyE;AACzE,MAAM,CAAC,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { findUp, pathExists } from "find-up";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ /**
5
+ * Finds the root directory of the project by looking for a package.json file
6
+ * that contains a "workspaces" configuration.
7
+ *
8
+ * @returns The absolute path to the root directory
9
+ */
10
+ export async function findRootDir() {
11
+ const rootPackageJsonPath = await findUp(async (directory) => {
12
+ const packageJsonPath = path.join(directory, "package.json");
13
+ const exists = await pathExists(packageJsonPath);
14
+ if (exists) {
15
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
16
+ if (packageJson.workspaces) {
17
+ return packageJsonPath;
18
+ }
19
+ }
20
+ }, { type: "file" });
21
+ return rootPackageJsonPath
22
+ ? path.dirname(rootPackageJsonPath)
23
+ : process.cwd();
24
+ }
25
+ //# sourceMappingURL=root.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root.js","sourceRoot":"","sources":["../../src/lib/root.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAE,MAAM,aAAa,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,MAAM,mBAAmB,GAAG,MAAM,MAAM,CACtC,KAAK,EAAE,SAAS,EAAE,EAAE;QAClB,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;QAC7D,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,eAAe,CAAC,CAAC;QACjD,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAC5B,MAAM,EAAE,CAAC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,CAC3C,CAAC;YACF,IAAI,WAAW,CAAC,UAAU,EAAE,CAAC;gBAC3B,OAAO,eAAe,CAAC;YACzB,CAAC;QACH,CAAC;IACH,CAAC,EACD,EAAE,IAAI,EAAE,MAAM,EAAE,CACjB,CAAC;IAEF,OAAO,mBAAmB;QACxB,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,mBAAmB,CAAC;QACnC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;AACpB,CAAC"}