dekit-cli 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.
@@ -0,0 +1,55 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { assemblePageHtml } from "./vite-plugin-dekit.js";
3
+ import type { DesignConfig } from "./types.js";
4
+ import { join } from "node:path";
5
+
6
+ const DESIGN_DIR = join(import.meta.dirname, "../example");
7
+
8
+ const mockConfig: DesignConfig = {
9
+ version: 1.0,
10
+ globalStyle: "global.css",
11
+ components: {
12
+ "my-banner": {
13
+ template: "components/my-banner/banner.html",
14
+ style: "components/my-banner/banner.css",
15
+ },
16
+ },
17
+ pages: {
18
+ cover: {
19
+ template: "pages/cover/cover.html",
20
+ style: "pages/cover/cover.css",
21
+ },
22
+ },
23
+ baseDir: DESIGN_DIR,
24
+ };
25
+
26
+ describe("assemblePageHtml", () => {
27
+ test("generates valid HTML for a page", async () => {
28
+ const html = await assemblePageHtml(mockConfig, "cover");
29
+ expect(html).toContain("<!DOCTYPE html>");
30
+ expect(html).toContain("global.css");
31
+ expect(html).toContain("pages/cover/cover.css");
32
+ expect(html).toContain("Welcome to Dekit");
33
+ expect(html).toContain("customElements.define");
34
+ expect(html).toContain("my-banner");
35
+ });
36
+
37
+ test("throws for unknown page", async () => {
38
+ await expect(
39
+ assemblePageHtml(mockConfig, "nonexistent")
40
+ ).rejects.toThrow(/not found/i);
41
+ });
42
+
43
+ test("includes inspector script", async () => {
44
+ const html = await assemblePageHtml(mockConfig, "cover");
45
+ expect(html).toContain("postMessage");
46
+ expect(html).toContain("NODE_SELECTED");
47
+ });
48
+
49
+ test("extracts template inner content for component registration", async () => {
50
+ const html = await assemblePageHtml(mockConfig, "cover");
51
+ expect(html).toContain("class=\"banner\"");
52
+ expect(html).toContain("<slot></slot>");
53
+ expect(html).not.toMatch(/innerHTML\s*=\s*`\s*<template>/);
54
+ });
55
+ });
@@ -0,0 +1,107 @@
1
+ import type { Plugin } from "vite";
2
+ import type { DesignConfig } from "./types.js";
3
+ import { readFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { generateComponentRegistrationScript } from "./injected/register-components.js";
6
+ import { generateInspectorScript } from "./injected/inspector.js";
7
+
8
+ export async function assemblePageHtml(
9
+ config: DesignConfig,
10
+ pageName: string
11
+ ): Promise<string> {
12
+ const pageDef = config.pages[pageName];
13
+ if (!pageDef) {
14
+ throw new Error(`Page "${pageName}" not found in design config`);
15
+ }
16
+
17
+ const pageTemplate = await readFile(
18
+ join(config.baseDir, pageDef.template),
19
+ "utf-8"
20
+ );
21
+ const componentScript = await generateComponentRegistrationScript(
22
+ config.components,
23
+ config.baseDir
24
+ );
25
+ const inspectorScript = generateInspectorScript();
26
+
27
+ return `<!DOCTYPE html>
28
+ <html>
29
+ <head>
30
+ <meta charset="UTF-8">
31
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
32
+ <link rel="stylesheet" href="/@design/${config.globalStyle}">
33
+ <link rel="stylesheet" href="/@design/${pageDef.style}">
34
+ </head>
35
+ <body>
36
+ ${pageTemplate}
37
+ <script type="module">
38
+ ${componentScript}
39
+ </script>
40
+ <script>
41
+ ${inspectorScript}
42
+ </script>
43
+ </body>
44
+ </html>`;
45
+ }
46
+
47
+ export function dekitPlugin(configRef: { current: DesignConfig }): Plugin {
48
+ return {
49
+ name: "vite-plugin-dekit",
50
+ configureServer(server) {
51
+ server.middlewares.use(async (req, res, next) => {
52
+ const url = req.url ?? "";
53
+
54
+ if (url.startsWith("/@design/")) {
55
+ const filePath = join(
56
+ configRef.current.baseDir,
57
+ url.replace("/@design/", "")
58
+ );
59
+ try {
60
+ const content = await readFile(filePath, "utf-8");
61
+ const ext = filePath.split(".").pop();
62
+ const mimeTypes: Record<string, string> = {
63
+ css: "text/css",
64
+ html: "text/html",
65
+ js: "application/javascript",
66
+ };
67
+ res.setHeader(
68
+ "Content-Type",
69
+ mimeTypes[ext ?? ""] ?? "text/plain"
70
+ );
71
+ res.end(content);
72
+ } catch {
73
+ res.statusCode = 404;
74
+ res.end("Not found");
75
+ }
76
+ return;
77
+ }
78
+
79
+ const pageMatch = url.match(/^\/page\/([^/?#]+)/);
80
+ if (pageMatch) {
81
+ const pageName = pageMatch[1];
82
+ try {
83
+ const html = await assemblePageHtml(configRef.current, pageName);
84
+ res.setHeader("Content-Type", "text/html");
85
+ res.end(html);
86
+ } catch (err) {
87
+ res.statusCode = 404;
88
+ res.end(
89
+ err instanceof Error ? err.message : "Page not found"
90
+ );
91
+ }
92
+ return;
93
+ }
94
+
95
+ next();
96
+ });
97
+
98
+ const watcher = server.watcher;
99
+ watcher.add(configRef.current.baseDir);
100
+ watcher.on("change", (changedPath) => {
101
+ if (changedPath.startsWith(configRef.current.baseDir)) {
102
+ server.ws.send({ type: "full-reload" });
103
+ }
104
+ });
105
+ },
106
+ };
107
+ }