dekit-cli 0.4.1 → 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/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
- pages[key] = { template: val.template, style: val.style };
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/server.ts CHANGED
@@ -33,8 +33,6 @@ async function findAvailablePort(preferred: number): Promise<number> {
33
33
  export async function startServers(options: ServerOptions) {
34
34
  const configRef = { current: options.config };
35
35
 
36
- console.log("\n Dekit Dev Server\n");
37
-
38
36
  const designServer = await startDesignServer(configRef);
39
37
  const designAddr = designServer.httpServer!.address();
40
38
  const designPort =
@@ -48,7 +46,7 @@ export async function startServers(options: ServerOptions) {
48
46
  );
49
47
 
50
48
  const editorUrl = `http://localhost:${editorPort}`;
51
- console.log(`\n Open ${editorUrl} in your browser\n`);
49
+ console.log(`\n Dekit running at ${editorUrl}\n`);
52
50
 
53
51
  // Try to open browser
54
52
  if (!options.noOpen) {
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
  });
@@ -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);
@@ -1,4 +1,5 @@
1
1
  version: 1.0
2
+ device: "iPhone 16"
2
3
  global-style: "global.css"
3
4
 
4
5
  components: {}