@kokimoki/kit 1.7.0 → 1.8.1

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/dist/api.js CHANGED
@@ -4,12 +4,7 @@
4
4
  * Shared between @kokimoki/cli and @kokimoki/kit
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.DEFAULT_ENDPOINT = void 0;
8
- exports.createConcept = createConcept;
9
- exports.getOrganization = getOrganization;
10
- exports.getConceptOrganization = getConceptOrganization;
11
- exports.createBuild = createBuild;
12
- exports.getDeployUrl = getDeployUrl;
7
+ exports.getDeployUrl = exports.createBuild = exports.getConceptOrganization = exports.getOrganization = exports.createConcept = exports.DEFAULT_ENDPOINT = void 0;
13
8
  exports.DEFAULT_ENDPOINT = "https://api.kokimoki.com";
14
9
  /**
15
10
  * Make a request to the Kokimoki API
@@ -39,6 +34,7 @@ async function createConcept(options, request) {
39
34
  body: JSON.stringify(request),
40
35
  });
41
36
  }
37
+ exports.createConcept = createConcept;
42
38
  /**
43
39
  * Get organization details via the Kokimoki API
44
40
  * Used for authentication validation
@@ -46,6 +42,7 @@ async function createConcept(options, request) {
46
42
  async function getOrganization(endpoint, apiKey) {
47
43
  return kokimokiApiRequest("/auth", { endpoint, apiKey }, { method: "GET" });
48
44
  }
45
+ exports.getOrganization = getOrganization;
49
46
  /**
50
47
  * Get the organization that owns a concept (by concept ID)
51
48
  */
@@ -63,6 +60,7 @@ async function getConceptOrganization(endpoint, conceptId) {
63
60
  }
64
61
  return await res.json();
65
62
  }
63
+ exports.getConceptOrganization = getConceptOrganization;
66
64
  /**
67
65
  * Create a new build via the Kokimoki API
68
66
  */
@@ -75,6 +73,7 @@ async function createBuild(options, request, cliVersion) {
75
73
  body: JSON.stringify(request),
76
74
  });
77
75
  }
76
+ exports.createBuild = createBuild;
78
77
  /**
79
78
  * Get the deploy URL for a build
80
79
  */
@@ -93,3 +92,4 @@ async function getDeployUrl(endpoint, apiKey, buildId) {
93
92
  }
94
93
  return await res.json();
95
94
  }
95
+ exports.getDeployUrl = getDeployUrl;
@@ -7,9 +7,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
7
7
  return (mod && mod.__esModule) ? mod : { "default": mod };
8
8
  };
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.getCredentialsPath = getCredentialsPath;
11
- exports.readCredentials = readCredentials;
12
- exports.writeCredentials = writeCredentials;
10
+ exports.writeCredentials = exports.readCredentials = exports.getCredentialsPath = void 0;
13
11
  const promises_1 = __importDefault(require("fs/promises"));
14
12
  const os_1 = require("os");
15
13
  const path_1 = __importDefault(require("path"));
@@ -19,6 +17,7 @@ const path_1 = __importDefault(require("path"));
19
17
  function getCredentialsPath() {
20
18
  return path_1.default.join((0, os_1.homedir)(), ".kokimoki");
21
19
  }
20
+ exports.getCredentialsPath = getCredentialsPath;
22
21
  /**
23
22
  * Read credentials from the ~/.kokimoki file
24
23
  */
@@ -35,6 +34,7 @@ async function readCredentials() {
35
34
  throw e;
36
35
  }
37
36
  }
37
+ exports.readCredentials = readCredentials;
38
38
  /**
39
39
  * Write credentials to the ~/.kokimoki file
40
40
  */
@@ -42,3 +42,4 @@ async function writeCredentials(credentials) {
42
42
  const credentialsPath = getCredentialsPath();
43
43
  await promises_1.default.writeFile(credentialsPath, JSON.stringify(credentials, null, 2));
44
44
  }
45
+ exports.writeCredentials = writeCredentials;
package/dist/dev-app.js CHANGED
@@ -3,13 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.deleteAppId = deleteAppId;
7
- exports.setDevAppI18n = setDevAppI18n;
8
- exports.setDevAppTranslation = setDevAppTranslation;
9
- exports.computeStoresHash = computeStoresHash;
10
- exports.readStoresHash = readStoresHash;
11
- exports.writeStoresHash = writeStoresHash;
12
- exports.getOrCreateDevApp = getOrCreateDevApp;
6
+ exports.getOrCreateDevApp = exports.writeStoresHash = exports.readStoresHash = exports.computeStoresHash = exports.setDevAppTranslation = exports.setDevAppI18n = exports.deleteAppId = void 0;
13
7
  const crypto_1 = __importDefault(require("crypto"));
14
8
  const bson_objectid_1 = __importDefault(require("bson-objectid"));
15
9
  const promises_1 = __importDefault(require("fs/promises"));
@@ -108,6 +102,7 @@ async function deleteAppId() {
108
102
  }
109
103
  }
110
104
  }
105
+ exports.deleteAppId = deleteAppId;
111
106
  /**
112
107
  * Set i18n metadata for a dev app
113
108
  */
@@ -126,6 +121,7 @@ async function setDevAppI18n(endpoint, apiKey, appId, i18nMeta) {
126
121
  throw new Error(`Failed to set dev app i18n: ${res.status} ${errorText}`);
127
122
  }
128
123
  }
124
+ exports.setDevAppI18n = setDevAppI18n;
129
125
  /**
130
126
  * Set translations for a specific language and namespace in a dev app
131
127
  */
@@ -144,6 +140,7 @@ async function setDevAppTranslation(endpoint, apiKey, appId, lng, namespace, tra
144
140
  throw new Error(`Failed to set dev app translation: ${res.status} ${errorText}`);
145
141
  }
146
142
  }
143
+ exports.setDevAppTranslation = setDevAppTranslation;
147
144
  /**
148
145
  * Compute a hash of the stores configuration
149
146
  */
@@ -151,6 +148,7 @@ function computeStoresHash(stores) {
151
148
  const content = JSON.stringify(stores ?? []);
152
149
  return crypto_1.default.createHash("sha256").update(content).digest("hex").slice(0, 16);
153
150
  }
151
+ exports.computeStoresHash = computeStoresHash;
154
152
  /**
155
153
  * Read the stored stores hash from .kokimoki/stores-hash
156
154
  */
@@ -166,6 +164,7 @@ async function readStoresHash() {
166
164
  throw e;
167
165
  }
168
166
  }
167
+ exports.readStoresHash = readStoresHash;
169
168
  /**
170
169
  * Write the stores hash to .kokimoki/stores-hash
171
170
  */
@@ -173,6 +172,7 @@ async function writeStoresHash(hash) {
173
172
  await ensureKokimokiDir();
174
173
  await promises_1.default.writeFile(path_1.default.join(KOKIMOKI_DIR, STORES_HASH_FILE), hash);
175
174
  }
175
+ exports.writeStoresHash = writeStoresHash;
176
176
  /**
177
177
  * Get or create a dev app for local development.
178
178
  *
@@ -269,3 +269,4 @@ async function getOrCreateDevApp(config) {
269
269
  };
270
270
  }
271
271
  }
272
+ exports.getOrCreateDevApp = getOrCreateDevApp;
@@ -10,6 +10,18 @@ export interface DevFrameCell {
10
10
  export interface DevFrameConfig {
11
11
  /** Title shown in browser tab */
12
12
  title?: string;
13
+ /**
14
+ * App meta for SEO and social sharing. Title will be appended with " - Dev View".
15
+ */
16
+ appMeta?: {
17
+ lang?: string;
18
+ title?: string;
19
+ description?: string;
20
+ ogTitle?: string;
21
+ ogDescription?: string;
22
+ ogImage?: string;
23
+ favicon?: string;
24
+ };
13
25
  /**
14
26
  * Grid layout for the dev frame. Each inner array is a row,
15
27
  * and each item in the row is a cell/frame.
@@ -3,7 +3,7 @@
3
3
  * Renders the dev frame HTML page - a multi-window view for development
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.renderDevFrame = renderDevFrame;
6
+ exports.renderDevFrame = void 0;
7
7
  const styles_1 = require("./styles");
8
8
  /**
9
9
  * Generate URL for a dev frame iframe
@@ -138,16 +138,44 @@ function renderScript() {
138
138
  * Render the complete dev frame HTML page
139
139
  */
140
140
  function renderDevFrame(config) {
141
- const { title = "Dev View", rows } = config;
141
+ const { rows, appMeta } = config;
142
+ // Build title: use appMeta.title if available, append " - Dev View"
143
+ const baseTitle = appMeta?.title || config.title || "Kokimoki";
144
+ const title = `${baseTitle} - Dev View`;
145
+ // Build meta tags from appMeta
146
+ const lang = appMeta?.lang || "en";
147
+ const description = appMeta?.description || "";
148
+ const ogTitle = appMeta?.ogTitle || baseTitle;
149
+ const ogDescription = appMeta?.ogDescription || description;
150
+ const ogImage = appMeta?.ogImage || "";
151
+ const favicon = appMeta?.favicon || "";
142
152
  // Track seen labels across all rows for duplicate detection
143
153
  const seenLabels = new Set();
144
154
  const rowsHtml = rows.map((row) => renderRow(row, seenLabels)).join("\n");
155
+ // Build optional meta tags
156
+ const metaTags = [];
157
+ if (description) {
158
+ metaTags.push(`<meta name="description" content="${description}">`);
159
+ }
160
+ if (ogTitle) {
161
+ metaTags.push(`<meta property="og:title" content="${ogTitle}">`);
162
+ }
163
+ if (ogDescription) {
164
+ metaTags.push(`<meta property="og:description" content="${ogDescription}">`);
165
+ }
166
+ if (ogImage) {
167
+ metaTags.push(`<meta property="og:image" content="${ogImage}">`);
168
+ }
169
+ if (favicon) {
170
+ metaTags.push(`<link rel="icon" href="${favicon}">`);
171
+ }
145
172
  return `<!DOCTYPE html>
146
- <html lang="en">
173
+ <html lang="${lang}">
147
174
  <head>
148
175
  <meta charset="UTF-8">
149
176
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
150
177
  <title>${title}</title>
178
+ ${metaTags.join("\n ")}
151
179
  <style>${styles_1.devFrameStyles}</style>
152
180
  </head>
153
181
  <body>
@@ -158,3 +186,4 @@ function renderDevFrame(config) {
158
186
  </body>
159
187
  </html>`;
160
188
  }
189
+ exports.renderDevFrame = renderDevFrame;
package/dist/dev-i18n.js CHANGED
@@ -3,10 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.loadI18nFromPath = loadI18nFromPath;
7
- exports.getI18nMeta = getI18nMeta;
8
- exports.syncAllI18nToDevApp = syncAllI18nToDevApp;
9
- exports.syncI18nFile = syncI18nFile;
6
+ exports.syncI18nFile = exports.syncAllI18nToDevApp = exports.getI18nMeta = exports.loadI18nFromPath = void 0;
10
7
  const promises_1 = __importDefault(require("fs/promises"));
11
8
  const path_1 = __importDefault(require("path"));
12
9
  const dev_app_1 = require("./dev-app");
@@ -41,6 +38,7 @@ async function loadI18nFromPath(i18nPath) {
41
38
  }
42
39
  return resources;
43
40
  }
41
+ exports.loadI18nFromPath = loadI18nFromPath;
44
42
  /**
45
43
  * Scan an i18n folder and return i18n metadata.
46
44
  * Expected structure: `{i18nPath}/{lng}/{namespace}.json`
@@ -94,6 +92,7 @@ async function getI18nMeta(i18nPath, primaryLng = "en") {
94
92
  languages,
95
93
  };
96
94
  }
95
+ exports.getI18nMeta = getI18nMeta;
97
96
  /**
98
97
  * Sync primary language i18n files to the dev app build directory (initial sync).
99
98
  * Only uploads primaryLng since that's what AI translations use as source.
@@ -135,6 +134,7 @@ async function syncAllI18nToDevApp(devAppInfo, i18nResources, primaryLng = "en")
135
134
  console.warn(`[kokimoki-kit] Synced i18n: ${syncedFiles.join(", ")}`);
136
135
  }
137
136
  }
137
+ exports.syncAllI18nToDevApp = syncAllI18nToDevApp;
138
138
  /**
139
139
  * Sync a single i18n file to the dev app build directory.
140
140
  * Only syncs if the file is for the primary language.
@@ -162,3 +162,4 @@ async function syncI18nFile(devAppInfo, i18nPath, changedFilePath, primaryLng =
162
162
  console.warn(`[kokimoki-kit] Failed to sync i18n ${lng}/${ns}:`, e instanceof Error ? e.message : e);
163
163
  }
164
164
  }
165
+ exports.syncI18nFile = syncI18nFile;
@@ -3,9 +3,7 @@
3
3
  * Development overlay HTML templates for kokimoki-kit plugin
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.renderLoadingPage = renderLoadingPage;
7
- exports.renderErrorPage = renderErrorPage;
8
- exports.renderStoresChangedPage = renderStoresChangedPage;
6
+ exports.renderStoresChangedPage = exports.renderErrorPage = exports.renderLoadingPage = void 0;
9
7
  const pageBaseStyles = `
10
8
  color: white;
11
9
  display: flex;
@@ -111,6 +109,7 @@ function renderLoadingPage() {
111
109
  </body>
112
110
  </html>`;
113
111
  }
112
+ exports.renderLoadingPage = renderLoadingPage;
114
113
  /**
115
114
  * Generate error page HTML for dev app errors (full page)
116
115
  */
@@ -176,6 +175,7 @@ function renderErrorPage(error) {
176
175
  </body>
177
176
  </html>`;
178
177
  }
178
+ exports.renderErrorPage = renderErrorPage;
179
179
  /**
180
180
  * Generate stores changed warning page HTML (full page, not overlay)
181
181
  */
@@ -298,3 +298,4 @@ function renderStoresChangedPage(canReset) {
298
298
  </body>
299
299
  </html>`;
300
300
  }
301
+ exports.renderStoresChangedPage = renderStoresChangedPage;
package/dist/index.d.ts CHANGED
@@ -3,5 +3,6 @@ export * from "./dev-frame";
3
3
  export * from "./kokimoki-kit-plugin";
4
4
  export * from "./preprocess-style";
5
5
  export * from "./zod";
6
+ export * from "./schemas/app-meta-schema";
6
7
  export * from "./api";
7
8
  export * from "./credentials";
package/dist/index.js CHANGED
@@ -20,6 +20,8 @@ __exportStar(require("./kokimoki-kit-plugin"), exports);
20
20
  __exportStar(require("./preprocess-style"), exports);
21
21
  // Re-export Zod utilities for schema definition
22
22
  __exportStar(require("./zod"), exports);
23
+ // App Meta schema for built-in store
24
+ __exportStar(require("./schemas/app-meta-schema"), exports);
23
25
  // Shared utilities (used by both kit and cli)
24
26
  __exportStar(require("./api"), exports);
25
27
  __exportStar(require("./credentials"), exports);
@@ -1,6 +1,7 @@
1
1
  import type { PluginOption } from "vite";
2
2
  import { ZodType } from "zod/v4";
3
3
  import type { DevFrameCell } from "./dev-frame";
4
+ import { type AppMetaState } from "./schemas/app-meta-schema";
4
5
  export { getI18nMeta, type I18nMeta } from "./dev-i18n";
5
6
  export interface KokimokiKitConfig {
6
7
  conceptId: string;
@@ -27,6 +28,7 @@ export interface KokimokiKitConfig {
27
28
  pattern: string;
28
29
  schema: ZodType;
29
30
  local?: boolean;
31
+ isTransferable?: boolean;
30
32
  }[];
31
33
  /** API endpoint for kokimoki services. Defaults to https://api.kokimoki.com */
32
34
  endpoint?: string;
@@ -53,5 +55,11 @@ export interface KokimokiKitConfig {
53
55
  * ```
54
56
  */
55
57
  devView?: readonly (readonly DevFrameCell[])[] | false;
58
+ /**
59
+ * Default app meta values for SEO and social sharing.
60
+ * These values are used as initial state for the built-in appMetaStore
61
+ * and for server-side HTML meta tag injection.
62
+ */
63
+ defaultAppMeta?: AppMetaState;
56
64
  }
57
65
  export declare function kokimokiKitPlugin(config: KokimokiKitConfig): PluginOption;
@@ -15,30 +15,20 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
15
15
  }) : function(o, v) {
16
16
  o["default"] = v;
17
17
  });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
35
25
  var __importDefault = (this && this.__importDefault) || function (mod) {
36
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
27
  };
38
28
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.getI18nMeta = void 0;
40
- exports.kokimokiKitPlugin = kokimokiKitPlugin;
29
+ exports.kokimokiKitPlugin = exports.getI18nMeta = void 0;
41
30
  const promises_1 = __importDefault(require("fs/promises"));
31
+ const node_html_parser_1 = require("node-html-parser");
42
32
  const path_1 = __importDefault(require("path"));
43
33
  const yaml = __importStar(require("yaml"));
44
34
  const v4_1 = require("zod/v4");
@@ -50,10 +40,23 @@ const dev_i18n_1 = require("./dev-i18n");
50
40
  const dev_overlays_1 = require("./dev-overlays");
51
41
  const preprocess_style_1 = require("./preprocess-style");
52
42
  const production_loading_screen_1 = require("./production-loading-screen");
43
+ const app_meta_schema_1 = require("./schemas/app-meta-schema");
53
44
  const version_1 = require("./version");
54
45
  var dev_i18n_2 = require("./dev-i18n");
55
46
  Object.defineProperty(exports, "getI18nMeta", { enumerable: true, get: function () { return dev_i18n_2.getI18nMeta; } });
47
+ /**
48
+ * Built-in app meta store configuration.
49
+ * Automatically injected into all apps for server-side meta tag injection.
50
+ */
51
+ const BUILT_IN_APP_META_STORE = {
52
+ pattern: app_meta_schema_1.APP_META_STORE_NAME,
53
+ schema: app_meta_schema_1.appMetaStoreSchema,
54
+ local: false,
55
+ isTransferable: true,
56
+ };
56
57
  function kokimokiKitPlugin(config) {
58
+ // Combine user stores with built-in stores
59
+ const allStores = [...(config.stores ?? []), BUILT_IN_APP_META_STORE];
57
60
  // Cache for loaded i18n resources
58
61
  let cachedI18n = null;
59
62
  // Cache dev app info for syncing
@@ -63,6 +66,37 @@ function kokimokiKitPlugin(config) {
63
66
  let initPromise = null;
64
67
  // Cached dev app result
65
68
  let cachedDevAppResult = null;
69
+ /**
70
+ * Resolve asset path for meta tags.
71
+ * In production, prepends %KM_ASSETS% placeholder for relative paths.
72
+ * In dev, returns path as-is since assets are served from root.
73
+ */
74
+ function resolveAssetUrl(path, isProduction) {
75
+ if (!path)
76
+ return path;
77
+ // Only prefix relative paths (starting with /)
78
+ if (isProduction && path.startsWith("/")) {
79
+ return `%KM_ASSETS%${path}`;
80
+ }
81
+ return path;
82
+ }
83
+ /**
84
+ * Resolve asset URLs in defaultAppMeta object.
85
+ * Creates a new object with ogImage and favicon paths resolved.
86
+ */
87
+ function resolveAppMetaAssets(meta, isProduction) {
88
+ if (!meta)
89
+ return null;
90
+ return {
91
+ ...meta,
92
+ ogImage: meta.ogImage
93
+ ? resolveAssetUrl(meta.ogImage, isProduction)
94
+ : undefined,
95
+ favicon: meta.favicon
96
+ ? resolveAssetUrl(meta.favicon, isProduction)
97
+ : undefined,
98
+ };
99
+ }
66
100
  /**
67
101
  * Start initialization if not already started.
68
102
  * Returns a promise that resolves when initialization is complete.
@@ -137,11 +171,15 @@ function kokimokiKitPlugin(config) {
137
171
  if (process.env.NODE_ENV !== "development") {
138
172
  // Remove any existing km-loading element from the HTML
139
173
  const processedHtml = (0, production_loading_screen_1.removeExistingLoadingScreen)(html);
140
- // NOTE: Try https://github.com/jsdom/jsdom instead of regex parsing
141
- const result = processedHtml
142
- .replace("<head>", `<head>
143
- <base href="%KM_BASE%">
144
- <script id="kokimoki-env" type="application/json">
174
+ // Parse HTML for DOM manipulation
175
+ const doc = (0, node_html_parser_1.parse)(processedHtml);
176
+ const head = doc.querySelector("head");
177
+ const body = doc.querySelector("body");
178
+ const htmlEl = doc.querySelector("html");
179
+ // Add base tag
180
+ head?.insertAdjacentHTML("afterbegin", `<base href="%KM_BASE%">`);
181
+ // Inject kokimoki-env script
182
+ head?.insertAdjacentHTML("afterbegin", `<script id="kokimoki-env" type="application/json">
145
183
  {
146
184
  "dev": %KM_DEV%,
147
185
  "test": %KM_TEST%,
@@ -154,10 +192,12 @@ function kokimokiKitPlugin(config) {
154
192
  "i18nNamespaces": ${JSON.stringify(i18nNamespaces)},
155
193
  "i18nLanguages": ${JSON.stringify(i18nLanguages)},
156
194
  "base": "%KM_BASE%",
157
- "assets": "%KM_ASSETS%"
195
+ "assets": "%KM_ASSETS%",
196
+ "defaultAppMeta": ${JSON.stringify(resolveAppMetaAssets(config.defaultAppMeta, true))}
158
197
  }
159
- </script>
160
- <script>
198
+ </script>`);
199
+ // Inject assets URL helper script
200
+ head?.insertAdjacentHTML("beforeend", `<script>
161
201
  window.__toAssetsUrl = (path) => {
162
202
  if (path.startsWith("km-proxy")) {
163
203
  return "/%KM_BUILD_ID%/" + path;
@@ -165,16 +205,50 @@ function kokimokiKitPlugin(config) {
165
205
 
166
206
  return "%KM_ASSETS%/" + path;
167
207
  };
168
- </script>${production_loading_screen_1.loadingScreenStyles}${production_loading_screen_1.loadingScreenScript}
169
- `)
170
- .replace(/<body([^>]*)>/, `<body$1>${production_loading_screen_1.loadingScreenElement}`)
171
- .replace(/<link.*?href="(.*?)".*?>/g, (match, p1) => {
172
- return match.replace(p1, `%KM_ASSETS%${p1.startsWith("/") ? "" : "/"}${p1}`);
173
- })
174
- .replace(/<script.*?src="(.*?)".*?>/g, (match, p1) => {
175
- return match.replace(p1, `%KM_ASSETS%${p1.startsWith("/") ? "" : "/"}${p1}`);
208
+ </script>`);
209
+ // Inject loading screen
210
+ head?.insertAdjacentHTML("beforeend", production_loading_screen_1.loadingScreenStyles);
211
+ head?.insertAdjacentHTML("beforeend", production_loading_screen_1.loadingScreenScript);
212
+ body?.insertAdjacentHTML("afterbegin", production_loading_screen_1.loadingScreenElement);
213
+ // Inject meta tags from defaultAppMeta
214
+ if (config.defaultAppMeta) {
215
+ const meta = config.defaultAppMeta;
216
+ if (meta.lang) {
217
+ htmlEl?.setAttribute("lang", meta.lang);
218
+ }
219
+ if (meta.title) {
220
+ head?.insertAdjacentHTML("beforeend", `<title>${meta.title}</title>`);
221
+ }
222
+ if (meta.description) {
223
+ head?.insertAdjacentHTML("beforeend", `<meta name="description" content="${meta.description}" />`);
224
+ }
225
+ if (meta.ogTitle ?? meta.title) {
226
+ head?.insertAdjacentHTML("beforeend", `<meta property="og:title" content="${meta.ogTitle ?? meta.title}" />`);
227
+ }
228
+ if (meta.ogDescription ?? meta.description) {
229
+ head?.insertAdjacentHTML("beforeend", `<meta property="og:description" content="${meta.ogDescription ?? meta.description}" />`);
230
+ }
231
+ if (meta.ogImage) {
232
+ head?.insertAdjacentHTML("beforeend", `<meta property="og:image" content="${resolveAssetUrl(meta.ogImage, true)}" />`);
233
+ }
234
+ if (meta.favicon) {
235
+ head?.insertAdjacentHTML("beforeend", `<link rel="icon" type="image/png" href="${resolveAssetUrl(meta.favicon, true)}" />`);
236
+ }
237
+ }
238
+ // Update asset URLs in link and script tags
239
+ doc.querySelectorAll("link[href]").forEach((el) => {
240
+ const href = el.getAttribute("href");
241
+ if (href) {
242
+ el.setAttribute("href", `%KM_ASSETS%${href.startsWith("/") ? "" : "/"}${href}`);
243
+ }
244
+ });
245
+ doc.querySelectorAll("script[src]").forEach((el) => {
246
+ const src = el.getAttribute("src");
247
+ if (src) {
248
+ el.setAttribute("src", `%KM_ASSETS%${src.startsWith("/") ? "" : "/"}${src}`);
249
+ }
176
250
  });
177
- return result;
251
+ return doc.toString();
178
252
  }
179
253
  // Development mode: ensure initialization is started and wait for it
180
254
  if (initState === "pending" || initState === "initializing") {
@@ -191,7 +265,7 @@ function kokimokiKitPlugin(config) {
191
265
  error: { code: "INIT_ERROR", message: "Initialization failed" },
192
266
  };
193
267
  // Check if stores configuration has changed
194
- const currentStoresHash = (0, dev_app_1.computeStoresHash)(config.stores);
268
+ const currentStoresHash = (0, dev_app_1.computeStoresHash)(allStores);
195
269
  const previousStoresHash = await (0, dev_app_1.readStoresHash)();
196
270
  const storesChanged = previousStoresHash !== null && previousStoresHash !== currentStoresHash;
197
271
  // Write initial stores hash if it doesn't exist yet
@@ -212,9 +286,12 @@ function kokimokiKitPlugin(config) {
212
286
  const defaultProjectConfigFile = await promises_1.default.readFile(config.defaultProjectConfigPath, "utf8");
213
287
  defaultProjectConfig = config.schema.parse(yaml.parse(defaultProjectConfigFile));
214
288
  }
215
- // Inject the app id into the index.html
216
- html = html.replace("<head>", `<head>
217
- <script id="kokimoki-env" type="application/json">
289
+ // Parse HTML for DOM manipulation
290
+ const doc = (0, node_html_parser_1.parse)(html);
291
+ const head = doc.querySelector("head");
292
+ const htmlEl = doc.querySelector("html");
293
+ // Inject kokimoki-env script
294
+ const envScript = (0, node_html_parser_1.parse)(`<script id="kokimoki-env" type="application/json">
218
295
  {
219
296
  "dev": true,
220
297
  "test": true,
@@ -225,15 +302,43 @@ function kokimokiKitPlugin(config) {
225
302
  "i18nLanguages": ${JSON.stringify(i18nLanguages)},
226
303
  "base": "/",
227
304
  "assets": "/",
228
- "buildUrl": ${JSON.stringify(buildUrl ?? null)}
305
+ "buildUrl": ${JSON.stringify(buildUrl ?? null)},
306
+ "defaultAppMeta": ${JSON.stringify(config.defaultAppMeta ?? null)}
229
307
  }
230
308
  </script>`);
309
+ head?.insertAdjacentHTML("afterbegin", envScript.toString());
310
+ // Inject meta tags from defaultAppMeta in development
311
+ if (config.defaultAppMeta) {
312
+ const meta = config.defaultAppMeta;
313
+ if (meta.lang) {
314
+ htmlEl?.setAttribute("lang", meta.lang);
315
+ }
316
+ if (meta.title) {
317
+ head?.insertAdjacentHTML("beforeend", `<title>${meta.title}</title>`);
318
+ }
319
+ if (meta.description) {
320
+ head?.insertAdjacentHTML("beforeend", `<meta name="description" content="${meta.description}" />`);
321
+ }
322
+ if (meta.ogTitle ?? meta.title) {
323
+ head?.insertAdjacentHTML("beforeend", `<meta property="og:title" content="${meta.ogTitle ?? meta.title}" />`);
324
+ }
325
+ if (meta.ogDescription ?? meta.description) {
326
+ head?.insertAdjacentHTML("beforeend", `<meta property="og:description" content="${meta.ogDescription ?? meta.description}" />`);
327
+ }
328
+ if (meta.ogImage) {
329
+ head?.insertAdjacentHTML("beforeend", `<meta property="og:image" content="${meta.ogImage}" />`);
330
+ }
331
+ if (meta.favicon) {
332
+ head?.insertAdjacentHTML("beforeend", `<link rel="icon" type="image/png" href="${meta.favicon}" />`);
333
+ }
334
+ }
231
335
  // Inject default project style in development
232
336
  if (config.defaultProjectStylePath) {
233
337
  const defaultProjectStyle = await promises_1.default.readFile(config.defaultProjectStylePath, "utf8");
234
- html = html.replace("</body>", `<style id="km-dev-style">${(0, preprocess_style_1.preprocessStyle)(defaultProjectStyle)}</style></body>`);
338
+ const body = doc.querySelector("body");
339
+ body?.insertAdjacentHTML("beforeend", `<style id="km-dev-style">${(0, preprocess_style_1.preprocessStyle)(defaultProjectStyle)}</style>`);
235
340
  }
236
- return html;
341
+ return doc.toString();
237
342
  },
238
343
  // write kokimoki metadata to .kokimoki directory
239
344
  async generateBundle(_, _bundle) {
@@ -259,6 +364,7 @@ function kokimokiKitPlugin(config) {
259
364
  build: "dist",
260
365
  defaultProjectConfigPath: config.defaultProjectConfigPath,
261
366
  defaultProjectStylePath: config.defaultProjectStylePath,
367
+ defaultAppMeta: config.defaultAppMeta,
262
368
  i18n,
263
369
  }, null, 2));
264
370
  // write schema
@@ -297,19 +403,18 @@ function kokimokiKitPlugin(config) {
297
403
  properties: {},
298
404
  };
299
405
  await promises_1.default.writeFile(".kokimoki/schema.json", JSON.stringify(jsonSchema, null, 2));
300
- // write stores config with JSON schemas to build output
301
- if (config.stores) {
302
- const storesWithJsonSchema = config.stores.map((store) => ({
303
- pattern: store.pattern,
304
- local: store.local,
305
- schema: v4_1.z.toJSONSchema(store.schema),
306
- }));
307
- this.emitFile({
308
- type: "asset",
309
- fileName: "km-stores.json",
310
- source: JSON.stringify(storesWithJsonSchema, null, 2),
311
- });
312
- }
406
+ // write stores config with JSON schemas to build output (always includes built-in stores)
407
+ const storesWithJsonSchema = allStores.map((store) => ({
408
+ pattern: store.pattern,
409
+ local: store.local,
410
+ schema: v4_1.z.toJSONSchema(store.schema),
411
+ isTransferable: store.isTransferable ?? false,
412
+ }));
413
+ this.emitFile({
414
+ type: "asset",
415
+ fileName: "km-stores.json",
416
+ source: JSON.stringify(storesWithJsonSchema, null, 2),
417
+ });
313
418
  // write i18n files to build output as individual files per lng/ns
314
419
  const i18nResources = await getI18nResources();
315
420
  if (Object.keys(i18nResources).length > 0) {
@@ -340,7 +445,7 @@ function kokimokiKitPlugin(config) {
340
445
  startInitialization();
341
446
  // Helper to check stores changed status
342
447
  async function checkStoresChanged() {
343
- const currentStoresHash = (0, dev_app_1.computeStoresHash)(config.stores);
448
+ const currentStoresHash = (0, dev_app_1.computeStoresHash)(allStores);
344
449
  const previousStoresHash = await (0, dev_app_1.readStoresHash)();
345
450
  return (previousStoresHash !== null &&
346
451
  previousStoresHash !== currentStoresHash);
@@ -387,7 +492,7 @@ function kokimokiKitPlugin(config) {
387
492
  // API endpoint to acknowledge stores hash change (dismiss the popup)
388
493
  server.middlewares.use("/__kokimoki/stores-hash/acknowledge", async (_req, res) => {
389
494
  try {
390
- const currentStoresHash = (0, dev_app_1.computeStoresHash)(config.stores);
495
+ const currentStoresHash = (0, dev_app_1.computeStoresHash)(allStores);
391
496
  await (0, dev_app_1.writeStoresHash)(currentStoresHash);
392
497
  res.statusCode = 200;
393
498
  res.setHeader("Content-Type", "application/json");
@@ -407,7 +512,7 @@ function kokimokiKitPlugin(config) {
407
512
  // Delete the app-id file so a new dev app will be created
408
513
  await (0, dev_app_1.deleteAppId)();
409
514
  // Update the stores hash
410
- const currentStoresHash = (0, dev_app_1.computeStoresHash)(config.stores);
515
+ const currentStoresHash = (0, dev_app_1.computeStoresHash)(allStores);
411
516
  await (0, dev_app_1.writeStoresHash)(currentStoresHash);
412
517
  // Reset initialization state so next request triggers re-initialization
413
518
  initState = "pending";
@@ -523,8 +628,8 @@ function kokimokiKitPlugin(config) {
523
628
  }
524
629
  // Render dev view HTML at root
525
630
  const devViewHtml = (0, dev_frame_1.renderDevFrame)({
526
- title: "Dev View",
527
631
  rows: config.devView,
632
+ appMeta: config.defaultAppMeta,
528
633
  });
529
634
  res.statusCode = 200;
530
635
  res.setHeader("Content-Type", "text/html");
@@ -534,3 +639,4 @@ function kokimokiKitPlugin(config) {
534
639
  },
535
640
  };
536
641
  }
642
+ exports.kokimokiKitPlugin = kokimokiKitPlugin;
@@ -3,17 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.hexToRgb = hexToRgb;
7
- exports.rgbToHex = rgbToHex;
8
- exports.revertReplaceForColorPickers = revertReplaceForColorPickers;
9
- exports.hexToTailwindRgbString = hexToTailwindRgbString;
10
- exports.getLuminance = getLuminance;
11
- exports.calculateRatio = calculateRatio;
12
- exports.handleStringColor = handleStringColor;
13
- exports.destringRgb = destringRgb;
14
- exports.generateA11yOnColor = generateA11yOnColor;
15
- exports.generatePalette = generatePalette;
16
- exports.preprocessStyle = preprocessStyle;
6
+ exports.preprocessStyle = exports.generatePalette = exports.generateA11yOnColor = exports.destringRgb = exports.handleStringColor = exports.calculateRatio = exports.getLuminance = exports.hexToTailwindRgbString = exports.revertReplaceForColorPickers = exports.rgbToHex = exports.hexToRgb = void 0;
17
7
  const colorjs_io_1 = __importDefault(require("colorjs.io"));
18
8
  const colornames_1 = __importDefault(require("colornames"));
19
9
  // List of rgb tuple variable names (possibly temporary system to support color picker for these variables)
@@ -113,10 +103,12 @@ function hexToRgb(hex) {
113
103
  b: parseInt(b, 16),
114
104
  };
115
105
  }
106
+ exports.hexToRgb = hexToRgb;
116
107
  function rgbToHex(r, g, b) {
117
108
  const toHex = (c) => `0${c.toString(16)}`.slice(-2);
118
109
  return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
119
110
  }
111
+ exports.rgbToHex = rgbToHex;
120
112
  // export function replaceForColorPickers(code: string) {
121
113
  // return code.replace(
122
114
  // /--([a-z0-9-]+):\s*(\d+)\s+(\d+)\s+(\d+);/gi,
@@ -153,6 +145,7 @@ function revertReplaceForColorPickers(code) {
153
145
  return match;
154
146
  });
155
147
  }
148
+ exports.revertReplaceForColorPickers = revertReplaceForColorPickers;
156
149
  function lighten(hex, intensity) {
157
150
  const color = hexToRgb(`#${hex}`);
158
151
  if (!color)
@@ -179,6 +172,7 @@ function hexToTailwindRgbString(hex) {
179
172
  const [, r, g, b] = colorParts;
180
173
  return `${parseInt(r, 16)} ${parseInt(g, 16)} ${parseInt(b, 16)}`;
181
174
  }
175
+ exports.hexToTailwindRgbString = hexToTailwindRgbString;
182
176
  function getLuminance(r, g, b) {
183
177
  const { _r, _g, _b } = typeof r === "object"
184
178
  ? { _r: r.r, _g: r.g, _b: r.b }
@@ -192,6 +186,7 @@ function getLuminance(r, g, b) {
192
186
  });
193
187
  return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
194
188
  }
189
+ exports.getLuminance = getLuminance;
195
190
  function calculateRatio(luminance1, luminance2) {
196
191
  const lum1 = typeof luminance1 === "string"
197
192
  ? getLuminance(handleStringColor(luminance1))
@@ -205,6 +200,7 @@ function calculateRatio(luminance1, luminance2) {
205
200
  ? (lum2 + 0.05) / (lum1 + 0.05)
206
201
  : (lum1 + 0.05) / (lum2 + 0.05);
207
202
  }
203
+ exports.calculateRatio = calculateRatio;
208
204
  function handleStringColor(colorString, returnType = "rgb") {
209
205
  // if it's a css variable
210
206
  if (colorString.includes("--")) {
@@ -228,6 +224,7 @@ function handleStringColor(colorString, returnType = "rgb") {
228
224
  }
229
225
  return colorString;
230
226
  }
227
+ exports.handleStringColor = handleStringColor;
231
228
  function cssColorToHex(colorString) {
232
229
  if (colorString.includes("#"))
233
230
  return colorString;
@@ -254,11 +251,13 @@ function destringRgb(rgbString) {
254
251
  b: parseInt(rgb[3], 10),
255
252
  };
256
253
  }
254
+ exports.destringRgb = destringRgb;
257
255
  function generateA11yOnColor(hex) {
258
256
  const black = calculateRatio(hex, "#000000");
259
257
  const white = calculateRatio(hex, "#FFFFFF");
260
258
  return black < white ? "0 0 0" : "255 255 255";
261
259
  }
260
+ exports.generateA11yOnColor = generateA11yOnColor;
262
261
  function generatePalette(baseColor) {
263
262
  const hexValidation = new RegExp(/^#[0-9a-f]{6}$/i);
264
263
  if (!hexValidation.test(baseColor))
@@ -300,6 +299,7 @@ function generatePalette(baseColor) {
300
299
  });
301
300
  return response;
302
301
  }
302
+ exports.generatePalette = generatePalette;
303
303
  function preprocessGfcThemeBlock(code) {
304
304
  // Generate map of defined css variables
305
305
  const cssVariableMap = {};
@@ -376,3 +376,4 @@ function preprocessStyle(code) {
376
376
  }
377
377
  });
378
378
  }
379
+ exports.preprocessStyle = preprocessStyle;
@@ -4,8 +4,7 @@
4
4
  * This screen is injected into production builds and removed when km:ready is received.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.loadingScreenElement = exports.loadingScreenScript = exports.loadingScreenStyles = void 0;
8
- exports.removeExistingLoadingScreen = removeExistingLoadingScreen;
7
+ exports.removeExistingLoadingScreen = exports.loadingScreenElement = exports.loadingScreenScript = exports.loadingScreenStyles = void 0;
9
8
  /**
10
9
  * Loading screen styles
11
10
  */
@@ -121,3 +120,4 @@ function removeExistingLoadingScreen(html) {
121
120
  .replace(/<div[^>]*id=["']km-loading["'][^>]*>[\s\S]*?<\/div>/gi, "")
122
121
  .replace(/<style[^>]*id=["']km-loading-style["'][^>]*>[\s\S]*?<\/style>/gi, "");
123
122
  }
123
+ exports.removeExistingLoadingScreen = removeExistingLoadingScreen;
@@ -0,0 +1,22 @@
1
+ import { z } from "zod/v4";
2
+ /**
3
+ * Reserved store name for the App Meta store.
4
+ * Stores with the `__km/` prefix are reserved for SDK internal use.
5
+ */
6
+ export declare const APP_META_STORE_NAME = "__km/app-meta";
7
+ /**
8
+ * Schema for the App Meta store.
9
+ * Used by the server to inject meta tags into the HTML response.
10
+ *
11
+ * All fields are optional - missing fields will fall back to defaults in index.html.
12
+ */
13
+ export declare const appMetaStoreSchema: z.ZodObject<{
14
+ lang: z.ZodOptional<z.ZodString>;
15
+ title: z.ZodOptional<z.ZodString>;
16
+ description: z.ZodOptional<z.ZodString>;
17
+ ogTitle: z.ZodOptional<z.ZodString>;
18
+ ogDescription: z.ZodOptional<z.ZodString>;
19
+ ogImage: z.ZodOptional<z.ZodString>;
20
+ favicon: z.ZodOptional<z.ZodString>;
21
+ }, z.core.$strip>;
22
+ export type AppMetaState = z.infer<typeof appMetaStoreSchema>;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.appMetaStoreSchema = exports.APP_META_STORE_NAME = void 0;
4
+ const v4_1 = require("zod/v4");
5
+ /**
6
+ * Reserved store name for the App Meta store.
7
+ * Stores with the `__km/` prefix are reserved for SDK internal use.
8
+ */
9
+ exports.APP_META_STORE_NAME = "__km/app-meta";
10
+ /**
11
+ * Schema for the App Meta store.
12
+ * Used by the server to inject meta tags into the HTML response.
13
+ *
14
+ * All fields are optional - missing fields will fall back to defaults in index.html.
15
+ */
16
+ exports.appMetaStoreSchema = v4_1.z.object({
17
+ /** HTML lang attribute (e.g., 'en', 'et', 'de') */
18
+ lang: v4_1.z.string().optional(),
19
+ /** Document title (browser tab) */
20
+ title: v4_1.z.string().optional(),
21
+ /** Meta description */
22
+ description: v4_1.z.string().optional(),
23
+ /** Open Graph title (defaults to title if not set) */
24
+ ogTitle: v4_1.z.string().optional(),
25
+ /** Open Graph description (defaults to description if not set) */
26
+ ogDescription: v4_1.z.string().optional(),
27
+ /** Open Graph image URL */
28
+ ogImage: v4_1.z.string().optional(),
29
+ /** Favicon URL */
30
+ favicon: v4_1.z.string().optional(),
31
+ });
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const KOKIMOKI_KIT_VERSION = "1.7.0";
1
+ export declare const KOKIMOKI_KIT_VERSION = "1.8.1";
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.KOKIMOKI_KIT_VERSION = void 0;
4
- exports.KOKIMOKI_KIT_VERSION = "1.7.0";
4
+ exports.KOKIMOKI_KIT_VERSION = "1.8.1";
@@ -15,77 +15,93 @@ npm install @kokimoki/kit
15
15
 
16
16
  ## Plugin Configuration
17
17
 
18
+ Create a `kokimoki.config.ts` file in your project root to define your Kokimoki Kit configuration:
19
+
20
+ ```typescript
21
+ export const kokimokiConfig: KokimokiKitConfig = {
22
+ // Required: Your concept ID from Kokimoki
23
+ conceptId: "your-concept-id",
24
+
25
+ // Required: Deploy configurations
26
+ deployCodes: [
27
+ {
28
+ name: "default",
29
+ description: "Default game mode",
30
+ clientContext: { mode: "standard" },
31
+ },
32
+ {
33
+ name: "tournament",
34
+ description: "Tournament mode with stricter rules",
35
+ clientContext: { mode: "tournament", maxPlayers: 100 },
36
+ },
37
+ ],
38
+
39
+ /**
40
+ * Store schemas for validation
41
+ * marking stores as transferable allows their content to be used
42
+ * when creating new apps based on this one.
43
+ *
44
+ * For example, game configuration can contain content and settings
45
+ * that can be reused when setting up a new game instance. Dynamic
46
+ * game state, on the other hand, should not be transferable.
47
+ */
48
+ stores: [
49
+ {
50
+ pattern: "config",
51
+ schema: z.object({
52
+ mode: z.enum(["standard", "tournament"]),
53
+ duration: z.number().min(1).describe("Game duration in minutes"),
54
+ }),
55
+ isTransferable: true, // This store content can be used to create a new app with the same content
56
+ },
57
+ {
58
+ pattern: "game",
59
+ schema: z.object({
60
+ status: z.enum(["waiting", "playing", "finished"]),
61
+ round: z.number(),
62
+ }),
63
+ isTransferable: false, // Dynamic game state should not be marked as transferable
64
+ },
65
+ {
66
+ pattern: "player-*",
67
+ schema: z.object({
68
+ name: z.string(),
69
+ score: z.number(),
70
+ }),
71
+ local: true, // Local store (per-client)
72
+ isTransferable: false, // Local stores cannot be transferable
73
+ },
74
+ ],
75
+
76
+ // Optional: i18n configuration
77
+ i18nPath: "./src/i18n",
78
+ i18nPrimaryLng: "en", // Source language for AI translations
79
+
80
+ // Optional: Custom API endpoint
81
+ endpoint: "https://api.kokimoki.com",
82
+ host: "y-wss.kokimoki.com",
83
+
84
+ // Optional: Dev view layout (see Dev Frame section)
85
+ devView: [
86
+ [{ label: "host", clientContext: { mode: "host" } }],
87
+ [
88
+ { label: "player1", clientContext: { mode: "player" } },
89
+ { label: "player2", clientContext: { mode: "player" } },
90
+ ],
91
+ ],
92
+ };
93
+ ```
94
+
18
95
  Add the plugin to your `vite.config.ts`:
19
96
 
20
97
  ```typescript
21
98
  import { defineConfig } from "vite";
22
99
  import { kokimokiKitPlugin } from "@kokimoki/kit";
23
100
  import { z } from "@kokimoki/kit";
101
+ import { kokimokiConfig } from "./kokimoki.config";
24
102
 
25
103
  export default defineConfig({
26
- plugins: [
27
- kokimokiKitPlugin({
28
- // Required: Your concept ID from Kokimoki
29
- conceptId: "your-concept-id",
30
-
31
- // Required: Deploy configurations
32
- deployCodes: [
33
- {
34
- name: "default",
35
- description: "Default game mode",
36
- clientContext: { mode: "standard" },
37
- },
38
- {
39
- name: "tournament",
40
- description: "Tournament mode with stricter rules",
41
- clientContext: { mode: "tournament", maxPlayers: 100 },
42
- },
43
- ],
44
-
45
- // Optional: Project config schema (validated in admin panel)
46
- schema: z.object({
47
- title: z.string(),
48
- maxPlayers: z.number().min(2).max(100),
49
- timeLimit: z.number().optional(),
50
- }),
51
-
52
- // Optional: Store schemas for validation
53
- stores: [
54
- {
55
- pattern: "game",
56
- schema: z.object({
57
- status: z.enum(["waiting", "playing", "finished"]),
58
- round: z.number(),
59
- }),
60
- },
61
- {
62
- pattern: "player-*",
63
- schema: z.object({
64
- name: z.string(),
65
- score: z.number(),
66
- }),
67
- local: true, // Local store (per-client)
68
- },
69
- ],
70
-
71
- // Optional: i18n configuration
72
- i18nPath: "./src/i18n",
73
- i18nPrimaryLng: "en", // Source language for AI translations
74
-
75
- // Optional: Custom API endpoint
76
- endpoint: "https://api.kokimoki.com",
77
- host: "y-wss.kokimoki.com",
78
-
79
- // Optional: Dev view layout (see Dev Frame section)
80
- devView: [
81
- [{ label: "host", clientContext: { mode: "host" } }],
82
- [
83
- { label: "player1", clientContext: { mode: "player" } },
84
- { label: "player2", clientContext: { mode: "player" } },
85
- ],
86
- ],
87
- }),
88
- ],
104
+ plugins: [kokimokiKitPlugin(kokimokiConfig)],
89
105
  });
90
106
  ```
91
107
 
@@ -100,17 +116,65 @@ export default defineConfig({
100
116
 
101
117
  ### Optional Options
102
118
 
103
- | Option | Type | Description |
104
- | -------------------------- | ---------------- | -------------------------------------------------- |
105
- | `schema` | `ZodType` | Zod schema for project configuration |
106
- | `stores` | `array` | Store definitions with patterns and schemas |
107
- | `i18nPath` | `string` | Path to i18n folder (e.g., `./src/i18n`) |
108
- | `i18nPrimaryLng` | `string` | Source language code (default: `"en"`) |
109
- | `endpoint` | `string` | API endpoint (default: `https://api.kokimoki.com`) |
110
- | `host` | `string` | WebSocket host (default: `y-wss.kokimoki.com`) |
111
- | `devView` | `array \| false` | Dev frame layout or `false` to disable |
112
- | `defaultProjectConfigPath` | `string` | Path to default project config file |
113
- | `defaultProjectStylePath` | `string` | Path to default project style file |
119
+ | Option | Type | Description |
120
+ | ------------------------- | ---------------- | -------------------------------------------------- |
121
+ | `schema` | `ZodType` | Zod schema for project configuration |
122
+ | `stores` | `array` | Store definitions with patterns and schemas |
123
+ | `i18nPath` | `string` | Path to i18n folder (e.g., `./src/i18n`) |
124
+ | `i18nPrimaryLng` | `string` | Source language code (default: `"en"`) |
125
+ | `endpoint` | `string` | API endpoint (default: `https://api.kokimoki.com`) |
126
+ | `host` | `string` | WebSocket host (default: `y-wss.kokimoki.com`) |
127
+ | `devView` | `array \| false` | Dev frame layout or `false` to disable |
128
+ | `defaultAppMeta` | `object` | Default meta tags for SEO and social sharing |
129
+ | `defaultProjectStylePath` | `string` | Path to default project style file |
130
+
131
+ ## App Meta
132
+
133
+ The `defaultAppMeta` option configures HTML meta tags for SEO and social sharing previews. These defaults are injected into the HTML at build time and used as the initial state for `kmClient.metaStore`.
134
+
135
+ ### Configuration
136
+
137
+ ```typescript
138
+ kokimokiKitPlugin({
139
+ // ...
140
+ defaultAppMeta: {
141
+ lang: "en",
142
+ title: "My Game",
143
+ description: "An awesome multiplayer game",
144
+ ogTitle: "My Game",
145
+ ogDescription: "Join the fun!",
146
+ ogImage: "/og-image.png",
147
+ favicon: "/favicon.png",
148
+ },
149
+ });
150
+ ```
151
+
152
+ ### Fields
153
+
154
+ | Field | Description |
155
+ | --------------- | ------------------------------------------------------------- |
156
+ | `lang` | HTML `lang` attribute (e.g., `"en"`, `"de"`) |
157
+ | `title` | Document title (browser tab) |
158
+ | `description` | Meta description for SEO |
159
+ | `ogTitle` | Open Graph title (defaults to `title` if not set) |
160
+ | `ogDescription` | Open Graph description (defaults to `description`) |
161
+ | `ogImage` | Open Graph image URL (use relative path like `/og-image.png`) |
162
+ | `favicon` | Favicon URL (use relative path like `/favicon.png`) |
163
+
164
+ ### Asset Path Resolution
165
+
166
+ Relative paths (starting with `/`) for `ogImage` and `favicon` are automatically prefixed with the assets base URL in production builds. This ensures images work correctly when served from a CDN.
167
+
168
+ ### Runtime Updates
169
+
170
+ The meta state can be updated at runtime via `kmClient.metaStore`:
171
+
172
+ ```typescript
173
+ await kmClient.transact([kmClient.metaStore], ([meta]) => {
174
+ meta.title = "Game Room: Epic Battle";
175
+ meta.ogImage = "https://example.com/custom-preview.png";
176
+ });
177
+ ```
114
178
 
115
179
  ## Dev Frame
116
180
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kokimoki/kit",
3
- "version": "1.7.0",
3
+ "version": "1.8.1",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -26,6 +26,7 @@
26
26
  "bson-objectid": "^2.0.4",
27
27
  "colorjs.io": "^0.5.2",
28
28
  "colornames": "^1.1.1",
29
+ "node-html-parser": "^7.0.1",
29
30
  "yaml": "^2.7.0",
30
31
  "zod": "^4.3.5"
31
32
  },