decantr 0.9.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/AGENTS.md +868 -0
- package/CHANGELOG.md +255 -0
- package/CLAUDE.md +178 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/cli/art.js +127 -0
- package/cli/commands/a11y.js +61 -0
- package/cli/commands/audit.js +225 -0
- package/cli/commands/build.js +38 -0
- package/cli/commands/dev.js +18 -0
- package/cli/commands/doctor.js +197 -0
- package/cli/commands/figma-sync.js +48 -0
- package/cli/commands/figma-tokens.js +55 -0
- package/cli/commands/generate.js +26 -0
- package/cli/commands/init.js +116 -0
- package/cli/commands/lint.js +209 -0
- package/cli/commands/mcp.js +530 -0
- package/cli/commands/migrate.js +175 -0
- package/cli/commands/test.js +38 -0
- package/cli/commands/validate.js +354 -0
- package/cli/index.js +113 -0
- package/package.json +95 -0
- package/reference/atoms.md +517 -0
- package/reference/behaviors.md +384 -0
- package/reference/build-tooling.md +275 -0
- package/reference/color-guidelines.md +965 -0
- package/reference/component-lifecycle.md +137 -0
- package/reference/compound-spacing.md +95 -0
- package/reference/decantation-process.md +499 -0
- package/reference/dev-server-routes.md +93 -0
- package/reference/form-system.md +253 -0
- package/reference/i18n.md +336 -0
- package/reference/icons.md +576 -0
- package/reference/llm-primer.md +953 -0
- package/reference/plugins.md +252 -0
- package/reference/registry-consumption.md +76 -0
- package/reference/router.md +217 -0
- package/reference/shells.md +116 -0
- package/reference/spatial-guidelines.md +541 -0
- package/reference/ssr.md +234 -0
- package/reference/state-data.md +215 -0
- package/reference/state-patterns.md +166 -0
- package/reference/state.md +194 -0
- package/reference/style-system.md +110 -0
- package/reference/tokens.md +460 -0
- package/src/app.js +19 -0
- package/src/chart/_animate.js +266 -0
- package/src/chart/_base.js +109 -0
- package/src/chart/_data.js +209 -0
- package/src/chart/_format.js +106 -0
- package/src/chart/_interact.js +364 -0
- package/src/chart/_palette.js +105 -0
- package/src/chart/_renderer.js +52 -0
- package/src/chart/_scene.js +262 -0
- package/src/chart/_shared.js +371 -0
- package/src/chart/index.js +637 -0
- package/src/chart/layouts/_layout-base.js +328 -0
- package/src/chart/layouts/cartesian.js +148 -0
- package/src/chart/layouts/hierarchy.js +562 -0
- package/src/chart/layouts/polar.js +101 -0
- package/src/chart/renderers/canvas.js +179 -0
- package/src/chart/renderers/svg.js +256 -0
- package/src/chart/renderers/webgpu.js +715 -0
- package/src/chart/types/_type-base.js +26 -0
- package/src/chart/types/area.js +134 -0
- package/src/chart/types/bar.js +173 -0
- package/src/chart/types/box-plot.js +125 -0
- package/src/chart/types/bubble.js +63 -0
- package/src/chart/types/candlestick.js +115 -0
- package/src/chart/types/chord.js +85 -0
- package/src/chart/types/combination.js +108 -0
- package/src/chart/types/funnel.js +68 -0
- package/src/chart/types/gauge.js +163 -0
- package/src/chart/types/heatmap.js +98 -0
- package/src/chart/types/histogram.js +71 -0
- package/src/chart/types/line.js +111 -0
- package/src/chart/types/org-chart.js +93 -0
- package/src/chart/types/pie.js +81 -0
- package/src/chart/types/radar.js +96 -0
- package/src/chart/types/radial.js +68 -0
- package/src/chart/types/range-area.js +55 -0
- package/src/chart/types/range-bar.js +61 -0
- package/src/chart/types/sankey.js +73 -0
- package/src/chart/types/scatter.js +66 -0
- package/src/chart/types/sparkline.js +81 -0
- package/src/chart/types/sunburst.js +69 -0
- package/src/chart/types/swimlane.js +88 -0
- package/src/chart/types/treemap.js +62 -0
- package/src/chart/types/waterfall.js +100 -0
- package/src/components/_base.js +1658 -0
- package/src/components/_behaviors.js +1140 -0
- package/src/components/_primitives.js +534 -0
- package/src/components/_qr-encoder.js +539 -0
- package/src/components/accordion.js +207 -0
- package/src/components/affix.js +62 -0
- package/src/components/alert-dialog.js +75 -0
- package/src/components/alert.js +47 -0
- package/src/components/aspect-ratio.js +24 -0
- package/src/components/avatar-group.js +55 -0
- package/src/components/avatar.js +38 -0
- package/src/components/back-top.js +75 -0
- package/src/components/badge.js +74 -0
- package/src/components/banner.js +68 -0
- package/src/components/breadcrumb.js +162 -0
- package/src/components/button.js +115 -0
- package/src/components/calendar.js +131 -0
- package/src/components/card.js +192 -0
- package/src/components/carousel.js +98 -0
- package/src/components/cascader.js +261 -0
- package/src/components/checkbox.js +80 -0
- package/src/components/chip.js +81 -0
- package/src/components/code-block.js +82 -0
- package/src/components/collapsible.js +50 -0
- package/src/components/color-palette.js +438 -0
- package/src/components/color-picker.js +314 -0
- package/src/components/combobox.js +181 -0
- package/src/components/command.js +174 -0
- package/src/components/comment.js +206 -0
- package/src/components/context-menu.js +76 -0
- package/src/components/data-table.js +724 -0
- package/src/components/date-picker.js +217 -0
- package/src/components/date-range-picker.js +244 -0
- package/src/components/datetime-picker.js +271 -0
- package/src/components/descriptions.js +68 -0
- package/src/components/drawer.js +179 -0
- package/src/components/dropdown.js +88 -0
- package/src/components/empty.js +41 -0
- package/src/components/float-button.js +90 -0
- package/src/components/form.js +106 -0
- package/src/components/hover-card.js +49 -0
- package/src/components/icon.js +87 -0
- package/src/components/image.js +97 -0
- package/src/components/index.js +117 -0
- package/src/components/input-group.js +75 -0
- package/src/components/input-number.js +155 -0
- package/src/components/input-otp.js +178 -0
- package/src/components/input.js +91 -0
- package/src/components/kbd.js +36 -0
- package/src/components/label.js +25 -0
- package/src/components/list.js +118 -0
- package/src/components/masked-input.js +236 -0
- package/src/components/mentions.js +165 -0
- package/src/components/menu.js +259 -0
- package/src/components/message.js +80 -0
- package/src/components/modal.js +147 -0
- package/src/components/navigation-menu.js +166 -0
- package/src/components/notification.js +84 -0
- package/src/components/pagination.js +104 -0
- package/src/components/placeholder.js +132 -0
- package/src/components/popconfirm.js +70 -0
- package/src/components/popover.js +58 -0
- package/src/components/progress.js +61 -0
- package/src/components/qrcode.js +251 -0
- package/src/components/radiogroup.js +120 -0
- package/src/components/range-slider.js +176 -0
- package/src/components/rate.js +186 -0
- package/src/components/resizable.js +83 -0
- package/src/components/result.js +57 -0
- package/src/components/scroll-area.js +43 -0
- package/src/components/segmented.js +97 -0
- package/src/components/select.js +165 -0
- package/src/components/separator.js +31 -0
- package/src/components/shell.js +407 -0
- package/src/components/skeleton.js +39 -0
- package/src/components/slider.js +141 -0
- package/src/components/sortable-list.js +176 -0
- package/src/components/space.js +42 -0
- package/src/components/spinner.js +112 -0
- package/src/components/splitter.js +147 -0
- package/src/components/statistic.js +136 -0
- package/src/components/steps.js +99 -0
- package/src/components/switch.js +95 -0
- package/src/components/table.js +44 -0
- package/src/components/tabs.js +216 -0
- package/src/components/tag.js +115 -0
- package/src/components/textarea.js +82 -0
- package/src/components/time-picker.js +153 -0
- package/src/components/time-range-picker.js +170 -0
- package/src/components/timeline.js +226 -0
- package/src/components/toast.js +71 -0
- package/src/components/toggle.js +213 -0
- package/src/components/tooltip.js +57 -0
- package/src/components/tour.js +159 -0
- package/src/components/transfer.js +163 -0
- package/src/components/tree-select.js +274 -0
- package/src/components/tree.js +141 -0
- package/src/components/typography.js +136 -0
- package/src/components/upload.js +118 -0
- package/src/components/visually-hidden.js +20 -0
- package/src/components/watermark.js +124 -0
- package/src/core/index.js +539 -0
- package/src/core/lifecycle.js +69 -0
- package/src/css/atoms.js +651 -0
- package/src/css/components.js +940 -0
- package/src/css/derive.js +1296 -0
- package/src/css/index.js +265 -0
- package/src/css/runtime.js +268 -0
- package/src/css/styles/addons/bioluminescent.js +93 -0
- package/src/css/styles/addons/clay.js +70 -0
- package/src/css/styles/addons/clean.js +57 -0
- package/src/css/styles/addons/command-center.js +143 -0
- package/src/css/styles/addons/dopamine.js +83 -0
- package/src/css/styles/addons/editorial.js +80 -0
- package/src/css/styles/addons/glassmorphism.js +99 -0
- package/src/css/styles/addons/liquid-glass.js +105 -0
- package/src/css/styles/addons/prismatic.js +100 -0
- package/src/css/styles/addons/retro.js +63 -0
- package/src/css/styles/auradecantism.js +96 -0
- package/src/css/theme-registry.js +444 -0
- package/src/data/entity.js +281 -0
- package/src/data/index.js +13 -0
- package/src/data/persist.js +225 -0
- package/src/data/query.js +839 -0
- package/src/data/realtime.js +299 -0
- package/src/data/url.js +177 -0
- package/src/data/worker.js +134 -0
- package/src/explorer/archetypes.js +243 -0
- package/src/explorer/atoms.js +228 -0
- package/src/explorer/charts.js +497 -0
- package/src/explorer/components.js +129 -0
- package/src/explorer/foundations.js +949 -0
- package/src/explorer/icons.js +178 -0
- package/src/explorer/patterns.js +247 -0
- package/src/explorer/recipes.js +194 -0
- package/src/explorer/shared/pattern-examples.js +1337 -0
- package/src/explorer/shared/showcase-renderer.js +958 -0
- package/src/explorer/shared/spec-table.js +41 -0
- package/src/explorer/shared/usage-links.js +87 -0
- package/src/explorer/shell-config.js +10 -0
- package/src/explorer/shells.js +551 -0
- package/src/explorer/styles.js +161 -0
- package/src/explorer/tokens.js +262 -0
- package/src/explorer/tools.js +525 -0
- package/src/form/index.js +804 -0
- package/src/i18n/index.js +251 -0
- package/src/icons/essential.js +479 -0
- package/src/icons/index.js +53 -0
- package/src/plugins/index.js +282 -0
- package/src/registry/archetypes/content-site.json +71 -0
- package/src/registry/archetypes/docs-explorer.json +23 -0
- package/src/registry/archetypes/ecommerce.json +104 -0
- package/src/registry/archetypes/financial-dashboard.json +77 -0
- package/src/registry/archetypes/index.json +41 -0
- package/src/registry/archetypes/portfolio.json +82 -0
- package/src/registry/archetypes/recipe-community.json +159 -0
- package/src/registry/archetypes/saas-dashboard.json +86 -0
- package/src/registry/architect/cross-cutting.json +45 -0
- package/src/registry/architect/domains/ecommerce.json +294 -0
- package/src/registry/architect/domains/financial-services.json +302 -0
- package/src/registry/architect/index.json +26 -0
- package/src/registry/architect/traits.json +379 -0
- package/src/registry/atoms.json +16 -0
- package/src/registry/chart-showcase.json +160 -0
- package/src/registry/chart.json +136 -0
- package/src/registry/components.json +8616 -0
- package/src/registry/core.json +216 -0
- package/src/registry/css.json +319 -0
- package/src/registry/data.json +135 -0
- package/src/registry/foundations.json +11 -0
- package/src/registry/icons.json +463 -0
- package/src/registry/index.json +101 -0
- package/src/registry/patterns/activity-feed.json +37 -0
- package/src/registry/patterns/article-content.json +27 -0
- package/src/registry/patterns/auth-form.json +37 -0
- package/src/registry/patterns/author-card.json +20 -0
- package/src/registry/patterns/card-grid.json +127 -0
- package/src/registry/patterns/category-nav.json +26 -0
- package/src/registry/patterns/chart-grid.json +36 -0
- package/src/registry/patterns/chat-interface.json +37 -0
- package/src/registry/patterns/checklist-card.json +55 -0
- package/src/registry/patterns/comparison-panel.json +27 -0
- package/src/registry/patterns/component-showcase.json +24 -0
- package/src/registry/patterns/contact-form.json +31 -0
- package/src/registry/patterns/cta-section.json +20 -0
- package/src/registry/patterns/data-table.json +37 -0
- package/src/registry/patterns/detail-header.json +83 -0
- package/src/registry/patterns/detail-panel.json +27 -0
- package/src/registry/patterns/explorer-shell.json +22 -0
- package/src/registry/patterns/filter-bar.json +33 -0
- package/src/registry/patterns/filter-sidebar.json +27 -0
- package/src/registry/patterns/form-sections.json +110 -0
- package/src/registry/patterns/goal-tracker.json +27 -0
- package/src/registry/patterns/hero.json +107 -0
- package/src/registry/patterns/index.json +47 -0
- package/src/registry/patterns/kpi-grid.json +36 -0
- package/src/registry/patterns/media-gallery.json +20 -0
- package/src/registry/patterns/order-history.json +20 -0
- package/src/registry/patterns/pagination.json +19 -0
- package/src/registry/patterns/photo-to-recipe.json +36 -0
- package/src/registry/patterns/pipeline-tracker.json +28 -0
- package/src/registry/patterns/post-list.json +27 -0
- package/src/registry/patterns/pricing-table.json +32 -0
- package/src/registry/patterns/scorecard.json +28 -0
- package/src/registry/patterns/search-bar.json +20 -0
- package/src/registry/patterns/specimen-grid.json +19 -0
- package/src/registry/patterns/stat-card.json +55 -0
- package/src/registry/patterns/stats-bar.json +55 -0
- package/src/registry/patterns/steps-card.json +55 -0
- package/src/registry/patterns/table-of-contents.json +19 -0
- package/src/registry/patterns/testimonials.json +21 -0
- package/src/registry/patterns/timeline.json +27 -0
- package/src/registry/patterns/token-inspector.json +21 -0
- package/src/registry/patterns/wizard.json +27 -0
- package/src/registry/recipe-auradecantism.json +69 -0
- package/src/registry/recipe-clean.json +65 -0
- package/src/registry/recipe-command-center.json +78 -0
- package/src/registry/router.json +73 -0
- package/src/registry/schema/README.md +197 -0
- package/src/registry/skeletons.json +259 -0
- package/src/registry/state.json +137 -0
- package/src/registry/tokens.json +40 -0
- package/src/router/hash.js +17 -0
- package/src/router/history.js +18 -0
- package/src/router/index.js +598 -0
- package/src/ssr/index.js +922 -0
- package/src/state/arrays.js +181 -0
- package/src/state/devtools.js +647 -0
- package/src/state/index.js +498 -0
- package/src/state/middleware.js +288 -0
- package/src/state/scheduler.js +206 -0
- package/src/state/store.js +300 -0
- package/src/tags/index.js +19 -0
- package/src/tannins/auth.js +396 -0
- package/src/test/dom.js +352 -0
- package/src/test/index.js +62 -0
- package/src/test/state.js +306 -0
- package/tools/a11y-audit.js +487 -0
- package/tools/analyzer.js +315 -0
- package/tools/audit.js +706 -0
- package/tools/builder.js +1422 -0
- package/tools/css-extract.js +188 -0
- package/tools/dev-server.js +316 -0
- package/tools/dts-gen.js +1260 -0
- package/tools/figma-components.js +329 -0
- package/tools/figma-patterns.js +516 -0
- package/tools/figma-plugin/code.js +453 -0
- package/tools/figma-plugin/manifest.json +14 -0
- package/tools/figma-plugin/ui.html +268 -0
- package/tools/figma-render.js +293 -0
- package/tools/figma-tokens.js +712 -0
- package/tools/figma-upload.js +318 -0
- package/tools/generate.js +738 -0
- package/tools/icons.js +133 -0
- package/tools/init-templates.js +265 -0
- package/tools/install-hooks.sh +5 -0
- package/tools/migrations/0.5.0.js +53 -0
- package/tools/migrations/0.6.0.js +95 -0
- package/tools/minify.js +170 -0
- package/tools/pre-commit +4 -0
- package/tools/registry.js +662 -0
- package/tools/reset-playground.js +61 -0
- package/tools/starter-templates/content-site/app.js +49 -0
- package/tools/starter-templates/content-site/essence.js +19 -0
- package/tools/starter-templates/content-site/pages.js +31 -0
- package/tools/starter-templates/ecommerce/app.js +50 -0
- package/tools/starter-templates/ecommerce/essence.js +19 -0
- package/tools/starter-templates/ecommerce/pages.js +31 -0
- package/tools/starter-templates/landing-page/app.js +38 -0
- package/tools/starter-templates/landing-page/essence.js +18 -0
- package/tools/starter-templates/landing-page/pages.js +21 -0
- package/tools/starter-templates/portfolio/app.js +45 -0
- package/tools/starter-templates/portfolio/essence.js +19 -0
- package/tools/starter-templates/portfolio/pages.js +33 -0
- package/tools/starter-templates/saas-dashboard/app.js +70 -0
- package/tools/starter-templates/saas-dashboard/essence.js +19 -0
- package/tools/starter-templates/saas-dashboard/pages.js +31 -0
- package/tools/verify-pack.js +203 -0
- package/types/chart.d.ts +77 -0
- package/types/components.d.ts +587 -0
- package/types/core.d.ts +89 -0
- package/types/css.d.ts +149 -0
- package/types/data.d.ts +238 -0
- package/types/form.d.ts +164 -0
- package/types/i18n.d.ts +51 -0
- package/types/icons.d.ts +27 -0
- package/types/index.d.ts +13 -0
- package/types/router.d.ts +116 -0
- package/types/ssr.d.ts +102 -0
- package/types/state.d.ts +83 -0
- package/types/tags.d.ts +62 -0
- package/types/tannins.d.ts +63 -0
- package/types/test.d.ts +48 -0
package/tools/audit.js
ADDED
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { join, dirname, resolve, relative, extname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { gzipSync, brotliCompressSync, constants as zlibConstants } from 'node:zlib';
|
|
5
|
+
import { performance } from 'node:perf_hooks';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const registryDir = resolve(__dirname, '..', 'src', 'registry');
|
|
9
|
+
|
|
10
|
+
// ─── Helpers ─────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Recursively find all .js files under a directory.
|
|
14
|
+
* @param {string} dir
|
|
15
|
+
* @returns {Promise<string[]>}
|
|
16
|
+
*/
|
|
17
|
+
async function findJsFiles(dir) {
|
|
18
|
+
const results = [];
|
|
19
|
+
let entries;
|
|
20
|
+
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return results; }
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const full = join(dir, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
// Skip node_modules, dist, .decantr
|
|
25
|
+
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name.startsWith('.')) continue;
|
|
26
|
+
results.push(...await findJsFiles(full));
|
|
27
|
+
} else if (entry.name.endsWith('.js')) {
|
|
28
|
+
results.push(full);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Recursively find all files under a directory.
|
|
36
|
+
* @param {string} dir
|
|
37
|
+
* @returns {Promise<string[]>}
|
|
38
|
+
*/
|
|
39
|
+
async function findAllFiles(dir) {
|
|
40
|
+
const results = [];
|
|
41
|
+
let entries;
|
|
42
|
+
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return results; }
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const full = join(dir, entry.name);
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
results.push(...await findAllFiles(full));
|
|
47
|
+
} else {
|
|
48
|
+
results.push(full);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return results;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatSize(bytes) {
|
|
55
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
56
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
57
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function brotliSize(content) {
|
|
61
|
+
return brotliCompressSync(Buffer.from(content), {
|
|
62
|
+
params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 11 }
|
|
63
|
+
}).length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Safely read and parse a JSON file. Returns null on failure.
|
|
68
|
+
* @param {string} filePath
|
|
69
|
+
* @returns {Promise<any>}
|
|
70
|
+
*/
|
|
71
|
+
async function readJSON(filePath) {
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(await readFile(filePath, 'utf-8'));
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Registry Loaders ────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Load known component names from components.json.
|
|
83
|
+
* @returns {Promise<string[]>}
|
|
84
|
+
*/
|
|
85
|
+
async function loadComponentNames() {
|
|
86
|
+
const data = await readJSON(join(registryDir, 'components.json'));
|
|
87
|
+
if (!data || !data.components) return [];
|
|
88
|
+
return Object.keys(data.components);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Load known pattern names from patterns/index.json.
|
|
93
|
+
* @returns {Promise<string[]>}
|
|
94
|
+
*/
|
|
95
|
+
async function loadPatternNames() {
|
|
96
|
+
const data = await readJSON(join(registryDir, 'patterns', 'index.json'));
|
|
97
|
+
if (!data || !data.patterns) return [];
|
|
98
|
+
return Object.keys(data.patterns);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Pass 1: Essence Compliance ──────────────────────────────────
|
|
102
|
+
|
|
103
|
+
const DECANTATION_STAGES = ['POUR', 'SETTLE', 'CLARIFY', 'DECANT', 'SERVE', 'AGE'];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Analyze essence file for decantation stage compliance.
|
|
107
|
+
* @param {string} projectRoot
|
|
108
|
+
* @returns {Promise<{valid: boolean, terroir: string|null, stagesCompleted: string[], stagesSkipped: string[], errors: string[]}>}
|
|
109
|
+
*/
|
|
110
|
+
async function analyzeEssence(projectRoot) {
|
|
111
|
+
const result = {
|
|
112
|
+
valid: false,
|
|
113
|
+
terroir: null,
|
|
114
|
+
stagesCompleted: [],
|
|
115
|
+
stagesSkipped: [],
|
|
116
|
+
errors: []
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const essencePath = join(projectRoot, 'decantr.essence.json');
|
|
120
|
+
const essence = await readJSON(essencePath);
|
|
121
|
+
|
|
122
|
+
if (!essence) {
|
|
123
|
+
result.errors.push('No decantr.essence.json found');
|
|
124
|
+
result.stagesSkipped = [...DECANTATION_STAGES];
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
result.valid = true;
|
|
129
|
+
|
|
130
|
+
// POUR: always complete if essence exists — it means intent was captured
|
|
131
|
+
result.stagesCompleted.push('POUR');
|
|
132
|
+
|
|
133
|
+
// SETTLE: terroir + vintage + character must be present
|
|
134
|
+
const hasTerroir = !!(essence.terroir || (essence.sections && essence.sections.length > 0));
|
|
135
|
+
const hasVintage = !!(essence.vintage || (essence.sections && essence.sections.some(s => s.vintage)));
|
|
136
|
+
const hasCharacter = !!(essence.character && Array.isArray(essence.character) && essence.character.length > 0);
|
|
137
|
+
|
|
138
|
+
if (hasTerroir) {
|
|
139
|
+
result.terroir = essence.terroir || essence.sections?.map(s => s.terroir).join(', ') || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (hasTerroir && hasVintage && hasCharacter) {
|
|
143
|
+
result.stagesCompleted.push('SETTLE');
|
|
144
|
+
} else {
|
|
145
|
+
result.stagesSkipped.push('SETTLE');
|
|
146
|
+
if (!hasTerroir) result.errors.push('Missing terroir');
|
|
147
|
+
if (!hasVintage) result.errors.push('Missing vintage');
|
|
148
|
+
if (!hasCharacter) result.errors.push('Missing character');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// CLARIFY: structure + tannins present
|
|
152
|
+
const hasStructure = !!(
|
|
153
|
+
(essence.structure && Array.isArray(essence.structure) && essence.structure.length > 0) ||
|
|
154
|
+
(essence.sections && essence.sections.some(s => s.structure && s.structure.length > 0))
|
|
155
|
+
);
|
|
156
|
+
const hasTannins = !!(
|
|
157
|
+
(essence.tannins && Array.isArray(essence.tannins) && essence.tannins.length > 0) ||
|
|
158
|
+
(essence.sections && essence.sections.some(s => s.tannins && s.tannins.length > 0))
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (hasStructure && hasTannins) {
|
|
162
|
+
result.stagesCompleted.push('CLARIFY');
|
|
163
|
+
} else {
|
|
164
|
+
result.stagesSkipped.push('CLARIFY');
|
|
165
|
+
if (!hasStructure) result.errors.push('Missing structure');
|
|
166
|
+
if (!hasTannins) result.errors.push('Missing tannins');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// DECANT: style file exists in src/css/styles/
|
|
170
|
+
const vintage = essence.vintage || (essence.sections && essence.sections[0]?.vintage) || {};
|
|
171
|
+
const styleId = vintage.style;
|
|
172
|
+
if (styleId) {
|
|
173
|
+
const stylePath = resolve(__dirname, '..', 'src', 'css', 'styles', `${styleId}.js`);
|
|
174
|
+
try {
|
|
175
|
+
await stat(stylePath);
|
|
176
|
+
result.stagesCompleted.push('DECANT');
|
|
177
|
+
} catch {
|
|
178
|
+
result.stagesSkipped.push('DECANT');
|
|
179
|
+
result.errors.push(`Style file not found: ${styleId}.js`);
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
result.stagesSkipped.push('DECANT');
|
|
183
|
+
result.errors.push('No style specified in vintage');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// SERVE: pages exist in src/pages/
|
|
187
|
+
const pagesDir = join(projectRoot, 'src', 'pages');
|
|
188
|
+
try {
|
|
189
|
+
const pagesEntries = await readdir(pagesDir);
|
|
190
|
+
if (pagesEntries.length > 0) {
|
|
191
|
+
result.stagesCompleted.push('SERVE');
|
|
192
|
+
} else {
|
|
193
|
+
result.stagesSkipped.push('SERVE');
|
|
194
|
+
result.errors.push('src/pages/ exists but is empty');
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
result.stagesSkipped.push('SERVE');
|
|
198
|
+
result.errors.push('No src/pages/ directory found');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// AGE: cork rules present
|
|
202
|
+
const hasCork = !!(essence.cork && (essence.cork.rules || essence.cork.enforce_style || essence.cork.enforce_recipe));
|
|
203
|
+
if (hasCork) {
|
|
204
|
+
result.stagesCompleted.push('AGE');
|
|
205
|
+
} else {
|
|
206
|
+
result.stagesSkipped.push('AGE');
|
|
207
|
+
result.errors.push('No cork rules defined');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── Pass 2: Source Analysis ─────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @typedef {Object} SourceAnalysis
|
|
217
|
+
* @property {number} frameworkDerivedPct
|
|
218
|
+
* @property {number} projectSpecificPct
|
|
219
|
+
* @property {string[]} componentsUsed
|
|
220
|
+
* @property {number} componentsTotal
|
|
221
|
+
* @property {string[]} patternsUsed
|
|
222
|
+
* @property {number} patternsTotal
|
|
223
|
+
* @property {number} atomCalls
|
|
224
|
+
* @property {number} inlineStyleViolations
|
|
225
|
+
* @property {number} frameworkImports
|
|
226
|
+
* @property {number} totalStatements
|
|
227
|
+
*/
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Analyze source files for framework derivation vs improvised code.
|
|
231
|
+
* @param {string} projectRoot
|
|
232
|
+
* @param {string[]} knownComponents
|
|
233
|
+
* @param {string[]} knownPatterns
|
|
234
|
+
* @returns {Promise<SourceAnalysis>}
|
|
235
|
+
*/
|
|
236
|
+
async function analyzeSource(projectRoot, knownComponents, knownPatterns) {
|
|
237
|
+
const srcDir = join(projectRoot, 'src');
|
|
238
|
+
const files = await findJsFiles(srcDir);
|
|
239
|
+
|
|
240
|
+
let frameworkImports = 0;
|
|
241
|
+
let atomCalls = 0;
|
|
242
|
+
let inlineStyleViolations = 0;
|
|
243
|
+
let totalStatements = 0;
|
|
244
|
+
|
|
245
|
+
const usedComponents = new Set();
|
|
246
|
+
const usedPatterns = new Set();
|
|
247
|
+
|
|
248
|
+
// Build regex patterns for component detection
|
|
249
|
+
// Match ComponentName( — function call usage
|
|
250
|
+
const componentCallRe = new RegExp(
|
|
251
|
+
'\\b(' + knownComponents.map(c => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + ')\\s*\\(',
|
|
252
|
+
'g'
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// Build pattern detection regex — pattern names in string literals or comments
|
|
256
|
+
const patternNameSet = new Set(knownPatterns);
|
|
257
|
+
|
|
258
|
+
for (const file of files) {
|
|
259
|
+
let source;
|
|
260
|
+
try { source = await readFile(file, 'utf-8'); } catch { continue; }
|
|
261
|
+
|
|
262
|
+
// Count approximate statements (lines with meaningful code)
|
|
263
|
+
const lines = source.split('\n');
|
|
264
|
+
for (const line of lines) {
|
|
265
|
+
const trimmed = line.trim();
|
|
266
|
+
if (trimmed && !trimmed.startsWith('//') && !trimmed.startsWith('/*') && !trimmed.startsWith('*')) {
|
|
267
|
+
totalStatements++;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Count decantr imports
|
|
272
|
+
const importMatches = source.match(/from\s+['"]decantr\//g);
|
|
273
|
+
if (importMatches) frameworkImports += importMatches.length;
|
|
274
|
+
|
|
275
|
+
// Count css() calls
|
|
276
|
+
const cssMatches = source.match(/\bcss\s*\(/g);
|
|
277
|
+
if (cssMatches) atomCalls += cssMatches.length;
|
|
278
|
+
|
|
279
|
+
// Detect inline style violations — static px/rem/hex values in style: or .style.
|
|
280
|
+
// Match style: 'anything with px/rem/#hex' (static values only)
|
|
281
|
+
const styleAttrRe = /style:\s*['"`]([^'"`]*?)['"`]/g;
|
|
282
|
+
let styleMatch;
|
|
283
|
+
while ((styleMatch = styleAttrRe.exec(source)) !== null) {
|
|
284
|
+
const val = styleMatch[1];
|
|
285
|
+
// Flag if contains px, rem, em, hex color — these are static values
|
|
286
|
+
if (/\d+px|\d+rem|\d+em|#[0-9a-fA-F]{3,8}/.test(val)) {
|
|
287
|
+
inlineStyleViolations++;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Also check .style.property = 'value' with static values
|
|
291
|
+
const dotStyleRe = /\.style\.\w+\s*=\s*['"`]([^'"`]*?)['"`]/g;
|
|
292
|
+
while ((styleMatch = dotStyleRe.exec(source)) !== null) {
|
|
293
|
+
const val = styleMatch[1];
|
|
294
|
+
if (/\d+px|\d+rem|\d+em|#[0-9a-fA-F]{3,8}/.test(val)) {
|
|
295
|
+
inlineStyleViolations++;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Detect component usage
|
|
300
|
+
let compMatch;
|
|
301
|
+
while ((compMatch = componentCallRe.exec(source)) !== null) {
|
|
302
|
+
usedComponents.add(compMatch[1]);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Detect pattern references in strings or comments
|
|
306
|
+
for (const pName of patternNameSet) {
|
|
307
|
+
if (source.includes(pName)) {
|
|
308
|
+
usedPatterns.add(pName);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Compute derivation percentage
|
|
314
|
+
// Framework-derived signals: imports + atom calls + component uses
|
|
315
|
+
const frameworkSignals = frameworkImports + atomCalls + usedComponents.size;
|
|
316
|
+
// Total signals approximate: all statements
|
|
317
|
+
const derivedPct = totalStatements > 0
|
|
318
|
+
? Math.min(100, Math.round((frameworkSignals / totalStatements) * 100))
|
|
319
|
+
: 0;
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
frameworkDerivedPct: derivedPct,
|
|
323
|
+
projectSpecificPct: 100 - derivedPct,
|
|
324
|
+
componentsUsed: [...usedComponents].sort(),
|
|
325
|
+
componentsTotal: knownComponents.length,
|
|
326
|
+
patternsUsed: [...usedPatterns].sort(),
|
|
327
|
+
patternsTotal: knownPatterns.length,
|
|
328
|
+
atomCalls,
|
|
329
|
+
inlineStyleViolations,
|
|
330
|
+
frameworkImports,
|
|
331
|
+
totalStatements
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ─── Pass 3: Quality Checks ─────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* @typedef {Object} QualityReport
|
|
339
|
+
* @property {{file: string, line: number, value: string}[]} hardcodedCSS
|
|
340
|
+
* @property {{file: string, line: number}[]} missingAria
|
|
341
|
+
* @property {{file: string, line: number}[]} leakedListeners
|
|
342
|
+
* @property {{file: string, line: number}[]} missingFocusTrap
|
|
343
|
+
*/
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Scan source files for quality issues.
|
|
347
|
+
* @param {string} projectRoot
|
|
348
|
+
* @returns {Promise<QualityReport>}
|
|
349
|
+
*/
|
|
350
|
+
async function checkQuality(projectRoot) {
|
|
351
|
+
const srcDir = join(projectRoot, 'src');
|
|
352
|
+
const files = await findJsFiles(srcDir);
|
|
353
|
+
|
|
354
|
+
/** @type {QualityReport} */
|
|
355
|
+
const report = {
|
|
356
|
+
hardcodedCSS: [],
|
|
357
|
+
missingAria: [],
|
|
358
|
+
leakedListeners: [],
|
|
359
|
+
missingFocusTrap: []
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
for (const file of files) {
|
|
363
|
+
let source;
|
|
364
|
+
try { source = await readFile(file, 'utf-8'); } catch { continue; }
|
|
365
|
+
|
|
366
|
+
const relFile = relative(projectRoot, file);
|
|
367
|
+
const lines = source.split('\n');
|
|
368
|
+
|
|
369
|
+
// Track whether file has onDestroy somewhere
|
|
370
|
+
const hasOnDestroy = /\bonDestroy\b/.test(source);
|
|
371
|
+
|
|
372
|
+
// Track whether file has createFocusTrap
|
|
373
|
+
const hasFocusTrap = /\bcreateFocusTrap\b/.test(source);
|
|
374
|
+
|
|
375
|
+
// Track whether file creates overlays (Modal, Drawer, Popover)
|
|
376
|
+
const isOverlay = /\b(Modal|Drawer|Popover)\s*\(/.test(source) ||
|
|
377
|
+
/role:\s*['"]dialog['"]/.test(source);
|
|
378
|
+
|
|
379
|
+
for (let i = 0; i < lines.length; i++) {
|
|
380
|
+
const line = lines[i];
|
|
381
|
+
const lineNum = i + 1;
|
|
382
|
+
|
|
383
|
+
// Hardcoded CSS: style: with px/rem/hex static values
|
|
384
|
+
const styleAttrMatch = line.match(/style:\s*['"`]([^'"`]*?)['"`]/);
|
|
385
|
+
if (styleAttrMatch) {
|
|
386
|
+
const val = styleAttrMatch[1];
|
|
387
|
+
if (/\d+px|\d+rem|\d+em|#[0-9a-fA-F]{3,8}/.test(val)) {
|
|
388
|
+
report.hardcodedCSS.push({ file: relFile, line: lineNum, value: val.slice(0, 60) });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const dotStyleMatch = line.match(/\.style\.\w+\s*=\s*['"`]([^'"`]*?)['"`]/);
|
|
392
|
+
if (dotStyleMatch) {
|
|
393
|
+
const val = dotStyleMatch[1];
|
|
394
|
+
if (/\d+px|\d+rem|\d+em|#[0-9a-fA-F]{3,8}/.test(val)) {
|
|
395
|
+
report.hardcodedCSS.push({ file: relFile, line: lineNum, value: val.slice(0, 60) });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Missing aria-label on icon-only buttons
|
|
400
|
+
// Detect icon( call near Button( without aria-label nearby
|
|
401
|
+
if (/\bicon\s*\(/.test(line)) {
|
|
402
|
+
// Look within a 5-line window for Button( without aria-label
|
|
403
|
+
const windowStart = Math.max(0, i - 3);
|
|
404
|
+
const windowEnd = Math.min(lines.length - 1, i + 3);
|
|
405
|
+
const window = lines.slice(windowStart, windowEnd + 1).join('\n');
|
|
406
|
+
if (/\bButton\s*\(/.test(window) && !/aria-label/.test(window)) {
|
|
407
|
+
report.missingAria.push({ file: relFile, line: lineNum });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Leaked listeners: addEventListener without onDestroy in the same file
|
|
412
|
+
if (/\.addEventListener\s*\(/.test(line) || /document\.addEventListener/.test(line) || /window\.addEventListener/.test(line)) {
|
|
413
|
+
if (!hasOnDestroy) {
|
|
414
|
+
report.leakedListeners.push({ file: relFile, line: lineNum });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Missing focus trap in overlay components
|
|
420
|
+
if (isOverlay && !hasFocusTrap) {
|
|
421
|
+
report.missingFocusTrap.push({ file: relFile, line: 1 });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return report;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ─── Pass 4: Coverage Gaps ──────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* @typedef {Object} CoverageGaps
|
|
432
|
+
* @property {string[]} unusedPatterns
|
|
433
|
+
* @property {string[]} unusedComponents
|
|
434
|
+
* @property {string[]} missingPages
|
|
435
|
+
* @property {string[]} unimplementedTannins
|
|
436
|
+
*/
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Compare used vs available patterns, components, pages, tannins.
|
|
440
|
+
* @param {string} projectRoot
|
|
441
|
+
* @param {string[]} usedComponents
|
|
442
|
+
* @param {string[]} usedPatterns
|
|
443
|
+
* @param {string[]} allComponents
|
|
444
|
+
* @param {string[]} allPatterns
|
|
445
|
+
* @returns {Promise<CoverageGaps>}
|
|
446
|
+
*/
|
|
447
|
+
async function analyzeCoverageGaps(projectRoot, usedComponents, usedPatterns, allComponents, allPatterns) {
|
|
448
|
+
const usedCompSet = new Set(usedComponents);
|
|
449
|
+
const usedPatSet = new Set(usedPatterns);
|
|
450
|
+
|
|
451
|
+
const unusedComponents = allComponents.filter(c => !usedCompSet.has(c));
|
|
452
|
+
const unusedPatterns = allPatterns.filter(p => !usedPatSet.has(p));
|
|
453
|
+
|
|
454
|
+
// Pages: compare essence structure vs src/pages/ files
|
|
455
|
+
const missingPages = [];
|
|
456
|
+
const essence = await readJSON(join(projectRoot, 'decantr.essence.json'));
|
|
457
|
+
|
|
458
|
+
if (essence) {
|
|
459
|
+
const declaredPages = [];
|
|
460
|
+
if (essence.structure && Array.isArray(essence.structure)) {
|
|
461
|
+
for (const page of essence.structure) {
|
|
462
|
+
if (page.id) declaredPages.push(page.id);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (essence.sections && Array.isArray(essence.sections)) {
|
|
466
|
+
for (const section of essence.sections) {
|
|
467
|
+
if (section.structure && Array.isArray(section.structure)) {
|
|
468
|
+
for (const page of section.structure) {
|
|
469
|
+
if (page.id) declaredPages.push(`${section.id}/${page.id}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Check which pages have corresponding files
|
|
476
|
+
const pagesDir = join(projectRoot, 'src', 'pages');
|
|
477
|
+
let pageFiles = [];
|
|
478
|
+
try {
|
|
479
|
+
pageFiles = await findJsFiles(pagesDir);
|
|
480
|
+
pageFiles = pageFiles.map(f => relative(pagesDir, f).replace(/\.js$/, '').replace(/\\/g, '/'));
|
|
481
|
+
} catch { /* no pages dir */ }
|
|
482
|
+
|
|
483
|
+
const pageFileSet = new Set(pageFiles);
|
|
484
|
+
for (const pageId of declaredPages) {
|
|
485
|
+
// Check for exact match or index file
|
|
486
|
+
if (!pageFileSet.has(pageId) && !pageFileSet.has(`${pageId}/index`)) {
|
|
487
|
+
missingPages.push(pageId);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Tannins: compare declared vs implemented
|
|
493
|
+
const unimplementedTannins = [];
|
|
494
|
+
if (essence) {
|
|
495
|
+
const declaredTannins = [];
|
|
496
|
+
if (essence.tannins && Array.isArray(essence.tannins)) {
|
|
497
|
+
declaredTannins.push(...essence.tannins);
|
|
498
|
+
}
|
|
499
|
+
if (essence.sections && Array.isArray(essence.sections)) {
|
|
500
|
+
for (const section of essence.sections) {
|
|
501
|
+
if (section.tannins && Array.isArray(section.tannins)) {
|
|
502
|
+
declaredTannins.push(...section.tannins);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Look for tannin implementations in src/
|
|
508
|
+
const srcDir = join(projectRoot, 'src');
|
|
509
|
+
const allSrcFiles = await findJsFiles(srcDir);
|
|
510
|
+
const allSrcContent = [];
|
|
511
|
+
for (const f of allSrcFiles) {
|
|
512
|
+
try {
|
|
513
|
+
const content = await readFile(f, 'utf-8');
|
|
514
|
+
allSrcContent.push({ file: f, content });
|
|
515
|
+
} catch { /* skip */ }
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
for (const tannin of declaredTannins) {
|
|
519
|
+
// Normalize: check for file name match or string reference
|
|
520
|
+
const tanninSlug = tannin.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase();
|
|
521
|
+
const found = allSrcContent.some(({ file, content }) => {
|
|
522
|
+
const fileName = relative(srcDir, file).toLowerCase();
|
|
523
|
+
return fileName.includes(tanninSlug) || content.includes(tannin);
|
|
524
|
+
});
|
|
525
|
+
if (!found) {
|
|
526
|
+
unimplementedTannins.push(tannin);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
unusedPatterns,
|
|
533
|
+
unusedComponents,
|
|
534
|
+
missingPages,
|
|
535
|
+
unimplementedTannins
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ─── Pass 5: Bundle Metrics ─────────────────────────────────────
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* @typedef {Object} SizeMetrics
|
|
543
|
+
* @property {number} raw
|
|
544
|
+
* @property {number} gzip
|
|
545
|
+
* @property {number} brotli
|
|
546
|
+
* @property {string} rawFormatted
|
|
547
|
+
* @property {string} gzipFormatted
|
|
548
|
+
* @property {string} brotliFormatted
|
|
549
|
+
*/
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* @typedef {Object} BundleReport
|
|
553
|
+
* @property {number} buildTimeMs
|
|
554
|
+
* @property {SizeMetrics} js
|
|
555
|
+
* @property {SizeMetrics} css
|
|
556
|
+
* @property {SizeMetrics} html
|
|
557
|
+
* @property {SizeMetrics} total
|
|
558
|
+
* @property {number} moduleCount
|
|
559
|
+
* @property {string|null} error
|
|
560
|
+
*/
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Build the project and measure output sizes.
|
|
564
|
+
* @param {string} projectRoot
|
|
565
|
+
* @returns {Promise<BundleReport>}
|
|
566
|
+
*/
|
|
567
|
+
async function measureBundle(projectRoot) {
|
|
568
|
+
/** @type {BundleReport} */
|
|
569
|
+
const report = {
|
|
570
|
+
buildTimeMs: 0,
|
|
571
|
+
js: { raw: 0, gzip: 0, brotli: 0, rawFormatted: '0 B', gzipFormatted: '0 B', brotliFormatted: '0 B' },
|
|
572
|
+
css: { raw: 0, gzip: 0, brotli: 0, rawFormatted: '0 B', gzipFormatted: '0 B', brotliFormatted: '0 B' },
|
|
573
|
+
html: { raw: 0, gzip: 0, brotli: 0, rawFormatted: '0 B', gzipFormatted: '0 B', brotliFormatted: '0 B' },
|
|
574
|
+
total: { raw: 0, gzip: 0, brotli: 0, rawFormatted: '0 B', gzipFormatted: '0 B', brotliFormatted: '0 B' },
|
|
575
|
+
moduleCount: 0,
|
|
576
|
+
error: null
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
const { build } = await import('./builder.js');
|
|
581
|
+
|
|
582
|
+
const t0 = performance.now();
|
|
583
|
+
await build(projectRoot, {
|
|
584
|
+
sourcemap: false,
|
|
585
|
+
analyze: false,
|
|
586
|
+
incremental: false
|
|
587
|
+
});
|
|
588
|
+
const t1 = performance.now();
|
|
589
|
+
report.buildTimeMs = Math.round(t1 - t0);
|
|
590
|
+
|
|
591
|
+
// Scan dist/ for file sizes
|
|
592
|
+
const distDir = join(projectRoot, 'dist');
|
|
593
|
+
const distFiles = await findAllFiles(distDir);
|
|
594
|
+
|
|
595
|
+
let jsRaw = 0, cssRaw = 0, htmlRaw = 0;
|
|
596
|
+
let jsContent = '', cssContent = '', htmlContent = '';
|
|
597
|
+
|
|
598
|
+
for (const file of distFiles) {
|
|
599
|
+
const ext = extname(file).toLowerCase();
|
|
600
|
+
try {
|
|
601
|
+
const content = await readFile(file, 'utf-8');
|
|
602
|
+
const size = Buffer.byteLength(content);
|
|
603
|
+
if (ext === '.js') {
|
|
604
|
+
jsRaw += size;
|
|
605
|
+
jsContent += content;
|
|
606
|
+
report.moduleCount++;
|
|
607
|
+
} else if (ext === '.css') {
|
|
608
|
+
cssRaw += size;
|
|
609
|
+
cssContent += content;
|
|
610
|
+
} else if (ext === '.html') {
|
|
611
|
+
htmlRaw += size;
|
|
612
|
+
htmlContent += content;
|
|
613
|
+
}
|
|
614
|
+
// Skip .map and other files
|
|
615
|
+
} catch { /* binary files — skip */ }
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function buildSizeMetrics(content, raw) {
|
|
619
|
+
if (raw === 0 || !content) {
|
|
620
|
+
return { raw: 0, gzip: 0, brotli: 0, rawFormatted: '0 B', gzipFormatted: '0 B', brotliFormatted: '0 B' };
|
|
621
|
+
}
|
|
622
|
+
const gz = gzipSync(content).length;
|
|
623
|
+
const br = brotliSize(content);
|
|
624
|
+
return {
|
|
625
|
+
raw,
|
|
626
|
+
gzip: gz,
|
|
627
|
+
brotli: br,
|
|
628
|
+
rawFormatted: formatSize(raw),
|
|
629
|
+
gzipFormatted: formatSize(gz),
|
|
630
|
+
brotliFormatted: formatSize(br)
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
report.js = buildSizeMetrics(jsContent, jsRaw);
|
|
635
|
+
report.css = buildSizeMetrics(cssContent, cssRaw);
|
|
636
|
+
report.html = buildSizeMetrics(htmlContent, htmlRaw);
|
|
637
|
+
|
|
638
|
+
const totalRaw = jsRaw + cssRaw + htmlRaw;
|
|
639
|
+
const totalContent = jsContent + cssContent + htmlContent;
|
|
640
|
+
report.total = buildSizeMetrics(totalContent, totalRaw);
|
|
641
|
+
|
|
642
|
+
} catch (err) {
|
|
643
|
+
report.error = err.message || String(err);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return report;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ─── Main Audit Function ─────────────────────────────────────────
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Run all 5 audit passes against a project.
|
|
653
|
+
*
|
|
654
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
655
|
+
* @returns {Promise<{
|
|
656
|
+
* essence: { valid: boolean, terroir: string|null, stagesCompleted: string[], stagesSkipped: string[], errors: string[] },
|
|
657
|
+
* coverage: { frameworkDerivedPct: number, componentsUsed: string[], componentsTotal: number, patternsUsed: string[], patternsTotal: number, atomCalls: number, violations: number },
|
|
658
|
+
* quality: { hardcodedCSS: {file: string, line: number, value: string}[], missingAria: {file: string, line: number}[], leakedListeners: {file: string, line: number}[], missingFocusTrap: {file: string, line: number}[] },
|
|
659
|
+
* gaps: { unusedPatterns: string[], unusedComponents: string[], missingPages: string[], unimplementedTannins: string[] },
|
|
660
|
+
* bundle: { buildTimeMs: number, js: SizeMetrics, css: SizeMetrics, html: SizeMetrics, total: SizeMetrics, moduleCount: number, error: string|null }
|
|
661
|
+
* }>}
|
|
662
|
+
*/
|
|
663
|
+
export async function audit(projectRoot) {
|
|
664
|
+
projectRoot = resolve(projectRoot);
|
|
665
|
+
|
|
666
|
+
// Load registry data
|
|
667
|
+
const [knownComponents, knownPatterns] = await Promise.all([
|
|
668
|
+
loadComponentNames(),
|
|
669
|
+
loadPatternNames()
|
|
670
|
+
]);
|
|
671
|
+
|
|
672
|
+
// Run pass 1 (essence) and pass 2 (source) and pass 3 (quality) in parallel
|
|
673
|
+
const [essenceResult, sourceResult, qualityResult] = await Promise.all([
|
|
674
|
+
analyzeEssence(projectRoot),
|
|
675
|
+
analyzeSource(projectRoot, knownComponents, knownPatterns),
|
|
676
|
+
checkQuality(projectRoot)
|
|
677
|
+
]);
|
|
678
|
+
|
|
679
|
+
// Pass 4 depends on pass 2 results
|
|
680
|
+
const gapsResult = await analyzeCoverageGaps(
|
|
681
|
+
projectRoot,
|
|
682
|
+
sourceResult.componentsUsed,
|
|
683
|
+
sourceResult.patternsUsed,
|
|
684
|
+
knownComponents,
|
|
685
|
+
knownPatterns
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
// Pass 5: bundle metrics (runs build, so do it last)
|
|
689
|
+
const bundleResult = await measureBundle(projectRoot);
|
|
690
|
+
|
|
691
|
+
return {
|
|
692
|
+
essence: essenceResult,
|
|
693
|
+
coverage: {
|
|
694
|
+
frameworkDerivedPct: sourceResult.frameworkDerivedPct,
|
|
695
|
+
componentsUsed: sourceResult.componentsUsed,
|
|
696
|
+
componentsTotal: sourceResult.componentsTotal,
|
|
697
|
+
patternsUsed: sourceResult.patternsUsed,
|
|
698
|
+
patternsTotal: sourceResult.patternsTotal,
|
|
699
|
+
atomCalls: sourceResult.atomCalls,
|
|
700
|
+
violations: sourceResult.inlineStyleViolations
|
|
701
|
+
},
|
|
702
|
+
quality: qualityResult,
|
|
703
|
+
gaps: gapsResult,
|
|
704
|
+
bundle: bundleResult
|
|
705
|
+
};
|
|
706
|
+
}
|