create-cmp-cli 0.4.0 → 0.6.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 +27 -10
- package/llms.txt +18 -1
- package/package.json +8 -3
- package/src/bootstrap/checks.mjs +9 -2
- package/src/lib/tabs.mjs +113 -0
- package/src/lib/verify.mjs +24 -1
- package/src/scaffold.mjs +60 -2
- package/template/AGENTS.md +8 -0
- package/template/CLAUDE.md +32 -0
- package/template/README.md +5 -0
- package/template/composeApp/build.gradle.kts +66 -1
- package/template/composeApp/src/commonMain/kotlin/com/example/app/core/format/Format.kt +36 -0
- package/template/composeApp/src/commonTest/kotlin/com/example/app/core/format/FormatTest.kt +36 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/inspector/PreviewDaemon.kt +219 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/inspector/PreviewHarness.kt +265 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/inspector/PreviewRegistry.kt +61 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/inspector/PreviewSemanticsJson.kt +83 -0
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ArchitectureConformanceTest.kt +83 -12
- package/template/manifest.json +4 -2
- package/template/qa/lib/a11y.mjs +104 -0
- package/template/qa/lib/render.mjs +254 -0
- package/template/qa/lib/tree.mjs +108 -0
- package/template/qa/preview-gallery.mjs +107 -0
- package/template/specs/app-base.spec.md +15 -7
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// preview-gallery.mjs — build a self-contained HTML gallery from renderScreens output.
|
|
3
|
+
//
|
|
4
|
+
// node qa/preview-gallery.mjs [previewsDir]
|
|
5
|
+
//
|
|
6
|
+
// Reads <previewsDir>/manifest.json (default composeApp/build/previews — the output of
|
|
7
|
+
// `./gradlew :composeApp:renderScreens`), renders each screen's tree.json to a wireframe
|
|
8
|
+
// SVG with the vendored inspector render lib (structure for the AI), and embeds the
|
|
9
|
+
// harness PNGs (pixels for the human) into ONE index.html — no server, no device, open
|
|
10
|
+
// the file. Also drops <id>/wireframe.svg next to each tree.
|
|
11
|
+
//
|
|
12
|
+
// Zero dependencies beyond qa/lib (vendored, pure logic) — works without the create-cmp
|
|
13
|
+
// plugin installed, like every other qa/ script.
|
|
14
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { join, resolve, dirname } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const previewsDir = resolve(
|
|
20
|
+
process.argv[2] || join(HERE, "..", "composeApp", "build", "previews"),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const { renderTreeSvg } = await import(new URL("./lib/render.mjs", import.meta.url));
|
|
24
|
+
const { auditA11y } = await import(new URL("./lib/a11y.mjs", import.meta.url));
|
|
25
|
+
|
|
26
|
+
const manifest = JSON.parse(readFileSync(join(previewsDir, "manifest.json"), "utf8"));
|
|
27
|
+
const { width, height, pngScale } = manifest.viewport;
|
|
28
|
+
|
|
29
|
+
const cards = [];
|
|
30
|
+
for (const screen of manifest.screens) {
|
|
31
|
+
const tree = JSON.parse(readFileSync(join(previewsDir, screen.tree), "utf8"));
|
|
32
|
+
const audit = auditA11y(tree);
|
|
33
|
+
const svg = renderTreeSvg(tree, { a11y: audit });
|
|
34
|
+
writeFileSync(join(previewsDir, screen.id, "wireframe.svg"), svg);
|
|
35
|
+
|
|
36
|
+
const png = readFileSync(join(previewsDir, screen.png));
|
|
37
|
+
const summary = summarize(tree);
|
|
38
|
+
cards.push({ screen, svg, pngB64: png.toString("base64"), audit, summary });
|
|
39
|
+
console.log(
|
|
40
|
+
`${screen.id}: ${summary.nodes} nodes, ${summary.tokenized} tokenized, ` +
|
|
41
|
+
`${summary.tagged} tagged, a11y ${audit.pass ? "PASS" : audit.violations.length + " violation(s)"}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function summarize(tree) {
|
|
46
|
+
let nodes = 0, tokenized = 0, tagged = 0;
|
|
47
|
+
(function walk(n) {
|
|
48
|
+
nodes++;
|
|
49
|
+
if (n.designToken) tokenized++;
|
|
50
|
+
if (n.testTag) tagged++;
|
|
51
|
+
(n.children || []).forEach(walk);
|
|
52
|
+
})(tree.root);
|
|
53
|
+
return { nodes, tokenized, tagged };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
57
|
+
|
|
58
|
+
const html = `<!doctype html>
|
|
59
|
+
<meta charset="utf-8">
|
|
60
|
+
<title>__APP_NAME__ — screen previews (headless)</title>
|
|
61
|
+
<style>
|
|
62
|
+
:root { color-scheme: light; }
|
|
63
|
+
body { font-family: -apple-system, system-ui, sans-serif; margin: 0; background: #F7F9FC; color: #1A1A1A; }
|
|
64
|
+
header { padding: 20px 28px 8px; }
|
|
65
|
+
header h1 { margin: 0 0 4px; font-size: 20px; }
|
|
66
|
+
header p { margin: 0; color: #6B7280; font-size: 13px; }
|
|
67
|
+
.grid { display: flex; flex-wrap: wrap; gap: 24px; padding: 20px 28px 40px; }
|
|
68
|
+
.card { background: #fff; border: 1px solid #E5E7EB; border-radius: 16px; padding: 16px; }
|
|
69
|
+
.card h2 { margin: 0 0 2px; font-size: 15px; }
|
|
70
|
+
.meta { color: #6B7280; font-size: 12px; margin: 0 0 10px; }
|
|
71
|
+
.meta .fail { color: #DC2626; font-weight: 600; }
|
|
72
|
+
.meta .pass { color: #16A34A; font-weight: 600; }
|
|
73
|
+
.panes { display: flex; gap: 12px; align-items: flex-start; }
|
|
74
|
+
.panes img { width: ${Math.round(width * 0.62)}px; border: 1px solid #E5E7EB; border-radius: 12px; display: block; }
|
|
75
|
+
.panes .wire svg { width: ${Math.round(width * 0.78)}px; height: auto; display: block; }
|
|
76
|
+
.wire { border: 1px dashed #C8D0DA; border-radius: 12px; overflow: hidden; }
|
|
77
|
+
.lbl { font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: #9CA3AF; margin: 0 0 4px; }
|
|
78
|
+
</style>
|
|
79
|
+
<header>
|
|
80
|
+
<h1>__APP_NAME__ — screen previews</h1>
|
|
81
|
+
<p>Rendered headlessly (no device/emulator) by <code>:composeApp:renderScreens</code> —
|
|
82
|
+
${width}×${height}dp, PNG @${pngScale}x · pixels for humans, wireframe+tree for the AI ·
|
|
83
|
+
regenerate: <code>./gradlew :composeApp:renderScreens && node qa/preview-gallery.mjs</code></p>
|
|
84
|
+
</header>
|
|
85
|
+
<div class="grid">
|
|
86
|
+
${cards
|
|
87
|
+
.map(
|
|
88
|
+
({ screen, svg, pngB64, audit, summary }) => ` <div class="card">
|
|
89
|
+
<h2>${esc(screen.title)}</h2>
|
|
90
|
+
<p class="meta">id <code>${esc(screen.id)}</code> · ${summary.nodes} nodes ·
|
|
91
|
+
${summary.tokenized} tokenized · ${summary.tagged} tagged ·
|
|
92
|
+
a11y <span class="${audit.pass ? "pass" : "fail"}">${
|
|
93
|
+
audit.pass ? "PASS" : esc(audit.violations.length + " violation(s)")
|
|
94
|
+
}</span></p>
|
|
95
|
+
<div class="panes">
|
|
96
|
+
<div><p class="lbl">pixels</p><img alt="${esc(screen.id)} pixels" src="data:image/png;base64,${pngB64}"></div>
|
|
97
|
+
<div><p class="lbl">structure</p><div class="wire">${svg}</div></div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>`,
|
|
100
|
+
)
|
|
101
|
+
.join("\n")}
|
|
102
|
+
</div>
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
const outFile = join(previewsDir, "index.html");
|
|
106
|
+
writeFileSync(outFile, html);
|
|
107
|
+
console.log(`gallery -> ${outFile}`);
|
|
@@ -5,15 +5,19 @@
|
|
|
5
5
|
|
|
6
6
|
## Architecture invariants
|
|
7
7
|
|
|
8
|
-
- **ARCH-01** — Given any file in `presentation`, When its imports
|
|
9
|
-
resolve into the `data` layer (presentation
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
- **ARCH-01** — Given any file in `presentation`, When its imports **and fully-qualified
|
|
9
|
+
inline references** are inspected, Then none resolve into the `data` layer (presentation
|
|
10
|
+
depends on domain only; qualifying the name inline instead of importing is the same
|
|
11
|
+
violation).
|
|
12
|
+
- **ARCH-02** — Given any file in `domain`, When its imports **and fully-qualified inline
|
|
13
|
+
references** are inspected, Then none resolve into `presentation`, `data`, or `di`, and
|
|
14
|
+
none reference Compose, Koin, or platform types (domain is pure Kotlin).
|
|
13
15
|
- **ARCH-03** — Given any ViewModel class, When the test sources are inspected, Then a
|
|
14
16
|
corresponding `*ViewModelTest` exists (no untested presentation state).
|
|
15
|
-
- **ARCH-04** — Given any file
|
|
16
|
-
inspected, Then it declares at least one
|
|
17
|
+
- **ARCH-04** — Given any file in a `presentation` feature package that contains a
|
|
18
|
+
`@Composable` function, When its source is inspected, Then it declares at least one
|
|
19
|
+
`testTag` (scoped by content, not `*Screen.kt` filename — split `Content.kt` UI files are
|
|
20
|
+
covered, ViewModel-only files are exempt).
|
|
17
21
|
- **ARCH-05** — Given any file outside `presentation/theme`, When its source is inspected,
|
|
18
22
|
Then it constructs no literal `Color(0x…)` values (design colors come from the token
|
|
19
23
|
catalog).
|
|
@@ -28,3 +32,7 @@
|
|
|
28
32
|
the safe-area insets owned by `BaseScreen` (edge-to-edge without overlap).
|
|
29
33
|
- **SHELL-04** — Given the app renders any screen, When interactive elements are present,
|
|
30
34
|
Then each is perceivable by automation: it exposes a testTag, text, or content description.
|
|
35
|
+
- **SHELL-05** — Given any screen registered directly on the NavHost (not a shell tab), When
|
|
36
|
+
it renders, Then its content is composed inside `BaseScreen` — a bare destination that
|
|
37
|
+
never touches inset APIs still renders under the status bar, which SHELL-03 alone cannot
|
|
38
|
+
catch.
|