mokup 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vite.mjs CHANGED
@@ -135,6 +135,18 @@ function toHonoPath(route) {
135
135
  });
136
136
  return `/${segments.join("/")}`;
137
137
  }
138
+ function isValidStatus(status) {
139
+ return typeof status === "number" && Number.isFinite(status) && status >= 200 && status <= 599;
140
+ }
141
+ function resolveStatus(routeStatus, responseStatus) {
142
+ if (isValidStatus(routeStatus)) {
143
+ return routeStatus;
144
+ }
145
+ if (isValidStatus(responseStatus)) {
146
+ return responseStatus;
147
+ }
148
+ return 200;
149
+ }
138
150
  function applyRouteOverrides(response, route) {
139
151
  const headers = new Headers(response.headers);
140
152
  const hasHeaders = !!route.headers && Object.keys(route.headers).length > 0;
@@ -143,7 +155,7 @@ function applyRouteOverrides(response, route) {
143
155
  headers.set(key, value);
144
156
  }
145
157
  }
146
- const status = route.status ?? response.status;
158
+ const status = resolveStatus(route.status, response.status);
147
159
  if (status === response.status && !hasHeaders) {
148
160
  return response;
149
161
  }
@@ -329,17 +341,28 @@ function normalizePlaygroundPath(value) {
329
341
  const normalized = value.startsWith("/") ? value : `/${value}`;
330
342
  return normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
331
343
  }
332
- function normalizeBase(base) {
344
+ function normalizeBase$1(base) {
333
345
  if (!base || base === "/") {
334
346
  return "";
335
347
  }
336
348
  return base.endsWith("/") ? base.slice(0, -1) : base;
337
349
  }
350
+ function resolvePlaygroundRequestPath(base, playgroundPath) {
351
+ const normalizedBase = normalizeBase$1(base);
352
+ const normalizedPath = normalizePlaygroundPath(playgroundPath);
353
+ if (!normalizedBase) {
354
+ return normalizedPath;
355
+ }
356
+ if (normalizedPath.startsWith(normalizedBase)) {
357
+ return normalizedPath;
358
+ }
359
+ return `${normalizedBase}${normalizedPath}`;
360
+ }
338
361
  function injectPlaygroundHmr(html, base) {
339
362
  if (html.includes("mokup-playground-hmr")) {
340
363
  return html;
341
364
  }
342
- const normalizedBase = normalizeBase(base);
365
+ const normalizedBase = normalizeBase$1(base);
343
366
  const clientPath = `${normalizedBase}/@vite/client`;
344
367
  const snippet = [
345
368
  '<script type="module" id="mokup-playground-hmr">',
@@ -363,6 +386,29 @@ function injectPlaygroundHmr(html, base) {
363
386
  return `${html}
364
387
  ${snippet}`;
365
388
  }
389
+ function injectPlaygroundSw(html, script) {
390
+ if (!script) {
391
+ return html;
392
+ }
393
+ if (html.includes("mokup-playground-sw")) {
394
+ return html;
395
+ }
396
+ const snippet = [
397
+ '<script type="module" id="mokup-playground-sw">',
398
+ script,
399
+ "<\/script>"
400
+ ].join("\n");
401
+ if (html.includes("</head>")) {
402
+ return html.replace("</head>", `${snippet}
403
+ </head>`);
404
+ }
405
+ if (html.includes("</body>")) {
406
+ return html.replace("</body>", `${snippet}
407
+ </body>`);
408
+ }
409
+ return `${html}
410
+ ${snippet}`;
411
+ }
366
412
  function isViteDevServer$1(server) {
367
413
  return !!server && "ws" in server;
368
414
  }
@@ -501,25 +547,31 @@ function createPlaygroundMiddleware(params) {
501
547
  if (!params.config.enabled) {
502
548
  return next();
503
549
  }
550
+ const server = params.getServer?.();
551
+ const requestPath = resolvePlaygroundRequestPath(server?.config?.base ?? "/", playgroundPath);
504
552
  const requestUrl = req.url ?? "/";
505
553
  const url = new URL(requestUrl, "http://mokup.local");
506
554
  const pathname = url.pathname;
507
- if (!pathname.startsWith(playgroundPath)) {
555
+ const matchedPath = pathname.startsWith(requestPath) ? requestPath : pathname.startsWith(playgroundPath) ? playgroundPath : null;
556
+ if (!matchedPath) {
508
557
  return next();
509
558
  }
510
- const subPath = pathname.slice(playgroundPath.length);
559
+ const subPath = pathname.slice(matchedPath.length);
511
560
  if (subPath === "") {
512
561
  const suffix = url.search ?? "";
513
562
  res.statusCode = 302;
514
- res.setHeader("Location", `${playgroundPath}/${suffix}`);
563
+ res.setHeader("Location", `${matchedPath}/${suffix}`);
515
564
  res.end();
516
565
  return;
517
566
  }
518
567
  if (subPath === "" || subPath === "/" || subPath === "/index.html") {
519
568
  try {
520
569
  const html = await promises.readFile(indexPath, "utf8");
521
- const server = params.getServer?.();
522
- const output = isViteDevServer$1(server) ? injectPlaygroundHmr(html, server.config.base ?? "/") : html;
570
+ let output = html;
571
+ if (isViteDevServer$1(server)) {
572
+ output = injectPlaygroundHmr(output, server.config.base ?? "/");
573
+ output = injectPlaygroundSw(output, params.getSwScript?.());
574
+ }
523
575
  const contentType = mimeTypes[".html"] ?? "text/html; charset=utf-8";
524
576
  sendFile(res, output, contentType);
525
577
  } catch (error) {
@@ -530,13 +582,12 @@ function createPlaygroundMiddleware(params) {
530
582
  return;
531
583
  }
532
584
  if (subPath === "/routes") {
533
- const server = params.getServer?.();
534
585
  const dirs = params.getDirs?.() ?? [];
535
586
  const baseRoot = resolveGroupRoot(dirs, server?.config?.root);
536
587
  const groups = resolveGroups(dirs, baseRoot);
537
588
  const routes = params.getRoutes();
538
589
  sendJson(res, {
539
- basePath: playgroundPath,
590
+ basePath: matchedPath,
540
591
  count: routes.length,
541
592
  groups: groups.map((group) => ({ key: group.key, label: group.label })),
542
593
  routes: routes.map((route) => toPlaygroundRoute(route, baseRoot, groups))
@@ -761,12 +812,12 @@ function normalizeMiddlewares(value, source, logger) {
761
812
  }
762
813
  const list = Array.isArray(value) ? value : [value];
763
814
  const middlewares = [];
764
- for (const entry of list) {
815
+ for (const [index, entry] of list.entries()) {
765
816
  if (typeof entry !== "function") {
766
817
  logger.warn(`Invalid middleware in ${source}`);
767
818
  continue;
768
819
  }
769
- middlewares.push({ handle: entry, source });
820
+ middlewares.push({ handle: entry, source, index });
770
821
  }
771
822
  return middlewares;
772
823
  }
@@ -989,7 +1040,7 @@ async function scanRoutes(params) {
989
1040
  continue;
990
1041
  }
991
1042
  const rules = await loadRules(fileInfo.file, params.server, params.logger);
992
- for (const rule of rules) {
1043
+ for (const [index, rule] of rules.entries()) {
993
1044
  if (!rule || typeof rule !== "object") {
994
1045
  continue;
995
1046
  }
@@ -1018,6 +1069,7 @@ async function scanRoutes(params) {
1018
1069
  if (!resolved) {
1019
1070
  continue;
1020
1071
  }
1072
+ resolved.ruleIndex = index;
1021
1073
  if (config.headers) {
1022
1074
  resolved.headers = { ...config.headers, ...resolved.headers ?? {} };
1023
1075
  }
@@ -1041,6 +1093,330 @@ async function scanRoutes(params) {
1041
1093
  return sortRoutes(routes);
1042
1094
  }
1043
1095
 
1096
+ const defaultSwPath = "/mokup-sw.js";
1097
+ const defaultSwScope = "/";
1098
+ function normalizeSwPath(path) {
1099
+ if (!path) {
1100
+ return defaultSwPath;
1101
+ }
1102
+ return path.startsWith("/") ? path : `/${path}`;
1103
+ }
1104
+ function normalizeSwScope(scope) {
1105
+ if (!scope) {
1106
+ return defaultSwScope;
1107
+ }
1108
+ return scope.startsWith("/") ? scope : `/${scope}`;
1109
+ }
1110
+ function normalizeBasePath(value) {
1111
+ if (!value) {
1112
+ return "/";
1113
+ }
1114
+ const normalized = value.startsWith("/") ? value : `/${value}`;
1115
+ if (normalized.length > 1 && normalized.endsWith("/")) {
1116
+ return normalized.slice(0, -1);
1117
+ }
1118
+ return normalized;
1119
+ }
1120
+ function resolveSwConfigFromEntries(entries, logger) {
1121
+ let path = defaultSwPath;
1122
+ let scope = defaultSwScope;
1123
+ let register = true;
1124
+ let unregister = false;
1125
+ const basePaths = [];
1126
+ let hasPath = false;
1127
+ let hasScope = false;
1128
+ let hasRegister = false;
1129
+ let hasUnregister = false;
1130
+ for (const entry of entries) {
1131
+ const config = entry.sw;
1132
+ if (config?.path) {
1133
+ const next = normalizeSwPath(config.path);
1134
+ if (!hasPath) {
1135
+ path = next;
1136
+ hasPath = true;
1137
+ } else if (path !== next) {
1138
+ logger.warn(`SW path "${next}" ignored; using "${path}".`);
1139
+ }
1140
+ }
1141
+ if (config?.scope) {
1142
+ const next = normalizeSwScope(config.scope);
1143
+ if (!hasScope) {
1144
+ scope = next;
1145
+ hasScope = true;
1146
+ } else if (scope !== next) {
1147
+ logger.warn(`SW scope "${next}" ignored; using "${scope}".`);
1148
+ }
1149
+ }
1150
+ if (typeof config?.register === "boolean") {
1151
+ if (!hasRegister) {
1152
+ register = config.register;
1153
+ hasRegister = true;
1154
+ } else if (register !== config.register) {
1155
+ logger.warn(
1156
+ `SW register="${String(config.register)}" ignored; using "${String(register)}".`
1157
+ );
1158
+ }
1159
+ }
1160
+ if (typeof config?.unregister === "boolean") {
1161
+ if (!hasUnregister) {
1162
+ unregister = config.unregister;
1163
+ hasUnregister = true;
1164
+ } else if (unregister !== config.unregister) {
1165
+ logger.warn(
1166
+ `SW unregister="${String(config.unregister)}" ignored; using "${String(unregister)}".`
1167
+ );
1168
+ }
1169
+ }
1170
+ if (typeof config?.basePath !== "undefined") {
1171
+ const values = Array.isArray(config.basePath) ? config.basePath : [config.basePath];
1172
+ for (const value of values) {
1173
+ basePaths.push(normalizeBasePath(value));
1174
+ }
1175
+ continue;
1176
+ }
1177
+ const normalizedPrefix = normalizePrefix(entry.prefix ?? "");
1178
+ if (normalizedPrefix) {
1179
+ basePaths.push(normalizedPrefix);
1180
+ }
1181
+ }
1182
+ return {
1183
+ path,
1184
+ scope,
1185
+ register,
1186
+ unregister,
1187
+ basePaths: Array.from(new Set(basePaths))
1188
+ };
1189
+ }
1190
+ function resolveSwConfig(options, logger) {
1191
+ const swEntries = options.filter((entry) => entry.mode === "sw");
1192
+ if (swEntries.length === 0) {
1193
+ return null;
1194
+ }
1195
+ return resolveSwConfigFromEntries(swEntries, logger);
1196
+ }
1197
+ function resolveSwUnregisterConfig(options, logger) {
1198
+ return resolveSwConfigFromEntries(options, logger);
1199
+ }
1200
+ function toViteImportPath(file, root) {
1201
+ const absolute = isAbsolute(file) ? file : resolve(root, file);
1202
+ const rel = relative(root, absolute);
1203
+ if (!rel.startsWith("..") && !isAbsolute(rel)) {
1204
+ return `/${toPosix(rel)}`;
1205
+ }
1206
+ return `/@fs/${toPosix(absolute)}`;
1207
+ }
1208
+ function shouldModuleize(handler) {
1209
+ if (typeof handler === "function") {
1210
+ return true;
1211
+ }
1212
+ if (typeof Response !== "undefined" && handler instanceof Response) {
1213
+ return true;
1214
+ }
1215
+ return false;
1216
+ }
1217
+ function toBinaryBody(handler) {
1218
+ if (handler instanceof ArrayBuffer) {
1219
+ return Buffer.from(new Uint8Array(handler)).toString("base64");
1220
+ }
1221
+ if (handler instanceof Uint8Array) {
1222
+ return Buffer.from(handler).toString("base64");
1223
+ }
1224
+ if (Buffer.isBuffer(handler)) {
1225
+ return handler.toString("base64");
1226
+ }
1227
+ return null;
1228
+ }
1229
+ function buildManifestResponse(route, moduleId) {
1230
+ if (moduleId) {
1231
+ const response = {
1232
+ type: "module",
1233
+ module: moduleId
1234
+ };
1235
+ if (typeof route.ruleIndex === "number") {
1236
+ response.ruleIndex = route.ruleIndex;
1237
+ }
1238
+ return response;
1239
+ }
1240
+ const handler = route.handler;
1241
+ if (typeof handler === "string") {
1242
+ return {
1243
+ type: "text",
1244
+ body: handler
1245
+ };
1246
+ }
1247
+ const binary = toBinaryBody(handler);
1248
+ if (binary) {
1249
+ return {
1250
+ type: "binary",
1251
+ body: binary,
1252
+ encoding: "base64"
1253
+ };
1254
+ }
1255
+ return {
1256
+ type: "json",
1257
+ body: handler
1258
+ };
1259
+ }
1260
+ function buildSwScript(params) {
1261
+ const { routes, root } = params;
1262
+ const runtimeImportPath = params.runtimeImportPath ?? "mokup/runtime";
1263
+ const basePaths = params.basePaths ?? [];
1264
+ const ruleModules = /* @__PURE__ */ new Map();
1265
+ const middlewareModules = /* @__PURE__ */ new Map();
1266
+ const manifestRoutes = routes.map((route) => {
1267
+ const moduleId = shouldModuleize(route.handler) ? toViteImportPath(route.file, root) : null;
1268
+ if (moduleId) {
1269
+ ruleModules.set(moduleId, moduleId);
1270
+ }
1271
+ const middleware = route.middlewares?.map((entry) => {
1272
+ const modulePath = toViteImportPath(entry.source, root);
1273
+ middlewareModules.set(modulePath, modulePath);
1274
+ return {
1275
+ module: modulePath,
1276
+ ruleIndex: entry.index
1277
+ };
1278
+ });
1279
+ const response = buildManifestResponse(route, moduleId);
1280
+ const manifestRoute = {
1281
+ method: route.method,
1282
+ url: route.template,
1283
+ ...route.tokens ? { tokens: route.tokens } : {},
1284
+ ...route.score ? { score: route.score } : {},
1285
+ ...route.status ? { status: route.status } : {},
1286
+ ...route.headers ? { headers: route.headers } : {},
1287
+ ...route.delay ? { delay: route.delay } : {},
1288
+ ...middleware && middleware.length > 0 ? { middleware } : {},
1289
+ response
1290
+ };
1291
+ return manifestRoute;
1292
+ });
1293
+ const manifest = {
1294
+ version: 1,
1295
+ routes: manifestRoutes
1296
+ };
1297
+ const imports = [
1298
+ `import { createRuntimeApp, handle } from ${JSON.stringify(runtimeImportPath)}`
1299
+ ];
1300
+ const moduleEntries = [];
1301
+ let moduleIndex = 0;
1302
+ for (const id of ruleModules.keys()) {
1303
+ const name = `module${moduleIndex++}`;
1304
+ imports.push(`import * as ${name} from '${id}'`);
1305
+ moduleEntries.push({ id, name, kind: "rule" });
1306
+ }
1307
+ for (const id of middlewareModules.keys()) {
1308
+ const name = `module${moduleIndex++}`;
1309
+ imports.push(`import * as ${name} from '${id}'`);
1310
+ moduleEntries.push({ id, name, kind: "middleware" });
1311
+ }
1312
+ const lines = [];
1313
+ lines.push(...imports, "");
1314
+ lines.push(
1315
+ "const resolveModuleExport = (mod) => mod?.default ?? mod",
1316
+ "",
1317
+ "const toRuntimeRule = (value) => {",
1318
+ " if (typeof value === 'undefined') {",
1319
+ " return null",
1320
+ " }",
1321
+ " if (typeof value === 'function') {",
1322
+ " return { response: value }",
1323
+ " }",
1324
+ " if (value === null) {",
1325
+ " return { response: null }",
1326
+ " }",
1327
+ " if (typeof value === 'object') {",
1328
+ " if ('response' in value) {",
1329
+ " return value",
1330
+ " }",
1331
+ " if ('handler' in value) {",
1332
+ " const handlerRule = value",
1333
+ " return {",
1334
+ " response: handlerRule.handler,",
1335
+ " ...(typeof handlerRule.status === 'number' ? { status: handlerRule.status } : {}),",
1336
+ " ...(handlerRule.headers ? { headers: handlerRule.headers } : {}),",
1337
+ " ...(typeof handlerRule.delay === 'number' ? { delay: handlerRule.delay } : {}),",
1338
+ " }",
1339
+ " }",
1340
+ " return { response: value }",
1341
+ " }",
1342
+ " return { response: value }",
1343
+ "}",
1344
+ "",
1345
+ "const toRuntimeRules = (value) => {",
1346
+ " if (typeof value === 'undefined') {",
1347
+ " return []",
1348
+ " }",
1349
+ " if (Array.isArray(value)) {",
1350
+ " return value.map(toRuntimeRule).filter(Boolean)",
1351
+ " }",
1352
+ " const rule = toRuntimeRule(value)",
1353
+ " return rule ? [rule] : []",
1354
+ "}",
1355
+ ""
1356
+ );
1357
+ lines.push(
1358
+ `const manifest = ${JSON.stringify(manifest, null, 2)}`,
1359
+ ""
1360
+ );
1361
+ if (moduleEntries.length > 0) {
1362
+ lines.push("const moduleMap = {");
1363
+ for (const entry of moduleEntries) {
1364
+ if (entry.kind === "rule") {
1365
+ lines.push(
1366
+ ` ${JSON.stringify(entry.id)}: { default: toRuntimeRules(resolveModuleExport(${entry.name})) },`
1367
+ );
1368
+ continue;
1369
+ }
1370
+ lines.push(
1371
+ ` ${JSON.stringify(entry.id)}: ${entry.name},`
1372
+ );
1373
+ }
1374
+ lines.push("}", "");
1375
+ }
1376
+ const runtimeOptions = moduleEntries.length > 0 ? "{ manifest, moduleMap }" : "{ manifest }";
1377
+ lines.push(
1378
+ `const basePaths = ${JSON.stringify(basePaths)}`,
1379
+ "",
1380
+ "self.addEventListener('install', () => {",
1381
+ " self.skipWaiting()",
1382
+ "})",
1383
+ "",
1384
+ "self.addEventListener('activate', (event) => {",
1385
+ " event.waitUntil(self.clients.claim())",
1386
+ "})",
1387
+ "",
1388
+ "const shouldHandle = (request) => {",
1389
+ " if (!basePaths || basePaths.length === 0) {",
1390
+ " return true",
1391
+ " }",
1392
+ " const pathname = new URL(request.url).pathname",
1393
+ " return basePaths.some((basePath) => {",
1394
+ " if (basePath === '/') {",
1395
+ " return true",
1396
+ " }",
1397
+ " return pathname === basePath || pathname.startsWith(basePath + '/')",
1398
+ " })",
1399
+ "}",
1400
+ "",
1401
+ "const registerHandler = async () => {",
1402
+ ` const app = await createRuntimeApp(${runtimeOptions})`,
1403
+ " const handler = handle(app)",
1404
+ " self.addEventListener('fetch', (event) => {",
1405
+ " if (!shouldHandle(event.request)) {",
1406
+ " return",
1407
+ " }",
1408
+ " handler(event)",
1409
+ " })",
1410
+ "}",
1411
+ "",
1412
+ "registerHandler().catch((error) => {",
1413
+ " console.error('[mokup] Failed to build service worker app:', error)",
1414
+ "})",
1415
+ ""
1416
+ );
1417
+ return lines.join("\n");
1418
+ }
1419
+
1044
1420
  function buildRouteSignature(routes) {
1045
1421
  return routes.map(
1046
1422
  (route) => [
@@ -1068,9 +1444,61 @@ function resolvePlaygroundInput(list) {
1068
1444
  }
1069
1445
  return void 0;
1070
1446
  }
1447
+ function normalizeBase(base) {
1448
+ if (!base) {
1449
+ return "/";
1450
+ }
1451
+ if (base.startsWith(".")) {
1452
+ return "/";
1453
+ }
1454
+ let normalized = base.startsWith("/") ? base : `/${base}`;
1455
+ if (!normalized.endsWith("/")) {
1456
+ normalized = `${normalized}/`;
1457
+ }
1458
+ return normalized;
1459
+ }
1460
+ function resolveRegisterPath(base, path) {
1461
+ const normalizedBase = normalizeBase(base);
1462
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
1463
+ if (normalizedPath.startsWith(normalizedBase)) {
1464
+ return normalizedPath;
1465
+ }
1466
+ return `${normalizedBase}${normalizedPath.slice(1)}`;
1467
+ }
1468
+ function resolveRegisterScope(base, scope) {
1469
+ const normalizedBase = normalizeBase(base);
1470
+ const normalizedScope = scope.startsWith("/") ? scope : `/${scope}`;
1471
+ if (normalizedScope.startsWith(normalizedBase)) {
1472
+ return normalizedScope;
1473
+ }
1474
+ return `${normalizedBase}${normalizedScope.slice(1)}`;
1475
+ }
1476
+ function resolveSwImportPath(base) {
1477
+ const normalizedBase = normalizeBase(base);
1478
+ return `${normalizedBase}@id/mokup/sw`;
1479
+ }
1480
+ function resolveSwRuntimeImportPath(base) {
1481
+ const normalizedBase = normalizeBase(base);
1482
+ return `${normalizedBase}@id/mokup/runtime`;
1483
+ }
1484
+ function hasMiddlewareStack(middlewares) {
1485
+ const candidate = middlewares;
1486
+ return Array.isArray(candidate.stack);
1487
+ }
1488
+ function addMiddlewareFirst(server, middleware) {
1489
+ if (hasMiddlewareStack(server.middlewares)) {
1490
+ server.middlewares.stack.unshift({ route: "", handle: middleware });
1491
+ return;
1492
+ }
1493
+ server.middlewares.use(middleware);
1494
+ }
1071
1495
  function createMokupPlugin(options = {}) {
1072
1496
  let root = cwd();
1497
+ let base = "/";
1498
+ let command = "serve";
1073
1499
  let routes = [];
1500
+ let serverRoutes = [];
1501
+ let swRoutes = [];
1074
1502
  let app = null;
1075
1503
  let previewWatcher = null;
1076
1504
  let currentServer = null;
@@ -1080,6 +1508,9 @@ function createMokupPlugin(options = {}) {
1080
1508
  const watchEnabled = optionList.every((entry) => entry.watch !== false);
1081
1509
  const playgroundConfig = resolvePlaygroundOptions(resolvePlaygroundInput(optionList));
1082
1510
  const logger = createLogger(logEnabled);
1511
+ const hasSwEntries = optionList.some((entry) => entry.mode === "sw");
1512
+ const swConfig = resolveSwConfig(optionList, logger);
1513
+ const unregisterConfig = resolveSwUnregisterConfig(optionList, logger);
1083
1514
  const resolveAllDirs = () => {
1084
1515
  const dirs = [];
1085
1516
  const seen = /* @__PURE__ */ new Set();
@@ -1094,15 +1525,55 @@ function createMokupPlugin(options = {}) {
1094
1525
  }
1095
1526
  return dirs;
1096
1527
  };
1528
+ const hasSwRoutes = () => !!swConfig && swRoutes.length > 0;
1529
+ const resolveSwRequestPath = (path) => resolveRegisterPath(base, path);
1530
+ const resolveSwRegisterScope = (scope) => resolveRegisterScope(base, scope);
1531
+ const swVirtualId = "virtual:mokup-sw";
1532
+ const resolvedSwVirtualId = `\0${swVirtualId}`;
1533
+ function buildSwLifecycleScript(importPath = "mokup/sw") {
1534
+ const shouldUnregister = unregisterConfig.unregister === true || !hasSwEntries;
1535
+ if (shouldUnregister) {
1536
+ const path2 = resolveSwRequestPath(unregisterConfig.path);
1537
+ const scope2 = resolveSwRegisterScope(unregisterConfig.scope);
1538
+ return [
1539
+ `import { unregisterMokupServiceWorker } from ${JSON.stringify(importPath)}`,
1540
+ "(async () => {",
1541
+ ` await unregisterMokupServiceWorker({ path: ${JSON.stringify(path2)}, scope: ${JSON.stringify(scope2)} })`,
1542
+ "})()"
1543
+ ].join("\n");
1544
+ }
1545
+ if (!swConfig || swConfig.register === false) {
1546
+ return null;
1547
+ }
1548
+ if (!hasSwRoutes()) {
1549
+ return null;
1550
+ }
1551
+ const path = resolveSwRequestPath(swConfig.path);
1552
+ const scope = resolveSwRegisterScope(swConfig.scope);
1553
+ return [
1554
+ `import { registerMokupServiceWorker } from ${JSON.stringify(importPath)}`,
1555
+ "(async () => {",
1556
+ ` const registration = await registerMokupServiceWorker({ path: ${JSON.stringify(path)}, scope: ${JSON.stringify(scope)} })`,
1557
+ " if (import.meta.hot && registration) {",
1558
+ " import.meta.hot.on('mokup:routes-changed', () => {",
1559
+ " registration.update()",
1560
+ " })",
1561
+ " }",
1562
+ "})()"
1563
+ ].join("\n");
1564
+ }
1097
1565
  const playgroundMiddleware = createPlaygroundMiddleware({
1098
1566
  getRoutes: () => routes,
1099
1567
  config: playgroundConfig,
1100
1568
  logger,
1101
1569
  getServer: () => currentServer,
1102
- getDirs: () => resolveAllDirs()
1570
+ getDirs: () => resolveAllDirs(),
1571
+ getSwScript: () => buildSwLifecycleScript(resolveSwImportPath(base))
1103
1572
  });
1104
1573
  const refreshRoutes = async (server) => {
1105
1574
  const collected = [];
1575
+ const collectedServer = [];
1576
+ const collectedSw = [];
1106
1577
  for (const entry of optionList) {
1107
1578
  const dirs = resolveDirs(entry.dir, root);
1108
1579
  const scanParams = {
@@ -1121,9 +1592,19 @@ function createMokupPlugin(options = {}) {
1121
1592
  }
1122
1593
  const scanned = await scanRoutes(scanParams);
1123
1594
  collected.push(...scanned);
1595
+ if (entry.mode === "sw") {
1596
+ collectedSw.push(...scanned);
1597
+ if (entry.sw?.fallback !== false) {
1598
+ collectedServer.push(...scanned);
1599
+ }
1600
+ } else {
1601
+ collectedServer.push(...scanned);
1602
+ }
1124
1603
  }
1125
1604
  routes = sortRoutes(collected);
1126
- app = createHonoApp(routes);
1605
+ serverRoutes = sortRoutes(collectedServer);
1606
+ swRoutes = sortRoutes(collectedSw);
1607
+ app = serverRoutes.length > 0 ? createHonoApp(serverRoutes) : null;
1127
1608
  const signature = buildRouteSignature(routes);
1128
1609
  if (isViteDevServer(server) && server.ws) {
1129
1610
  if (lastSignature && signature !== lastSignature) {
@@ -1139,14 +1620,99 @@ function createMokupPlugin(options = {}) {
1139
1620
  return {
1140
1621
  name: "mokup:vite",
1141
1622
  enforce: "pre",
1623
+ resolveId(id) {
1624
+ if (id === swVirtualId) {
1625
+ return resolvedSwVirtualId;
1626
+ }
1627
+ return null;
1628
+ },
1629
+ async load(id) {
1630
+ if (id !== resolvedSwVirtualId) {
1631
+ return null;
1632
+ }
1633
+ if (swRoutes.length === 0) {
1634
+ await refreshRoutes();
1635
+ }
1636
+ return buildSwScript({
1637
+ routes: swRoutes,
1638
+ root,
1639
+ basePaths: swConfig?.basePaths ?? []
1640
+ });
1641
+ },
1642
+ async buildStart() {
1643
+ if (!swConfig || command !== "build") {
1644
+ return;
1645
+ }
1646
+ await refreshRoutes();
1647
+ if (!hasSwRoutes()) {
1648
+ return;
1649
+ }
1650
+ const fileName = swConfig.path.startsWith("/") ? swConfig.path.slice(1) : swConfig.path;
1651
+ this.emitFile({
1652
+ type: "chunk",
1653
+ id: swVirtualId,
1654
+ fileName
1655
+ });
1656
+ },
1657
+ async transformIndexHtml(html) {
1658
+ if (swRoutes.length === 0) {
1659
+ await refreshRoutes(currentServer ?? void 0);
1660
+ }
1661
+ const script = buildSwLifecycleScript();
1662
+ if (!script) {
1663
+ return html;
1664
+ }
1665
+ return {
1666
+ html,
1667
+ tags: [
1668
+ {
1669
+ tag: "script",
1670
+ attrs: { type: "module" },
1671
+ children: script,
1672
+ injectTo: "head"
1673
+ }
1674
+ ]
1675
+ };
1676
+ },
1142
1677
  configResolved(config) {
1143
1678
  root = config.root;
1679
+ base = config.base ?? "/";
1680
+ command = config.command;
1144
1681
  },
1145
1682
  async configureServer(server) {
1146
1683
  currentServer = server;
1147
1684
  await refreshRoutes(server);
1148
- server.middlewares.use(playgroundMiddleware);
1149
- server.middlewares.use(createMiddleware(() => app, logger));
1685
+ addMiddlewareFirst(server, playgroundMiddleware);
1686
+ const swPath = swConfig ? resolveSwRequestPath(swConfig.path) : null;
1687
+ if (swPath && hasSwRoutes()) {
1688
+ server.middlewares.use(async (req, res, next) => {
1689
+ const requestUrl = req.url ?? "/";
1690
+ const parsed = new URL(requestUrl, "http://mokup.local");
1691
+ if (parsed.pathname !== swPath) {
1692
+ return next();
1693
+ }
1694
+ try {
1695
+ const code = buildSwScript({
1696
+ routes: swRoutes,
1697
+ root,
1698
+ runtimeImportPath: resolveSwRuntimeImportPath(base),
1699
+ basePaths: swConfig?.basePaths ?? []
1700
+ });
1701
+ res.statusCode = 200;
1702
+ res.setHeader("Content-Type", "application/javascript; charset=utf-8");
1703
+ res.setHeader("Cache-Control", "no-cache");
1704
+ res.end(code);
1705
+ } catch (error) {
1706
+ res.statusCode = 500;
1707
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
1708
+ res.end("Failed to generate mokup service worker.");
1709
+ logger.error("SW generation failed:", error);
1710
+ }
1711
+ });
1712
+ }
1713
+ if (serverRoutes.length > 0) {
1714
+ server.middlewares.use(createMiddleware(() => app, logger));
1715
+ }
1150
1716
  if (!watchEnabled) {
1151
1717
  return;
1152
1718
  }
@@ -1172,8 +1738,36 @@ function createMokupPlugin(options = {}) {
1172
1738
  async configurePreviewServer(server) {
1173
1739
  currentServer = server;
1174
1740
  await refreshRoutes(server);
1175
- server.middlewares.use(playgroundMiddleware);
1176
- server.middlewares.use(createMiddleware(() => app, logger));
1741
+ addMiddlewareFirst(server, playgroundMiddleware);
1742
+ const swPath = swConfig ? resolveSwRequestPath(swConfig.path) : null;
1743
+ if (swPath && hasSwRoutes()) {
1744
+ server.middlewares.use(async (req, res, next) => {
1745
+ const requestUrl = req.url ?? "/";
1746
+ const parsed = new URL(requestUrl, "http://mokup.local");
1747
+ if (parsed.pathname !== swPath) {
1748
+ return next();
1749
+ }
1750
+ try {
1751
+ const code = buildSwScript({
1752
+ routes: swRoutes,
1753
+ root,
1754
+ basePaths: swConfig?.basePaths ?? []
1755
+ });
1756
+ res.statusCode = 200;
1757
+ res.setHeader("Content-Type", "application/javascript; charset=utf-8");
1758
+ res.setHeader("Cache-Control", "no-cache");
1759
+ res.end(code);
1760
+ } catch (error) {
1761
+ res.statusCode = 500;
1762
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
1763
+ res.end("Failed to generate mokup service worker.");
1764
+ logger.error("SW generation failed:", error);
1765
+ }
1766
+ });
1767
+ }
1768
+ if (serverRoutes.length > 0) {
1769
+ server.middlewares.use(createMiddleware(() => app, logger));
1770
+ }
1177
1771
  if (!watchEnabled) {
1178
1772
  return;
1179
1773
  }