dekit-cli 0.4.2 → 0.4.3
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 +10 -26
- package/dist/editor/assets/{index-D16Bm-Rw.js → index-hySfUCIY.js} +10 -10
- package/dist/editor/index.html +1 -1
- package/docs/usage.md +196 -7
- package/editor/src/App.tsx +8 -0
- package/editor/src/types.ts +8 -0
- package/package.json +1 -1
- package/src/cli/init.test.ts +22 -0
- package/src/cli/init.ts +12 -1
- package/src/cli/screenshot.ts +21 -7
- package/src/editor-server.ts +8 -0
- package/src/parser.test.ts +164 -0
- package/src/parser.ts +74 -3
- package/src/types.ts +16 -0
- package/src/vite-plugin-dekit.test.ts +85 -1
- package/src/vite-plugin-dekit.ts +42 -2
- package/templates/projects/mobile/dekit.yaml +1 -0
package/src/parser.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import yaml from "js-yaml";
|
|
5
|
-
import type { DesignConfig, ComponentDef, PageDef } from "./types.js";
|
|
5
|
+
import type { DesignConfig, DeviceConfig, ComponentDef, PageDef, PropertyDef, PropertyType } from "./types.js";
|
|
6
|
+
import { DEVICE_PRESETS } from "./devices.js";
|
|
6
7
|
|
|
7
8
|
export async function parseDesignConfig(
|
|
8
9
|
configPath: string,
|
|
@@ -24,6 +25,8 @@ export async function parseDesignConfig(
|
|
|
24
25
|
// Validate file paths if the base directory actually exists on disk
|
|
25
26
|
const shouldValidateFiles = existsSync(baseDir);
|
|
26
27
|
|
|
28
|
+
const device = parseDevice(doc.device);
|
|
29
|
+
|
|
27
30
|
const globalStyle = (doc["global-style"] as string) ?? "";
|
|
28
31
|
if (globalStyle && shouldValidateFiles) {
|
|
29
32
|
const globalStylePath = join(baseDir, globalStyle);
|
|
@@ -51,24 +54,92 @@ export async function parseDesignConfig(
|
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
const rawPages =
|
|
54
|
-
(doc.pages as Record<string, { template: string; style: string }>) ?? {};
|
|
57
|
+
(doc.pages as Record<string, { template: string; style: string; properties?: Record<string, unknown> }>) ?? {};
|
|
55
58
|
const pages: Record<string, PageDef> = {};
|
|
56
59
|
for (const [key, val] of Object.entries(rawPages)) {
|
|
57
60
|
if (shouldValidateFiles) {
|
|
58
61
|
validateFilePaths(baseDir, key, val.template, val.style);
|
|
59
62
|
}
|
|
60
|
-
|
|
63
|
+
const properties = val.properties ? parseProperties(key, val.properties) : undefined;
|
|
64
|
+
pages[key] = { template: val.template, style: val.style, properties };
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
return {
|
|
64
68
|
version: doc.version as number,
|
|
65
69
|
globalStyle,
|
|
70
|
+
device,
|
|
66
71
|
components,
|
|
67
72
|
pages,
|
|
68
73
|
baseDir,
|
|
69
74
|
};
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
function parseDevice(raw: unknown): DeviceConfig | undefined {
|
|
78
|
+
if (raw == null) return undefined;
|
|
79
|
+
|
|
80
|
+
if (typeof raw === "string") {
|
|
81
|
+
const normalizedInput = raw.toLowerCase().replace(/\s+/g, "-");
|
|
82
|
+
const preset = DEVICE_PRESETS.find(
|
|
83
|
+
(d) => d.name.toLowerCase().replace(/\s+/g, "-") === normalizedInput
|
|
84
|
+
);
|
|
85
|
+
if (!preset) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Unknown device: "${raw}". Available: ${DEVICE_PRESETS.map((d) => d.name).join(", ")}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return { width: preset.width, height: preset.height, dpr: preset.dpr, name: preset.name };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof raw === "object") {
|
|
94
|
+
const obj = raw as Record<string, unknown>;
|
|
95
|
+
const width = obj.width;
|
|
96
|
+
const height = obj.height;
|
|
97
|
+
if (typeof width !== "number" || typeof height !== "number") {
|
|
98
|
+
throw new Error("Device config must have numeric 'width' and 'height'");
|
|
99
|
+
}
|
|
100
|
+
const dpr = typeof obj.dpr === "number" ? obj.dpr : 1;
|
|
101
|
+
return { width, height, dpr };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw new Error("Invalid device config: expected a device name string or {width, height, dpr} object");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const VALID_PROPERTY_TYPES: PropertyType[] = ["boolean", "number", "string"];
|
|
108
|
+
|
|
109
|
+
function parseProperties(
|
|
110
|
+
pageKey: string,
|
|
111
|
+
raw: Record<string, unknown>
|
|
112
|
+
): Record<string, PropertyDef> {
|
|
113
|
+
const result: Record<string, PropertyDef> = {};
|
|
114
|
+
for (const [name, def] of Object.entries(raw)) {
|
|
115
|
+
if (!def || typeof def !== "object") {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Invalid property "${name}" on page "${pageKey}": expected {type, default}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const obj = def as Record<string, unknown>;
|
|
121
|
+
const type = obj.type as string;
|
|
122
|
+
if (!VALID_PROPERTY_TYPES.includes(type as PropertyType)) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Invalid type "${type}" for property "${name}" on page "${pageKey}". Must be: ${VALID_PROPERTY_TYPES.join(", ")}`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (obj.default == null) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Missing default for property "${name}" on page "${pageKey}"`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const expectedType = type === "boolean" ? "boolean" : type === "number" ? "number" : "string";
|
|
133
|
+
if (typeof obj.default !== expectedType) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Default value for "${name}" on page "${pageKey}" must be a ${type}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
result[name] = { type: type as PropertyType, default: obj.default };
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
72
143
|
function validateFilePaths(
|
|
73
144
|
baseDir: string,
|
|
74
145
|
key: string,
|
package/src/types.ts
CHANGED
|
@@ -3,14 +3,30 @@ export interface ComponentDef {
|
|
|
3
3
|
style: string;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
export type PropertyType = "boolean" | "number" | "string";
|
|
7
|
+
|
|
8
|
+
export interface PropertyDef {
|
|
9
|
+
type: PropertyType;
|
|
10
|
+
default: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export interface PageDef {
|
|
7
14
|
template: string;
|
|
8
15
|
style: string;
|
|
16
|
+
properties?: Record<string, PropertyDef>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DeviceConfig {
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
dpr: number;
|
|
23
|
+
name?: string;
|
|
9
24
|
}
|
|
10
25
|
|
|
11
26
|
export interface DesignConfig {
|
|
12
27
|
version: number;
|
|
13
28
|
globalStyle: string;
|
|
29
|
+
device?: DeviceConfig;
|
|
14
30
|
components: Record<string, ComponentDef>;
|
|
15
31
|
pages: Record<string, PageDef>;
|
|
16
32
|
baseDir: string; // absolute path to .dekit/ directory (where dekit.yaml lives)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
|
-
import { assemblePageHtml } from "./vite-plugin-dekit.js";
|
|
2
|
+
import { assemblePageHtml, resolvePageProps } from "./vite-plugin-dekit.js";
|
|
3
3
|
import type { DesignConfig } from "./types.js";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
@@ -59,4 +59,88 @@ describe("assemblePageHtml", () => {
|
|
|
59
59
|
expect(html).toContain("<slot></slot>");
|
|
60
60
|
expect(html).not.toMatch(/innerHTML\s*=\s*`\s*<template>/);
|
|
61
61
|
});
|
|
62
|
+
|
|
63
|
+
test("injects __DEKIT_PROPS__ when page has properties", async () => {
|
|
64
|
+
const configWithProps: DesignConfig = {
|
|
65
|
+
...mockConfig,
|
|
66
|
+
pages: {
|
|
67
|
+
home: {
|
|
68
|
+
...mockConfig.pages["home"],
|
|
69
|
+
properties: {
|
|
70
|
+
showBanner: { type: "boolean", default: true },
|
|
71
|
+
count: { type: "number", default: 3 },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const html = await assemblePageHtml(configWithProps, "home");
|
|
77
|
+
expect(html).toContain("window.__DEKIT_PROPS__");
|
|
78
|
+
expect(html).toContain('"showBanner":true');
|
|
79
|
+
expect(html).toContain('"count":3');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("does not inject __DEKIT_PROPS__ when page has no properties", async () => {
|
|
83
|
+
const html = await assemblePageHtml(mockConfig, "home");
|
|
84
|
+
expect(html).not.toContain("__DEKIT_PROPS__");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("props override applies over defaults", async () => {
|
|
88
|
+
const configWithProps: DesignConfig = {
|
|
89
|
+
...mockConfig,
|
|
90
|
+
pages: {
|
|
91
|
+
home: {
|
|
92
|
+
...mockConfig.pages["home"],
|
|
93
|
+
properties: {
|
|
94
|
+
showBanner: { type: "boolean", default: true },
|
|
95
|
+
count: { type: "number", default: 3 },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const html = await assemblePageHtml(configWithProps, "home", {
|
|
101
|
+
props: { showBanner: "false", count: "10" },
|
|
102
|
+
});
|
|
103
|
+
expect(html).toContain('"showBanner":false');
|
|
104
|
+
expect(html).toContain('"count":10');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("resolvePageProps", () => {
|
|
109
|
+
test("returns defaults when no overrides", () => {
|
|
110
|
+
const result = resolvePageProps({
|
|
111
|
+
properties: {
|
|
112
|
+
flag: { type: "boolean", default: true },
|
|
113
|
+
num: { type: "number", default: 5 },
|
|
114
|
+
str: { type: "string", default: "hi" },
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
expect(result).toEqual({ flag: true, num: 5, str: "hi" });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("overrides convert types based on schema", () => {
|
|
121
|
+
const result = resolvePageProps(
|
|
122
|
+
{
|
|
123
|
+
properties: {
|
|
124
|
+
flag: { type: "boolean", default: true },
|
|
125
|
+
num: { type: "number", default: 5 },
|
|
126
|
+
str: { type: "string", default: "hi" },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{ flag: "false", num: "42", str: "world" }
|
|
130
|
+
);
|
|
131
|
+
expect(result).toEqual({ flag: false, num: 42, str: "world" });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("ignores unknown override keys", () => {
|
|
135
|
+
const result = resolvePageProps(
|
|
136
|
+
{ properties: { flag: { type: "boolean", default: true } } },
|
|
137
|
+
{ flag: "false", unknown: "value" }
|
|
138
|
+
);
|
|
139
|
+
expect(result).toEqual({ flag: false });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("returns empty object when no properties defined", () => {
|
|
143
|
+
expect(resolvePageProps({})).toEqual({});
|
|
144
|
+
expect(resolvePageProps({ properties: undefined })).toEqual({});
|
|
145
|
+
});
|
|
62
146
|
});
|
package/src/vite-plugin-dekit.ts
CHANGED
|
@@ -5,10 +5,38 @@ import { join } from "node:path";
|
|
|
5
5
|
import { generateComponentRegistrationScript } from "./injected/register-components.js";
|
|
6
6
|
import { generateInspectorScript } from "./injected/inspector.js";
|
|
7
7
|
|
|
8
|
+
export function resolvePageProps(
|
|
9
|
+
pageDef: { properties?: Record<string, { type: string; default: unknown }> },
|
|
10
|
+
overrides?: Record<string, string>
|
|
11
|
+
): Record<string, unknown> {
|
|
12
|
+
const props: Record<string, unknown> = {};
|
|
13
|
+
if (!pageDef.properties) return props;
|
|
14
|
+
|
|
15
|
+
for (const [key, def] of Object.entries(pageDef.properties)) {
|
|
16
|
+
props[key] = def.default;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (overrides) {
|
|
20
|
+
for (const [key, raw] of Object.entries(overrides)) {
|
|
21
|
+
const def = pageDef.properties[key];
|
|
22
|
+
if (!def) continue;
|
|
23
|
+
if (def.type === "boolean") {
|
|
24
|
+
props[key] = raw === "true";
|
|
25
|
+
} else if (def.type === "number") {
|
|
26
|
+
props[key] = Number(raw);
|
|
27
|
+
} else {
|
|
28
|
+
props[key] = raw;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return props;
|
|
34
|
+
}
|
|
35
|
+
|
|
8
36
|
export async function assemblePageHtml(
|
|
9
37
|
config: DesignConfig,
|
|
10
38
|
pageName: string,
|
|
11
|
-
options?: { inspector?: boolean }
|
|
39
|
+
options?: { inspector?: boolean; props?: Record<string, string> }
|
|
12
40
|
): Promise<string> {
|
|
13
41
|
const pageDef = config.pages[pageName];
|
|
14
42
|
if (!pageDef) {
|
|
@@ -29,6 +57,11 @@ export async function assemblePageHtml(
|
|
|
29
57
|
? `<script>\n${generateInspectorScript()}\n</script>`
|
|
30
58
|
: "";
|
|
31
59
|
|
|
60
|
+
const resolvedProps = resolvePageProps(pageDef, options?.props);
|
|
61
|
+
const propsBlock = Object.keys(resolvedProps).length > 0
|
|
62
|
+
? `<script>window.__DEKIT_PROPS__ = ${JSON.stringify(resolvedProps)};</script>\n`
|
|
63
|
+
: "";
|
|
64
|
+
|
|
32
65
|
return `<!DOCTYPE html>
|
|
33
66
|
<html>
|
|
34
67
|
<head>
|
|
@@ -39,7 +72,7 @@ export async function assemblePageHtml(
|
|
|
39
72
|
<link rel="stylesheet" href="/@design/${pageDef.style}">
|
|
40
73
|
</head>
|
|
41
74
|
<body>
|
|
42
|
-
${pageTemplate}
|
|
75
|
+
${propsBlock}${pageTemplate}
|
|
43
76
|
<script type="module">
|
|
44
77
|
${componentScript}
|
|
45
78
|
</script>
|
|
@@ -85,9 +118,16 @@ export function dekitPlugin(configRef: { current: DesignConfig }): Plugin {
|
|
|
85
118
|
const pageName = pageMatch[1];
|
|
86
119
|
const urlObj = new URL(url, "http://localhost");
|
|
87
120
|
const noInspector = urlObj.searchParams.has("noinspector");
|
|
121
|
+
const props: Record<string, string> = {};
|
|
122
|
+
for (const [key, value] of urlObj.searchParams.entries()) {
|
|
123
|
+
if (key.startsWith("props.")) {
|
|
124
|
+
props[key.slice(6)] = value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
88
127
|
try {
|
|
89
128
|
const html = await assemblePageHtml(configRef.current, pageName, {
|
|
90
129
|
inspector: !noInspector,
|
|
130
|
+
props: Object.keys(props).length > 0 ? props : undefined,
|
|
91
131
|
});
|
|
92
132
|
res.setHeader("Content-Type", "text/html");
|
|
93
133
|
res.end(html);
|