@kokimoki/kit 1.6.7 → 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.
@@ -15,35 +15,143 @@ 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.kokimokiKitPlugin = kokimokiKitPlugin;
40
- const bson_objectid_1 = __importDefault(require("bson-objectid"));
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");
32
+ const path_1 = __importDefault(require("path"));
42
33
  const yaml = __importStar(require("yaml"));
43
34
  const v4_1 = require("zod/v4");
35
+ const api_1 = require("./api");
36
+ const credentials_1 = require("./credentials");
37
+ const dev_app_1 = require("./dev-app");
38
+ const dev_frame_1 = require("./dev-frame");
39
+ const dev_i18n_1 = require("./dev-i18n");
40
+ const dev_overlays_1 = require("./dev-overlays");
44
41
  const preprocess_style_1 = require("./preprocess-style");
42
+ const production_loading_screen_1 = require("./production-loading-screen");
43
+ const app_meta_schema_1 = require("./schemas/app-meta-schema");
45
44
  const version_1 = require("./version");
45
+ var dev_i18n_2 = require("./dev-i18n");
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
+ };
46
56
  function kokimokiKitPlugin(config) {
57
+ // Combine user stores with built-in stores
58
+ const allStores = [...(config.stores ?? []), BUILT_IN_APP_META_STORE];
59
+ // Cache for loaded i18n resources
60
+ let cachedI18n = null;
61
+ // Cache dev app info for syncing
62
+ let devAppInfo = null;
63
+ // Initialization state
64
+ let initState = "pending";
65
+ let initPromise = null;
66
+ // Cached dev app result
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
+ }
99
+ /**
100
+ * Start initialization if not already started.
101
+ * Returns a promise that resolves when initialization is complete.
102
+ */
103
+ function startInitialization() {
104
+ if (initPromise) {
105
+ return initPromise;
106
+ }
107
+ if (initState !== "pending") {
108
+ return Promise.resolve();
109
+ }
110
+ initState = "initializing";
111
+ initPromise = (async () => {
112
+ try {
113
+ // Get or create dev app
114
+ cachedDevAppResult = await (0, dev_app_1.getOrCreateDevApp)({
115
+ conceptId: config.conceptId,
116
+ endpoint: config.endpoint,
117
+ });
118
+ const { appId, buildUrl, organization, error: devAppError, } = cachedDevAppResult;
119
+ // Store dev app info for i18n syncing
120
+ if (organization && !devAppError) {
121
+ const credentials = await (0, credentials_1.readCredentials)();
122
+ const apiKey = credentials?.apiKeys?.[organization.id];
123
+ if (apiKey) {
124
+ devAppInfo = {
125
+ appId,
126
+ apiKey,
127
+ endpoint: config.endpoint ?? api_1.DEFAULT_ENDPOINT,
128
+ buildUrl,
129
+ };
130
+ // Sync i18n files to dev app on startup
131
+ const i18nResources = await getI18nResources();
132
+ await (0, dev_i18n_1.syncAllI18nToDevApp)(devAppInfo, i18nResources, config.i18nPrimaryLng ?? "en");
133
+ }
134
+ }
135
+ initState = "ready";
136
+ }
137
+ catch (e) {
138
+ console.error("[kokimoki-kit] Initialization error:", e);
139
+ initState = "error";
140
+ }
141
+ })();
142
+ return initPromise;
143
+ }
144
+ async function getI18nResources() {
145
+ if (cachedI18n)
146
+ return cachedI18n;
147
+ if (config.i18nPath) {
148
+ cachedI18n = await (0, dev_i18n_1.loadI18nFromPath)(config.i18nPath);
149
+ }
150
+ else {
151
+ cachedI18n = {};
152
+ }
153
+ return cachedI18n;
154
+ }
47
155
  return {
48
156
  name: "kokimoki-kit",
49
157
  async transform(code, id) {
@@ -54,12 +162,23 @@ function kokimokiKitPlugin(config) {
54
162
  return code;
55
163
  },
56
164
  async transformIndexHtml(html) {
165
+ // Load i18n resources (from path or config)
166
+ const i18nResources = await getI18nResources();
167
+ // Extract namespace names and language codes from i18n config
168
+ const i18nNamespaces = Object.keys(Object.values(i18nResources)[0] ?? {});
169
+ const i18nLanguages = Object.keys(i18nResources);
57
170
  if (process.env.NODE_ENV !== "development") {
58
- // NOTE: Try https://github.com/jsdom/jsdom instead of regex parsing
59
- return html
60
- .replace("<head>", `<head>
61
- <base href="%KM_BASE%">
62
- <script id="kokimoki-env" type="application/json">
171
+ // Remove any existing km-loading element from the HTML
172
+ const processedHtml = (0, production_loading_screen_1.removeExistingLoadingScreen)(html);
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">
63
182
  {
64
183
  "dev": %KM_DEV%,
65
184
  "test": %KM_TEST%,
@@ -68,11 +187,16 @@ function kokimokiKitPlugin(config) {
68
187
  "code": "%KM_CODE%",
69
188
  "clientContext": "%KM_CLIENT_CONTEXT%",
70
189
  "config": "%KM_CONFIG%",
190
+ "i18nPath": "km-i18n",
191
+ "i18nNamespaces": ${JSON.stringify(i18nNamespaces)},
192
+ "i18nLanguages": ${JSON.stringify(i18nLanguages)},
71
193
  "base": "%KM_BASE%",
72
- "assets": "%KM_ASSETS%"
194
+ "assets": "%KM_ASSETS%",
195
+ "defaultAppMeta": ${JSON.stringify(resolveAppMetaAssets(config.defaultAppMeta, true))}
73
196
  }
74
- </script>
75
- <script>
197
+ </script>`);
198
+ // Inject assets URL helper script
199
+ head?.insertAdjacentHTML("beforeend", `<script>
76
200
  window.__toAssetsUrl = (path) => {
77
201
  if (path.startsWith("km-proxy")) {
78
202
  return "/%KM_BUILD_ID%/" + path;
@@ -80,66 +204,140 @@ function kokimokiKitPlugin(config) {
80
204
 
81
205
  return "%KM_ASSETS%/" + path;
82
206
  };
83
- </script>
84
- `)
85
- .replace(/<link.*?href="(.*?)".*?>/g, (match, p1) => {
86
- return match.replace(p1, `%KM_ASSETS%${p1.startsWith("/") ? "" : "/"}${p1}`);
87
- })
88
- .replace(/<script.*?src="(.*?)".*?>/g, (match, p1) => {
89
- 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
+ }
90
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
+ }
249
+ });
250
+ return doc.toString();
91
251
  }
92
- // Ensure .kokimoki directory exists
93
- try {
94
- await promises_1.default.mkdir(".kokimoki");
95
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
- }
97
- catch (e) {
98
- if (e?.code !== "EEXIST") {
99
- throw e;
100
- }
252
+ // Development mode: ensure initialization is started and wait for it
253
+ if (initState === "pending" || initState === "initializing") {
254
+ // Start initialization if not already started
255
+ startInitialization();
256
+ // Return loading page - it will poll and reload when ready
257
+ return (0, dev_overlays_1.renderLoadingPage)();
101
258
  }
102
- // Try to read the app id from the .kokimoki/app-id file
103
- let appId;
104
- try {
105
- appId = await promises_1.default.readFile(".kokimoki/app-id", "utf8");
106
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
259
+ // Get cached dev app result (should be available after initialization)
260
+ const { appId, buildUrl, organization, error: devAppError, } = cachedDevAppResult ?? {
261
+ appId: "",
262
+ buildUrl: undefined,
263
+ organization: undefined,
264
+ error: { code: "INIT_ERROR", message: "Initialization failed" },
265
+ };
266
+ // Check if stores configuration has changed
267
+ const currentStoresHash = (0, dev_app_1.computeStoresHash)(allStores);
268
+ const previousStoresHash = await (0, dev_app_1.readStoresHash)();
269
+ const storesChanged = previousStoresHash !== null && previousStoresHash !== currentStoresHash;
270
+ // Write initial stores hash if it doesn't exist yet
271
+ if (previousStoresHash === null) {
272
+ await (0, dev_app_1.writeStoresHash)(currentStoresHash);
107
273
  }
108
- catch (e) {
109
- if (e?.code !== "ENOENT") {
110
- throw e;
111
- }
274
+ // Return full stores changed page if stores configuration has changed
275
+ if (storesChanged) {
276
+ return (0, dev_overlays_1.renderStoresChangedPage)(!!organization);
112
277
  }
113
- // If the app id doesn't exist, generate a new one
114
- if (!appId) {
115
- appId = new bson_objectid_1.default().toHexString();
116
- await promises_1.default.writeFile(".kokimoki/app-id", appId);
278
+ // Return full error page if there's a dev app error
279
+ if (devAppError) {
280
+ return (0, dev_overlays_1.renderErrorPage)(devAppError);
117
281
  }
118
282
  // Get default config
119
- // let defaultProjectConfig = config.schema.parse(undefined);
120
- // if (config.defaultProjectConfigPath) {
121
- const defaultProjectConfigFile = await promises_1.default.readFile(config.defaultProjectConfigPath, "utf8");
122
- const defaultProjectConfig = config.schema.parse(yaml.parse(defaultProjectConfigFile));
123
- // }
124
- // Inject the app id into the index.html
125
- html = html.replace("<head>", `<head>
126
- <script id="kokimoki-env" type="application/json">
283
+ let defaultProjectConfig = {};
284
+ if (config.defaultProjectConfigPath && config.schema) {
285
+ const defaultProjectConfigFile = await promises_1.default.readFile(config.defaultProjectConfigPath, "utf8");
286
+ defaultProjectConfig = config.schema.parse(yaml.parse(defaultProjectConfigFile));
287
+ }
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">
127
294
  {
128
295
  "dev": true,
129
296
  "test": true,
130
- "host": "y-wss.kokimoki.com",
297
+ "host": "${config.host ?? "y-wss.kokimoki.com"}",
131
298
  "appId": "${appId}",
132
299
  "configObject": ${JSON.stringify(defaultProjectConfig)},
300
+ "i18nNamespaces": ${JSON.stringify(i18nNamespaces)},
301
+ "i18nLanguages": ${JSON.stringify(i18nLanguages)},
133
302
  "base": "/",
134
- "assets": "/"
303
+ "assets": "/",
304
+ "buildUrl": ${JSON.stringify(buildUrl ?? null)},
305
+ "defaultAppMeta": ${JSON.stringify(config.defaultAppMeta ?? null)}
135
306
  }
136
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
+ }
137
334
  // Inject default project style in development
138
335
  if (config.defaultProjectStylePath) {
139
336
  const defaultProjectStyle = await promises_1.default.readFile(config.defaultProjectStylePath, "utf8");
140
- 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>`);
141
339
  }
142
- return html;
340
+ return doc.toString();
143
341
  },
144
342
  // write kokimoki metadata to .kokimoki directory
145
343
  async generateBundle(_, _bundle) {
@@ -153,6 +351,10 @@ function kokimokiKitPlugin(config) {
153
351
  throw e;
154
352
  }
155
353
  }
354
+ // Get i18n metadata
355
+ const i18n = config.i18nPath
356
+ ? await (0, dev_i18n_1.getI18nMeta)(config.i18nPath, config.i18nPrimaryLng ?? "en")
357
+ : null;
156
358
  // write config
157
359
  await promises_1.default.writeFile(".kokimoki/config.json", JSON.stringify({
158
360
  kokimokiKitVersion: version_1.KOKIMOKI_KIT_VERSION,
@@ -161,49 +363,173 @@ function kokimokiKitPlugin(config) {
161
363
  build: "dist",
162
364
  defaultProjectConfigPath: config.defaultProjectConfigPath,
163
365
  defaultProjectStylePath: config.defaultProjectStylePath,
366
+ defaultAppMeta: config.defaultAppMeta,
367
+ i18n,
164
368
  }, null, 2));
165
369
  // write schema
166
- const jsonSchema = v4_1.z.toJSONSchema(config.schema, {
167
- override(ctx) {
168
- // check if schema is discriminated union
169
- if (ctx.zodSchema._zod.def.type === "union" &&
170
- "discriminator" in ctx.zodSchema._zod.def) {
171
- const discriminator = ctx.zodSchema._zod.def
172
- .discriminator;
173
- ctx.jsonSchema.type = "object";
174
- ctx.jsonSchema.discriminator = { propertyName: discriminator };
175
- ctx.jsonSchema.required = [discriminator];
176
- ctx.jsonSchema.oneOf = ctx.jsonSchema.anyOf.map((objectSchema) => ({
177
- properties: objectSchema.properties,
178
- additionalProperties: objectSchema.additionalProperties,
179
- required: (objectSchema.required ?? []).filter((prop) => prop !== discriminator),
180
- }));
181
- delete ctx.jsonSchema.anyOf;
182
- }
183
- // Remove fields that have a default from the required list
184
- if (ctx.jsonSchema.properties && ctx.jsonSchema.required) {
185
- const properties = ctx.jsonSchema.properties;
186
- ctx.jsonSchema.required = ctx.jsonSchema.required.filter((field) => !("default" in properties[field]));
187
- }
188
- // Make sure property has description if set in zod schema
189
- if ("description" in ctx.zodSchema) {
190
- ctx.jsonSchema.description = ctx.zodSchema.description;
191
- }
192
- },
193
- });
370
+ const jsonSchema = config.schema
371
+ ? v4_1.z.toJSONSchema(config.schema, {
372
+ override(ctx) {
373
+ // check if schema is discriminated union
374
+ if (ctx.zodSchema._zod.def.type === "union" &&
375
+ "discriminator" in ctx.zodSchema._zod.def) {
376
+ const discriminator = ctx.zodSchema._zod.def
377
+ .discriminator;
378
+ ctx.jsonSchema.type = "object";
379
+ ctx.jsonSchema.discriminator = { propertyName: discriminator };
380
+ ctx.jsonSchema.required = [discriminator];
381
+ ctx.jsonSchema.oneOf = ctx.jsonSchema.anyOf.map((objectSchema) => ({
382
+ properties: objectSchema.properties,
383
+ additionalProperties: objectSchema.additionalProperties,
384
+ required: (objectSchema.required ?? []).filter((prop) => prop !== discriminator),
385
+ }));
386
+ delete ctx.jsonSchema.anyOf;
387
+ }
388
+ // Remove fields that have a default from the required list
389
+ if (ctx.jsonSchema.properties && ctx.jsonSchema.required) {
390
+ const properties = ctx.jsonSchema.properties;
391
+ ctx.jsonSchema.required = ctx.jsonSchema.required.filter((field) => !("default" in properties[field]));
392
+ }
393
+ // Make sure property has description if set in zod schema
394
+ if ("description" in ctx.zodSchema) {
395
+ ctx.jsonSchema.description = ctx.zodSchema
396
+ .description;
397
+ }
398
+ },
399
+ })
400
+ : {
401
+ type: "object",
402
+ properties: {},
403
+ };
194
404
  await promises_1.default.writeFile(".kokimoki/schema.json", JSON.stringify(jsonSchema, null, 2));
195
- // // write schema defaults as json
196
- // await fs.writeFile(
197
- // ".kokimoki/schema-defaults.json",
198
- // JSON.stringify(defaultJsonSchemaValue, null, 2)
199
- // );
200
- // // write schema defaults as yaml
201
- // await fs.writeFile(
202
- // ".kokimoki/schema-defaults.yaml",
203
- // yaml.stringify(defaultJsonSchemaValue)
204
- // );
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
+ });
416
+ // write i18n files to build output as individual files per lng/ns
417
+ const i18nResources = await getI18nResources();
418
+ if (Object.keys(i18nResources).length > 0) {
419
+ const i18nManifest = {};
420
+ for (const [lng, namespaces] of Object.entries(i18nResources)) {
421
+ i18nManifest[lng] = {};
422
+ for (const [ns, translations] of Object.entries(namespaces)) {
423
+ const fileName = `km-i18n/${lng}/${ns}.json`;
424
+ this.emitFile({
425
+ type: "asset",
426
+ fileName,
427
+ source: JSON.stringify(translations, null, 2),
428
+ });
429
+ // Store relative path - server can prepend assets base or modify URLs
430
+ i18nManifest[lng][ns] = fileName;
431
+ }
432
+ }
433
+ // Emit manifest mapping lng/ns to URLs
434
+ this.emitFile({
435
+ type: "asset",
436
+ fileName: "km-i18n.json",
437
+ source: JSON.stringify(i18nManifest, null, 2),
438
+ });
439
+ }
205
440
  },
206
441
  configureServer(server) {
442
+ // Start initialization as early as possible (don't wait for first request)
443
+ startInitialization();
444
+ // Helper to check stores changed status
445
+ async function checkStoresChanged() {
446
+ const currentStoresHash = (0, dev_app_1.computeStoresHash)(allStores);
447
+ const previousStoresHash = await (0, dev_app_1.readStoresHash)();
448
+ return (previousStoresHash !== null &&
449
+ previousStoresHash !== currentStoresHash);
450
+ }
451
+ // Serve i18n JSON files in development
452
+ server.middlewares.use("/__kokimoki/i18n", async (req, res, next) => {
453
+ // Parse URL: /__kokimoki/i18n/{lng}/{ns}.json
454
+ const match = req.url?.match(/^\/([^/]+)\/([^/]+)\.json$/);
455
+ if (!match) {
456
+ return next();
457
+ }
458
+ const [, lng, ns] = match;
459
+ try {
460
+ const i18nResources = await getI18nResources();
461
+ const translations = i18nResources[lng]?.[ns];
462
+ if (!translations) {
463
+ res.statusCode = 404;
464
+ res.setHeader("Content-Type", "application/json");
465
+ res.end(JSON.stringify({ error: `Translation not found: ${lng}/${ns}` }));
466
+ return;
467
+ }
468
+ res.statusCode = 200;
469
+ res.setHeader("Content-Type", "application/json");
470
+ res.setHeader("Cache-Control", "no-cache");
471
+ res.end(JSON.stringify(translations));
472
+ }
473
+ catch (e) {
474
+ res.statusCode = 500;
475
+ res.setHeader("Content-Type", "application/json");
476
+ res.end(JSON.stringify({
477
+ error: e instanceof Error ? e.message : "Unknown error",
478
+ }));
479
+ }
480
+ });
481
+ // API endpoint to check if initialization is complete
482
+ server.middlewares.use("/__kokimoki/ready", async (_req, res) => {
483
+ res.statusCode = 200;
484
+ res.setHeader("Content-Type", "application/json");
485
+ res.setHeader("Cache-Control", "no-cache");
486
+ res.end(JSON.stringify({
487
+ ready: initState === "ready" || initState === "error",
488
+ }));
489
+ });
490
+ // API endpoint to acknowledge stores hash change (dismiss the popup)
491
+ server.middlewares.use("/__kokimoki/stores-hash/acknowledge", async (_req, res) => {
492
+ try {
493
+ const currentStoresHash = (0, dev_app_1.computeStoresHash)(allStores);
494
+ await (0, dev_app_1.writeStoresHash)(currentStoresHash);
495
+ res.statusCode = 200;
496
+ res.setHeader("Content-Type", "application/json");
497
+ res.end(JSON.stringify({ success: true }));
498
+ }
499
+ catch (e) {
500
+ res.statusCode = 500;
501
+ res.setHeader("Content-Type", "application/json");
502
+ res.end(JSON.stringify({
503
+ error: e instanceof Error ? e.message : "Unknown error",
504
+ }));
505
+ }
506
+ });
507
+ // API endpoint to reset dev app state (deletes app-id to create fresh dev app)
508
+ server.middlewares.use("/__kokimoki/dev-app/reset", async (_req, res) => {
509
+ try {
510
+ // Delete the app-id file so a new dev app will be created
511
+ await (0, dev_app_1.deleteAppId)();
512
+ // Update the stores hash
513
+ const currentStoresHash = (0, dev_app_1.computeStoresHash)(allStores);
514
+ await (0, dev_app_1.writeStoresHash)(currentStoresHash);
515
+ // Reset initialization state so next request triggers re-initialization
516
+ initState = "pending";
517
+ initPromise = null;
518
+ cachedDevAppResult = null;
519
+ // Start re-initialization (polling from loading page will detect when ready)
520
+ startInitialization();
521
+ res.statusCode = 200;
522
+ res.setHeader("Content-Type", "application/json");
523
+ res.end(JSON.stringify({ success: true }));
524
+ }
525
+ catch (e) {
526
+ res.statusCode = 500;
527
+ res.setHeader("Content-Type", "application/json");
528
+ res.end(JSON.stringify({
529
+ error: e instanceof Error ? e.message : "Unknown error",
530
+ }));
531
+ }
532
+ });
207
533
  // reload when defaultProjectConfigPath or defaultProjectStylePath changes
208
534
  if (config.defaultProjectConfigPath) {
209
535
  server.watcher.add(config.defaultProjectConfigPath);
@@ -211,7 +537,25 @@ function kokimokiKitPlugin(config) {
211
537
  if (config.defaultProjectStylePath) {
212
538
  server.watcher.add(config.defaultProjectStylePath);
213
539
  }
214
- server.watcher.on("change", (file) => {
540
+ // Watch i18n folder for changes
541
+ if (config.i18nPath) {
542
+ const i18nAbsolutePath = path_1.default.resolve(process.cwd(), config.i18nPath);
543
+ server.watcher.add(i18nAbsolutePath);
544
+ }
545
+ server.watcher.on("change", async (file) => {
546
+ // Invalidate i18n cache on i18n file changes and sync to dev app
547
+ if (config.i18nPath) {
548
+ const i18nAbsolutePath = path_1.default.resolve(process.cwd(), config.i18nPath);
549
+ if (file.startsWith(i18nAbsolutePath) && file.endsWith(".json")) {
550
+ cachedI18n = null;
551
+ // Sync only the changed file to dev app (only primary language)
552
+ if (devAppInfo) {
553
+ await (0, dev_i18n_1.syncI18nFile)(devAppInfo, config.i18nPath, file, config.i18nPrimaryLng ?? "en");
554
+ }
555
+ server.ws.send({ type: "full-reload" });
556
+ return;
557
+ }
558
+ }
215
559
  if (config.defaultProjectConfigPath?.match(file) ||
216
560
  config.defaultProjectStylePath?.match(file)) {
217
561
  server.ws.send({
@@ -219,6 +563,78 @@ function kokimokiKitPlugin(config) {
219
563
  });
220
564
  }
221
565
  });
566
+ // Intercept all root HTML requests to show loading/error/stores-changed/dev-view at root level
567
+ // This runs BEFORE Vite's built-in middleware so we can intercept index.html requests
568
+ server.middlewares.use(async (req, res, next) => {
569
+ const url = new URL(req.url || "/", "http://localhost");
570
+ // Only intercept root path requests (with or without key param)
571
+ if (url.pathname !== "/") {
572
+ return next();
573
+ }
574
+ const hasKeyParam = url.searchParams.has("key");
575
+ // Show loading page while initializing (at root level only)
576
+ if (initState === "pending" || initState === "initializing") {
577
+ if (!hasKeyParam) {
578
+ res.statusCode = 200;
579
+ res.setHeader("Content-Type", "text/html");
580
+ res.setHeader("Cache-Control", "no-cache");
581
+ res.end((0, dev_overlays_1.renderLoadingPage)());
582
+ return;
583
+ }
584
+ // For iframe requests during init, let them wait via transformIndexHtml
585
+ return next();
586
+ }
587
+ const { organization, error: devAppError } = cachedDevAppResult ?? {
588
+ organization: undefined,
589
+ error: { code: "INIT_ERROR", message: "Initialization failed" },
590
+ };
591
+ // Show error page at root level only
592
+ if (devAppError && !hasKeyParam) {
593
+ res.statusCode = 200;
594
+ res.setHeader("Content-Type", "text/html");
595
+ res.setHeader("Cache-Control", "no-cache");
596
+ res.end((0, dev_overlays_1.renderErrorPage)(devAppError));
597
+ return;
598
+ }
599
+ // Check if stores configuration has changed
600
+ const storesChanged = await checkStoresChanged();
601
+ if (storesChanged) {
602
+ if (!hasKeyParam) {
603
+ // Root page: show stores changed page
604
+ res.statusCode = 200;
605
+ res.setHeader("Content-Type", "text/html");
606
+ res.setHeader("Cache-Control", "no-cache");
607
+ res.end((0, dev_overlays_1.renderStoresChangedPage)(!!organization));
608
+ }
609
+ else {
610
+ // Iframe: reload parent to show stores changed page at root level
611
+ res.statusCode = 200;
612
+ res.setHeader("Content-Type", "text/html");
613
+ res.setHeader("Cache-Control", "no-cache");
614
+ res.end(`<!DOCTYPE html>
615
+ <html><head><script>window.parent.location.reload();</script></head><body></body></html>`);
616
+ }
617
+ return;
618
+ }
619
+ // For requests with key param, continue to normal handling (Vite will serve index.html)
620
+ if (hasKeyParam) {
621
+ return next();
622
+ }
623
+ // Dev view disabled or not configured, continue to normal handling
624
+ if (config.devView === false || config.devView === undefined) {
625
+ return next();
626
+ }
627
+ // Render dev view HTML at root
628
+ const devViewHtml = (0, dev_frame_1.renderDevFrame)({
629
+ rows: config.devView,
630
+ appMeta: config.defaultAppMeta,
631
+ });
632
+ res.statusCode = 200;
633
+ res.setHeader("Content-Type", "text/html");
634
+ res.setHeader("Cache-Control", "no-cache");
635
+ res.end(devViewHtml);
636
+ });
222
637
  },
223
638
  };
224
639
  }
640
+ exports.kokimokiKitPlugin = kokimokiKitPlugin;