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.
Files changed (57) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +169 -131
  3. package/bin/create-koppajs.js +201 -175
  4. package/package.json +54 -34
  5. package/template/AI_CONSTITUTION.md +50 -0
  6. package/template/ARCHITECTURE.md +86 -0
  7. package/template/CHANGELOG.md +34 -0
  8. package/template/CONTRIBUTING.md +92 -0
  9. package/template/DECISION_HIERARCHY.md +32 -0
  10. package/template/DEVELOPMENT_RULES.md +57 -0
  11. package/template/LICENSE +1 -1
  12. package/template/README.md +241 -49
  13. package/template/RELEASE.md +230 -0
  14. package/template/ROADMAP.md +34 -0
  15. package/template/TESTING_STRATEGY.md +93 -0
  16. package/template/_editorconfig +12 -0
  17. package/template/_gitattributes +1 -0
  18. package/template/_github/instructions/ai-workflow.md +33 -0
  19. package/template/_github/workflows/ci.yml +38 -0
  20. package/template/_github/workflows/release.yml +58 -0
  21. package/template/_gitignore +5 -0
  22. package/template/_husky/commit-msg +8 -0
  23. package/template/_husky/pre-commit +1 -0
  24. package/template/_npmrc +1 -0
  25. package/template/_prettierignore +7 -0
  26. package/template/commitlint.config.mjs +6 -0
  27. package/template/docs/adr/0001-keep-the-starter-minimal.md +32 -0
  28. package/template/docs/adr/0002-adopt-a-living-meta-layer.md +30 -0
  29. package/template/docs/adr/0003-normalize-kpa-plugin-output.md +24 -0
  30. package/template/docs/adr/0004-adopt-an-automated-quality-baseline.md +31 -0
  31. package/template/docs/adr/0005-adopt-a-tag-driven-release-baseline.md +45 -0
  32. package/template/docs/adr/0006-adopt-commit-message-conventions.md +39 -0
  33. package/template/docs/adr/README.md +37 -0
  34. package/template/docs/adr/TEMPLATE.md +18 -0
  35. package/template/docs/architecture/module-boundaries.md +48 -0
  36. package/template/docs/meta/maintenance.md +40 -0
  37. package/template/docs/quality/quality-gates.md +39 -0
  38. package/template/docs/specs/README.md +36 -0
  39. package/template/docs/specs/TEMPLATE.md +34 -0
  40. package/template/docs/specs/app-bootstrap.md +46 -0
  41. package/template/docs/specs/counter-component.md +48 -0
  42. package/template/docs/specs/quality-workflow.md +62 -0
  43. package/template/eslint.config.mjs +54 -0
  44. package/template/package.json +57 -6
  45. package/template/playwright.config.ts +31 -0
  46. package/template/pnpm-lock.yaml +3784 -0
  47. package/template/prettier.config.mjs +6 -0
  48. package/template/src/app-view.kpa +35 -36
  49. package/template/src/counter-component.kpa +87 -87
  50. package/template/src/style.css +5 -5
  51. package/template/tests/e2e/app.spec.ts +18 -0
  52. package/template/tests/integration/main-bootstrap.test.ts +33 -0
  53. package/template/tests/unit/normalize-kpa-module-export.test.ts +46 -0
  54. package/template/tsconfig.json +13 -2
  55. package/template/vite.config.d.mts +7 -0
  56. package/template/vite.config.mjs +39 -4
  57. package/template/vitest.config.mjs +19 -0
@@ -0,0 +1,6 @@
1
+ export default {
2
+ semi: true,
3
+ singleQuote: false,
4
+ tabWidth: 2,
5
+ trailingComma: "all",
6
+ };
@@ -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]
@@ -1,5 +1,5 @@
1
- * {
2
- box-sizing: border-box;
3
- margin: 0;
4
- padding: 0;
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
+ });
@@ -10,6 +10,17 @@
10
10
  "forceConsistentCasingInFileNames": true,
11
11
  "noEmit": true
12
12
  },
13
- "include": ["src/**/*.ts"],
14
- "exclude": ["node_modules", "dist"]
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
  }
@@ -0,0 +1,7 @@
1
+ import type { Plugin, UserConfig } from "vite";
2
+
3
+ export declare function normalizeKpaModuleExport(): Plugin;
4
+
5
+ declare const config: UserConfig;
6
+
7
+ export default config;
@@ -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
+ );