@pathscale/rebuild-plugin-ui-css-purge 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.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # bun-plugin-ui-css-purge
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Lib-side database generator.
3
+ *
4
+ * Reads all `*.classnames.ts` files from @pathscale/ui's component tree
5
+ * and produces a `purge-manifest.json` that the consumer-side plugin uses
6
+ * to build safelists.
7
+ *
8
+ * Usage: bun run src/generate-manifest.ts <path-to-ui-src/components>
9
+ * Output: purge-manifest.json in cwd (or pass --out <path>)
10
+ */
11
+ export {};
@@ -0,0 +1,2 @@
1
+ export { pluginCssPurge } from "./rsbuild-plugin";
2
+ export type { CssPurgeOptions } from "./rsbuild-plugin";
package/dist/index.js ADDED
@@ -0,0 +1,337 @@
1
+ // src/rsbuild-plugin.ts
2
+ import { PurgeCSS } from "purgecss";
3
+ import postcss from "postcss";
4
+ import swc from "@swc/core";
5
+ import path from "path";
6
+ import { readFile as nodeReadFile } from "node:fs/promises";
7
+ import { glob as fastGlob } from "fast-glob";
8
+ function walkAST(node, visitor) {
9
+ if (!node || typeof node !== "object")
10
+ return;
11
+ visitor(node);
12
+ for (const key of Object.keys(node)) {
13
+ if (key === "span")
14
+ continue;
15
+ const val = node[key];
16
+ if (Array.isArray(val)) {
17
+ for (const item of val)
18
+ walkAST(item, visitor);
19
+ } else if (val && typeof val === "object") {
20
+ walkAST(val, visitor);
21
+ }
22
+ }
23
+ }
24
+ function extractUIImports(ast) {
25
+ const imports = new Map;
26
+ for (const node of ast.body) {
27
+ if (node.type !== "ImportDeclaration")
28
+ continue;
29
+ const src = node.source?.value;
30
+ if (!src || !src.startsWith("@pathscale/ui"))
31
+ continue;
32
+ for (const spec of node.specifiers) {
33
+ if (spec.type === "ImportSpecifier") {
34
+ const imported = spec.imported?.value ?? spec.local?.value;
35
+ const local = spec.local?.value;
36
+ if (local && imported)
37
+ imports.set(local, imported);
38
+ } else if (spec.type === "ImportDefaultSpecifier") {
39
+ const local = spec.local?.value;
40
+ if (local)
41
+ imports.set(local, local);
42
+ }
43
+ }
44
+ }
45
+ return imports;
46
+ }
47
+ function extractJSXUsages(ast, uiComponents) {
48
+ const usages = [];
49
+ walkAST(ast, (node) => {
50
+ if (node.type !== "JSXOpeningElement")
51
+ return;
52
+ let elementName = null;
53
+ let rootName = null;
54
+ if (node.name?.type === "Identifier") {
55
+ elementName = node.name.value;
56
+ rootName = elementName;
57
+ } else if (node.name?.type === "JSXMemberExpression") {
58
+ const parts = [];
59
+ let cursor = node.name;
60
+ while (cursor?.type === "JSXMemberExpression") {
61
+ parts.unshift(cursor.property?.value);
62
+ cursor = cursor.object;
63
+ }
64
+ if (cursor?.type === "Identifier") {
65
+ parts.unshift(cursor.value);
66
+ rootName = cursor.value;
67
+ }
68
+ elementName = parts.join(".");
69
+ }
70
+ if (!rootName || !uiComponents.has(rootName))
71
+ return;
72
+ const usage = {
73
+ component: elementName,
74
+ props: new Map,
75
+ booleanProps: new Set,
76
+ hasSpread: false
77
+ };
78
+ for (const attr of node.attributes || []) {
79
+ if (attr.type === "SpreadElement" || attr.type === "JSXSpreadAttribute") {
80
+ usage.hasSpread = true;
81
+ continue;
82
+ }
83
+ if (attr.type !== "JSXAttribute")
84
+ continue;
85
+ const propName = attr.name?.value;
86
+ if (!propName)
87
+ continue;
88
+ if (!attr.value) {
89
+ usage.booleanProps.add(propName);
90
+ } else if (attr.value.type === "StringLiteral") {
91
+ usage.props.set(propName, attr.value.value);
92
+ } else {
93
+ usage.props.set(propName, "DYNAMIC");
94
+ }
95
+ }
96
+ usages.push(usage);
97
+ });
98
+ return usages;
99
+ }
100
+ function buildSafelists(allUsages, manifest) {
101
+ const classSafelist = new Set;
102
+ const attrSafelist = new Set;
103
+ const componentUsages = new Map;
104
+ for (const usage of allUsages) {
105
+ const existing = componentUsages.get(usage.component) ?? [];
106
+ existing.push(usage);
107
+ componentUsages.set(usage.component, existing);
108
+ }
109
+ for (const [entryName, entry] of Object.entries(manifest)) {
110
+ const matchingUsages = findMatchingUsages(entryName, componentUsages);
111
+ if (matchingUsages.length === 0)
112
+ continue;
113
+ for (const cls of entry.classes.always)
114
+ classSafelist.add(cls);
115
+ for (const [propOrSlot, value] of Object.entries(entry.classes.byProp)) {
116
+ if (Array.isArray(value)) {
117
+ if (isPropUsed(propOrSlot, matchingUsages)) {
118
+ for (const cls of value)
119
+ classSafelist.add(cls);
120
+ }
121
+ } else {
122
+ const usedValues = getUsedEnumValues(propOrSlot, matchingUsages);
123
+ if (usedValues === "ALL") {
124
+ for (const classes of Object.values(value)) {
125
+ for (const cls of classes)
126
+ classSafelist.add(cls);
127
+ }
128
+ } else {
129
+ for (const val of usedValues) {
130
+ if (value[val]) {
131
+ for (const cls of value[val])
132
+ classSafelist.add(cls);
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+ if (entry.attrs) {
139
+ for (const [propName, attrMap] of Object.entries(entry.attrs)) {
140
+ if (isPropUsed(propName, matchingUsages)) {
141
+ for (const [attr, val] of Object.entries(attrMap)) {
142
+ attrSafelist.add(`[${attr}="${val}"]`);
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
148
+ return { classSafelist, attrSafelist };
149
+ }
150
+ function findMatchingUsages(entryName, usageMap) {
151
+ if (usageMap.has(entryName))
152
+ return usageMap.get(entryName);
153
+ const results = [];
154
+ for (const [usageName, usages] of usageMap) {
155
+ if (usageName === entryName) {
156
+ results.push(...usages);
157
+ }
158
+ if (entryName.includes(".")) {
159
+ const [family, part] = entryName.split(".");
160
+ if (part === family && usageName === family) {
161
+ results.push(...usages);
162
+ }
163
+ }
164
+ }
165
+ return results;
166
+ }
167
+ function isPropUsed(propName, usages) {
168
+ for (const usage of usages) {
169
+ if (usage.hasSpread)
170
+ return true;
171
+ if (usage.booleanProps.has(propName))
172
+ return true;
173
+ if (usage.props.has(propName))
174
+ return true;
175
+ }
176
+ return false;
177
+ }
178
+ function getUsedEnumValues(slotName, usages) {
179
+ const values = new Set;
180
+ for (const usage of usages) {
181
+ if (usage.hasSpread)
182
+ return "ALL";
183
+ const val = usage.props.get(slotName);
184
+ if (val === "DYNAMIC")
185
+ return "ALL";
186
+ if (val !== undefined)
187
+ values.add(val);
188
+ if (usage.booleanProps.has(slotName))
189
+ return "ALL";
190
+ }
191
+ return values;
192
+ }
193
+ async function scanConsumerSource(srcDir) {
194
+ const allUsages = [];
195
+ const files = await fastGlob("**/*.{tsx,ts,jsx,js}", { cwd: srcDir, ignore: ["**/node_modules/**"] });
196
+ for (const relPath of files) {
197
+ const fullPath = path.join(srcDir, relPath);
198
+ const code = await nodeReadFile(fullPath, "utf-8");
199
+ if (!code.includes("@pathscale/ui"))
200
+ continue;
201
+ const isTsx = /\.[tj]sx$/.test(relPath);
202
+ const ast = await swc.parse(code, { syntax: "typescript", tsx: isTsx });
203
+ const uiImports = extractUIImports(ast);
204
+ if (uiImports.size === 0)
205
+ continue;
206
+ allUsages.push(...extractJSXUsages(ast, uiImports));
207
+ }
208
+ return allUsages;
209
+ }
210
+ function purgeAttributes(css, attrSafelist) {
211
+ const root = postcss.parse(css);
212
+ root.walkRules((rule) => {
213
+ const selectors = rule.selectors;
214
+ const kept = [];
215
+ for (const sel of selectors) {
216
+ const attrMatches = sel.matchAll(/\[(data-[a-z-]+|aria-[a-z-]+)="([^"]+)"\]/g);
217
+ let shouldKeep = true;
218
+ for (const match of attrMatches) {
219
+ const attrSelector = `[${match[1]}="${match[2]}"]`;
220
+ if (match[1] === "data-slot")
221
+ continue;
222
+ if (!attrSafelist.has(attrSelector)) {
223
+ shouldKeep = false;
224
+ break;
225
+ }
226
+ }
227
+ if (shouldKeep)
228
+ kept.push(sel);
229
+ }
230
+ if (kept.length === 0) {
231
+ rule.remove();
232
+ } else if (kept.length < selectors.length) {
233
+ rule.selectors = kept;
234
+ }
235
+ });
236
+ return root.toString();
237
+ }
238
+ function cleanUnusedVars(css) {
239
+ let changed = true;
240
+ let result = css;
241
+ while (changed) {
242
+ changed = false;
243
+ const root = postcss.parse(result);
244
+ const declared = new Map;
245
+ root.walkDecls(/^--/, (decl) => {
246
+ const entries = declared.get(decl.prop) ?? [];
247
+ entries.push({ rule: decl.parent, prop: decl.prop, index: entries.length });
248
+ declared.set(decl.prop, entries);
249
+ });
250
+ const referenced = new Set;
251
+ root.walkDecls((decl) => {
252
+ const refs = decl.value.matchAll(/var\(\s*(--[a-zA-Z0-9_-]+)/g);
253
+ for (const ref of refs) {
254
+ referenced.add(ref[1]);
255
+ }
256
+ });
257
+ for (const [varName, entries] of declared) {
258
+ if (!referenced.has(varName)) {
259
+ for (const entry of entries) {
260
+ entry.rule.walkDecls(entry.prop, (decl) => {
261
+ decl.remove();
262
+ changed = true;
263
+ });
264
+ }
265
+ }
266
+ }
267
+ root.walkRules((rule) => {
268
+ if (rule.nodes && rule.nodes.length === 0)
269
+ rule.remove();
270
+ });
271
+ root.walkAtRules((atRule) => {
272
+ if (atRule.nodes && atRule.nodes.length === 0)
273
+ atRule.remove();
274
+ });
275
+ result = root.toString();
276
+ }
277
+ return result;
278
+ }
279
+ var pluginCssPurge = (options) => ({
280
+ name: "plugin-css-purge",
281
+ setup(api) {
282
+ const {
283
+ manifest: manifestPath,
284
+ srcDir = "src",
285
+ attrPurge = true,
286
+ cleanVars = true,
287
+ verbose = true
288
+ } = options;
289
+ api.processAssets({ stage: "optimize-size" }, async ({ assets, sources, compilation }) => {
290
+ const log = verbose ? console.log.bind(console) : () => {};
291
+ const manifest = JSON.parse(await nodeReadFile(path.resolve(manifestPath), "utf-8"));
292
+ log(`[css-purge] Manifest loaded: ${Object.keys(manifest).length} entries`);
293
+ const resolvedSrc = path.resolve(srcDir);
294
+ const usages = await scanConsumerSource(resolvedSrc);
295
+ log(`[css-purge] Scanned ${resolvedSrc}: ${usages.length} component usages`);
296
+ const { classSafelist, attrSafelist } = buildSafelists(usages, manifest);
297
+ log(`[css-purge] Safelist: ${classSafelist.size} classes, ${attrSafelist.size} attrs`);
298
+ for (const [name, asset] of Object.entries(assets)) {
299
+ if (!name.endsWith(".css"))
300
+ continue;
301
+ const originalCss = asset.source().toString();
302
+ const originalSize = Buffer.byteLength(originalCss, "utf-8");
303
+ log(`[css-purge] Processing ${name} (${(originalSize / 1024).toFixed(1)} KB)`);
304
+ const purgeResult = await new PurgeCSS().purge({
305
+ content: [],
306
+ css: [{ raw: originalCss }],
307
+ safelist: [...classSafelist],
308
+ keyframes: false,
309
+ fontFace: false
310
+ });
311
+ let purgedCss = purgeResult[0]?.css ?? originalCss;
312
+ const afterL1 = Buffer.byteLength(purgedCss, "utf-8");
313
+ log(`[css-purge] L1 class purge: ${(originalSize / 1024).toFixed(1)} → ${(afterL1 / 1024).toFixed(1)} KB`);
314
+ if (attrPurge && attrSafelist.size > 0) {
315
+ purgedCss = purgeAttributes(purgedCss, attrSafelist);
316
+ const afterL2 = Buffer.byteLength(purgedCss, "utf-8");
317
+ log(`[css-purge] L2 attr purge: ${(afterL1 / 1024).toFixed(1)} → ${(afterL2 / 1024).toFixed(1)} KB`);
318
+ }
319
+ if (cleanVars) {
320
+ purgedCss = cleanUnusedVars(purgedCss);
321
+ const afterL3 = Buffer.byteLength(purgedCss, "utf-8");
322
+ log(`[css-purge] L3 var cleanup: → ${(afterL3 / 1024).toFixed(1)} KB`);
323
+ }
324
+ const finalSize = Buffer.byteLength(purgedCss, "utf-8");
325
+ log(`[css-purge] Final: ${(originalSize / 1024).toFixed(1)} → ${(finalSize / 1024).toFixed(1)} KB (${((1 - finalSize / originalSize) * 100).toFixed(1)}% reduction)`);
326
+ const source = new sources.RawSource(purgedCss);
327
+ compilation.updateAsset(name, source);
328
+ }
329
+ });
330
+ }
331
+ });
332
+ export {
333
+ pluginCssPurge
334
+ };
335
+
336
+ //# debugId=CB5B2093063663E864756E2164756E21
337
+ //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi4vc3JjL3JzYnVpbGQtcGx1Z2luLnRzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWwogICAgIi8qKlxuICogcnNidWlsZC1wbHVnaW4tY3NzLXB1cmdlXG4gKlxuICogVHdvLWxldmVsIENTUyBwdXJnZSBmb3IgQHBhdGhzY2FsZS91aSBjb25zdW1lcnMuXG4gKlxuICogTGV2ZWwgMTogY2xhc3MtbGV2ZWwgcHVyZ2UgdmlhIHB1cmdlY3NzIOKAlCByZW1vdmVzIGVudGlyZSBydWxlcyB3aG9zZSBzZWxlY3RvcnNcbiAqICAgICAgICAgIGRvbid0IG1hdGNoIHRoZSBzYWZlbGlzdCBidWlsdCBmcm9tIGNvbnN1bWVyIEpTWCBhbmFseXNpcy5cbiAqIExldmVsIDI6IGF0dHJpYnV0ZS1sZXZlbCBwdXJnZSDigJQgd2l0aGluIGtlcHQgcnVsZXMsIHN0cmlwcyBjb21wb3VuZCBzZWxlY3RvcnNcbiAqICAgICAgICAgIGNvbnRhaW5pbmcgZGF0YS1hdHRyIC8gYXJpYS1hdHRyIGF0dHJpYnV0ZSBzZWxlY3RvcnMgbm90IGluIHRoZSBhdHRyIHNhZmVsaXN0LlxuICpcbiAqIFVzYWdlIGluIHJzYnVpbGQuY29uZmlnLnRzOlxuICogICBpbXBvcnQgeyBwbHVnaW5Dc3NQdXJnZSB9IGZyb20gXCJAcGF0aHNjYWxlL3JlYnVpbGQtcGx1Z2luLXVpLWNzcy1wdXJnZVwiO1xuICogICBleHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoeyBwbHVnaW5zOiBbcGx1Z2luQ3NzUHVyZ2UoeyBtYW5pZmVzdDogXCIuLi5cIiB9KV0gfSk7XG4gKi9cblxuaW1wb3J0IHR5cGUgeyBSc2J1aWxkUGx1Z2luIH0gZnJvbSBcIkByc2J1aWxkL2NvcmVcIjtcbmltcG9ydCB7IFB1cmdlQ1NTIH0gZnJvbSBcInB1cmdlY3NzXCI7XG5pbXBvcnQgcG9zdGNzcyBmcm9tIFwicG9zdGNzc1wiO1xuaW1wb3J0IHR5cGUgeyBSdWxlLCBBdFJ1bGUgfSBmcm9tIFwicG9zdGNzc1wiO1xuaW1wb3J0IHN3YyBmcm9tIFwiQHN3Yy9jb3JlXCI7XG5pbXBvcnQgcGF0aCBmcm9tIFwicGF0aFwiO1xuaW1wb3J0IHsgcmVhZEZpbGUgYXMgbm9kZVJlYWRGaWxlIH0gZnJvbSBcIm5vZGU6ZnMvcHJvbWlzZXNcIjtcbmltcG9ydCB7IGdsb2IgYXMgZmFzdEdsb2IgfSBmcm9tIFwiZmFzdC1nbG9iXCI7XG5cbi8vIOKUgOKUgCBUeXBlcyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIBcblxuaW50ZXJmYWNlIENvbXBvbmVudE1hbmlmZXN0IHtcbiAgY2xhc3Nlczoge1xuICAgIGFsd2F5czogc3RyaW5nW107XG4gICAgYnlQcm9wOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmdbXSB8IFJlY29yZDxzdHJpbmcsIHN0cmluZ1tdPj47XG4gIH07XG4gIGF0dHJzPzogUmVjb3JkPHN0cmluZywgUmVjb3JkPHN0cmluZywgc3RyaW5nPj47XG59XG5cbnR5cGUgUHVyZ2VNYW5pZmVzdCA9IFJlY29yZDxzdHJpbmcsIENvbXBvbmVudE1hbmlmZXN0PjtcblxuaW50ZXJmYWNlIFByb3BVc2FnZSB7XG4gIGNvbXBvbmVudDogc3RyaW5nO1xuICBwcm9wczogTWFwPHN0cmluZywgc3RyaW5nIHwgXCJEWU5BTUlDXCI+O1xuICBib29sZWFuUHJvcHM6IFNldDxzdHJpbmc+O1xuICBoYXNTcHJlYWQ6IGJvb2xlYW47XG59XG5cbi8vIOKUgOKUgCBQbHVnaW4gb3B0aW9ucyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIBcblxuZXhwb3J0IGludGVyZmFjZSBDc3NQdXJnZU9wdGlvbnMge1xuICAvKiogUGF0aCB0byBwdXJnZS1tYW5pZmVzdC5qc29uIChnZW5lcmF0ZWQgYnkgZ2VuZXJhdGUtbWFuaWZlc3QudHMpICovXG4gIG1hbmlmZXN0OiBzdHJpbmc7XG4gIC8qKiBDb25zdW1lciBzb3VyY2UgZGlyZWN0b3J5IHRvIHNjYW4gZm9yIEpTWCB1c2FnZSAoZGVmYXVsdDogXCJzcmNcIikgKi9cbiAgc3JjRGlyPzogc3RyaW5nO1xuICAvKiogRW5hYmxlIExldmVsIDIgYXR0cmlidXRlIHB1cmdlIChkZWZhdWx0OiB0cnVlKSAqL1xuICBhdHRyUHVyZ2U/OiBib29sZWFuO1xuICAvKiogRW5hYmxlIENTUyB2YXJpYWJsZSBjbGVhbnVwIChkZWZhdWx0OiB0cnVlKSAqL1xuICBjbGVhblZhcnM/OiBib29sZWFuO1xuICAvKiogTG9nIHB1cmdlIHN0YXRzIChkZWZhdWx0OiB0cnVlKSAqL1xuICB2ZXJib3NlPzogYm9vbGVhbjtcbn1cblxuLy8g4pSA4pSAIEFTVCB1dGlsaXRpZXMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSAXG5cbmZ1bmN0aW9uIHdhbGtBU1Qobm9kZTogYW55LCB2aXNpdG9yOiAobm9kZTogYW55KSA9PiB2b2lkKSB7XG4gIGlmICghbm9kZSB8fCB0eXBlb2Ygbm9kZSAhPT0gXCJvYmplY3RcIikgcmV0dXJuO1xuICB2aXNpdG9yKG5vZGUpO1xuICBmb3IgKGNvbnN0IGtleSBvZiBPYmplY3Qua2V5cyhub2RlKSkge1xuICAgIGlmIChrZXkgPT09IFwic3BhblwiKSBjb250aW51ZTtcbiAgICBjb25zdCB2YWwgPSBub2RlW2tleV07XG4gICAgaWYgKEFycmF5LmlzQXJyYXkodmFsKSkge1xuICAgICAgZm9yIChjb25zdCBpdGVtIG9mIHZhbCkgd2Fsa0FTVChpdGVtLCB2aXNpdG9yKTtcbiAgICB9IGVsc2UgaWYgKHZhbCAmJiB0eXBlb2YgdmFsID09PSBcIm9iamVjdFwiKSB7XG4gICAgICB3YWxrQVNUKHZhbCwgdmlzaXRvcik7XG4gICAgfVxuICB9XG59XG5cbmZ1bmN0aW9uIGV4dHJhY3RVSUltcG9ydHMoYXN0OiBhbnkpOiBNYXA8c3RyaW5nLCBzdHJpbmc+IHtcbiAgY29uc3QgaW1wb3J0cyA9IG5ldyBNYXA8c3RyaW5nLCBzdHJpbmc+KCk7XG4gIGZvciAoY29uc3Qgbm9kZSBvZiBhc3QuYm9keSkge1xuICAgIGlmIChub2RlLnR5cGUgIT09IFwiSW1wb3J0RGVjbGFyYXRpb25cIikgY29udGludWU7XG4gICAgY29uc3Qgc3JjID0gbm9kZS5zb3VyY2U/LnZhbHVlIGFzIHN0cmluZztcbiAgICBpZiAoIXNyYyB8fCAhc3JjLnN0YXJ0c1dpdGgoXCJAcGF0aHNjYWxlL3VpXCIpKSBjb250aW51ZTtcbiAgICBmb3IgKGNvbnN0IHNwZWMgb2Ygbm9kZS5zcGVjaWZpZXJzKSB7XG4gICAgICBpZiAoc3BlYy50eXBlID09PSBcIkltcG9ydFNwZWNpZmllclwiKSB7XG4gICAgICAgIGNvbnN0IGltcG9ydGVkID0gc3BlYy5pbXBvcnRlZD8udmFsdWUgPz8gc3BlYy5sb2NhbD8udmFsdWU7XG4gICAgICAgIGNvbnN0IGxvY2FsID0gc3BlYy5sb2NhbD8udmFsdWU7XG4gICAgICAgIGlmIChsb2NhbCAmJiBpbXBvcnRlZCkgaW1wb3J0cy5zZXQobG9jYWwsIGltcG9ydGVkKTtcbiAgICAgIH0gZWxzZSBpZiAoc3BlYy50eXBlID09PSBcIkltcG9ydERlZmF1bHRTcGVjaWZpZXJcIikge1xuICAgICAgICBjb25zdCBsb2NhbCA9IHNwZWMubG9jYWw/LnZhbHVlO1xuICAgICAgICBpZiAobG9jYWwpIGltcG9ydHMuc2V0KGxvY2FsLCBsb2NhbCk7XG4gICAgICB9XG4gICAgfVxuICB9XG4gIHJldHVybiBpbXBvcnRzO1xufVxuXG5mdW5jdGlvbiBleHRyYWN0SlNYVXNhZ2VzKGFzdDogYW55LCB1aUNvbXBvbmVudHM6IE1hcDxzdHJpbmcsIHN0cmluZz4pOiBQcm9wVXNhZ2VbXSB7XG4gIGNvbnN0IHVzYWdlczogUHJvcFVzYWdlW10gPSBbXTtcbiAgd2Fsa0FTVChhc3QsIChub2RlKSA9PiB7XG4gICAgaWYgKG5vZGUudHlwZSAhPT0gXCJKU1hPcGVuaW5nRWxlbWVudFwiKSByZXR1cm47XG4gICAgbGV0IGVsZW1lbnROYW1lOiBzdHJpbmcgfCBudWxsID0gbnVsbDtcbiAgICBsZXQgcm9vdE5hbWU6IHN0cmluZyB8IG51bGwgPSBudWxsO1xuICAgIGlmIChub2RlLm5hbWU/LnR5cGUgPT09IFwiSWRlbnRpZmllclwiKSB7XG4gICAgICBlbGVtZW50TmFtZSA9IG5vZGUubmFtZS52YWx1ZTtcbiAgICAgIHJvb3ROYW1lID0gZWxlbWVudE5hbWU7XG4gICAgfSBlbHNlIGlmIChub2RlLm5hbWU/LnR5cGUgPT09IFwiSlNYTWVtYmVyRXhwcmVzc2lvblwiKSB7XG4gICAgICBjb25zdCBwYXJ0czogc3RyaW5nW10gPSBbXTtcbiAgICAgIGxldCBjdXJzb3IgPSBub2RlLm5hbWU7XG4gICAgICB3aGlsZSAoY3Vyc29yPy50eXBlID09PSBcIkpTWE1lbWJlckV4cHJlc3Npb25cIikge1xuICAgICAgICBwYXJ0cy51bnNoaWZ0KGN1cnNvci5wcm9wZXJ0eT8udmFsdWUpO1xuICAgICAgICBjdXJzb3IgPSBjdXJzb3Iub2JqZWN0O1xuICAgICAgfVxuICAgICAgaWYgKGN1cnNvcj8udHlwZSA9PT0gXCJJZGVudGlmaWVyXCIpIHtcbiAgICAgICAgcGFydHMudW5zaGlmdChjdXJzb3IudmFsdWUpO1xuICAgICAgICByb290TmFtZSA9IGN1cnNvci52YWx1ZTtcbiAgICAgIH1cbiAgICAgIGVsZW1lbnROYW1lID0gcGFydHMuam9pbihcIi5cIik7XG4gICAgfVxuICAgIGlmICghcm9vdE5hbWUgfHwgIXVpQ29tcG9uZW50cy5oYXMocm9vdE5hbWUpKSByZXR1cm47XG4gICAgY29uc3QgdXNhZ2U6IFByb3BVc2FnZSA9IHtcbiAgICAgIGNvbXBvbmVudDogZWxlbWVudE5hbWUhLFxuICAgICAgcHJvcHM6IG5ldyBNYXAoKSxcbiAgICAgIGJvb2xlYW5Qcm9wczogbmV3IFNldCgpLFxuICAgICAgaGFzU3ByZWFkOiBmYWxzZSxcbiAgICB9O1xuICAgIGZvciAoY29uc3QgYXR0ciBvZiBub2RlLmF0dHJpYnV0ZXMgfHwgW10pIHtcbiAgICAgIGlmIChhdHRyLnR5cGUgPT09IFwiU3ByZWFkRWxlbWVudFwiIHx8IGF0dHIudHlwZSA9PT0gXCJKU1hTcHJlYWRBdHRyaWJ1dGVcIikge1xuICAgICAgICB1c2FnZS5oYXNTcHJlYWQgPSB0cnVlO1xuICAgICAgICBjb250aW51ZTtcbiAgICAgIH1cbiAgICAgIGlmIChhdHRyLnR5cGUgIT09IFwiSlNYQXR0cmlidXRlXCIpIGNvbnRpbnVlO1xuICAgICAgY29uc3QgcHJvcE5hbWUgPSBhdHRyLm5hbWU/LnZhbHVlO1xuICAgICAgaWYgKCFwcm9wTmFtZSkgY29udGludWU7XG4gICAgICBpZiAoIWF0dHIudmFsdWUpIHtcbiAgICAgICAgdXNhZ2UuYm9vbGVhblByb3BzLmFkZChwcm9wTmFtZSk7XG4gICAgICB9IGVsc2UgaWYgKGF0dHIudmFsdWUudHlwZSA9PT0gXCJTdHJpbmdMaXRlcmFsXCIpIHtcbiAgICAgICAgdXNhZ2UucHJvcHMuc2V0KHByb3BOYW1lLCBhdHRyLnZhbHVlLnZhbHVlKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHVzYWdlLnByb3BzLnNldChwcm9wTmFtZSwgXCJEWU5BTUlDXCIpO1xuICAgICAgfVxuICAgIH1cbiAgICB1c2FnZXMucHVzaCh1c2FnZSk7XG4gIH0pO1xuICByZXR1cm4gdXNhZ2VzO1xufVxuXG4vLyDilIDilIAgU2FmZWxpc3QgYnVpbGRlciDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIBcblxuZnVuY3Rpb24gYnVpbGRTYWZlbGlzdHMoXG4gIGFsbFVzYWdlczogUHJvcFVzYWdlW10sXG4gIG1hbmlmZXN0OiBQdXJnZU1hbmlmZXN0LFxuKTogeyBjbGFzc1NhZmVsaXN0OiBTZXQ8c3RyaW5nPjsgYXR0clNhZmVsaXN0OiBTZXQ8c3RyaW5nPiB9IHtcbiAgY29uc3QgY2xhc3NTYWZlbGlzdCA9IG5ldyBTZXQ8c3RyaW5nPigpO1xuICBjb25zdCBhdHRyU2FmZWxpc3QgPSBuZXcgU2V0PHN0cmluZz4oKTtcblxuICBjb25zdCBjb21wb25lbnRVc2FnZXMgPSBuZXcgTWFwPHN0cmluZywgUHJvcFVzYWdlW10+KCk7XG4gIGZvciAoY29uc3QgdXNhZ2Ugb2YgYWxsVXNhZ2VzKSB7XG4gICAgY29uc3QgZXhpc3RpbmcgPSBjb21wb25lbnRVc2FnZXMuZ2V0KHVzYWdlLmNvbXBvbmVudCkgPz8gW107XG4gICAgZXhpc3RpbmcucHVzaCh1c2FnZSk7XG4gICAgY29tcG9uZW50VXNhZ2VzLnNldCh1c2FnZS5jb21wb25lbnQsIGV4aXN0aW5nKTtcbiAgfVxuXG4gIGZvciAoY29uc3QgW2VudHJ5TmFtZSwgZW50cnldIG9mIE9iamVjdC5lbnRyaWVzKG1hbmlmZXN0KSkge1xuICAgIGNvbnN0IG1hdGNoaW5nVXNhZ2VzID0gZmluZE1hdGNoaW5nVXNhZ2VzKGVudHJ5TmFtZSwgY29tcG9uZW50VXNhZ2VzKTtcbiAgICBpZiAobWF0Y2hpbmdVc2FnZXMubGVuZ3RoID09PSAwKSBjb250aW51ZTtcblxuICAgIGZvciAoY29uc3QgY2xzIG9mIGVudHJ5LmNsYXNzZXMuYWx3YXlzKSBjbGFzc1NhZmVsaXN0LmFkZChjbHMpO1xuXG4gICAgZm9yIChjb25zdCBbcHJvcE9yU2xvdCwgdmFsdWVdIG9mIE9iamVjdC5lbnRyaWVzKGVudHJ5LmNsYXNzZXMuYnlQcm9wKSkge1xuICAgICAgaWYgKEFycmF5LmlzQXJyYXkodmFsdWUpKSB7XG4gICAgICAgIGlmIChpc1Byb3BVc2VkKHByb3BPclNsb3QsIG1hdGNoaW5nVXNhZ2VzKSkge1xuICAgICAgICAgIGZvciAoY29uc3QgY2xzIG9mIHZhbHVlKSBjbGFzc1NhZmVsaXN0LmFkZChjbHMpO1xuICAgICAgICB9XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBjb25zdCB1c2VkVmFsdWVzID0gZ2V0VXNlZEVudW1WYWx1ZXMocHJvcE9yU2xvdCwgbWF0Y2hpbmdVc2FnZXMpO1xuICAgICAgICBpZiAodXNlZFZhbHVlcyA9PT0gXCJBTExcIikge1xuICAgICAgICAgIGZvciAoY29uc3QgY2xhc3NlcyBvZiBPYmplY3QudmFsdWVzKHZhbHVlKSkge1xuICAgICAgICAgICAgZm9yIChjb25zdCBjbHMgb2YgY2xhc3NlcykgY2xhc3NTYWZlbGlzdC5hZGQoY2xzKTtcbiAgICAgICAgICB9XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgZm9yIChjb25zdCB2YWwgb2YgdXNlZFZhbHVlcykge1xuICAgICAgICAgICAgaWYgKHZhbHVlW3ZhbF0pIHtcbiAgICAgICAgICAgICAgZm9yIChjb25zdCBjbHMgb2YgdmFsdWVbdmFsXSkgY2xhc3NTYWZlbGlzdC5hZGQoY2xzKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG5cbiAgICBpZiAoZW50cnkuYXR0cnMpIHtcbiAgICAgIGZvciAoY29uc3QgW3Byb3BOYW1lLCBhdHRyTWFwXSBvZiBPYmplY3QuZW50cmllcyhlbnRyeS5hdHRycykpIHtcbiAgICAgICAgaWYgKGlzUHJvcFVzZWQocHJvcE5hbWUsIG1hdGNoaW5nVXNhZ2VzKSkge1xuICAgICAgICAgIGZvciAoY29uc3QgW2F0dHIsIHZhbF0gb2YgT2JqZWN0LmVudHJpZXMoYXR0ck1hcCkpIHtcbiAgICAgICAgICAgIGF0dHJTYWZlbGlzdC5hZGQoYFske2F0dHJ9PVwiJHt2YWx9XCJdYCk7XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgcmV0dXJuIHsgY2xhc3NTYWZlbGlzdCwgYXR0clNhZmVsaXN0IH07XG59XG5cbmZ1bmN0aW9uIGZpbmRNYXRjaGluZ1VzYWdlcyhlbnRyeU5hbWU6IHN0cmluZywgdXNhZ2VNYXA6IE1hcDxzdHJpbmcsIFByb3BVc2FnZVtdPik6IFByb3BVc2FnZVtdIHtcbiAgaWYgKHVzYWdlTWFwLmhhcyhlbnRyeU5hbWUpKSByZXR1cm4gdXNhZ2VNYXAuZ2V0KGVudHJ5TmFtZSkhO1xuICBjb25zdCByZXN1bHRzOiBQcm9wVXNhZ2VbXSA9IFtdO1xuICBmb3IgKGNvbnN0IFt1c2FnZU5hbWUsIHVzYWdlc10gb2YgdXNhZ2VNYXApIHtcbiAgICBpZiAodXNhZ2VOYW1lID09PSBlbnRyeU5hbWUpIHtcbiAgICAgIHJlc3VsdHMucHVzaCguLi51c2FnZXMpO1xuICAgIH1cbiAgICBpZiAoZW50cnlOYW1lLmluY2x1ZGVzKFwiLlwiKSkge1xuICAgICAgY29uc3QgW2ZhbWlseSwgcGFydF0gPSBlbnRyeU5hbWUuc3BsaXQoXCIuXCIpO1xuICAgICAgaWYgKHBhcnQgPT09IGZhbWlseSAmJiB1c2FnZU5hbWUgPT09IGZhbWlseSkge1xuICAgICAgICByZXN1bHRzLnB1c2goLi4udXNhZ2VzKTtcbiAgICAgIH1cbiAgICB9XG4gIH1cbiAgcmV0dXJuIHJlc3VsdHM7XG59XG5cbmZ1bmN0aW9uIGlzUHJvcFVzZWQocHJvcE5hbWU6IHN0cmluZywgdXNhZ2VzOiBQcm9wVXNhZ2VbXSk6IGJvb2xlYW4ge1xuICBmb3IgKGNvbnN0IHVzYWdlIG9mIHVzYWdlcykge1xuICAgIGlmICh1c2FnZS5oYXNTcHJlYWQpIHJldHVybiB0cnVlO1xuICAgIGlmICh1c2FnZS5ib29sZWFuUHJvcHMuaGFzKHByb3BOYW1lKSkgcmV0dXJuIHRydWU7XG4gICAgaWYgKHVzYWdlLnByb3BzLmhhcyhwcm9wTmFtZSkpIHJldHVybiB0cnVlO1xuICB9XG4gIHJldHVybiBmYWxzZTtcbn1cblxuZnVuY3Rpb24gZ2V0VXNlZEVudW1WYWx1ZXMoc2xvdE5hbWU6IHN0cmluZywgdXNhZ2VzOiBQcm9wVXNhZ2VbXSk6IFNldDxzdHJpbmc+IHwgXCJBTExcIiB7XG4gIGNvbnN0IHZhbHVlcyA9IG5ldyBTZXQ8c3RyaW5nPigpO1xuICBmb3IgKGNvbnN0IHVzYWdlIG9mIHVzYWdlcykge1xuICAgIGlmICh1c2FnZS5oYXNTcHJlYWQpIHJldHVybiBcIkFMTFwiO1xuICAgIGNvbnN0IHZhbCA9IHVzYWdlLnByb3BzLmdldChzbG90TmFtZSk7XG4gICAgaWYgKHZhbCA9PT0gXCJEWU5BTUlDXCIpIHJldHVybiBcIkFMTFwiO1xuICAgIGlmICh2YWwgIT09IHVuZGVmaW5lZCkgdmFsdWVzLmFkZCh2YWwpO1xuICAgIGlmICh1c2FnZS5ib29sZWFuUHJvcHMuaGFzKHNsb3ROYW1lKSkgcmV0dXJuIFwiQUxMXCI7XG4gIH1cbiAgcmV0dXJuIHZhbHVlcztcbn1cblxuLy8g4pSA4pSAIENvbnN1bWVyIHNvdXJjZSBzY2FubmluZyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIBcblxuYXN5bmMgZnVuY3Rpb24gc2NhbkNvbnN1bWVyU291cmNlKHNyY0Rpcjogc3RyaW5nKTogUHJvbWlzZTxQcm9wVXNhZ2VbXT4ge1xuICBjb25zdCBhbGxVc2FnZXM6IFByb3BVc2FnZVtdID0gW107XG4gIGNvbnN0IGZpbGVzID0gYXdhaXQgZmFzdEdsb2IoXCIqKi8qLnt0c3gsdHMsanN4LGpzfVwiLCB7IGN3ZDogc3JjRGlyLCBpZ25vcmU6IFtcIioqL25vZGVfbW9kdWxlcy8qKlwiXSB9KTtcblxuICBmb3IgKGNvbnN0IHJlbFBhdGggb2YgZmlsZXMpIHtcbiAgICBjb25zdCBmdWxsUGF0aCA9IHBhdGguam9pbihzcmNEaXIsIHJlbFBhdGgpO1xuICAgIGNvbnN0IGNvZGUgPSBhd2FpdCBub2RlUmVhZEZpbGUoZnVsbFBhdGgsIFwidXRmLThcIik7XG4gICAgaWYgKCFjb2RlLmluY2x1ZGVzKFwiQHBhdGhzY2FsZS91aVwiKSkgY29udGludWU7XG5cbiAgICBjb25zdCBpc1RzeCA9IC9cXC5bdGpdc3gkLy50ZXN0KHJlbFBhdGgpO1xuICAgIGNvbnN0IGFzdCA9IGF3YWl0IHN3Yy5wYXJzZShjb2RlLCB7IHN5bnRheDogXCJ0eXBlc2NyaXB0XCIsIHRzeDogaXNUc3ggfSk7XG4gICAgY29uc3QgdWlJbXBvcnRzID0gZXh0cmFjdFVJSW1wb3J0cyhhc3QpO1xuICAgIGlmICh1aUltcG9ydHMuc2l6ZSA9PT0gMCkgY29udGludWU7XG5cbiAgICBhbGxVc2FnZXMucHVzaCguLi5leHRyYWN0SlNYVXNhZ2VzKGFzdCwgdWlJbXBvcnRzKSk7XG4gIH1cblxuICByZXR1cm4gYWxsVXNhZ2VzO1xufVxuXG4vLyDilIDilIAgTGV2ZWwgMjogYXR0cmlidXRlIHB1cmdlIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgFxuXG5mdW5jdGlvbiBwdXJnZUF0dHJpYnV0ZXMoY3NzOiBzdHJpbmcsIGF0dHJTYWZlbGlzdDogU2V0PHN0cmluZz4pOiBzdHJpbmcge1xuICBjb25zdCByb290ID0gcG9zdGNzcy5wYXJzZShjc3MpO1xuXG4gIHJvb3Qud2Fsa1J1bGVzKChydWxlKSA9PiB7XG4gICAgY29uc3Qgc2VsZWN0b3JzID0gcnVsZS5zZWxlY3RvcnM7XG4gICAgY29uc3Qga2VwdDogc3RyaW5nW10gPSBbXTtcblxuICAgIGZvciAoY29uc3Qgc2VsIG9mIHNlbGVjdG9ycykge1xuICAgICAgY29uc3QgYXR0ck1hdGNoZXMgPSBzZWwubWF0Y2hBbGwoL1xcWyhkYXRhLVthLXotXSt8YXJpYS1bYS16LV0rKT1cIihbXlwiXSspXCJcXF0vZyk7XG4gICAgICBsZXQgc2hvdWxkS2VlcCA9IHRydWU7XG5cbiAgICAgIGZvciAoY29uc3QgbWF0Y2ggb2YgYXR0ck1hdGNoZXMpIHtcbiAgICAgICAgY29uc3QgYXR0clNlbGVjdG9yID0gYFske21hdGNoWzFdfT1cIiR7bWF0Y2hbMl19XCJdYDtcbiAgICAgICAgaWYgKG1hdGNoWzFdID09PSBcImRhdGEtc2xvdFwiKSBjb250aW51ZTtcbiAgICAgICAgaWYgKCFhdHRyU2FmZWxpc3QuaGFzKGF0dHJTZWxlY3RvcikpIHtcbiAgICAgICAgICBzaG91bGRLZWVwID0gZmFsc2U7XG4gICAgICAgICAgYnJlYWs7XG4gICAgICAgIH1cbiAgICAgIH1cblxuICAgICAgaWYgKHNob3VsZEtlZXApIGtlcHQucHVzaChzZWwpO1xuICAgIH1cblxuICAgIGlmIChrZXB0Lmxlbmd0aCA9PT0gMCkge1xuICAgICAgcnVsZS5yZW1vdmUoKTtcbiAgICB9IGVsc2UgaWYgKGtlcHQubGVuZ3RoIDwgc2VsZWN0b3JzLmxlbmd0aCkge1xuICAgICAgcnVsZS5zZWxlY3RvcnMgPSBrZXB0O1xuICAgIH1cbiAgfSk7XG5cbiAgcmV0dXJuIHJvb3QudG9TdHJpbmcoKTtcbn1cblxuLy8g4pSA4pSAIExldmVsIDM6IHVudXNlZCBDU1MgdmFyaWFibGUgY2xlYW51cCDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIBcblxuZnVuY3Rpb24gY2xlYW5VbnVzZWRWYXJzKGNzczogc3RyaW5nKTogc3RyaW5nIHtcbiAgbGV0IGNoYW5nZWQgPSB0cnVlO1xuICBsZXQgcmVzdWx0ID0gY3NzO1xuXG4gIHdoaWxlIChjaGFuZ2VkKSB7XG4gICAgY2hhbmdlZCA9IGZhbHNlO1xuICAgIGNvbnN0IHJvb3QgPSBwb3N0Y3NzLnBhcnNlKHJlc3VsdCk7XG5cbiAgICBjb25zdCBkZWNsYXJlZCA9IG5ldyBNYXA8c3RyaW5nLCB7IHJ1bGU6IFJ1bGUgfCBBdFJ1bGU7IHByb3A6IHN0cmluZzsgaW5kZXg6IG51bWJlciB9W10+KCk7XG4gICAgcm9vdC53YWxrRGVjbHMoL14tLS8sIChkZWNsKSA9PiB7XG4gICAgICBjb25zdCBlbnRyaWVzID0gZGVjbGFyZWQuZ2V0KGRlY2wucHJvcCkgPz8gW107XG4gICAgICBlbnRyaWVzLnB1c2goeyBydWxlOiBkZWNsLnBhcmVudCBhcyBSdWxlLCBwcm9wOiBkZWNsLnByb3AsIGluZGV4OiBlbnRyaWVzLmxlbmd0aCB9KTtcbiAgICAgIGRlY2xhcmVkLnNldChkZWNsLnByb3AsIGVudHJpZXMpO1xuICAgIH0pO1xuXG4gICAgY29uc3QgcmVmZXJlbmNlZCA9IG5ldyBTZXQ8c3RyaW5nPigpO1xuICAgIHJvb3Qud2Fsa0RlY2xzKChkZWNsKSA9PiB7XG4gICAgICBjb25zdCByZWZzID0gZGVjbC52YWx1ZS5tYXRjaEFsbCgvdmFyXFwoXFxzKigtLVthLXpBLVowLTlfLV0rKS9nKTtcbiAgICAgIGZvciAoY29uc3QgcmVmIG9mIHJlZnMpIHtcbiAgICAgICAgcmVmZXJlbmNlZC5hZGQocmVmWzFdKTtcbiAgICAgIH1cbiAgICB9KTtcblxuICAgIGZvciAoY29uc3QgW3Zhck5hbWUsIGVudHJpZXNdIG9mIGRlY2xhcmVkKSB7XG4gICAgICBpZiAoIXJlZmVyZW5jZWQuaGFzKHZhck5hbWUpKSB7XG4gICAgICAgIGZvciAoY29uc3QgZW50cnkgb2YgZW50cmllcykge1xuICAgICAgICAgIGVudHJ5LnJ1bGUud2Fsa0RlY2xzKGVudHJ5LnByb3AsIChkZWNsKSA9PiB7XG4gICAgICAgICAgICBkZWNsLnJlbW92ZSgpO1xuICAgICAgICAgICAgY2hhbmdlZCA9IHRydWU7XG4gICAgICAgICAgfSk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG5cbiAgICByb290LndhbGtSdWxlcygocnVsZSkgPT4ge1xuICAgICAgaWYgKHJ1bGUubm9kZXMgJiYgcnVsZS5ub2Rlcy5sZW5ndGggPT09IDApIHJ1bGUucmVtb3ZlKCk7XG4gICAgfSk7XG4gICAgcm9vdC53YWxrQXRSdWxlcygoYXRSdWxlKSA9PiB7XG4gICAgICBpZiAoYXRSdWxlLm5vZGVzICYmIGF0UnVsZS5ub2Rlcy5sZW5ndGggPT09IDApIGF0UnVsZS5yZW1vdmUoKTtcbiAgICB9KTtcblxuICAgIHJlc3VsdCA9IHJvb3QudG9TdHJpbmcoKTtcbiAgfVxuXG4gIHJldHVybiByZXN1bHQ7XG59XG5cbi8vIOKUgOKUgCBUaGUgcnNidWlsZCBwbHVnaW4g4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSAXG5cbmV4cG9ydCBjb25zdCBwbHVnaW5Dc3NQdXJnZSA9IChvcHRpb25zOiBDc3NQdXJnZU9wdGlvbnMpOiBSc2J1aWxkUGx1Z2luID0+ICh7XG4gIG5hbWU6IFwicGx1Z2luLWNzcy1wdXJnZVwiLFxuXG4gIHNldHVwKGFwaSkge1xuICAgIGNvbnN0IHtcbiAgICAgIG1hbmlmZXN0OiBtYW5pZmVzdFBhdGgsXG4gICAgICBzcmNEaXIgPSBcInNyY1wiLFxuICAgICAgYXR0clB1cmdlID0gdHJ1ZSxcbiAgICAgIGNsZWFuVmFycyA9IHRydWUsXG4gICAgICB2ZXJib3NlID0gdHJ1ZSxcbiAgICB9ID0gb3B0aW9ucztcblxuICAgIGFwaS5wcm9jZXNzQXNzZXRzKFxuICAgICAgeyBzdGFnZTogXCJvcHRpbWl6ZS1zaXplXCIgfSxcbiAgICAgIGFzeW5jICh7IGFzc2V0cywgc291cmNlcywgY29tcGlsYXRpb24gfSkgPT4ge1xuICAgICAgICBjb25zdCBsb2cgPSB2ZXJib3NlID8gY29uc29sZS5sb2cuYmluZChjb25zb2xlKSA6ICgpID0+IHt9O1xuXG4gICAgICAgIC8vIDEuIExvYWQgbWFuaWZlc3RcbiAgICAgICAgY29uc3QgbWFuaWZlc3Q6IFB1cmdlTWFuaWZlc3QgPSBKU09OLnBhcnNlKFxuICAgICAgICAgIGF3YWl0IG5vZGVSZWFkRmlsZShwYXRoLnJlc29sdmUobWFuaWZlc3RQYXRoKSwgXCJ1dGYtOFwiKSxcbiAgICAgICAgKTtcbiAgICAgICAgbG9nKGBbY3NzLXB1cmdlXSBNYW5pZmVzdCBsb2FkZWQ6ICR7T2JqZWN0LmtleXMobWFuaWZlc3QpLmxlbmd0aH0gZW50cmllc2ApO1xuXG4gICAgICAgIC8vIDIuIFNjYW4gY29uc3VtZXIgc291cmNlXG4gICAgICAgIGNvbnN0IHJlc29sdmVkU3JjID0gcGF0aC5yZXNvbHZlKHNyY0Rpcik7XG4gICAgICAgIGNvbnN0IHVzYWdlcyA9IGF3YWl0IHNjYW5Db25zdW1lclNvdXJjZShyZXNvbHZlZFNyYyk7XG4gICAgICAgIGxvZyhgW2Nzcy1wdXJnZV0gU2Nhbm5lZCAke3Jlc29sdmVkU3JjfTogJHt1c2FnZXMubGVuZ3RofSBjb21wb25lbnQgdXNhZ2VzYCk7XG5cbiAgICAgICAgLy8gMy4gQnVpbGQgc2FmZWxpc3RzXG4gICAgICAgIGNvbnN0IHsgY2xhc3NTYWZlbGlzdCwgYXR0clNhZmVsaXN0IH0gPSBidWlsZFNhZmVsaXN0cyh1c2FnZXMsIG1hbmlmZXN0KTtcbiAgICAgICAgbG9nKGBbY3NzLXB1cmdlXSBTYWZlbGlzdDogJHtjbGFzc1NhZmVsaXN0LnNpemV9IGNsYXNzZXMsICR7YXR0clNhZmVsaXN0LnNpemV9IGF0dHJzYCk7XG5cbiAgICAgICAgLy8gNC4gUHJvY2VzcyBlYWNoIENTUyBhc3NldFxuICAgICAgICBmb3IgKGNvbnN0IFtuYW1lLCBhc3NldF0gb2YgT2JqZWN0LmVudHJpZXMoYXNzZXRzKSkge1xuICAgICAgICAgIGlmICghbmFtZS5lbmRzV2l0aChcIi5jc3NcIikpIGNvbnRpbnVlO1xuXG4gICAgICAgICAgY29uc3Qgb3JpZ2luYWxDc3MgPSBhc3NldC5zb3VyY2UoKS50b1N0cmluZygpO1xuICAgICAgICAgIGNvbnN0IG9yaWdpbmFsU2l6ZSA9IEJ1ZmZlci5ieXRlTGVuZ3RoKG9yaWdpbmFsQ3NzLCBcInV0Zi04XCIpO1xuICAgICAgICAgIGxvZyhgW2Nzcy1wdXJnZV0gUHJvY2Vzc2luZyAke25hbWV9ICgkeyhvcmlnaW5hbFNpemUgLyAxMDI0KS50b0ZpeGVkKDEpfSBLQilgKTtcblxuICAgICAgICAgIC8vIExldmVsIDE6IGNsYXNzLWxldmVsIHB1cmdlXG4gICAgICAgICAgY29uc3QgcHVyZ2VSZXN1bHQgPSBhd2FpdCBuZXcgUHVyZ2VDU1MoKS5wdXJnZSh7XG4gICAgICAgICAgICBjb250ZW50OiBbXSxcbiAgICAgICAgICAgIGNzczogW3sgcmF3OiBvcmlnaW5hbENzcyB9XSxcbiAgICAgICAgICAgIHNhZmVsaXN0OiBbLi4uY2xhc3NTYWZlbGlzdF0sXG4gICAgICAgICAgICBrZXlmcmFtZXM6IGZhbHNlLFxuICAgICAgICAgICAgZm9udEZhY2U6IGZhbHNlLFxuICAgICAgICAgIH0pO1xuXG4gICAgICAgICAgbGV0IHB1cmdlZENzcyA9IHB1cmdlUmVzdWx0WzBdPy5jc3MgPz8gb3JpZ2luYWxDc3M7XG4gICAgICAgICAgY29uc3QgYWZ0ZXJMMSA9IEJ1ZmZlci5ieXRlTGVuZ3RoKHB1cmdlZENzcywgXCJ1dGYtOFwiKTtcbiAgICAgICAgICBsb2coYFtjc3MtcHVyZ2VdICAgTDEgY2xhc3MgcHVyZ2U6ICR7KG9yaWdpbmFsU2l6ZSAvIDEwMjQpLnRvRml4ZWQoMSl9IOKGkiAkeyhhZnRlckwxIC8gMTAyNCkudG9GaXhlZCgxKX0gS0JgKTtcblxuICAgICAgICAgIC8vIExldmVsIDI6IGF0dHJpYnV0ZS1sZXZlbCBwdXJnZVxuICAgICAgICAgIGlmIChhdHRyUHVyZ2UgJiYgYXR0clNhZmVsaXN0LnNpemUgPiAwKSB7XG4gICAgICAgICAgICBwdXJnZWRDc3MgPSBwdXJnZUF0dHJpYnV0ZXMocHVyZ2VkQ3NzLCBhdHRyU2FmZWxpc3QpO1xuICAgICAgICAgICAgY29uc3QgYWZ0ZXJMMiA9IEJ1ZmZlci5ieXRlTGVuZ3RoKHB1cmdlZENzcywgXCJ1dGYtOFwiKTtcbiAgICAgICAgICAgIGxvZyhgW2Nzcy1wdXJnZV0gICBMMiBhdHRyIHB1cmdlOiAkeyhhZnRlckwxIC8gMTAyNCkudG9GaXhlZCgxKX0g4oaSICR7KGFmdGVyTDIgLyAxMDI0KS50b0ZpeGVkKDEpfSBLQmApO1xuICAgICAgICAgIH1cblxuICAgICAgICAgIC8vIExldmVsIDM6IHVudXNlZCBDU1MgdmFyaWFibGUgY2xlYW51cFxuICAgICAgICAgIGlmIChjbGVhblZhcnMpIHtcbiAgICAgICAgICAgIHB1cmdlZENzcyA9IGNsZWFuVW51c2VkVmFycyhwdXJnZWRDc3MpO1xuICAgICAgICAgICAgY29uc3QgYWZ0ZXJMMyA9IEJ1ZmZlci5ieXRlTGVuZ3RoKHB1cmdlZENzcywgXCJ1dGYtOFwiKTtcbiAgICAgICAgICAgIGxvZyhgW2Nzcy1wdXJnZV0gICBMMyB2YXIgY2xlYW51cDog4oaSICR7KGFmdGVyTDMgLyAxMDI0KS50b0ZpeGVkKDEpfSBLQmApO1xuICAgICAgICAgIH1cblxuICAgICAgICAgIGNvbnN0IGZpbmFsU2l6ZSA9IEJ1ZmZlci5ieXRlTGVuZ3RoKHB1cmdlZENzcywgXCJ1dGYtOFwiKTtcbiAgICAgICAgICBsb2coYFtjc3MtcHVyZ2VdICAgRmluYWw6ICR7KG9yaWdpbmFsU2l6ZSAvIDEwMjQpLnRvRml4ZWQoMSl9IOKGkiAkeyhmaW5hbFNpemUgLyAxMDI0KS50b0ZpeGVkKDEpfSBLQiAoJHsoKDEgLSBmaW5hbFNpemUgLyBvcmlnaW5hbFNpemUpICogMTAwKS50b0ZpeGVkKDEpfSUgcmVkdWN0aW9uKWApO1xuXG4gICAgICAgICAgLy8gV3JpdGUgYmFja1xuICAgICAgICAgIGNvbnN0IHNvdXJjZSA9IG5ldyBzb3VyY2VzLlJhd1NvdXJjZShwdXJnZWRDc3MpO1xuICAgICAgICAgIGNvbXBpbGF0aW9uLnVwZGF0ZUFzc2V0KG5hbWUsIHNvdXJjZSk7XG4gICAgICAgIH1cbiAgICAgIH0sXG4gICAgKTtcbiAgfSxcbn0pO1xuIgogIF0sCiAgIm1hcHBpbmdzIjogIjtBQWdCQTtBQUNBO0FBRUE7QUFDQTtBQUNBLHFCQUFTO0FBQ1QsaUJBQVM7QUFzQ1QsU0FBUyxPQUFPLENBQUMsTUFBVyxTQUE4QjtBQUFBLEVBQ3hELElBQUksQ0FBQyxRQUFRLE9BQU8sU0FBUztBQUFBLElBQVU7QUFBQSxFQUN2QyxRQUFRLElBQUk7QUFBQSxFQUNaLFdBQVcsT0FBTyxPQUFPLEtBQUssSUFBSSxHQUFHO0FBQUEsSUFDbkMsSUFBSSxRQUFRO0FBQUEsTUFBUTtBQUFBLElBQ3BCLE1BQU0sTUFBTSxLQUFLO0FBQUEsSUFDakIsSUFBSSxNQUFNLFFBQVEsR0FBRyxHQUFHO0FBQUEsTUFDdEIsV0FBVyxRQUFRO0FBQUEsUUFBSyxRQUFRLE1BQU0sT0FBTztBQUFBLElBQy9DLEVBQU8sU0FBSSxPQUFPLE9BQU8sUUFBUSxVQUFVO0FBQUEsTUFDekMsUUFBUSxLQUFLLE9BQU87QUFBQSxJQUN0QjtBQUFBLEVBQ0Y7QUFBQTtBQUdGLFNBQVMsZ0JBQWdCLENBQUMsS0FBK0I7QUFBQSxFQUN2RCxNQUFNLFVBQVUsSUFBSTtBQUFBLEVBQ3BCLFdBQVcsUUFBUSxJQUFJLE1BQU07QUFBQSxJQUMzQixJQUFJLEtBQUssU0FBUztBQUFBLE1BQXFCO0FBQUEsSUFDdkMsTUFBTSxNQUFNLEtBQUssUUFBUTtBQUFBLElBQ3pCLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxXQUFXLGVBQWU7QUFBQSxNQUFHO0FBQUEsSUFDOUMsV0FBVyxRQUFRLEtBQUssWUFBWTtBQUFBLE1BQ2xDLElBQUksS0FBSyxTQUFTLG1CQUFtQjtBQUFBLFFBQ25DLE1BQU0sV0FBVyxLQUFLLFVBQVUsU0FBUyxLQUFLLE9BQU87QUFBQSxRQUNyRCxNQUFNLFFBQVEsS0FBSyxPQUFPO0FBQUEsUUFDMUIsSUFBSSxTQUFTO0FBQUEsVUFBVSxRQUFRLElBQUksT0FBTyxRQUFRO0FBQUEsTUFDcEQsRUFBTyxTQUFJLEtBQUssU0FBUywwQkFBMEI7QUFBQSxRQUNqRCxNQUFNLFFBQVEsS0FBSyxPQUFPO0FBQUEsUUFDMUIsSUFBSTtBQUFBLFVBQU8sUUFBUSxJQUFJLE9BQU8sS0FBSztBQUFBLE1BQ3JDO0FBQUEsSUFDRjtBQUFBLEVBQ0Y7QUFBQSxFQUNBLE9BQU87QUFBQTtBQUdULFNBQVMsZ0JBQWdCLENBQUMsS0FBVSxjQUFnRDtBQUFBLEVBQ2xGLE1BQU0sU0FBc0IsQ0FBQztBQUFBLEVBQzdCLFFBQVEsS0FBSyxDQUFDLFNBQVM7QUFBQSxJQUNyQixJQUFJLEtBQUssU0FBUztBQUFBLE1BQXFCO0FBQUEsSUFDdkMsSUFBSSxjQUE2QjtBQUFBLElBQ2pDLElBQUksV0FBMEI7QUFBQSxJQUM5QixJQUFJLEtBQUssTUFBTSxTQUFTLGNBQWM7QUFBQSxNQUNwQyxjQUFjLEtBQUssS0FBSztBQUFBLE1BQ3hCLFdBQVc7QUFBQSxJQUNiLEVBQU8sU0FBSSxLQUFLLE1BQU0sU0FBUyx1QkFBdUI7QUFBQSxNQUNwRCxNQUFNLFFBQWtCLENBQUM7QUFBQSxNQUN6QixJQUFJLFNBQVMsS0FBSztBQUFBLE1BQ2xCLE9BQU8sUUFBUSxTQUFTLHVCQUF1QjtBQUFBLFFBQzdDLE1BQU0sUUFBUSxPQUFPLFVBQVUsS0FBSztBQUFBLFFBQ3BDLFNBQVMsT0FBTztBQUFBLE1BQ2xCO0FBQUEsTUFDQSxJQUFJLFFBQVEsU0FBUyxjQUFjO0FBQUEsUUFDakMsTUFBTSxRQUFRLE9BQU8sS0FBSztBQUFBLFFBQzFCLFdBQVcsT0FBTztBQUFBLE1BQ3BCO0FBQUEsTUFDQSxjQUFjLE1BQU0sS0FBSyxHQUFHO0FBQUEsSUFDOUI7QUFBQSxJQUNBLElBQUksQ0FBQyxZQUFZLENBQUMsYUFBYSxJQUFJLFFBQVE7QUFBQSxNQUFHO0FBQUEsSUFDOUMsTUFBTSxRQUFtQjtBQUFBLE1BQ3ZCLFdBQVc7QUFBQSxNQUNYLE9BQU8sSUFBSTtBQUFBLE1BQ1gsY0FBYyxJQUFJO0FBQUEsTUFDbEIsV0FBVztBQUFBLElBQ2I7QUFBQSxJQUNBLFdBQVcsUUFBUSxLQUFLLGNBQWMsQ0FBQyxHQUFHO0FBQUEsTUFDeEMsSUFBSSxLQUFLLFNBQVMsbUJBQW1CLEtBQUssU0FBUyxzQkFBc0I7QUFBQSxRQUN2RSxNQUFNLFlBQVk7QUFBQSxRQUNsQjtBQUFBLE1BQ0Y7QUFBQSxNQUNBLElBQUksS0FBSyxTQUFTO0FBQUEsUUFBZ0I7QUFBQSxNQUNsQyxNQUFNLFdBQVcsS0FBSyxNQUFNO0FBQUEsTUFDNUIsSUFBSSxDQUFDO0FBQUEsUUFBVTtBQUFBLE1BQ2YsSUFBSSxDQUFDLEtBQUssT0FBTztBQUFBLFFBQ2YsTUFBTSxhQUFhLElBQUksUUFBUTtBQUFBLE1BQ2pDLEVBQU8sU0FBSSxLQUFLLE1BQU0sU0FBUyxpQkFBaUI7QUFBQSxRQUM5QyxNQUFNLE1BQU0sSUFBSSxVQUFVLEtBQUssTUFBTSxLQUFLO0FBQUEsTUFDNUMsRUFBTztBQUFBLFFBQ0wsTUFBTSxNQUFNLElBQUksVUFBVSxTQUFTO0FBQUE7QUFBQSxJQUV2QztBQUFBLElBQ0EsT0FBTyxLQUFLLEtBQUs7QUFBQSxHQUNsQjtBQUFBLEVBQ0QsT0FBTztBQUFBO0FBS1QsU0FBUyxjQUFjLENBQ3JCLFdBQ0EsVUFDMkQ7QUFBQSxFQUMzRCxNQUFNLGdCQUFnQixJQUFJO0FBQUEsRUFDMUIsTUFBTSxlQUFlLElBQUk7QUFBQSxFQUV6QixNQUFNLGtCQUFrQixJQUFJO0FBQUEsRUFDNUIsV0FBVyxTQUFTLFdBQVc7QUFBQSxJQUM3QixNQUFNLFdBQVcsZ0JBQWdCLElBQUksTUFBTSxTQUFTLEtBQUssQ0FBQztBQUFBLElBQzFELFNBQVMsS0FBSyxLQUFLO0FBQUEsSUFDbkIsZ0JBQWdCLElBQUksTUFBTSxXQUFXLFFBQVE7QUFBQSxFQUMvQztBQUFBLEVBRUEsWUFBWSxXQUFXLFVBQVUsT0FBTyxRQUFRLFFBQVEsR0FBRztBQUFBLElBQ3pELE1BQU0saUJBQWlCLG1CQUFtQixXQUFXLGVBQWU7QUFBQSxJQUNwRSxJQUFJLGVBQWUsV0FBVztBQUFBLE1BQUc7QUFBQSxJQUVqQyxXQUFXLE9BQU8sTUFBTSxRQUFRO0FBQUEsTUFBUSxjQUFjLElBQUksR0FBRztBQUFBLElBRTdELFlBQVksWUFBWSxVQUFVLE9BQU8sUUFBUSxNQUFNLFFBQVEsTUFBTSxHQUFHO0FBQUEsTUFDdEUsSUFBSSxNQUFNLFFBQVEsS0FBSyxHQUFHO0FBQUEsUUFDeEIsSUFBSSxXQUFXLFlBQVksY0FBYyxHQUFHO0FBQUEsVUFDMUMsV0FBVyxPQUFPO0FBQUEsWUFBTyxjQUFjLElBQUksR0FBRztBQUFBLFFBQ2hEO0FBQUEsTUFDRixFQUFPO0FBQUEsUUFDTCxNQUFNLGFBQWEsa0JBQWtCLFlBQVksY0FBYztBQUFBLFFBQy9ELElBQUksZUFBZSxPQUFPO0FBQUEsVUFDeEIsV0FBVyxXQUFXLE9BQU8sT0FBTyxLQUFLLEdBQUc7QUFBQSxZQUMxQyxXQUFXLE9BQU87QUFBQSxjQUFTLGNBQWMsSUFBSSxHQUFHO0FBQUEsVUFDbEQ7QUFBQSxRQUNGLEVBQU87QUFBQSxVQUNMLFdBQVcsT0FBTyxZQUFZO0FBQUEsWUFDNUIsSUFBSSxNQUFNLE1BQU07QUFBQSxjQUNkLFdBQVcsT0FBTyxNQUFNO0FBQUEsZ0JBQU0sY0FBYyxJQUFJLEdBQUc7QUFBQSxZQUNyRDtBQUFBLFVBQ0Y7QUFBQTtBQUFBO0FBQUEsSUFHTjtBQUFBLElBRUEsSUFBSSxNQUFNLE9BQU87QUFBQSxNQUNmLFlBQVksVUFBVSxZQUFZLE9BQU8sUUFBUSxNQUFNLEtBQUssR0FBRztBQUFBLFFBQzdELElBQUksV0FBVyxVQUFVLGNBQWMsR0FBRztBQUFBLFVBQ3hDLFlBQVksTUFBTSxRQUFRLE9BQU8sUUFBUSxPQUFPLEdBQUc7QUFBQSxZQUNqRCxhQUFhLElBQUksSUFBSSxTQUFTLE9BQU87QUFBQSxVQUN2QztBQUFBLFFBQ0Y7QUFBQSxNQUNGO0FBQUEsSUFDRjtBQUFBLEVBQ0Y7QUFBQSxFQUVBLE9BQU8sRUFBRSxlQUFlLGFBQWE7QUFBQTtBQUd2QyxTQUFTLGtCQUFrQixDQUFDLFdBQW1CLFVBQWlEO0FBQUEsRUFDOUYsSUFBSSxTQUFTLElBQUksU0FBUztBQUFBLElBQUcsT0FBTyxTQUFTLElBQUksU0FBUztBQUFBLEVBQzFELE1BQU0sVUFBdUIsQ0FBQztBQUFBLEVBQzlCLFlBQVksV0FBVyxXQUFXLFVBQVU7QUFBQSxJQUMxQyxJQUFJLGNBQWMsV0FBVztBQUFBLE1BQzNCLFFBQVEsS0FBSyxHQUFHLE1BQU07QUFBQSxJQUN4QjtBQUFBLElBQ0EsSUFBSSxVQUFVLFNBQVMsR0FBRyxHQUFHO0FBQUEsTUFDM0IsT0FBTyxRQUFRLFFBQVEsVUFBVSxNQUFNLEdBQUc7QUFBQSxNQUMxQyxJQUFJLFNBQVMsVUFBVSxjQUFjLFFBQVE7QUFBQSxRQUMzQyxRQUFRLEtBQUssR0FBRyxNQUFNO0FBQUEsTUFDeEI7QUFBQSxJQUNGO0FBQUEsRUFDRjtBQUFBLEVBQ0EsT0FBTztBQUFBO0FBR1QsU0FBUyxVQUFVLENBQUMsVUFBa0IsUUFBOEI7QUFBQSxFQUNsRSxXQUFXLFNBQVMsUUFBUTtBQUFBLElBQzFCLElBQUksTUFBTTtBQUFBLE1BQVcsT0FBTztBQUFBLElBQzVCLElBQUksTUFBTSxhQUFhLElBQUksUUFBUTtBQUFBLE1BQUcsT0FBTztBQUFBLElBQzdDLElBQUksTUFBTSxNQUFNLElBQUksUUFBUTtBQUFBLE1BQUcsT0FBTztBQUFBLEVBQ3hDO0FBQUEsRUFDQSxPQUFPO0FBQUE7QUFHVCxTQUFTLGlCQUFpQixDQUFDLFVBQWtCLFFBQTBDO0FBQUEsRUFDckYsTUFBTSxTQUFTLElBQUk7QUFBQSxFQUNuQixXQUFXLFNBQVMsUUFBUTtBQUFBLElBQzFCLElBQUksTUFBTTtBQUFBLE1BQVcsT0FBTztBQUFBLElBQzVCLE1BQU0sTUFBTSxNQUFNLE1BQU0sSUFBSSxRQUFRO0FBQUEsSUFDcEMsSUFBSSxRQUFRO0FBQUEsTUFBVyxPQUFPO0FBQUEsSUFDOUIsSUFBSSxRQUFRO0FBQUEsTUFBVyxPQUFPLElBQUksR0FBRztBQUFBLElBQ3JDLElBQUksTUFBTSxhQUFhLElBQUksUUFBUTtBQUFBLE1BQUcsT0FBTztBQUFBLEVBQy9DO0FBQUEsRUFDQSxPQUFPO0FBQUE7QUFLVCxlQUFlLGtCQUFrQixDQUFDLFFBQXNDO0FBQUEsRUFDdEUsTUFBTSxZQUF5QixDQUFDO0FBQUEsRUFDaEMsTUFBTSxRQUFRLE1BQU0sU0FBUyx3QkFBd0IsRUFBRSxLQUFLLFFBQVEsUUFBUSxDQUFDLG9CQUFvQixFQUFFLENBQUM7QUFBQSxFQUVwRyxXQUFXLFdBQVcsT0FBTztBQUFBLElBQzNCLE1BQU0sV0FBVyxLQUFLLEtBQUssUUFBUSxPQUFPO0FBQUEsSUFDMUMsTUFBTSxPQUFPLE1BQU0sYUFBYSxVQUFVLE9BQU87QUFBQSxJQUNqRCxJQUFJLENBQUMsS0FBSyxTQUFTLGVBQWU7QUFBQSxNQUFHO0FBQUEsSUFFckMsTUFBTSxRQUFRLFlBQVksS0FBSyxPQUFPO0FBQUEsSUFDdEMsTUFBTSxNQUFNLE1BQU0sSUFBSSxNQUFNLE1BQU0sRUFBRSxRQUFRLGNBQWMsS0FBSyxNQUFNLENBQUM7QUFBQSxJQUN0RSxNQUFNLFlBQVksaUJBQWlCLEdBQUc7QUFBQSxJQUN0QyxJQUFJLFVBQVUsU0FBUztBQUFBLE1BQUc7QUFBQSxJQUUxQixVQUFVLEtBQUssR0FBRyxpQkFBaUIsS0FBSyxTQUFTLENBQUM7QUFBQSxFQUNwRDtBQUFBLEVBRUEsT0FBTztBQUFBO0FBS1QsU0FBUyxlQUFlLENBQUMsS0FBYSxjQUFtQztBQUFBLEVBQ3ZFLE1BQU0sT0FBTyxRQUFRLE1BQU0sR0FBRztBQUFBLEVBRTlCLEtBQUssVUFBVSxDQUFDLFNBQVM7QUFBQSxJQUN2QixNQUFNLFlBQVksS0FBSztBQUFBLElBQ3ZCLE1BQU0sT0FBaUIsQ0FBQztBQUFBLElBRXhCLFdBQVcsT0FBTyxXQUFXO0FBQUEsTUFDM0IsTUFBTSxjQUFjLElBQUksU0FBUyw0Q0FBNEM7QUFBQSxNQUM3RSxJQUFJLGFBQWE7QUFBQSxNQUVqQixXQUFXLFNBQVMsYUFBYTtBQUFBLFFBQy9CLE1BQU0sZUFBZSxJQUFJLE1BQU0sT0FBTyxNQUFNO0FBQUEsUUFDNUMsSUFBSSxNQUFNLE9BQU87QUFBQSxVQUFhO0FBQUEsUUFDOUIsSUFBSSxDQUFDLGFBQWEsSUFBSSxZQUFZLEdBQUc7QUFBQSxVQUNuQyxhQUFhO0FBQUEsVUFDYjtBQUFBLFFBQ0Y7QUFBQSxNQUNGO0FBQUEsTUFFQSxJQUFJO0FBQUEsUUFBWSxLQUFLLEtBQUssR0FBRztBQUFBLElBQy9CO0FBQUEsSUFFQSxJQUFJLEtBQUssV0FBVyxHQUFHO0FBQUEsTUFDckIsS0FBSyxPQUFPO0FBQUEsSUFDZCxFQUFPLFNBQUksS0FBSyxTQUFTLFVBQVUsUUFBUTtBQUFBLE1BQ3pDLEtBQUssWUFBWTtBQUFBLElBQ25CO0FBQUEsR0FDRDtBQUFBLEVBRUQsT0FBTyxLQUFLLFNBQVM7QUFBQTtBQUt2QixTQUFTLGVBQWUsQ0FBQyxLQUFxQjtBQUFBLEVBQzVDLElBQUksVUFBVTtBQUFBLEVBQ2QsSUFBSSxTQUFTO0FBQUEsRUFFYixPQUFPLFNBQVM7QUFBQSxJQUNkLFVBQVU7QUFBQSxJQUNWLE1BQU0sT0FBTyxRQUFRLE1BQU0sTUFBTTtBQUFBLElBRWpDLE1BQU0sV0FBVyxJQUFJO0FBQUEsSUFDckIsS0FBSyxVQUFVLE9BQU8sQ0FBQyxTQUFTO0FBQUEsTUFDOUIsTUFBTSxVQUFVLFNBQVMsSUFBSSxLQUFLLElBQUksS0FBSyxDQUFDO0FBQUEsTUFDNUMsUUFBUSxLQUFLLEVBQUUsTUFBTSxLQUFLLFFBQWdCLE1BQU0sS0FBSyxNQUFNLE9BQU8sUUFBUSxPQUFPLENBQUM7QUFBQSxNQUNsRixTQUFTLElBQUksS0FBSyxNQUFNLE9BQU87QUFBQSxLQUNoQztBQUFBLElBRUQsTUFBTSxhQUFhLElBQUk7QUFBQSxJQUN2QixLQUFLLFVBQVUsQ0FBQyxTQUFTO0FBQUEsTUFDdkIsTUFBTSxPQUFPLEtBQUssTUFBTSxTQUFTLDZCQUE2QjtBQUFBLE1BQzlELFdBQVcsT0FBTyxNQUFNO0FBQUEsUUFDdEIsV0FBVyxJQUFJLElBQUksRUFBRTtBQUFBLE1BQ3ZCO0FBQUEsS0FDRDtBQUFBLElBRUQsWUFBWSxTQUFTLFlBQVksVUFBVTtBQUFBLE1BQ3pDLElBQUksQ0FBQyxXQUFXLElBQUksT0FBTyxHQUFHO0FBQUEsUUFDNUIsV0FBVyxTQUFTLFNBQVM7QUFBQSxVQUMzQixNQUFNLEtBQUssVUFBVSxNQUFNLE1BQU0sQ0FBQyxTQUFTO0FBQUEsWUFDekMsS0FBSyxPQUFPO0FBQUEsWUFDWixVQUFVO0FBQUEsV0FDWDtBQUFBLFFBQ0g7QUFBQSxNQUNGO0FBQUEsSUFDRjtBQUFBLElBRUEsS0FBSyxVQUFVLENBQUMsU0FBUztBQUFBLE1BQ3ZCLElBQUksS0FBSyxTQUFTLEtBQUssTUFBTSxXQUFXO0FBQUEsUUFBRyxLQUFLLE9BQU87QUFBQSxLQUN4RDtBQUFBLElBQ0QsS0FBSyxZQUFZLENBQUMsV0FBVztBQUFBLE1BQzNCLElBQUksT0FBTyxTQUFTLE9BQU8sTUFBTSxXQUFXO0FBQUEsUUFBRyxPQUFPLE9BQU87QUFBQSxLQUM5RDtBQUFBLElBRUQsU0FBUyxLQUFLLFNBQVM7QUFBQSxFQUN6QjtBQUFBLEVBRUEsT0FBTztBQUFBO0FBS0YsSUFBTSxpQkFBaUIsQ0FBQyxhQUE2QztBQUFBLEVBQzFFLE1BQU07QUFBQSxFQUVOLEtBQUssQ0FBQyxLQUFLO0FBQUEsSUFDVDtBQUFBLE1BQ0UsVUFBVTtBQUFBLE1BQ1YsU0FBUztBQUFBLE1BQ1QsWUFBWTtBQUFBLE1BQ1osWUFBWTtBQUFBLE1BQ1osVUFBVTtBQUFBLFFBQ1I7QUFBQSxJQUVKLElBQUksY0FDRixFQUFFLE9BQU8sZ0JBQWdCLEdBQ3pCLFNBQVMsUUFBUSxTQUFTLGtCQUFrQjtBQUFBLE1BQzFDLE1BQU0sTUFBTSxVQUFVLFFBQVEsSUFBSSxLQUFLLE9BQU8sSUFBSSxNQUFNO0FBQUEsTUFHeEQsTUFBTSxXQUEwQixLQUFLLE1BQ25DLE1BQU0sYUFBYSxLQUFLLFFBQVEsWUFBWSxHQUFHLE9BQU8sQ0FDeEQ7QUFBQSxNQUNBLElBQUksZ0NBQWdDLE9BQU8sS0FBSyxRQUFRLEVBQUUsZ0JBQWdCO0FBQUEsTUFHMUUsTUFBTSxjQUFjLEtBQUssUUFBUSxNQUFNO0FBQUEsTUFDdkMsTUFBTSxTQUFTLE1BQU0sbUJBQW1CLFdBQVc7QUFBQSxNQUNuRCxJQUFJLHVCQUF1QixnQkFBZ0IsT0FBTyx5QkFBeUI7QUFBQSxNQUczRSxRQUFRLGVBQWUsaUJBQWlCLGVBQWUsUUFBUSxRQUFRO0FBQUEsTUFDdkUsSUFBSSx5QkFBeUIsY0FBYyxpQkFBaUIsYUFBYSxZQUFZO0FBQUEsTUFHckYsWUFBWSxNQUFNLFVBQVUsT0FBTyxRQUFRLE1BQU0sR0FBRztBQUFBLFFBQ2xELElBQUksQ0FBQyxLQUFLLFNBQVMsTUFBTTtBQUFBLFVBQUc7QUFBQSxRQUU1QixNQUFNLGNBQWMsTUFBTSxPQUFPLEVBQUUsU0FBUztBQUFBLFFBQzVDLE1BQU0sZUFBZSxPQUFPLFdBQVcsYUFBYSxPQUFPO0FBQUEsUUFDM0QsSUFBSSwwQkFBMEIsVUFBVSxlQUFlLE1BQU0sUUFBUSxDQUFDLE9BQU87QUFBQSxRQUc3RSxNQUFNLGNBQWMsTUFBTSxJQUFJLFNBQVMsRUFBRSxNQUFNO0FBQUEsVUFDN0MsU0FBUyxDQUFDO0FBQUEsVUFDVixLQUFLLENBQUMsRUFBRSxLQUFLLFlBQVksQ0FBQztBQUFBLFVBQzFCLFVBQVUsQ0FBQyxHQUFHLGFBQWE7QUFBQSxVQUMzQixXQUFXO0FBQUEsVUFDWCxVQUFVO0FBQUEsUUFDWixDQUFDO0FBQUEsUUFFRCxJQUFJLFlBQVksWUFBWSxJQUFJLE9BQU87QUFBQSxRQUN2QyxNQUFNLFVBQVUsT0FBTyxXQUFXLFdBQVcsT0FBTztBQUFBLFFBQ3BELElBQUksa0NBQWtDLGVBQWUsTUFBTSxRQUFRLENBQUMsUUFBTyxVQUFVLE1BQU0sUUFBUSxDQUFDLE1BQU07QUFBQSxRQUcxRyxJQUFJLGFBQWEsYUFBYSxPQUFPLEdBQUc7QUFBQSxVQUN0QyxZQUFZLGdCQUFnQixXQUFXLFlBQVk7QUFBQSxVQUNuRCxNQUFNLFVBQVUsT0FBTyxXQUFXLFdBQVcsT0FBTztBQUFBLFVBQ3BELElBQUksaUNBQWlDLFVBQVUsTUFBTSxRQUFRLENBQUMsUUFBTyxVQUFVLE1BQU0sUUFBUSxDQUFDLE1BQU07QUFBQSxRQUN0RztBQUFBLFFBR0EsSUFBSSxXQUFXO0FBQUEsVUFDYixZQUFZLGdCQUFnQixTQUFTO0FBQUEsVUFDckMsTUFBTSxVQUFVLE9BQU8sV0FBVyxXQUFXLE9BQU87QUFBQSxVQUNwRCxJQUFJLG9DQUFtQyxVQUFVLE1BQU0sUUFBUSxDQUFDLE1BQU07QUFBQSxRQUN4RTtBQUFBLFFBRUEsTUFBTSxZQUFZLE9BQU8sV0FBVyxXQUFXLE9BQU87QUFBQSxRQUN0RCxJQUFJLHlCQUF5QixlQUFlLE1BQU0sUUFBUSxDQUFDLFFBQU8sWUFBWSxNQUFNLFFBQVEsQ0FBQyxXQUFXLElBQUksWUFBWSxnQkFBZ0IsS0FBSyxRQUFRLENBQUMsZUFBZTtBQUFBLFFBR3JLLE1BQU0sU0FBUyxJQUFJLFFBQVEsVUFBVSxTQUFTO0FBQUEsUUFDOUMsWUFBWSxZQUFZLE1BQU0sTUFBTTtBQUFBLE1BQ3RDO0FBQUEsS0FFSjtBQUFBO0FBRUo7IiwKICAiZGVidWdJZCI6ICJDQjVCMjA5MzA2MzY2M0U4NjQ3NTZFMjE2NDc1NkUyMSIsCiAgIm5hbWVzIjogW10KfQ==
@@ -0,0 +1,28 @@
1
+ /**
2
+ * rsbuild-plugin-css-purge
3
+ *
4
+ * Two-level CSS purge for @pathscale/ui consumers.
5
+ *
6
+ * Level 1: class-level purge via purgecss — removes entire rules whose selectors
7
+ * don't match the safelist built from consumer JSX analysis.
8
+ * Level 2: attribute-level purge — within kept rules, strips compound selectors
9
+ * containing data-attr / aria-attr attribute selectors not in the attr safelist.
10
+ *
11
+ * Usage in rsbuild.config.ts:
12
+ * import { pluginCssPurge } from "@pathscale/rebuild-plugin-ui-css-purge";
13
+ * export default defineConfig({ plugins: [pluginCssPurge({ manifest: "..." })] });
14
+ */
15
+ import type { RsbuildPlugin } from "@rsbuild/core";
16
+ export interface CssPurgeOptions {
17
+ /** Path to purge-manifest.json (generated by generate-manifest.ts) */
18
+ manifest: string;
19
+ /** Consumer source directory to scan for JSX usage (default: "src") */
20
+ srcDir?: string;
21
+ /** Enable Level 2 attribute purge (default: true) */
22
+ attrPurge?: boolean;
23
+ /** Enable CSS variable cleanup (default: true) */
24
+ cleanVars?: boolean;
25
+ /** Log purge stats (default: true) */
26
+ verbose?: boolean;
27
+ }
28
+ export declare const pluginCssPurge: (options: CssPurgeOptions) => RsbuildPlugin;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Consumer-side JSX scanner.
3
+ *
4
+ * Walks a consumer's source tree, finds component imports from @pathscale/ui,
5
+ * collects prop values, and cross-references with the purge manifest to build
6
+ * Level 1 (class) and Level 2 (attribute) safelists.
7
+ *
8
+ * Usage: bun run src/scan-consumer.ts <consumer-src-dir> <purge-manifest.json>
9
+ */
10
+ interface ComponentManifest {
11
+ classes: {
12
+ always: string[];
13
+ byProp: Record<string, string[] | Record<string, string[]>>;
14
+ };
15
+ attrs?: Record<string, Record<string, string>>;
16
+ }
17
+ type PurgeManifest = Record<string, ComponentManifest>;
18
+ /** What we collect per component usage from JSX */
19
+ interface PropUsage {
20
+ component: string;
21
+ props: Map<string, string | "DYNAMIC">;
22
+ booleanProps: Set<string>;
23
+ hasSpread: boolean;
24
+ }
25
+ /** Extract @pathscale/ui imports from a parsed module */
26
+ declare function extractUIImports(ast: any): Map<string, string>;
27
+ /** Extract JSX usages of UI components */
28
+ declare function extractJSXUsages(ast: any, uiComponents: Map<string, string>): PropUsage[];
29
+ interface Safelists {
30
+ classSafelist: Set<string>;
31
+ attrSafelist: Set<string>;
32
+ }
33
+ declare function buildSafelists(allUsages: PropUsage[], manifest: PurgeManifest): Safelists;
34
+ export { extractUIImports, extractJSXUsages, buildSafelists };
35
+ export type { PropUsage, PurgeManifest, ComponentManifest, Safelists };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@pathscale/rebuild-plugin-ui-css-purge",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "bin": {
14
+ "generate-manifest": "src/generate-manifest.ts"
15
+ },
16
+ "files": ["dist", "src/generate-manifest.ts"],
17
+ "dependencies": {
18
+ "@swc/core": "^1.15.24",
19
+ "fast-glob": "^3.3.3",
20
+ "postcss": "^8.5.9",
21
+ "purgecss": "^8.0.0"
22
+ },
23
+ "peerDependencies": {
24
+ "@rsbuild/core": "^1.7.5"
25
+ },
26
+ "scripts": {
27
+ "build": "bun run build.ts",
28
+ "lint": "biome check .",
29
+ "format": "biome format . --write"
30
+ },
31
+ "devDependencies": {
32
+ "@rsbuild/core": "^1.7.5",
33
+ "bun-types": "^1.3.12"
34
+ }
35
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Lib-side database generator.
3
+ *
4
+ * Reads all `*.classnames.ts` files from @pathscale/ui's component tree
5
+ * and produces a `purge-manifest.json` that the consumer-side plugin uses
6
+ * to build safelists.
7
+ *
8
+ * Usage: bun run src/generate-manifest.ts <path-to-ui-src/components>
9
+ * Output: purge-manifest.json in cwd (or pass --out <path>)
10
+ */
11
+
12
+ import { Glob } from "bun";
13
+ import path from "path";
14
+
15
+ // ── Types ──────────────────────────────────────────────────────────────────────
16
+
17
+ type ClassValue = string | readonly string[];
18
+
19
+ interface ComponentManifest {
20
+ classes: {
21
+ always: string[];
22
+ byProp: Record<string, string[] | Record<string, string[]>>;
23
+ };
24
+ attrs?: Record<string, Record<string, string>>;
25
+ }
26
+
27
+ type PurgeManifest = Record<string, ComponentManifest>;
28
+
29
+ // ── Helpers ────────────────────────────────────────────────────────────────────
30
+
31
+ /** Flatten a class value (string or string[]) into an array of individual class names. */
32
+ function flattenClasses(val: ClassValue): string[] {
33
+ if (typeof val === "string") {
34
+ return val.split(/\s+/).filter(Boolean);
35
+ }
36
+ return (val as readonly string[]).flatMap((s) => s.split(/\s+/).filter(Boolean));
37
+ }
38
+
39
+ /** Check if a value is a plain object (not array, not null). */
40
+ function isRecord(v: unknown): v is Record<string, unknown> {
41
+ return v !== null && typeof v === "object" && !Array.isArray(v);
42
+ }
43
+
44
+ /**
45
+ * Walk a single CLASSES object (one component or one part of a compound component)
46
+ * and produce the manifest entry.
47
+ */
48
+ function walkClassesObject(obj: Record<string, unknown>): ComponentManifest {
49
+ const always: string[] = [];
50
+ const byProp: Record<string, string[] | Record<string, string[]>> = {};
51
+ let attrs: Record<string, Record<string, string>> | undefined;
52
+
53
+ for (const [slot, value] of Object.entries(obj)) {
54
+ if (slot === "base") {
55
+ always.push(...flattenClasses(value as ClassValue));
56
+ } else if (slot === "attrs") {
57
+ if (isRecord(value)) {
58
+ attrs = {};
59
+ for (const [propName, attrMap] of Object.entries(value)) {
60
+ if (isRecord(attrMap)) {
61
+ attrs[propName] = attrMap as Record<string, string>;
62
+ }
63
+ }
64
+ }
65
+ } else if (slot === "flag") {
66
+ if (isRecord(value)) {
67
+ for (const [propName, classVal] of Object.entries(value)) {
68
+ byProp[propName] = flattenClasses(classVal as ClassValue);
69
+ }
70
+ }
71
+ } else if (isRecord(value)) {
72
+ const enumMap: Record<string, string[]> = {};
73
+ for (const [enumKey, classVal] of Object.entries(value)) {
74
+ enumMap[enumKey] = flattenClasses(classVal as ClassValue);
75
+ }
76
+ byProp[slot] = enumMap;
77
+ }
78
+ }
79
+
80
+ const manifest: ComponentManifest = { classes: { always, byProp } };
81
+ if (attrs && Object.keys(attrs).length > 0) {
82
+ manifest.attrs = attrs;
83
+ }
84
+ return manifest;
85
+ }
86
+
87
+ /**
88
+ * Detect whether CLASSES is compound (top-level keys are part names like Root, Item)
89
+ * or flat (top-level keys are slots like base, variant, flag).
90
+ */
91
+ const KNOWN_SLOTS = new Set(["base", "variant", "size", "flag", "color", "attrs"]);
92
+
93
+ function isCompound(obj: Record<string, unknown>): boolean {
94
+ for (const key of Object.keys(obj)) {
95
+ if (KNOWN_SLOTS.has(key)) return false;
96
+ }
97
+ return true;
98
+ }
99
+
100
+ // ── Main ───────────────────────────────────────────────────────────────────────
101
+
102
+ async function main() {
103
+ const args = process.argv.slice(2);
104
+ let componentsDir = args[0];
105
+ let outPath = "purge-manifest.json";
106
+
107
+ const outIdx = args.indexOf("--out");
108
+ if (outIdx !== -1 && args[outIdx + 1]) {
109
+ outPath = args[outIdx + 1];
110
+ }
111
+
112
+ if (!componentsDir) {
113
+ console.error("Usage: bun run src/generate-manifest.ts <path-to-components-dir> [--out <path>]");
114
+ process.exit(1);
115
+ }
116
+
117
+ componentsDir = path.resolve(componentsDir);
118
+ console.log(`Scanning ${componentsDir} for *.classnames.ts files…`);
119
+
120
+ const manifest: PurgeManifest = {};
121
+ const glob = new Glob("**/*.classnames.ts");
122
+
123
+ for await (const relPath of glob.scan({ cwd: componentsDir })) {
124
+ const fullPath = path.join(componentsDir, relPath);
125
+
126
+ const mod = await import(fullPath);
127
+ const CLASSES = mod.CLASSES;
128
+
129
+ if (!CLASSES || typeof CLASSES !== "object") {
130
+ console.warn(` SKIP ${relPath} — no CLASSES export found`);
131
+ continue;
132
+ }
133
+
134
+ const fileName = path.basename(relPath, ".classnames.ts");
135
+
136
+ if (isCompound(CLASSES as Record<string, unknown>)) {
137
+ for (const [partName, partObj] of Object.entries(CLASSES as Record<string, unknown>)) {
138
+ if (isRecord(partObj)) {
139
+ const entryName = `${fileName}.${partName}`;
140
+ manifest[entryName] = walkClassesObject(partObj as Record<string, unknown>);
141
+ console.log(` ✓ ${entryName}`);
142
+ }
143
+ }
144
+ } else {
145
+ manifest[fileName] = walkClassesObject(CLASSES as Record<string, unknown>);
146
+ console.log(` ✓ ${fileName}`);
147
+ }
148
+ }
149
+
150
+ const json = JSON.stringify(manifest, null, 2);
151
+ await Bun.write(outPath, json);
152
+ console.log(`\nWrote ${outPath} (${Object.keys(manifest).length} entries, ${json.length} bytes)`);
153
+ }
154
+
155
+ main();