@kokimoki/kit 1.7.0 → 1.8.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.
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;
@@ -53,5 +54,11 @@ export interface KokimokiKitConfig {
53
54
  * ```
54
55
  */
55
56
  devView?: readonly (readonly DevFrameCell[])[] | false;
57
+ /**
58
+ * Default app meta values for SEO and social sharing.
59
+ * These values are used as initial state for the built-in appMetaStore
60
+ * and for server-side HTML meta tag injection.
61
+ */
62
+ defaultAppMeta?: AppMetaState;
56
63
  }
57
64
  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,22 @@ 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
+ };
56
56
  function kokimokiKitPlugin(config) {
57
+ // Combine user stores with built-in stores
58
+ const allStores = [...(config.stores ?? []), BUILT_IN_APP_META_STORE];
57
59
  // Cache for loaded i18n resources
58
60
  let cachedI18n = null;
59
61
  // Cache dev app info for syncing
@@ -63,6 +65,37 @@ function kokimokiKitPlugin(config) {
63
65
  let initPromise = null;
64
66
  // Cached dev app result
65
67
  let cachedDevAppResult = null;
68
+ /**
69
+ * Resolve asset path for meta tags.
70
+ * In production, prepends %KM_ASSETS% placeholder for relative paths.
71
+ * In dev, returns path as-is since assets are served from root.
72
+ */
73
+ function resolveAssetUrl(path, isProduction) {
74
+ if (!path)
75
+ return path;
76
+ // Only prefix relative paths (starting with /)
77
+ if (isProduction && path.startsWith("/")) {
78
+ return `%KM_ASSETS%${path}`;
79
+ }
80
+ return path;
81
+ }
82
+ /**
83
+ * Resolve asset URLs in defaultAppMeta object.
84
+ * Creates a new object with ogImage and favicon paths resolved.
85
+ */
86
+ function resolveAppMetaAssets(meta, isProduction) {
87
+ if (!meta)
88
+ return null;
89
+ return {
90
+ ...meta,
91
+ ogImage: meta.ogImage
92
+ ? resolveAssetUrl(meta.ogImage, isProduction)
93
+ : undefined,
94
+ favicon: meta.favicon
95
+ ? resolveAssetUrl(meta.favicon, isProduction)
96
+ : undefined,
97
+ };
98
+ }
66
99
  /**
67
100
  * Start initialization if not already started.
68
101
  * Returns a promise that resolves when initialization is complete.
@@ -137,11 +170,15 @@ function kokimokiKitPlugin(config) {
137
170
  if (process.env.NODE_ENV !== "development") {
138
171
  // Remove any existing km-loading element from the HTML
139
172
  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">
173
+ // Parse HTML for DOM manipulation
174
+ const doc = (0, node_html_parser_1.parse)(processedHtml);
175
+ const head = doc.querySelector("head");
176
+ const body = doc.querySelector("body");
177
+ const htmlEl = doc.querySelector("html");
178
+ // Add base tag
179
+ head?.insertAdjacentHTML("afterbegin", `<base href="%KM_BASE%">`);
180
+ // Inject kokimoki-env script
181
+ head?.insertAdjacentHTML("afterbegin", `<script id="kokimoki-env" type="application/json">
145
182
  {
146
183
  "dev": %KM_DEV%,
147
184
  "test": %KM_TEST%,
@@ -154,10 +191,12 @@ function kokimokiKitPlugin(config) {
154
191
  "i18nNamespaces": ${JSON.stringify(i18nNamespaces)},
155
192
  "i18nLanguages": ${JSON.stringify(i18nLanguages)},
156
193
  "base": "%KM_BASE%",
157
- "assets": "%KM_ASSETS%"
194
+ "assets": "%KM_ASSETS%",
195
+ "defaultAppMeta": ${JSON.stringify(resolveAppMetaAssets(config.defaultAppMeta, true))}
158
196
  }
159
- </script>
160
- <script>
197
+ </script>`);
198
+ // Inject assets URL helper script
199
+ head?.insertAdjacentHTML("beforeend", `<script>
161
200
  window.__toAssetsUrl = (path) => {
162
201
  if (path.startsWith("km-proxy")) {
163
202
  return "/%KM_BUILD_ID%/" + path;
@@ -165,16 +204,50 @@ function kokimokiKitPlugin(config) {
165
204
 
166
205
  return "%KM_ASSETS%/" + path;
167
206
  };
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}`);
207
+ </script>`);
208
+ // Inject loading screen
209
+ head?.insertAdjacentHTML("beforeend", production_loading_screen_1.loadingScreenStyles);
210
+ head?.insertAdjacentHTML("beforeend", production_loading_screen_1.loadingScreenScript);
211
+ body?.insertAdjacentHTML("afterbegin", production_loading_screen_1.loadingScreenElement);
212
+ // Inject meta tags from defaultAppMeta
213
+ if (config.defaultAppMeta) {
214
+ const meta = config.defaultAppMeta;
215
+ if (meta.lang) {
216
+ htmlEl?.setAttribute("lang", meta.lang);
217
+ }
218
+ if (meta.title) {
219
+ head?.insertAdjacentHTML("beforeend", `<title>${meta.title}</title>`);
220
+ }
221
+ if (meta.description) {
222
+ head?.insertAdjacentHTML("beforeend", `<meta name="description" content="${meta.description}" />`);
223
+ }
224
+ if (meta.ogTitle ?? meta.title) {
225
+ head?.insertAdjacentHTML("beforeend", `<meta property="og:title" content="${meta.ogTitle ?? meta.title}" />`);
226
+ }
227
+ if (meta.ogDescription ?? meta.description) {
228
+ head?.insertAdjacentHTML("beforeend", `<meta property="og:description" content="${meta.ogDescription ?? meta.description}" />`);
229
+ }
230
+ if (meta.ogImage) {
231
+ head?.insertAdjacentHTML("beforeend", `<meta property="og:image" content="${resolveAssetUrl(meta.ogImage, true)}" />`);
232
+ }
233
+ if (meta.favicon) {
234
+ head?.insertAdjacentHTML("beforeend", `<link rel="icon" type="image/png" href="${resolveAssetUrl(meta.favicon, true)}" />`);
235
+ }
236
+ }
237
+ // Update asset URLs in link and script tags
238
+ doc.querySelectorAll("link[href]").forEach((el) => {
239
+ const href = el.getAttribute("href");
240
+ if (href) {
241
+ el.setAttribute("href", `%KM_ASSETS%${href.startsWith("/") ? "" : "/"}${href}`);
242
+ }
243
+ });
244
+ doc.querySelectorAll("script[src]").forEach((el) => {
245
+ const src = el.getAttribute("src");
246
+ if (src) {
247
+ el.setAttribute("src", `%KM_ASSETS%${src.startsWith("/") ? "" : "/"}${src}`);
248
+ }
176
249
  });
177
- return result;
250
+ return doc.toString();
178
251
  }
179
252
  // Development mode: ensure initialization is started and wait for it
180
253
  if (initState === "pending" || initState === "initializing") {
@@ -191,7 +264,7 @@ function kokimokiKitPlugin(config) {
191
264
  error: { code: "INIT_ERROR", message: "Initialization failed" },
192
265
  };
193
266
  // Check if stores configuration has changed
194
- const currentStoresHash = (0, dev_app_1.computeStoresHash)(config.stores);
267
+ const currentStoresHash = (0, dev_app_1.computeStoresHash)(allStores);
195
268
  const previousStoresHash = await (0, dev_app_1.readStoresHash)();
196
269
  const storesChanged = previousStoresHash !== null && previousStoresHash !== currentStoresHash;
197
270
  // Write initial stores hash if it doesn't exist yet
@@ -212,9 +285,12 @@ function kokimokiKitPlugin(config) {
212
285
  const defaultProjectConfigFile = await promises_1.default.readFile(config.defaultProjectConfigPath, "utf8");
213
286
  defaultProjectConfig = config.schema.parse(yaml.parse(defaultProjectConfigFile));
214
287
  }
215
- // Inject the app id into the index.html
216
- html = html.replace("<head>", `<head>
217
- <script id="kokimoki-env" type="application/json">
288
+ // Parse HTML for DOM manipulation
289
+ const doc = (0, node_html_parser_1.parse)(html);
290
+ const head = doc.querySelector("head");
291
+ const htmlEl = doc.querySelector("html");
292
+ // Inject kokimoki-env script
293
+ const envScript = (0, node_html_parser_1.parse)(`<script id="kokimoki-env" type="application/json">
218
294
  {
219
295
  "dev": true,
220
296
  "test": true,
@@ -225,15 +301,43 @@ function kokimokiKitPlugin(config) {
225
301
  "i18nLanguages": ${JSON.stringify(i18nLanguages)},
226
302
  "base": "/",
227
303
  "assets": "/",
228
- "buildUrl": ${JSON.stringify(buildUrl ?? null)}
304
+ "buildUrl": ${JSON.stringify(buildUrl ?? null)},
305
+ "defaultAppMeta": ${JSON.stringify(config.defaultAppMeta ?? null)}
229
306
  }
230
307
  </script>`);
308
+ head?.insertAdjacentHTML("afterbegin", envScript.toString());
309
+ // Inject meta tags from defaultAppMeta in development
310
+ if (config.defaultAppMeta) {
311
+ const meta = config.defaultAppMeta;
312
+ if (meta.lang) {
313
+ htmlEl?.setAttribute("lang", meta.lang);
314
+ }
315
+ if (meta.title) {
316
+ head?.insertAdjacentHTML("beforeend", `<title>${meta.title}</title>`);
317
+ }
318
+ if (meta.description) {
319
+ head?.insertAdjacentHTML("beforeend", `<meta name="description" content="${meta.description}" />`);
320
+ }
321
+ if (meta.ogTitle ?? meta.title) {
322
+ head?.insertAdjacentHTML("beforeend", `<meta property="og:title" content="${meta.ogTitle ?? meta.title}" />`);
323
+ }
324
+ if (meta.ogDescription ?? meta.description) {
325
+ head?.insertAdjacentHTML("beforeend", `<meta property="og:description" content="${meta.ogDescription ?? meta.description}" />`);
326
+ }
327
+ if (meta.ogImage) {
328
+ head?.insertAdjacentHTML("beforeend", `<meta property="og:image" content="${meta.ogImage}" />`);
329
+ }
330
+ if (meta.favicon) {
331
+ head?.insertAdjacentHTML("beforeend", `<link rel="icon" type="image/png" href="${meta.favicon}" />`);
332
+ }
333
+ }
231
334
  // Inject default project style in development
232
335
  if (config.defaultProjectStylePath) {
233
336
  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>`);
337
+ const body = doc.querySelector("body");
338
+ body?.insertAdjacentHTML("beforeend", `<style id="km-dev-style">${(0, preprocess_style_1.preprocessStyle)(defaultProjectStyle)}</style>`);
235
339
  }
236
- return html;
340
+ return doc.toString();
237
341
  },
238
342
  // write kokimoki metadata to .kokimoki directory
239
343
  async generateBundle(_, _bundle) {
@@ -259,6 +363,7 @@ function kokimokiKitPlugin(config) {
259
363
  build: "dist",
260
364
  defaultProjectConfigPath: config.defaultProjectConfigPath,
261
365
  defaultProjectStylePath: config.defaultProjectStylePath,
366
+ defaultAppMeta: config.defaultAppMeta,
262
367
  i18n,
263
368
  }, null, 2));
264
369
  // write schema
@@ -297,19 +402,17 @@ function kokimokiKitPlugin(config) {
297
402
  properties: {},
298
403
  };
299
404
  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
- }
405
+ // write stores config with JSON schemas to build output (always includes built-in stores)
406
+ const storesWithJsonSchema = allStores.map((store) => ({
407
+ pattern: store.pattern,
408
+ local: store.local,
409
+ schema: v4_1.z.toJSONSchema(store.schema),
410
+ }));
411
+ this.emitFile({
412
+ type: "asset",
413
+ fileName: "km-stores.json",
414
+ source: JSON.stringify(storesWithJsonSchema, null, 2),
415
+ });
313
416
  // write i18n files to build output as individual files per lng/ns
314
417
  const i18nResources = await getI18nResources();
315
418
  if (Object.keys(i18nResources).length > 0) {
@@ -340,7 +443,7 @@ function kokimokiKitPlugin(config) {
340
443
  startInitialization();
341
444
  // Helper to check stores changed status
342
445
  async function checkStoresChanged() {
343
- const currentStoresHash = (0, dev_app_1.computeStoresHash)(config.stores);
446
+ const currentStoresHash = (0, dev_app_1.computeStoresHash)(allStores);
344
447
  const previousStoresHash = await (0, dev_app_1.readStoresHash)();
345
448
  return (previousStoresHash !== null &&
346
449
  previousStoresHash !== currentStoresHash);
@@ -387,7 +490,7 @@ function kokimokiKitPlugin(config) {
387
490
  // API endpoint to acknowledge stores hash change (dismiss the popup)
388
491
  server.middlewares.use("/__kokimoki/stores-hash/acknowledge", async (_req, res) => {
389
492
  try {
390
- const currentStoresHash = (0, dev_app_1.computeStoresHash)(config.stores);
493
+ const currentStoresHash = (0, dev_app_1.computeStoresHash)(allStores);
391
494
  await (0, dev_app_1.writeStoresHash)(currentStoresHash);
392
495
  res.statusCode = 200;
393
496
  res.setHeader("Content-Type", "application/json");
@@ -407,7 +510,7 @@ function kokimokiKitPlugin(config) {
407
510
  // Delete the app-id file so a new dev app will be created
408
511
  await (0, dev_app_1.deleteAppId)();
409
512
  // Update the stores hash
410
- const currentStoresHash = (0, dev_app_1.computeStoresHash)(config.stores);
513
+ const currentStoresHash = (0, dev_app_1.computeStoresHash)(allStores);
411
514
  await (0, dev_app_1.writeStoresHash)(currentStoresHash);
412
515
  // Reset initialization state so next request triggers re-initialization
413
516
  initState = "pending";
@@ -523,8 +626,8 @@ function kokimokiKitPlugin(config) {
523
626
  }
524
627
  // Render dev view HTML at root
525
628
  const devViewHtml = (0, dev_frame_1.renderDevFrame)({
526
- title: "Dev View",
527
629
  rows: config.devView,
630
+ appMeta: config.defaultAppMeta,
528
631
  });
529
632
  res.statusCode = 200;
530
633
  res.setHeader("Content-Type", "text/html");
@@ -534,3 +637,4 @@ function kokimokiKitPlugin(config) {
534
637
  },
535
638
  };
536
639
  }
640
+ 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.0";
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.0";
@@ -109,9 +109,57 @@ export default defineConfig({
109
109
  | `endpoint` | `string` | API endpoint (default: `https://api.kokimoki.com`) |
110
110
  | `host` | `string` | WebSocket host (default: `y-wss.kokimoki.com`) |
111
111
  | `devView` | `array \| false` | Dev frame layout or `false` to disable |
112
- | `defaultProjectConfigPath` | `string` | Path to default project config file |
112
+ | `defaultAppMeta` | `object` | Default meta tags for SEO and social sharing |
113
113
  | `defaultProjectStylePath` | `string` | Path to default project style file |
114
114
 
115
+ ## App Meta
116
+
117
+ 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`.
118
+
119
+ ### Configuration
120
+
121
+ ```typescript
122
+ kokimokiKitPlugin({
123
+ // ...
124
+ defaultAppMeta: {
125
+ lang: "en",
126
+ title: "My Game",
127
+ description: "An awesome multiplayer game",
128
+ ogTitle: "My Game",
129
+ ogDescription: "Join the fun!",
130
+ ogImage: "/og-image.png",
131
+ favicon: "/favicon.png",
132
+ },
133
+ });
134
+ ```
135
+
136
+ ### Fields
137
+
138
+ | Field | Description |
139
+ | --------------- | ---------------------------------------------------- |
140
+ | `lang` | HTML `lang` attribute (e.g., `"en"`, `"de"`) |
141
+ | `title` | Document title (browser tab) |
142
+ | `description` | Meta description for SEO |
143
+ | `ogTitle` | Open Graph title (defaults to `title` if not set) |
144
+ | `ogDescription` | Open Graph description (defaults to `description`) |
145
+ | `ogImage` | Open Graph image URL (use relative path like `/og-image.png`) |
146
+ | `favicon` | Favicon URL (use relative path like `/favicon.png`) |
147
+
148
+ ### Asset Path Resolution
149
+
150
+ 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.
151
+
152
+ ### Runtime Updates
153
+
154
+ The meta state can be updated at runtime via `kmClient.metaStore`:
155
+
156
+ ```typescript
157
+ await kmClient.transact([kmClient.metaStore], ([meta]) => {
158
+ meta.title = "Game Room: Epic Battle";
159
+ meta.ogImage = "https://example.com/custom-preview.png";
160
+ });
161
+ ```
162
+
115
163
  ## Dev Frame
116
164
 
117
165
  The dev frame provides a multi-window view for testing different player modes simultaneously during development.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kokimoki/kit",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
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
  },