@kokimoki/kit 1.6.7 → 1.7.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.
@@ -36,14 +36,89 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.getI18nMeta = void 0;
39
40
  exports.kokimokiKitPlugin = kokimokiKitPlugin;
40
- const bson_objectid_1 = __importDefault(require("bson-objectid"));
41
41
  const promises_1 = __importDefault(require("fs/promises"));
42
+ const path_1 = __importDefault(require("path"));
42
43
  const yaml = __importStar(require("yaml"));
43
44
  const v4_1 = require("zod/v4");
45
+ const api_1 = require("./api");
46
+ const credentials_1 = require("./credentials");
47
+ const dev_app_1 = require("./dev-app");
48
+ const dev_frame_1 = require("./dev-frame");
49
+ const dev_i18n_1 = require("./dev-i18n");
50
+ const dev_overlays_1 = require("./dev-overlays");
44
51
  const preprocess_style_1 = require("./preprocess-style");
52
+ const production_loading_screen_1 = require("./production-loading-screen");
45
53
  const version_1 = require("./version");
54
+ var dev_i18n_2 = require("./dev-i18n");
55
+ Object.defineProperty(exports, "getI18nMeta", { enumerable: true, get: function () { return dev_i18n_2.getI18nMeta; } });
46
56
  function kokimokiKitPlugin(config) {
57
+ // Cache for loaded i18n resources
58
+ let cachedI18n = null;
59
+ // Cache dev app info for syncing
60
+ let devAppInfo = null;
61
+ // Initialization state
62
+ let initState = "pending";
63
+ let initPromise = null;
64
+ // Cached dev app result
65
+ let cachedDevAppResult = null;
66
+ /**
67
+ * Start initialization if not already started.
68
+ * Returns a promise that resolves when initialization is complete.
69
+ */
70
+ function startInitialization() {
71
+ if (initPromise) {
72
+ return initPromise;
73
+ }
74
+ if (initState !== "pending") {
75
+ return Promise.resolve();
76
+ }
77
+ initState = "initializing";
78
+ initPromise = (async () => {
79
+ try {
80
+ // Get or create dev app
81
+ cachedDevAppResult = await (0, dev_app_1.getOrCreateDevApp)({
82
+ conceptId: config.conceptId,
83
+ endpoint: config.endpoint,
84
+ });
85
+ const { appId, buildUrl, organization, error: devAppError, } = cachedDevAppResult;
86
+ // Store dev app info for i18n syncing
87
+ if (organization && !devAppError) {
88
+ const credentials = await (0, credentials_1.readCredentials)();
89
+ const apiKey = credentials?.apiKeys?.[organization.id];
90
+ if (apiKey) {
91
+ devAppInfo = {
92
+ appId,
93
+ apiKey,
94
+ endpoint: config.endpoint ?? api_1.DEFAULT_ENDPOINT,
95
+ buildUrl,
96
+ };
97
+ // Sync i18n files to dev app on startup
98
+ const i18nResources = await getI18nResources();
99
+ await (0, dev_i18n_1.syncAllI18nToDevApp)(devAppInfo, i18nResources, config.i18nPrimaryLng ?? "en");
100
+ }
101
+ }
102
+ initState = "ready";
103
+ }
104
+ catch (e) {
105
+ console.error("[kokimoki-kit] Initialization error:", e);
106
+ initState = "error";
107
+ }
108
+ })();
109
+ return initPromise;
110
+ }
111
+ async function getI18nResources() {
112
+ if (cachedI18n)
113
+ return cachedI18n;
114
+ if (config.i18nPath) {
115
+ cachedI18n = await (0, dev_i18n_1.loadI18nFromPath)(config.i18nPath);
116
+ }
117
+ else {
118
+ cachedI18n = {};
119
+ }
120
+ return cachedI18n;
121
+ }
47
122
  return {
48
123
  name: "kokimoki-kit",
49
124
  async transform(code, id) {
@@ -54,9 +129,16 @@ function kokimokiKitPlugin(config) {
54
129
  return code;
55
130
  },
56
131
  async transformIndexHtml(html) {
132
+ // Load i18n resources (from path or config)
133
+ const i18nResources = await getI18nResources();
134
+ // Extract namespace names and language codes from i18n config
135
+ const i18nNamespaces = Object.keys(Object.values(i18nResources)[0] ?? {});
136
+ const i18nLanguages = Object.keys(i18nResources);
57
137
  if (process.env.NODE_ENV !== "development") {
138
+ // Remove any existing km-loading element from the HTML
139
+ const processedHtml = (0, production_loading_screen_1.removeExistingLoadingScreen)(html);
58
140
  // NOTE: Try https://github.com/jsdom/jsdom instead of regex parsing
59
- return html
141
+ const result = processedHtml
60
142
  .replace("<head>", `<head>
61
143
  <base href="%KM_BASE%">
62
144
  <script id="kokimoki-env" type="application/json">
@@ -68,6 +150,9 @@ function kokimokiKitPlugin(config) {
68
150
  "code": "%KM_CODE%",
69
151
  "clientContext": "%KM_CLIENT_CONTEXT%",
70
152
  "config": "%KM_CONFIG%",
153
+ "i18nPath": "km-i18n",
154
+ "i18nNamespaces": ${JSON.stringify(i18nNamespaces)},
155
+ "i18nLanguages": ${JSON.stringify(i18nLanguages)},
71
156
  "base": "%KM_BASE%",
72
157
  "assets": "%KM_ASSETS%"
73
158
  }
@@ -80,58 +165,67 @@ function kokimokiKitPlugin(config) {
80
165
 
81
166
  return "%KM_ASSETS%/" + path;
82
167
  };
83
- </script>
168
+ </script>${production_loading_screen_1.loadingScreenStyles}${production_loading_screen_1.loadingScreenScript}
84
169
  `)
170
+ .replace(/<body([^>]*)>/, `<body$1>${production_loading_screen_1.loadingScreenElement}`)
85
171
  .replace(/<link.*?href="(.*?)".*?>/g, (match, p1) => {
86
172
  return match.replace(p1, `%KM_ASSETS%${p1.startsWith("/") ? "" : "/"}${p1}`);
87
173
  })
88
174
  .replace(/<script.*?src="(.*?)".*?>/g, (match, p1) => {
89
175
  return match.replace(p1, `%KM_ASSETS%${p1.startsWith("/") ? "" : "/"}${p1}`);
90
176
  });
177
+ return result;
91
178
  }
92
- // Ensure .kokimoki directory exists
93
- try {
94
- await promises_1.default.mkdir(".kokimoki");
95
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
179
+ // Development mode: ensure initialization is started and wait for it
180
+ if (initState === "pending" || initState === "initializing") {
181
+ // Start initialization if not already started
182
+ startInitialization();
183
+ // Return loading page - it will poll and reload when ready
184
+ return (0, dev_overlays_1.renderLoadingPage)();
96
185
  }
97
- catch (e) {
98
- if (e?.code !== "EEXIST") {
99
- throw e;
100
- }
101
- }
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
186
+ // Get cached dev app result (should be available after initialization)
187
+ const { appId, buildUrl, organization, error: devAppError, } = cachedDevAppResult ?? {
188
+ appId: "",
189
+ buildUrl: undefined,
190
+ organization: undefined,
191
+ error: { code: "INIT_ERROR", message: "Initialization failed" },
192
+ };
193
+ // Check if stores configuration has changed
194
+ const currentStoresHash = (0, dev_app_1.computeStoresHash)(config.stores);
195
+ const previousStoresHash = await (0, dev_app_1.readStoresHash)();
196
+ const storesChanged = previousStoresHash !== null && previousStoresHash !== currentStoresHash;
197
+ // Write initial stores hash if it doesn't exist yet
198
+ if (previousStoresHash === null) {
199
+ await (0, dev_app_1.writeStoresHash)(currentStoresHash);
107
200
  }
108
- catch (e) {
109
- if (e?.code !== "ENOENT") {
110
- throw e;
111
- }
201
+ // Return full stores changed page if stores configuration has changed
202
+ if (storesChanged) {
203
+ return (0, dev_overlays_1.renderStoresChangedPage)(!!organization);
112
204
  }
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);
205
+ // Return full error page if there's a dev app error
206
+ if (devAppError) {
207
+ return (0, dev_overlays_1.renderErrorPage)(devAppError);
117
208
  }
118
209
  // 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
- // }
210
+ let defaultProjectConfig = {};
211
+ if (config.defaultProjectConfigPath && config.schema) {
212
+ const defaultProjectConfigFile = await promises_1.default.readFile(config.defaultProjectConfigPath, "utf8");
213
+ defaultProjectConfig = config.schema.parse(yaml.parse(defaultProjectConfigFile));
214
+ }
124
215
  // Inject the app id into the index.html
125
216
  html = html.replace("<head>", `<head>
126
217
  <script id="kokimoki-env" type="application/json">
127
218
  {
128
219
  "dev": true,
129
220
  "test": true,
130
- "host": "y-wss.kokimoki.com",
221
+ "host": "${config.host ?? "y-wss.kokimoki.com"}",
131
222
  "appId": "${appId}",
132
223
  "configObject": ${JSON.stringify(defaultProjectConfig)},
224
+ "i18nNamespaces": ${JSON.stringify(i18nNamespaces)},
225
+ "i18nLanguages": ${JSON.stringify(i18nLanguages)},
133
226
  "base": "/",
134
- "assets": "/"
227
+ "assets": "/",
228
+ "buildUrl": ${JSON.stringify(buildUrl ?? null)}
135
229
  }
136
230
  </script>`);
137
231
  // Inject default project style in development
@@ -153,6 +247,10 @@ function kokimokiKitPlugin(config) {
153
247
  throw e;
154
248
  }
155
249
  }
250
+ // Get i18n metadata
251
+ const i18n = config.i18nPath
252
+ ? await (0, dev_i18n_1.getI18nMeta)(config.i18nPath, config.i18nPrimaryLng ?? "en")
253
+ : null;
156
254
  // write config
157
255
  await promises_1.default.writeFile(".kokimoki/config.json", JSON.stringify({
158
256
  kokimokiKitVersion: version_1.KOKIMOKI_KIT_VERSION,
@@ -161,49 +259,174 @@ function kokimokiKitPlugin(config) {
161
259
  build: "dist",
162
260
  defaultProjectConfigPath: config.defaultProjectConfigPath,
163
261
  defaultProjectStylePath: config.defaultProjectStylePath,
262
+ i18n,
164
263
  }, null, 2));
165
264
  // 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
- });
265
+ const jsonSchema = config.schema
266
+ ? v4_1.z.toJSONSchema(config.schema, {
267
+ override(ctx) {
268
+ // check if schema is discriminated union
269
+ if (ctx.zodSchema._zod.def.type === "union" &&
270
+ "discriminator" in ctx.zodSchema._zod.def) {
271
+ const discriminator = ctx.zodSchema._zod.def
272
+ .discriminator;
273
+ ctx.jsonSchema.type = "object";
274
+ ctx.jsonSchema.discriminator = { propertyName: discriminator };
275
+ ctx.jsonSchema.required = [discriminator];
276
+ ctx.jsonSchema.oneOf = ctx.jsonSchema.anyOf.map((objectSchema) => ({
277
+ properties: objectSchema.properties,
278
+ additionalProperties: objectSchema.additionalProperties,
279
+ required: (objectSchema.required ?? []).filter((prop) => prop !== discriminator),
280
+ }));
281
+ delete ctx.jsonSchema.anyOf;
282
+ }
283
+ // Remove fields that have a default from the required list
284
+ if (ctx.jsonSchema.properties && ctx.jsonSchema.required) {
285
+ const properties = ctx.jsonSchema.properties;
286
+ ctx.jsonSchema.required = ctx.jsonSchema.required.filter((field) => !("default" in properties[field]));
287
+ }
288
+ // Make sure property has description if set in zod schema
289
+ if ("description" in ctx.zodSchema) {
290
+ ctx.jsonSchema.description = ctx.zodSchema
291
+ .description;
292
+ }
293
+ },
294
+ })
295
+ : {
296
+ type: "object",
297
+ properties: {},
298
+ };
194
299
  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
- // );
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
+ }
313
+ // write i18n files to build output as individual files per lng/ns
314
+ const i18nResources = await getI18nResources();
315
+ if (Object.keys(i18nResources).length > 0) {
316
+ const i18nManifest = {};
317
+ for (const [lng, namespaces] of Object.entries(i18nResources)) {
318
+ i18nManifest[lng] = {};
319
+ for (const [ns, translations] of Object.entries(namespaces)) {
320
+ const fileName = `km-i18n/${lng}/${ns}.json`;
321
+ this.emitFile({
322
+ type: "asset",
323
+ fileName,
324
+ source: JSON.stringify(translations, null, 2),
325
+ });
326
+ // Store relative path - server can prepend assets base or modify URLs
327
+ i18nManifest[lng][ns] = fileName;
328
+ }
329
+ }
330
+ // Emit manifest mapping lng/ns to URLs
331
+ this.emitFile({
332
+ type: "asset",
333
+ fileName: "km-i18n.json",
334
+ source: JSON.stringify(i18nManifest, null, 2),
335
+ });
336
+ }
205
337
  },
206
338
  configureServer(server) {
339
+ // Start initialization as early as possible (don't wait for first request)
340
+ startInitialization();
341
+ // Helper to check stores changed status
342
+ async function checkStoresChanged() {
343
+ const currentStoresHash = (0, dev_app_1.computeStoresHash)(config.stores);
344
+ const previousStoresHash = await (0, dev_app_1.readStoresHash)();
345
+ return (previousStoresHash !== null &&
346
+ previousStoresHash !== currentStoresHash);
347
+ }
348
+ // Serve i18n JSON files in development
349
+ server.middlewares.use("/__kokimoki/i18n", async (req, res, next) => {
350
+ // Parse URL: /__kokimoki/i18n/{lng}/{ns}.json
351
+ const match = req.url?.match(/^\/([^/]+)\/([^/]+)\.json$/);
352
+ if (!match) {
353
+ return next();
354
+ }
355
+ const [, lng, ns] = match;
356
+ try {
357
+ const i18nResources = await getI18nResources();
358
+ const translations = i18nResources[lng]?.[ns];
359
+ if (!translations) {
360
+ res.statusCode = 404;
361
+ res.setHeader("Content-Type", "application/json");
362
+ res.end(JSON.stringify({ error: `Translation not found: ${lng}/${ns}` }));
363
+ return;
364
+ }
365
+ res.statusCode = 200;
366
+ res.setHeader("Content-Type", "application/json");
367
+ res.setHeader("Cache-Control", "no-cache");
368
+ res.end(JSON.stringify(translations));
369
+ }
370
+ catch (e) {
371
+ res.statusCode = 500;
372
+ res.setHeader("Content-Type", "application/json");
373
+ res.end(JSON.stringify({
374
+ error: e instanceof Error ? e.message : "Unknown error",
375
+ }));
376
+ }
377
+ });
378
+ // API endpoint to check if initialization is complete
379
+ server.middlewares.use("/__kokimoki/ready", async (_req, res) => {
380
+ res.statusCode = 200;
381
+ res.setHeader("Content-Type", "application/json");
382
+ res.setHeader("Cache-Control", "no-cache");
383
+ res.end(JSON.stringify({
384
+ ready: initState === "ready" || initState === "error",
385
+ }));
386
+ });
387
+ // API endpoint to acknowledge stores hash change (dismiss the popup)
388
+ server.middlewares.use("/__kokimoki/stores-hash/acknowledge", async (_req, res) => {
389
+ try {
390
+ const currentStoresHash = (0, dev_app_1.computeStoresHash)(config.stores);
391
+ await (0, dev_app_1.writeStoresHash)(currentStoresHash);
392
+ res.statusCode = 200;
393
+ res.setHeader("Content-Type", "application/json");
394
+ res.end(JSON.stringify({ success: true }));
395
+ }
396
+ catch (e) {
397
+ res.statusCode = 500;
398
+ res.setHeader("Content-Type", "application/json");
399
+ res.end(JSON.stringify({
400
+ error: e instanceof Error ? e.message : "Unknown error",
401
+ }));
402
+ }
403
+ });
404
+ // API endpoint to reset dev app state (deletes app-id to create fresh dev app)
405
+ server.middlewares.use("/__kokimoki/dev-app/reset", async (_req, res) => {
406
+ try {
407
+ // Delete the app-id file so a new dev app will be created
408
+ await (0, dev_app_1.deleteAppId)();
409
+ // Update the stores hash
410
+ const currentStoresHash = (0, dev_app_1.computeStoresHash)(config.stores);
411
+ await (0, dev_app_1.writeStoresHash)(currentStoresHash);
412
+ // Reset initialization state so next request triggers re-initialization
413
+ initState = "pending";
414
+ initPromise = null;
415
+ cachedDevAppResult = null;
416
+ // Start re-initialization (polling from loading page will detect when ready)
417
+ startInitialization();
418
+ res.statusCode = 200;
419
+ res.setHeader("Content-Type", "application/json");
420
+ res.end(JSON.stringify({ success: true }));
421
+ }
422
+ catch (e) {
423
+ res.statusCode = 500;
424
+ res.setHeader("Content-Type", "application/json");
425
+ res.end(JSON.stringify({
426
+ error: e instanceof Error ? e.message : "Unknown error",
427
+ }));
428
+ }
429
+ });
207
430
  // reload when defaultProjectConfigPath or defaultProjectStylePath changes
208
431
  if (config.defaultProjectConfigPath) {
209
432
  server.watcher.add(config.defaultProjectConfigPath);
@@ -211,7 +434,25 @@ function kokimokiKitPlugin(config) {
211
434
  if (config.defaultProjectStylePath) {
212
435
  server.watcher.add(config.defaultProjectStylePath);
213
436
  }
214
- server.watcher.on("change", (file) => {
437
+ // Watch i18n folder for changes
438
+ if (config.i18nPath) {
439
+ const i18nAbsolutePath = path_1.default.resolve(process.cwd(), config.i18nPath);
440
+ server.watcher.add(i18nAbsolutePath);
441
+ }
442
+ server.watcher.on("change", async (file) => {
443
+ // Invalidate i18n cache on i18n file changes and sync to dev app
444
+ if (config.i18nPath) {
445
+ const i18nAbsolutePath = path_1.default.resolve(process.cwd(), config.i18nPath);
446
+ if (file.startsWith(i18nAbsolutePath) && file.endsWith(".json")) {
447
+ cachedI18n = null;
448
+ // Sync only the changed file to dev app (only primary language)
449
+ if (devAppInfo) {
450
+ await (0, dev_i18n_1.syncI18nFile)(devAppInfo, config.i18nPath, file, config.i18nPrimaryLng ?? "en");
451
+ }
452
+ server.ws.send({ type: "full-reload" });
453
+ return;
454
+ }
455
+ }
215
456
  if (config.defaultProjectConfigPath?.match(file) ||
216
457
  config.defaultProjectStylePath?.match(file)) {
217
458
  server.ws.send({
@@ -219,6 +460,77 @@ function kokimokiKitPlugin(config) {
219
460
  });
220
461
  }
221
462
  });
463
+ // Intercept all root HTML requests to show loading/error/stores-changed/dev-view at root level
464
+ // This runs BEFORE Vite's built-in middleware so we can intercept index.html requests
465
+ server.middlewares.use(async (req, res, next) => {
466
+ const url = new URL(req.url || "/", "http://localhost");
467
+ // Only intercept root path requests (with or without key param)
468
+ if (url.pathname !== "/") {
469
+ return next();
470
+ }
471
+ const hasKeyParam = url.searchParams.has("key");
472
+ // Show loading page while initializing (at root level only)
473
+ if (initState === "pending" || initState === "initializing") {
474
+ if (!hasKeyParam) {
475
+ res.statusCode = 200;
476
+ res.setHeader("Content-Type", "text/html");
477
+ res.setHeader("Cache-Control", "no-cache");
478
+ res.end((0, dev_overlays_1.renderLoadingPage)());
479
+ return;
480
+ }
481
+ // For iframe requests during init, let them wait via transformIndexHtml
482
+ return next();
483
+ }
484
+ const { organization, error: devAppError } = cachedDevAppResult ?? {
485
+ organization: undefined,
486
+ error: { code: "INIT_ERROR", message: "Initialization failed" },
487
+ };
488
+ // Show error page at root level only
489
+ if (devAppError && !hasKeyParam) {
490
+ res.statusCode = 200;
491
+ res.setHeader("Content-Type", "text/html");
492
+ res.setHeader("Cache-Control", "no-cache");
493
+ res.end((0, dev_overlays_1.renderErrorPage)(devAppError));
494
+ return;
495
+ }
496
+ // Check if stores configuration has changed
497
+ const storesChanged = await checkStoresChanged();
498
+ if (storesChanged) {
499
+ if (!hasKeyParam) {
500
+ // Root page: show stores changed page
501
+ res.statusCode = 200;
502
+ res.setHeader("Content-Type", "text/html");
503
+ res.setHeader("Cache-Control", "no-cache");
504
+ res.end((0, dev_overlays_1.renderStoresChangedPage)(!!organization));
505
+ }
506
+ else {
507
+ // Iframe: reload parent to show stores changed page at root level
508
+ res.statusCode = 200;
509
+ res.setHeader("Content-Type", "text/html");
510
+ res.setHeader("Cache-Control", "no-cache");
511
+ res.end(`<!DOCTYPE html>
512
+ <html><head><script>window.parent.location.reload();</script></head><body></body></html>`);
513
+ }
514
+ return;
515
+ }
516
+ // For requests with key param, continue to normal handling (Vite will serve index.html)
517
+ if (hasKeyParam) {
518
+ return next();
519
+ }
520
+ // Dev view disabled or not configured, continue to normal handling
521
+ if (config.devView === false || config.devView === undefined) {
522
+ return next();
523
+ }
524
+ // Render dev view HTML at root
525
+ const devViewHtml = (0, dev_frame_1.renderDevFrame)({
526
+ title: "Dev View",
527
+ rows: config.devView,
528
+ });
529
+ res.statusCode = 200;
530
+ res.setHeader("Content-Type", "text/html");
531
+ res.setHeader("Cache-Control", "no-cache");
532
+ res.end(devViewHtml);
533
+ });
222
534
  },
223
535
  };
224
536
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Production loading screen HTML/CSS/JS for kokimoki-kit plugin.
3
+ * This screen is injected into production builds and removed when km:ready is received.
4
+ */
5
+ /**
6
+ * Loading screen styles
7
+ */
8
+ export declare const loadingScreenStyles = "\n<style id=\"km-loading-style\">\n #km-loading {\n position: fixed;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 1.5rem;\n background: #fff;\n z-index: 9999;\n transition: opacity 0.3s ease-out;\n color: #1a1a2e;\n text-decoration: none;\n }\n #km-loading.km-ready {\n opacity: 0;\n pointer-events: none;\n }\n #km-loading .km-spinner {\n height: 2rem;\n animation: km-spin 1s linear infinite;\n }\n #km-loading .km-logo {\n height: 3rem;\n }\n #km-loading .km-powered {\n font-size: 0.75rem;\n font-weight: 600;\n opacity: 0.65;\n font-family: system-ui, -apple-system, sans-serif;\n }\n @keyframes km-spin {\n to { transform: rotate(360deg); }\n }\n</style>";
9
+ /**
10
+ * km:ready listener script with 3s minimum display time
11
+ */
12
+ export declare const loadingScreenScript = "\n<script>\n (function() {\n var startTime = Date.now();\n var minDisplayTime = 3000;\n var appReady = false;\n \n function hideLoading() {\n var el = document.getElementById('km-loading');\n var style = document.getElementById('km-loading-style');\n if (el) {\n el.classList.add('km-ready');\n setTimeout(function() {\n el.remove();\n if (style) style.remove();\n }, 300);\n }\n }\n \n function tryHide() {\n if (!appReady) return;\n var elapsed = Date.now() - startTime;\n var remaining = Math.max(0, minDisplayTime - elapsed);\n setTimeout(hideLoading, remaining);\n }\n \n window.addEventListener('message', function(e) {\n if (e.data === 'km:ready' && !appReady) {\n appReady = true;\n tryHide();\n }\n });\n })();\n</script>";
13
+ /**
14
+ * Loading screen HTML element with Games for Crowds branding
15
+ */
16
+ export declare const loadingScreenElement = "<a href=\"https://gamesforcrowds.com\" target=\"_blank\" id=\"km-loading\">\n <svg class=\"km-spinner\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\">\n <path fill=\"currentColor\" d=\"M12 22q-2.05 0-3.875-.788t-3.187-2.15t-2.15-3.187T2 12q0-2.075.788-3.887t2.15-3.175t3.187-2.15T12 2q.425 0 .713.288T13 3t-.288.713T12 4Q8.675 4 6.337 6.338T4 12t2.338 5.663T12 20t5.663-2.337T20 12q0-.425.288-.712T21 11t.713.288T22 12q0 2.05-.788 3.875t-2.15 3.188t-3.175 2.15T12 22\"/>\n </svg>\n <svg class=\"km-logo\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 797.59 219.07\" fill=\"currentColor\">\n <g>\n <path d=\"M162.8,96.03c30.87-26.51,10.41-92.22-28.46-95.03-29.39-2.13-60.31-.26-90.99.07-.73,0-1.45-.02-2.17.09C20.3,4.26,3.91,21.33.05,41.8l-.05,123.54c2.6,4.28,5.58,6,10.54,3.94,6.74-3.82,26.58-29.17,31.77-30.22,1.69-.34,3.72.1,5.48-.03-1.15,26.53,19.87,47.52,45.62,49.88,10.24.94,31.34-1.93,39.43.57,8.71,2.7,26.57,28.84,33.53,29.55,3.29.34,7.74-2.14,8.38-5.52,1.72-9.15-2.16-26.7.23-34.77.33-1.1,7.39-8.47,9.02-10.98,16.21-24.98,7.36-61.07-21.22-71.74ZM50.83,121.59l-15.96,2.04-19.08,18.42V46.55c1.67-14.1,15.14-27.27,28.74-30.26,27.33,1.33,57.83-2.6,84.8-.28,20.06,1.73,36.5,34.92,31,55.08-7.53,27.65-45.72,17.08-67.02,18.97-18.52,1.65-36.21,13.88-42.47,31.53ZM170.32,159.08c-1.85,3-10.53,10.69-10.53,11.47v20.5c-1.75.44-2-.72-3.01-1.49-5.71-4.38-11.7-14.4-18.92-16.08-20.43-4.73-47.91,7.68-65.09-10.9-16.98-18.36-8.23-50.63,16.23-55.82,10.35-2.2,44.8-1.74,56.33-.74,24.82,2.14,38.05,31.97,25,53.07Z\"/>\n <polygon points=\"573.74 181.88 556.14 122.98 538.69 122.81 519.73 181.89 501.74 122.88 481.24 122.88 508.73 212.9 525.78 213.95 527.35 213.29 545.25 152.88 564.73 212.9 583.28 213.42 611.24 122.88 590.24 122.88 573.74 181.88\"/>\n <path d=\"M666.74,122.88c-13.74-1.63-30.44,1.21-44.5,0v91h40.5c2.92,0,11.75-2.85,14.86-4.14,38.57-16.14,31.3-81.86-10.86-86.86ZM678.08,187.22c-2.88,4.2-11.41,9.66-16.34,9.66h-20.5v-58c6.18.55,13.48-.75,19.5,0,22.38,2.79,28.85,31.55,17.34,48.34Z\"/>\n <path d=\"M425,121.14c-41.35,5.24-51.11,67.28-16.45,87.94,28.45,16.96,67.81.39,70.73-33.66,2.97-34.62-18.81-58.77-54.28-54.28ZM427.98,198.64c-27.94-4.46-28.04-57.3,1.02-61.5,40.78-5.9,43.14,68.55-1.02,61.5Z\"/>\n <path d=\"M363.48,127.14c-1.58-.84-10.61-4.26-11.74-4.26h-41.5v91h19v-33l11.58.92,18.92,32.08h21.5l-20.99-35.44c19.78-8.94,22.88-40.88,3.23-51.3ZM355.77,157.91c-1.32,4.05-6.98,7.97-11.02,7.97h-15.5v-27c5.36.62,12.39-.85,17.5,0,9.11,1.51,11.57,11.21,9.02,19.03Z\"/>\n <path d=\"M777.02,179.61c-3.95-19.44-32.26-17.78-42.74-26.26-5.4-4.37-4.24-12.34,1.69-15.23,9.47-4.61,20.58,1.49,27.6,7.63l12.65-9.36c-10.04-12.51-23.17-17.41-39.22-15.24-28.56,3.86-35.71,38.53-7.02,50.5,9.33,3.89,33.09,5.53,28.6,20.07-3.99,12.93-29.66,6.57-35.83-2.65l-12.44,11.18c19.81,25.44,74.88,19.65,66.7-20.64Z\"/>\n <path d=\"M267.5,198.64c-41.37,7.02-43.83-58.8-9.75-61.75,10.54-.91,17.28,3.56,24.13,10.88l14.35-7.92c-9.32-19.99-38.11-23.5-56.51-14.99-32.89,15.21-33.7,69.52-.86,85.89,19.28,9.61,48.03,5.78,58.38-14.86l-12.64-7.94c-5.5,3.74-10.09,9.5-17.1,10.69Z\"/>\n <path d=\"M433.4,44.09l21.29,35.46c2.72,2.8,9.85,1.78,13.55,1.07l22.47-37.65v59.55h21.35V4.76l-20.8,1.11-27.53,50.58-29.76-51.69h-20.79v97.76h20.23v-58.43Z\"/>\n <path d=\"M306.43,48.58h-42.7v16.85h21.35c-.27,13.22-7.04,21.88-20.74,22.52-41.54,1.94-38.23-76.19,5.07-66.97,7.84,1.67,10.69,7.85,16.21,11.88l15.05-8.01c-.07-9-13.02-17.18-20.95-19.79-43.1-14.17-73.97,23.73-63.98,65.1,11.75,48.67,85.6,47.68,90.69-4.17.54-5.55-.39-11.78,0-17.42Z\"/>\n <path d=\"M340.09,81.68l33.82-.02,6.67,20.85h23.6L368.81,5.87c-1.88-2.26-20.12-1.02-24.2-.56l-34.81,97.22h23.6l6.7-20.83ZM356.43,29.47l10.67,34.84h-21.35l10.67-34.84Z\"/>\n <polygon points=\"590.71 84.54 551.39 84.54 551.39 60.94 585.1 60.94 585.1 42.96 551.39 42.96 551.39 22.74 588.47 22.74 588.47 4.76 530.04 4.76 530.04 102.52 590.71 102.52 590.71 84.54\"/>\n <path d=\"M608.77,97.39c25.08,16.57,67.45,5.44,61.46-30-1.89-11.15-12.36-17.09-21.93-20.77-7.64-2.94-35.54-8.52-22.36-22.37,8.09-8.51,23.15-1.69,30.32,5.07,2.52-1.37,13.45-9.1,13.04-11.48-14.3-20.97-60.08-20.5-65.72,7.08-2.89,14.13,1.82,25,14.48,31.72,10.24,5.44,38.71,7.15,31.37,24.24-3.47,8.09-18.1,7.13-25,4.22-4.16-1.76-9.85-8.71-12.66-8.29-2.01.3-13.33,8.46-13.03,10.32.38,2.39,7.73,8.73,10.04,10.25Z\"/>\n </g>\n <g>\n <path fill=\"#3c63e7\" d=\"M735,55.98c-24.25,4.14-27.28,47.44,1.59,47.65,31.23.23,35.57-53.99-1.59-47.65ZM734.29,92.39c-9.51-2.94-5.29-24.14,2.49-26.5,16.88-5.12,14.38,31.73-2.49,26.5Z\"/>\n <path fill=\"#3c63e7\" d=\"M716.28,56.67c-3.34-.27-7.15.39-10.41,0-1.21-.14-1.64.47-1.24-1.25,1.77-7.6,5.49-10.69,13.44-7.79l.86-10.79c-14.12-3.91-26.67,3.85-26.53,18.98l-8.25,1.69-1.3,9.08,8.13.02-3.61,36.13,12.26-.85,4.05-35.24h11.1c1.99-.47,1.49-8.28,1.51-9.99Z\"/>\n <path fill=\"#3c63e7\" d=\"M796.72,56.17c-6.86-1.78-12.48,2-15.87,7.73l.45-7.22h-11.74s-4.51,46.06-4.51,46.06h12.65c2.16-13.01-1.47-38.8,18.96-34.32-.94-2.92,2.45-10.57.06-12.25Z\"/>\n </g>\n </svg>\n <p class=\"km-powered\">Powered by Kokimoki</p>\n</a>";
17
+ /**
18
+ * Removes any existing km-loading elements from HTML
19
+ */
20
+ export declare function removeExistingLoadingScreen(html: string): string;