create-koppajs 1.0.1 → 1.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/CHANGELOG.md +105 -0
- package/README.md +169 -131
- package/bin/create-koppajs.js +201 -175
- package/package.json +54 -34
- package/template/AI_CONSTITUTION.md +50 -0
- package/template/ARCHITECTURE.md +86 -0
- package/template/CHANGELOG.md +34 -0
- package/template/CONTRIBUTING.md +92 -0
- package/template/DECISION_HIERARCHY.md +32 -0
- package/template/DEVELOPMENT_RULES.md +57 -0
- package/template/LICENSE +1 -1
- package/template/README.md +241 -49
- package/template/RELEASE.md +230 -0
- package/template/ROADMAP.md +34 -0
- package/template/TESTING_STRATEGY.md +93 -0
- package/template/_editorconfig +12 -0
- package/template/_gitattributes +1 -0
- package/template/_github/instructions/ai-workflow.md +33 -0
- package/template/_github/workflows/ci.yml +38 -0
- package/template/_github/workflows/release.yml +58 -0
- package/template/_gitignore +5 -0
- package/template/_husky/commit-msg +8 -0
- package/template/_husky/pre-commit +1 -0
- package/template/_npmrc +1 -0
- package/template/_prettierignore +7 -0
- package/template/commitlint.config.mjs +6 -0
- package/template/docs/adr/0001-keep-the-starter-minimal.md +32 -0
- package/template/docs/adr/0002-adopt-a-living-meta-layer.md +30 -0
- package/template/docs/adr/0003-normalize-kpa-plugin-output.md +24 -0
- package/template/docs/adr/0004-adopt-an-automated-quality-baseline.md +31 -0
- package/template/docs/adr/0005-adopt-a-tag-driven-release-baseline.md +45 -0
- package/template/docs/adr/0006-adopt-commit-message-conventions.md +39 -0
- package/template/docs/adr/README.md +37 -0
- package/template/docs/adr/TEMPLATE.md +18 -0
- package/template/docs/architecture/module-boundaries.md +48 -0
- package/template/docs/meta/maintenance.md +40 -0
- package/template/docs/quality/quality-gates.md +39 -0
- package/template/docs/specs/README.md +36 -0
- package/template/docs/specs/TEMPLATE.md +34 -0
- package/template/docs/specs/app-bootstrap.md +46 -0
- package/template/docs/specs/counter-component.md +48 -0
- package/template/docs/specs/quality-workflow.md +62 -0
- package/template/eslint.config.mjs +54 -0
- package/template/package.json +57 -6
- package/template/playwright.config.ts +31 -0
- package/template/pnpm-lock.yaml +3784 -0
- package/template/prettier.config.mjs +6 -0
- package/template/src/app-view.kpa +35 -36
- package/template/src/counter-component.kpa +87 -87
- package/template/src/style.css +5 -5
- package/template/tests/e2e/app.spec.ts +18 -0
- package/template/tests/integration/main-bootstrap.test.ts +33 -0
- package/template/tests/unit/normalize-kpa-module-export.test.ts +46 -0
- package/template/tsconfig.json +13 -2
- package/template/vite.config.d.mts +7 -0
- package/template/vite.config.mjs +39 -4
- package/template/vitest.config.mjs +19 -0
|
@@ -1,36 +1,35 @@
|
|
|
1
|
-
[template]
|
|
2
|
-
<div class="app-root">
|
|
3
|
-
<img src="https://public-assets-1b57ca06-687a-4142-a525-0635f7649a5c.s3.eu-central-1.amazonaws.com/koppajs/koppajs-logo-text-900x226.png" width="420" alt="KoppaJS Logo" class="logo">
|
|
4
|
-
<p class="subtitle">Minimal Starter</p>
|
|
5
|
-
<counter-component></counter-component>
|
|
6
|
-
</div>
|
|
7
|
-
[/template]
|
|
8
|
-
|
|
9
|
-
[css]
|
|
10
|
-
.app-root {
|
|
11
|
-
display: flex;
|
|
12
|
-
flex-direction: column;
|
|
13
|
-
align-items: center;
|
|
14
|
-
justify-content: center;
|
|
15
|
-
min-height: 100vh;
|
|
16
|
-
background: #0f1117;
|
|
17
|
-
font-family: system-ui, -apple-system, sans-serif;
|
|
18
|
-
padding: 2rem;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.logo {
|
|
22
|
-
margin-bottom: .9rem;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.subtitle {
|
|
26
|
-
color: #64dce8;
|
|
27
|
-
font-size: 1rem;
|
|
28
|
-
letter-spacing: 0.25em;
|
|
29
|
-
text-transform: uppercase;
|
|
30
|
-
margin: 0 0 1.5rem 0;
|
|
31
|
-
opacity: 0.8;
|
|
32
|
-
}
|
|
33
|
-
[/css]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
[template]
|
|
2
|
+
<div class="app-root">
|
|
3
|
+
<img src="https://public-assets-1b57ca06-687a-4142-a525-0635f7649a5c.s3.eu-central-1.amazonaws.com/koppajs/koppajs-logo-text-900x226.png" width="420" alt="KoppaJS Logo" class="logo">
|
|
4
|
+
<p class="subtitle">Minimal Starter</p>
|
|
5
|
+
<counter-component></counter-component>
|
|
6
|
+
</div>
|
|
7
|
+
[/template]
|
|
8
|
+
|
|
9
|
+
[css]
|
|
10
|
+
.app-root {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
align-items: center;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
min-height: 100vh;
|
|
16
|
+
background: #0f1117;
|
|
17
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
18
|
+
padding: 2rem;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.logo {
|
|
22
|
+
margin-bottom: .9rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.subtitle {
|
|
26
|
+
color: #64dce8;
|
|
27
|
+
font-size: 1rem;
|
|
28
|
+
letter-spacing: 0.25em;
|
|
29
|
+
text-transform: uppercase;
|
|
30
|
+
margin: 0 0 1.5rem 0;
|
|
31
|
+
opacity: 0.8;
|
|
32
|
+
}
|
|
33
|
+
[/css]
|
|
34
|
+
|
|
35
|
+
|
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
[template]
|
|
2
|
-
<div class="counter-card">
|
|
3
|
-
<span class="counter-label">Counter</span>
|
|
4
|
-
<p class="counter-value">{{ count }}</p>
|
|
5
|
-
<div class="counter-buttons">
|
|
6
|
-
<button onClick="decrement">−</button>
|
|
7
|
-
<button onClick="increment">+</button>
|
|
8
|
-
</div>
|
|
9
|
-
</div>
|
|
10
|
-
[/template]
|
|
11
|
-
|
|
12
|
-
[ts]
|
|
13
|
-
return {
|
|
14
|
-
state: { count: 0 },
|
|
15
|
-
methods: {
|
|
16
|
-
increment() {
|
|
17
|
-
this.count++;
|
|
18
|
-
},
|
|
19
|
-
decrement() {
|
|
20
|
-
this.count--;
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
[/ts]
|
|
25
|
-
|
|
26
|
-
[css]
|
|
27
|
-
.counter-card {
|
|
28
|
-
background: #1a1d2e;
|
|
29
|
-
border: 1px solid #2a2d3e;
|
|
30
|
-
border-radius: 1.2rem;
|
|
31
|
-
padding: 2.5rem 3rem;
|
|
32
|
-
display: flex;
|
|
33
|
-
flex-direction: column;
|
|
34
|
-
align-items: center;
|
|
35
|
-
gap: 1.2rem;
|
|
36
|
-
min-width: 260px;
|
|
37
|
-
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 0 48px rgba(100, 220, 232, 0.05);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.counter-label {
|
|
41
|
-
font-size: 0.85rem;
|
|
42
|
-
color: #64dce8;
|
|
43
|
-
text-transform: uppercase;
|
|
44
|
-
letter-spacing: 0.2em;
|
|
45
|
-
opacity: 0.7;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
.counter-value {
|
|
49
|
-
font-size: 3rem;
|
|
50
|
-
font-weight: 700;
|
|
51
|
-
color: #e8eaf0;
|
|
52
|
-
margin: 0;
|
|
53
|
-
font-variant-numeric: tabular-nums;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.counter-buttons {
|
|
57
|
-
display: flex;
|
|
58
|
-
gap: 1rem;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
button {
|
|
62
|
-
background: transparent;
|
|
63
|
-
color: #64dce8;
|
|
64
|
-
border: 1px solid #64dce8;
|
|
65
|
-
border-radius: 0.6rem;
|
|
66
|
-
padding: 0.6rem 1.5rem;
|
|
67
|
-
font-size: 1.3rem;
|
|
68
|
-
font-weight: 600;
|
|
69
|
-
cursor: pointer;
|
|
70
|
-
transition: background 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
|
71
|
-
color 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
|
72
|
-
transform 0.15s cubic-bezier(0.4, 0, 0.2, 1),
|
|
73
|
-
border-color 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
button:hover {
|
|
77
|
-
background: rgba(100, 220, 232, 0.12);
|
|
78
|
-
color: #8ff0f8;
|
|
79
|
-
border-color: #8ff0f8;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
button:active {
|
|
83
|
-
background: #64dce8;
|
|
84
|
-
color: #0f1117;
|
|
85
|
-
transform: scale(0.93);
|
|
86
|
-
}
|
|
87
|
-
[/css]
|
|
1
|
+
[template]
|
|
2
|
+
<div class="counter-card">
|
|
3
|
+
<span class="counter-label">Counter</span>
|
|
4
|
+
<p class="counter-value" role="status" aria-live="polite">{{ count }}</p>
|
|
5
|
+
<div class="counter-buttons">
|
|
6
|
+
<button type="button" aria-label="Decrement counter" onClick="decrement">−</button>
|
|
7
|
+
<button type="button" aria-label="Increment counter" onClick="increment">+</button>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
[/template]
|
|
11
|
+
|
|
12
|
+
[ts]
|
|
13
|
+
return {
|
|
14
|
+
state: { count: 0 },
|
|
15
|
+
methods: {
|
|
16
|
+
increment() {
|
|
17
|
+
this.count++;
|
|
18
|
+
},
|
|
19
|
+
decrement() {
|
|
20
|
+
this.count--;
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
[/ts]
|
|
25
|
+
|
|
26
|
+
[css]
|
|
27
|
+
.counter-card {
|
|
28
|
+
background: #1a1d2e;
|
|
29
|
+
border: 1px solid #2a2d3e;
|
|
30
|
+
border-radius: 1.2rem;
|
|
31
|
+
padding: 2.5rem 3rem;
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: 1.2rem;
|
|
36
|
+
min-width: 260px;
|
|
37
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 0 48px rgba(100, 220, 232, 0.05);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.counter-label {
|
|
41
|
+
font-size: 0.85rem;
|
|
42
|
+
color: #64dce8;
|
|
43
|
+
text-transform: uppercase;
|
|
44
|
+
letter-spacing: 0.2em;
|
|
45
|
+
opacity: 0.7;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.counter-value {
|
|
49
|
+
font-size: 3rem;
|
|
50
|
+
font-weight: 700;
|
|
51
|
+
color: #e8eaf0;
|
|
52
|
+
margin: 0;
|
|
53
|
+
font-variant-numeric: tabular-nums;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.counter-buttons {
|
|
57
|
+
display: flex;
|
|
58
|
+
gap: 1rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
button {
|
|
62
|
+
background: transparent;
|
|
63
|
+
color: #64dce8;
|
|
64
|
+
border: 1px solid #64dce8;
|
|
65
|
+
border-radius: 0.6rem;
|
|
66
|
+
padding: 0.6rem 1.5rem;
|
|
67
|
+
font-size: 1.3rem;
|
|
68
|
+
font-weight: 600;
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
transition: background 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
|
71
|
+
color 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
|
72
|
+
transform 0.15s cubic-bezier(0.4, 0, 0.2, 1),
|
|
73
|
+
border-color 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
button:hover {
|
|
77
|
+
background: rgba(100, 220, 232, 0.12);
|
|
78
|
+
color: #8ff0f8;
|
|
79
|
+
border-color: #8ff0f8;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
button:active {
|
|
83
|
+
background: #64dce8;
|
|
84
|
+
color: #0f1117;
|
|
85
|
+
transform: scale(0.93);
|
|
86
|
+
}
|
|
87
|
+
[/css]
|
package/template/src/style.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
* {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
1
|
+
* {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
margin: 0;
|
|
4
|
+
padding: 0;
|
|
5
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
test("renders the starter and updates the counter", async ({ page }) => {
|
|
4
|
+
await page.goto("/");
|
|
5
|
+
|
|
6
|
+
await expect(page.getByText("Minimal Starter")).toBeVisible();
|
|
7
|
+
|
|
8
|
+
const counterValue = page.getByRole("status");
|
|
9
|
+
|
|
10
|
+
await expect(counterValue).toHaveText("0");
|
|
11
|
+
|
|
12
|
+
await page.getByRole("button", { name: "Increment counter" }).click();
|
|
13
|
+
await expect(counterValue).toHaveText("1");
|
|
14
|
+
|
|
15
|
+
await page.getByRole("button", { name: "Decrement counter" }).click();
|
|
16
|
+
await page.getByRole("button", { name: "Decrement counter" }).click();
|
|
17
|
+
await expect(counterValue).toHaveText("-1");
|
|
18
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const { core, take } = vi.hoisted(() => {
|
|
4
|
+
const take = vi.fn();
|
|
5
|
+
const core = Object.assign(vi.fn(), { take });
|
|
6
|
+
|
|
7
|
+
return { core, take };
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
vi.mock("@koppajs/koppajs-core", () => ({
|
|
11
|
+
Core: core,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe("main bootstrap", () => {
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
core.mockClear();
|
|
17
|
+
take.mockClear();
|
|
18
|
+
vi.resetModules();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("registers the root components and boots the app once", async () => {
|
|
22
|
+
await import("../../src/main");
|
|
23
|
+
|
|
24
|
+
expect(take).toHaveBeenCalledTimes(2);
|
|
25
|
+
expect(take).toHaveBeenNthCalledWith(1, expect.anything(), "app-view");
|
|
26
|
+
expect(take).toHaveBeenNthCalledWith(
|
|
27
|
+
2,
|
|
28
|
+
expect.anything(),
|
|
29
|
+
"counter-component",
|
|
30
|
+
);
|
|
31
|
+
expect(core).toHaveBeenCalledTimes(1);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { normalizeKpaModuleExport } from "../../vite.config.mjs";
|
|
5
|
+
|
|
6
|
+
async function transformWith(plugin: Plugin, code: string, id: string) {
|
|
7
|
+
const transform = plugin.transform;
|
|
8
|
+
|
|
9
|
+
if (!transform) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof transform === "function") {
|
|
14
|
+
return transform.call({} as never, code, id);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return transform.handler.call({} as never, code, id);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("normalizeKpaModuleExport", () => {
|
|
21
|
+
it("wraps raw KPA output in an ES module export", async () => {
|
|
22
|
+
const plugin = normalizeKpaModuleExport();
|
|
23
|
+
|
|
24
|
+
await expect(
|
|
25
|
+
transformWith(plugin, '{ template: "<div></div>" }', "/src/app-view.kpa"),
|
|
26
|
+
).resolves.toEqual({
|
|
27
|
+
code: 'export default { template: "<div></div>" };',
|
|
28
|
+
map: null,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("ignores non-KPA files and already exported modules", async () => {
|
|
33
|
+
const plugin = normalizeKpaModuleExport();
|
|
34
|
+
|
|
35
|
+
await expect(
|
|
36
|
+
transformWith(plugin, "const count = 0;", "/src/main.ts"),
|
|
37
|
+
).resolves.toBeNull();
|
|
38
|
+
await expect(
|
|
39
|
+
transformWith(
|
|
40
|
+
plugin,
|
|
41
|
+
"export default { template: '<div></div>' };",
|
|
42
|
+
"/src/app-view.kpa",
|
|
43
|
+
),
|
|
44
|
+
).resolves.toBeNull();
|
|
45
|
+
});
|
|
46
|
+
});
|
package/template/tsconfig.json
CHANGED
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
"forceConsistentCasingInFileNames": true,
|
|
11
11
|
"noEmit": true
|
|
12
12
|
},
|
|
13
|
-
"include": [
|
|
14
|
-
|
|
13
|
+
"include": [
|
|
14
|
+
"src/**/*.ts",
|
|
15
|
+
"tests/**/*.ts",
|
|
16
|
+
"playwright.config.ts",
|
|
17
|
+
"vite.config.d.mts"
|
|
18
|
+
],
|
|
19
|
+
"exclude": [
|
|
20
|
+
"coverage",
|
|
21
|
+
"dist",
|
|
22
|
+
"node_modules",
|
|
23
|
+
"playwright-report",
|
|
24
|
+
"test-results"
|
|
25
|
+
]
|
|
15
26
|
}
|
package/template/vite.config.mjs
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
|
-
import { defineConfig } from "vite";
|
|
2
1
|
import koppaPlugin from "@koppajs/koppajs-vite-plugin";
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wrap raw `.kpa` object output in a valid ES module export.
|
|
6
|
+
*
|
|
7
|
+
* @returns {import("vite").Plugin}
|
|
8
|
+
*/
|
|
9
|
+
export function normalizeKpaModuleExport() {
|
|
10
|
+
return {
|
|
11
|
+
name: "normalize-kpa-module-export",
|
|
12
|
+
enforce: "post",
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} code
|
|
15
|
+
* @param {string} id
|
|
16
|
+
*/
|
|
17
|
+
transform(code, id) {
|
|
18
|
+
const cleanId = id.split("?")[0];
|
|
19
|
+
|
|
20
|
+
if (!cleanId.endsWith(".kpa")) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const trimmed = code.trim();
|
|
25
|
+
|
|
26
|
+
if (!trimmed.startsWith("{") || trimmed.startsWith("export default")) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
code: `export default ${trimmed};`,
|
|
32
|
+
map: null,
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
3
37
|
|
|
4
38
|
export default defineConfig({
|
|
5
39
|
plugins: [
|
|
6
40
|
koppaPlugin({
|
|
7
|
-
tsconfigFile: "./tsconfig.json"
|
|
8
|
-
})
|
|
9
|
-
|
|
41
|
+
tsconfigFile: "./tsconfig.json",
|
|
42
|
+
}),
|
|
43
|
+
normalizeKpaModuleExport(),
|
|
44
|
+
],
|
|
10
45
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig, mergeConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
import viteConfig from "./vite.config.mjs";
|
|
4
|
+
|
|
5
|
+
export default mergeConfig(
|
|
6
|
+
viteConfig,
|
|
7
|
+
defineConfig({
|
|
8
|
+
test: {
|
|
9
|
+
include: ["tests/**/*.test.ts"],
|
|
10
|
+
coverage: {
|
|
11
|
+
provider: "v8",
|
|
12
|
+
reporter: ["text", "html"],
|
|
13
|
+
reportsDirectory: "./coverage",
|
|
14
|
+
include: ["src/**/*.ts"],
|
|
15
|
+
exclude: ["playwright.config.ts", "tests/**/*.ts", "vitest.config.mjs"],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
19
|
+
);
|