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.cjs CHANGED
@@ -142,6 +142,18 @@ function toHonoPath(route) {
142
142
  });
143
143
  return `/${segments.join("/")}`;
144
144
  }
145
+ function isValidStatus(status) {
146
+ return typeof status === "number" && Number.isFinite(status) && status >= 200 && status <= 599;
147
+ }
148
+ function resolveStatus(routeStatus, responseStatus) {
149
+ if (isValidStatus(routeStatus)) {
150
+ return routeStatus;
151
+ }
152
+ if (isValidStatus(responseStatus)) {
153
+ return responseStatus;
154
+ }
155
+ return 200;
156
+ }
145
157
  function applyRouteOverrides(response, route) {
146
158
  const headers = new Headers(response.headers);
147
159
  const hasHeaders = !!route.headers && Object.keys(route.headers).length > 0;
@@ -150,7 +162,7 @@ function applyRouteOverrides(response, route) {
150
162
  headers.set(key, value);
151
163
  }
152
164
  }
153
- const status = route.status ?? response.status;
165
+ const status = resolveStatus(route.status, response.status);
154
166
  if (status === response.status && !hasHeaders) {
155
167
  return response;
156
168
  }
@@ -336,17 +348,28 @@ function normalizePlaygroundPath(value) {
336
348
  const normalized = value.startsWith("/") ? value : `/${value}`;
337
349
  return normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
338
350
  }
339
- function normalizeBase(base) {
351
+ function normalizeBase$1(base) {
340
352
  if (!base || base === "/") {
341
353
  return "";
342
354
  }
343
355
  return base.endsWith("/") ? base.slice(0, -1) : base;
344
356
  }
357
+ function resolvePlaygroundRequestPath(base, playgroundPath) {
358
+ const normalizedBase = normalizeBase$1(base);
359
+ const normalizedPath = normalizePlaygroundPath(playgroundPath);
360
+ if (!normalizedBase) {
361
+ return normalizedPath;
362
+ }
363
+ if (normalizedPath.startsWith(normalizedBase)) {
364
+ return normalizedPath;
365
+ }
366
+ return `${normalizedBase}${normalizedPath}`;
367
+ }
345
368
  function injectPlaygroundHmr(html, base) {
346
369
  if (html.includes("mokup-playground-hmr")) {
347
370
  return html;
348
371
  }
349
- const normalizedBase = normalizeBase(base);
372
+ const normalizedBase = normalizeBase$1(base);
350
373
  const clientPath = `${normalizedBase}/@vite/client`;
351
374
  const snippet = [
352
375
  '<script type="module" id="mokup-playground-hmr">',
@@ -370,6 +393,29 @@ function injectPlaygroundHmr(html, base) {
370
393
  return `${html}
371
394
  ${snippet}`;
372
395
  }
396
+ function injectPlaygroundSw(html, script) {
397
+ if (!script) {
398
+ return html;
399
+ }
400
+ if (html.includes("mokup-playground-sw")) {
401
+ return html;
402
+ }
403
+ const snippet = [
404
+ '<script type="module" id="mokup-playground-sw">',
405
+ script,
406
+ "<\/script>"
407
+ ].join("\n");
408
+ if (html.includes("</head>")) {
409
+ return html.replace("</head>", `${snippet}
410
+ </head>`);
411
+ }
412
+ if (html.includes("</body>")) {
413
+ return html.replace("</body>", `${snippet}
414
+ </body>`);
415
+ }
416
+ return `${html}
417
+ ${snippet}`;
418
+ }
373
419
  function isViteDevServer$1(server) {
374
420
  return !!server && "ws" in server;
375
421
  }
@@ -508,25 +554,31 @@ function createPlaygroundMiddleware(params) {
508
554
  if (!params.config.enabled) {
509
555
  return next();
510
556
  }
557
+ const server = params.getServer?.();
558
+ const requestPath = resolvePlaygroundRequestPath(server?.config?.base ?? "/", playgroundPath);
511
559
  const requestUrl = req.url ?? "/";
512
560
  const url = new URL(requestUrl, "http://mokup.local");
513
561
  const pathname = url.pathname;
514
- if (!pathname.startsWith(playgroundPath)) {
562
+ const matchedPath = pathname.startsWith(requestPath) ? requestPath : pathname.startsWith(playgroundPath) ? playgroundPath : null;
563
+ if (!matchedPath) {
515
564
  return next();
516
565
  }
517
- const subPath = pathname.slice(playgroundPath.length);
566
+ const subPath = pathname.slice(matchedPath.length);
518
567
  if (subPath === "") {
519
568
  const suffix = url.search ?? "";
520
569
  res.statusCode = 302;
521
- res.setHeader("Location", `${playgroundPath}/${suffix}`);
570
+ res.setHeader("Location", `${matchedPath}/${suffix}`);
522
571
  res.end();
523
572
  return;
524
573
  }
525
574
  if (subPath === "" || subPath === "/" || subPath === "/index.html") {
526
575
  try {
527
576
  const html = await node_fs.promises.readFile(indexPath, "utf8");
528
- const server = params.getServer?.();
529
- const output = isViteDevServer$1(server) ? injectPlaygroundHmr(html, server.config.base ?? "/") : html;
577
+ let output = html;
578
+ if (isViteDevServer$1(server)) {
579
+ output = injectPlaygroundHmr(output, server.config.base ?? "/");
580
+ output = injectPlaygroundSw(output, params.getSwScript?.());
581
+ }
530
582
  const contentType = mimeTypes[".html"] ?? "text/html; charset=utf-8";
531
583
  sendFile(res, output, contentType);
532
584
  } catch (error) {
@@ -537,13 +589,12 @@ function createPlaygroundMiddleware(params) {
537
589
  return;
538
590
  }
539
591
  if (subPath === "/routes") {
540
- const server = params.getServer?.();
541
592
  const dirs = params.getDirs?.() ?? [];
542
593
  const baseRoot = resolveGroupRoot(dirs, server?.config?.root);
543
594
  const groups = resolveGroups(dirs, baseRoot);
544
595
  const routes = params.getRoutes();
545
596
  sendJson(res, {
546
- basePath: playgroundPath,
597
+ basePath: matchedPath,
547
598
  count: routes.length,
548
599
  groups: groups.map((group) => ({ key: group.key, label: group.label })),
549
600
  routes: routes.map((route) => toPlaygroundRoute(route, baseRoot, groups))
@@ -768,12 +819,12 @@ function normalizeMiddlewares(value, source, logger) {
768
819
  }
769
820
  const list = Array.isArray(value) ? value : [value];
770
821
  const middlewares = [];
771
- for (const entry of list) {
822
+ for (const [index, entry] of list.entries()) {
772
823
  if (typeof entry !== "function") {
773
824
  logger.warn(`Invalid middleware in ${source}`);
774
825
  continue;
775
826
  }
776
- middlewares.push({ handle: entry, source });
827
+ middlewares.push({ handle: entry, source, index });
777
828
  }
778
829
  return middlewares;
779
830
  }
@@ -996,7 +1047,7 @@ async function scanRoutes(params) {
996
1047
  continue;
997
1048
  }
998
1049
  const rules = await loadRules(fileInfo.file, params.server, params.logger);
999
- for (const rule of rules) {
1050
+ for (const [index, rule] of rules.entries()) {
1000
1051
  if (!rule || typeof rule !== "object") {
1001
1052
  continue;
1002
1053
  }
@@ -1025,6 +1076,7 @@ async function scanRoutes(params) {
1025
1076
  if (!resolved) {
1026
1077
  continue;
1027
1078
  }
1079
+ resolved.ruleIndex = index;
1028
1080
  if (config.headers) {
1029
1081
  resolved.headers = { ...config.headers, ...resolved.headers ?? {} };
1030
1082
  }
@@ -1048,6 +1100,330 @@ async function scanRoutes(params) {
1048
1100
  return sortRoutes(routes);
1049
1101
  }
1050
1102
 
1103
+ const defaultSwPath = "/mokup-sw.js";
1104
+ const defaultSwScope = "/";
1105
+ function normalizeSwPath(path) {
1106
+ if (!path) {
1107
+ return defaultSwPath;
1108
+ }
1109
+ return path.startsWith("/") ? path : `/${path}`;
1110
+ }
1111
+ function normalizeSwScope(scope) {
1112
+ if (!scope) {
1113
+ return defaultSwScope;
1114
+ }
1115
+ return scope.startsWith("/") ? scope : `/${scope}`;
1116
+ }
1117
+ function normalizeBasePath(value) {
1118
+ if (!value) {
1119
+ return "/";
1120
+ }
1121
+ const normalized = value.startsWith("/") ? value : `/${value}`;
1122
+ if (normalized.length > 1 && normalized.endsWith("/")) {
1123
+ return normalized.slice(0, -1);
1124
+ }
1125
+ return normalized;
1126
+ }
1127
+ function resolveSwConfigFromEntries(entries, logger) {
1128
+ let path = defaultSwPath;
1129
+ let scope = defaultSwScope;
1130
+ let register = true;
1131
+ let unregister = false;
1132
+ const basePaths = [];
1133
+ let hasPath = false;
1134
+ let hasScope = false;
1135
+ let hasRegister = false;
1136
+ let hasUnregister = false;
1137
+ for (const entry of entries) {
1138
+ const config = entry.sw;
1139
+ if (config?.path) {
1140
+ const next = normalizeSwPath(config.path);
1141
+ if (!hasPath) {
1142
+ path = next;
1143
+ hasPath = true;
1144
+ } else if (path !== next) {
1145
+ logger.warn(`SW path "${next}" ignored; using "${path}".`);
1146
+ }
1147
+ }
1148
+ if (config?.scope) {
1149
+ const next = normalizeSwScope(config.scope);
1150
+ if (!hasScope) {
1151
+ scope = next;
1152
+ hasScope = true;
1153
+ } else if (scope !== next) {
1154
+ logger.warn(`SW scope "${next}" ignored; using "${scope}".`);
1155
+ }
1156
+ }
1157
+ if (typeof config?.register === "boolean") {
1158
+ if (!hasRegister) {
1159
+ register = config.register;
1160
+ hasRegister = true;
1161
+ } else if (register !== config.register) {
1162
+ logger.warn(
1163
+ `SW register="${String(config.register)}" ignored; using "${String(register)}".`
1164
+ );
1165
+ }
1166
+ }
1167
+ if (typeof config?.unregister === "boolean") {
1168
+ if (!hasUnregister) {
1169
+ unregister = config.unregister;
1170
+ hasUnregister = true;
1171
+ } else if (unregister !== config.unregister) {
1172
+ logger.warn(
1173
+ `SW unregister="${String(config.unregister)}" ignored; using "${String(unregister)}".`
1174
+ );
1175
+ }
1176
+ }
1177
+ if (typeof config?.basePath !== "undefined") {
1178
+ const values = Array.isArray(config.basePath) ? config.basePath : [config.basePath];
1179
+ for (const value of values) {
1180
+ basePaths.push(normalizeBasePath(value));
1181
+ }
1182
+ continue;
1183
+ }
1184
+ const normalizedPrefix = normalizePrefix(entry.prefix ?? "");
1185
+ if (normalizedPrefix) {
1186
+ basePaths.push(normalizedPrefix);
1187
+ }
1188
+ }
1189
+ return {
1190
+ path,
1191
+ scope,
1192
+ register,
1193
+ unregister,
1194
+ basePaths: Array.from(new Set(basePaths))
1195
+ };
1196
+ }
1197
+ function resolveSwConfig(options, logger) {
1198
+ const swEntries = options.filter((entry) => entry.mode === "sw");
1199
+ if (swEntries.length === 0) {
1200
+ return null;
1201
+ }
1202
+ return resolveSwConfigFromEntries(swEntries, logger);
1203
+ }
1204
+ function resolveSwUnregisterConfig(options, logger) {
1205
+ return resolveSwConfigFromEntries(options, logger);
1206
+ }
1207
+ function toViteImportPath(file, root) {
1208
+ const absolute = pathe.isAbsolute(file) ? file : pathe.resolve(root, file);
1209
+ const rel = pathe.relative(root, absolute);
1210
+ if (!rel.startsWith("..") && !pathe.isAbsolute(rel)) {
1211
+ return `/${toPosix(rel)}`;
1212
+ }
1213
+ return `/@fs/${toPosix(absolute)}`;
1214
+ }
1215
+ function shouldModuleize(handler) {
1216
+ if (typeof handler === "function") {
1217
+ return true;
1218
+ }
1219
+ if (typeof Response !== "undefined" && handler instanceof Response) {
1220
+ return true;
1221
+ }
1222
+ return false;
1223
+ }
1224
+ function toBinaryBody(handler) {
1225
+ if (handler instanceof ArrayBuffer) {
1226
+ return node_buffer.Buffer.from(new Uint8Array(handler)).toString("base64");
1227
+ }
1228
+ if (handler instanceof Uint8Array) {
1229
+ return node_buffer.Buffer.from(handler).toString("base64");
1230
+ }
1231
+ if (node_buffer.Buffer.isBuffer(handler)) {
1232
+ return handler.toString("base64");
1233
+ }
1234
+ return null;
1235
+ }
1236
+ function buildManifestResponse(route, moduleId) {
1237
+ if (moduleId) {
1238
+ const response = {
1239
+ type: "module",
1240
+ module: moduleId
1241
+ };
1242
+ if (typeof route.ruleIndex === "number") {
1243
+ response.ruleIndex = route.ruleIndex;
1244
+ }
1245
+ return response;
1246
+ }
1247
+ const handler = route.handler;
1248
+ if (typeof handler === "string") {
1249
+ return {
1250
+ type: "text",
1251
+ body: handler
1252
+ };
1253
+ }
1254
+ const binary = toBinaryBody(handler);
1255
+ if (binary) {
1256
+ return {
1257
+ type: "binary",
1258
+ body: binary,
1259
+ encoding: "base64"
1260
+ };
1261
+ }
1262
+ return {
1263
+ type: "json",
1264
+ body: handler
1265
+ };
1266
+ }
1267
+ function buildSwScript(params) {
1268
+ const { routes, root } = params;
1269
+ const runtimeImportPath = params.runtimeImportPath ?? "mokup/runtime";
1270
+ const basePaths = params.basePaths ?? [];
1271
+ const ruleModules = /* @__PURE__ */ new Map();
1272
+ const middlewareModules = /* @__PURE__ */ new Map();
1273
+ const manifestRoutes = routes.map((route) => {
1274
+ const moduleId = shouldModuleize(route.handler) ? toViteImportPath(route.file, root) : null;
1275
+ if (moduleId) {
1276
+ ruleModules.set(moduleId, moduleId);
1277
+ }
1278
+ const middleware = route.middlewares?.map((entry) => {
1279
+ const modulePath = toViteImportPath(entry.source, root);
1280
+ middlewareModules.set(modulePath, modulePath);
1281
+ return {
1282
+ module: modulePath,
1283
+ ruleIndex: entry.index
1284
+ };
1285
+ });
1286
+ const response = buildManifestResponse(route, moduleId);
1287
+ const manifestRoute = {
1288
+ method: route.method,
1289
+ url: route.template,
1290
+ ...route.tokens ? { tokens: route.tokens } : {},
1291
+ ...route.score ? { score: route.score } : {},
1292
+ ...route.status ? { status: route.status } : {},
1293
+ ...route.headers ? { headers: route.headers } : {},
1294
+ ...route.delay ? { delay: route.delay } : {},
1295
+ ...middleware && middleware.length > 0 ? { middleware } : {},
1296
+ response
1297
+ };
1298
+ return manifestRoute;
1299
+ });
1300
+ const manifest = {
1301
+ version: 1,
1302
+ routes: manifestRoutes
1303
+ };
1304
+ const imports = [
1305
+ `import { createRuntimeApp, handle } from ${JSON.stringify(runtimeImportPath)}`
1306
+ ];
1307
+ const moduleEntries = [];
1308
+ let moduleIndex = 0;
1309
+ for (const id of ruleModules.keys()) {
1310
+ const name = `module${moduleIndex++}`;
1311
+ imports.push(`import * as ${name} from '${id}'`);
1312
+ moduleEntries.push({ id, name, kind: "rule" });
1313
+ }
1314
+ for (const id of middlewareModules.keys()) {
1315
+ const name = `module${moduleIndex++}`;
1316
+ imports.push(`import * as ${name} from '${id}'`);
1317
+ moduleEntries.push({ id, name, kind: "middleware" });
1318
+ }
1319
+ const lines = [];
1320
+ lines.push(...imports, "");
1321
+ lines.push(
1322
+ "const resolveModuleExport = (mod) => mod?.default ?? mod",
1323
+ "",
1324
+ "const toRuntimeRule = (value) => {",
1325
+ " if (typeof value === 'undefined') {",
1326
+ " return null",
1327
+ " }",
1328
+ " if (typeof value === 'function') {",
1329
+ " return { response: value }",
1330
+ " }",
1331
+ " if (value === null) {",
1332
+ " return { response: null }",
1333
+ " }",
1334
+ " if (typeof value === 'object') {",
1335
+ " if ('response' in value) {",
1336
+ " return value",
1337
+ " }",
1338
+ " if ('handler' in value) {",
1339
+ " const handlerRule = value",
1340
+ " return {",
1341
+ " response: handlerRule.handler,",
1342
+ " ...(typeof handlerRule.status === 'number' ? { status: handlerRule.status } : {}),",
1343
+ " ...(handlerRule.headers ? { headers: handlerRule.headers } : {}),",
1344
+ " ...(typeof handlerRule.delay === 'number' ? { delay: handlerRule.delay } : {}),",
1345
+ " }",
1346
+ " }",
1347
+ " return { response: value }",
1348
+ " }",
1349
+ " return { response: value }",
1350
+ "}",
1351
+ "",
1352
+ "const toRuntimeRules = (value) => {",
1353
+ " if (typeof value === 'undefined') {",
1354
+ " return []",
1355
+ " }",
1356
+ " if (Array.isArray(value)) {",
1357
+ " return value.map(toRuntimeRule).filter(Boolean)",
1358
+ " }",
1359
+ " const rule = toRuntimeRule(value)",
1360
+ " return rule ? [rule] : []",
1361
+ "}",
1362
+ ""
1363
+ );
1364
+ lines.push(
1365
+ `const manifest = ${JSON.stringify(manifest, null, 2)}`,
1366
+ ""
1367
+ );
1368
+ if (moduleEntries.length > 0) {
1369
+ lines.push("const moduleMap = {");
1370
+ for (const entry of moduleEntries) {
1371
+ if (entry.kind === "rule") {
1372
+ lines.push(
1373
+ ` ${JSON.stringify(entry.id)}: { default: toRuntimeRules(resolveModuleExport(${entry.name})) },`
1374
+ );
1375
+ continue;
1376
+ }
1377
+ lines.push(
1378
+ ` ${JSON.stringify(entry.id)}: ${entry.name},`
1379
+ );
1380
+ }
1381
+ lines.push("}", "");
1382
+ }
1383
+ const runtimeOptions = moduleEntries.length > 0 ? "{ manifest, moduleMap }" : "{ manifest }";
1384
+ lines.push(
1385
+ `const basePaths = ${JSON.stringify(basePaths)}`,
1386
+ "",
1387
+ "self.addEventListener('install', () => {",
1388
+ " self.skipWaiting()",
1389
+ "})",
1390
+ "",
1391
+ "self.addEventListener('activate', (event) => {",
1392
+ " event.waitUntil(self.clients.claim())",
1393
+ "})",
1394
+ "",
1395
+ "const shouldHandle = (request) => {",
1396
+ " if (!basePaths || basePaths.length === 0) {",
1397
+ " return true",
1398
+ " }",
1399
+ " const pathname = new URL(request.url).pathname",
1400
+ " return basePaths.some((basePath) => {",
1401
+ " if (basePath === '/') {",
1402
+ " return true",
1403
+ " }",
1404
+ " return pathname === basePath || pathname.startsWith(basePath + '/')",
1405
+ " })",
1406
+ "}",
1407
+ "",
1408
+ "const registerHandler = async () => {",
1409
+ ` const app = await createRuntimeApp(${runtimeOptions})`,
1410
+ " const handler = handle(app)",
1411
+ " self.addEventListener('fetch', (event) => {",
1412
+ " if (!shouldHandle(event.request)) {",
1413
+ " return",
1414
+ " }",
1415
+ " handler(event)",
1416
+ " })",
1417
+ "}",
1418
+ "",
1419
+ "registerHandler().catch((error) => {",
1420
+ " console.error('[mokup] Failed to build service worker app:', error)",
1421
+ "})",
1422
+ ""
1423
+ );
1424
+ return lines.join("\n");
1425
+ }
1426
+
1051
1427
  function buildRouteSignature(routes) {
1052
1428
  return routes.map(
1053
1429
  (route) => [
@@ -1075,9 +1451,61 @@ function resolvePlaygroundInput(list) {
1075
1451
  }
1076
1452
  return void 0;
1077
1453
  }
1454
+ function normalizeBase(base) {
1455
+ if (!base) {
1456
+ return "/";
1457
+ }
1458
+ if (base.startsWith(".")) {
1459
+ return "/";
1460
+ }
1461
+ let normalized = base.startsWith("/") ? base : `/${base}`;
1462
+ if (!normalized.endsWith("/")) {
1463
+ normalized = `${normalized}/`;
1464
+ }
1465
+ return normalized;
1466
+ }
1467
+ function resolveRegisterPath(base, path) {
1468
+ const normalizedBase = normalizeBase(base);
1469
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
1470
+ if (normalizedPath.startsWith(normalizedBase)) {
1471
+ return normalizedPath;
1472
+ }
1473
+ return `${normalizedBase}${normalizedPath.slice(1)}`;
1474
+ }
1475
+ function resolveRegisterScope(base, scope) {
1476
+ const normalizedBase = normalizeBase(base);
1477
+ const normalizedScope = scope.startsWith("/") ? scope : `/${scope}`;
1478
+ if (normalizedScope.startsWith(normalizedBase)) {
1479
+ return normalizedScope;
1480
+ }
1481
+ return `${normalizedBase}${normalizedScope.slice(1)}`;
1482
+ }
1483
+ function resolveSwImportPath(base) {
1484
+ const normalizedBase = normalizeBase(base);
1485
+ return `${normalizedBase}@id/mokup/sw`;
1486
+ }
1487
+ function resolveSwRuntimeImportPath(base) {
1488
+ const normalizedBase = normalizeBase(base);
1489
+ return `${normalizedBase}@id/mokup/runtime`;
1490
+ }
1491
+ function hasMiddlewareStack(middlewares) {
1492
+ const candidate = middlewares;
1493
+ return Array.isArray(candidate.stack);
1494
+ }
1495
+ function addMiddlewareFirst(server, middleware) {
1496
+ if (hasMiddlewareStack(server.middlewares)) {
1497
+ server.middlewares.stack.unshift({ route: "", handle: middleware });
1498
+ return;
1499
+ }
1500
+ server.middlewares.use(middleware);
1501
+ }
1078
1502
  function createMokupPlugin(options = {}) {
1079
1503
  let root = node_process.cwd();
1504
+ let base = "/";
1505
+ let command = "serve";
1080
1506
  let routes = [];
1507
+ let serverRoutes = [];
1508
+ let swRoutes = [];
1081
1509
  let app = null;
1082
1510
  let previewWatcher = null;
1083
1511
  let currentServer = null;
@@ -1087,6 +1515,9 @@ function createMokupPlugin(options = {}) {
1087
1515
  const watchEnabled = optionList.every((entry) => entry.watch !== false);
1088
1516
  const playgroundConfig = resolvePlaygroundOptions(resolvePlaygroundInput(optionList));
1089
1517
  const logger = createLogger(logEnabled);
1518
+ const hasSwEntries = optionList.some((entry) => entry.mode === "sw");
1519
+ const swConfig = resolveSwConfig(optionList, logger);
1520
+ const unregisterConfig = resolveSwUnregisterConfig(optionList, logger);
1090
1521
  const resolveAllDirs = () => {
1091
1522
  const dirs = [];
1092
1523
  const seen = /* @__PURE__ */ new Set();
@@ -1101,15 +1532,55 @@ function createMokupPlugin(options = {}) {
1101
1532
  }
1102
1533
  return dirs;
1103
1534
  };
1535
+ const hasSwRoutes = () => !!swConfig && swRoutes.length > 0;
1536
+ const resolveSwRequestPath = (path) => resolveRegisterPath(base, path);
1537
+ const resolveSwRegisterScope = (scope) => resolveRegisterScope(base, scope);
1538
+ const swVirtualId = "virtual:mokup-sw";
1539
+ const resolvedSwVirtualId = `\0${swVirtualId}`;
1540
+ function buildSwLifecycleScript(importPath = "mokup/sw") {
1541
+ const shouldUnregister = unregisterConfig.unregister === true || !hasSwEntries;
1542
+ if (shouldUnregister) {
1543
+ const path2 = resolveSwRequestPath(unregisterConfig.path);
1544
+ const scope2 = resolveSwRegisterScope(unregisterConfig.scope);
1545
+ return [
1546
+ `import { unregisterMokupServiceWorker } from ${JSON.stringify(importPath)}`,
1547
+ "(async () => {",
1548
+ ` await unregisterMokupServiceWorker({ path: ${JSON.stringify(path2)}, scope: ${JSON.stringify(scope2)} })`,
1549
+ "})()"
1550
+ ].join("\n");
1551
+ }
1552
+ if (!swConfig || swConfig.register === false) {
1553
+ return null;
1554
+ }
1555
+ if (!hasSwRoutes()) {
1556
+ return null;
1557
+ }
1558
+ const path = resolveSwRequestPath(swConfig.path);
1559
+ const scope = resolveSwRegisterScope(swConfig.scope);
1560
+ return [
1561
+ `import { registerMokupServiceWorker } from ${JSON.stringify(importPath)}`,
1562
+ "(async () => {",
1563
+ ` const registration = await registerMokupServiceWorker({ path: ${JSON.stringify(path)}, scope: ${JSON.stringify(scope)} })`,
1564
+ " if (import.meta.hot && registration) {",
1565
+ " import.meta.hot.on('mokup:routes-changed', () => {",
1566
+ " registration.update()",
1567
+ " })",
1568
+ " }",
1569
+ "})()"
1570
+ ].join("\n");
1571
+ }
1104
1572
  const playgroundMiddleware = createPlaygroundMiddleware({
1105
1573
  getRoutes: () => routes,
1106
1574
  config: playgroundConfig,
1107
1575
  logger,
1108
1576
  getServer: () => currentServer,
1109
- getDirs: () => resolveAllDirs()
1577
+ getDirs: () => resolveAllDirs(),
1578
+ getSwScript: () => buildSwLifecycleScript(resolveSwImportPath(base))
1110
1579
  });
1111
1580
  const refreshRoutes = async (server) => {
1112
1581
  const collected = [];
1582
+ const collectedServer = [];
1583
+ const collectedSw = [];
1113
1584
  for (const entry of optionList) {
1114
1585
  const dirs = resolveDirs(entry.dir, root);
1115
1586
  const scanParams = {
@@ -1128,9 +1599,19 @@ function createMokupPlugin(options = {}) {
1128
1599
  }
1129
1600
  const scanned = await scanRoutes(scanParams);
1130
1601
  collected.push(...scanned);
1602
+ if (entry.mode === "sw") {
1603
+ collectedSw.push(...scanned);
1604
+ if (entry.sw?.fallback !== false) {
1605
+ collectedServer.push(...scanned);
1606
+ }
1607
+ } else {
1608
+ collectedServer.push(...scanned);
1609
+ }
1131
1610
  }
1132
1611
  routes = sortRoutes(collected);
1133
- app = createHonoApp(routes);
1612
+ serverRoutes = sortRoutes(collectedServer);
1613
+ swRoutes = sortRoutes(collectedSw);
1614
+ app = serverRoutes.length > 0 ? createHonoApp(serverRoutes) : null;
1134
1615
  const signature = buildRouteSignature(routes);
1135
1616
  if (isViteDevServer(server) && server.ws) {
1136
1617
  if (lastSignature && signature !== lastSignature) {
@@ -1146,14 +1627,99 @@ function createMokupPlugin(options = {}) {
1146
1627
  return {
1147
1628
  name: "mokup:vite",
1148
1629
  enforce: "pre",
1630
+ resolveId(id) {
1631
+ if (id === swVirtualId) {
1632
+ return resolvedSwVirtualId;
1633
+ }
1634
+ return null;
1635
+ },
1636
+ async load(id) {
1637
+ if (id !== resolvedSwVirtualId) {
1638
+ return null;
1639
+ }
1640
+ if (swRoutes.length === 0) {
1641
+ await refreshRoutes();
1642
+ }
1643
+ return buildSwScript({
1644
+ routes: swRoutes,
1645
+ root,
1646
+ basePaths: swConfig?.basePaths ?? []
1647
+ });
1648
+ },
1649
+ async buildStart() {
1650
+ if (!swConfig || command !== "build") {
1651
+ return;
1652
+ }
1653
+ await refreshRoutes();
1654
+ if (!hasSwRoutes()) {
1655
+ return;
1656
+ }
1657
+ const fileName = swConfig.path.startsWith("/") ? swConfig.path.slice(1) : swConfig.path;
1658
+ this.emitFile({
1659
+ type: "chunk",
1660
+ id: swVirtualId,
1661
+ fileName
1662
+ });
1663
+ },
1664
+ async transformIndexHtml(html) {
1665
+ if (swRoutes.length === 0) {
1666
+ await refreshRoutes(currentServer ?? void 0);
1667
+ }
1668
+ const script = buildSwLifecycleScript();
1669
+ if (!script) {
1670
+ return html;
1671
+ }
1672
+ return {
1673
+ html,
1674
+ tags: [
1675
+ {
1676
+ tag: "script",
1677
+ attrs: { type: "module" },
1678
+ children: script,
1679
+ injectTo: "head"
1680
+ }
1681
+ ]
1682
+ };
1683
+ },
1149
1684
  configResolved(config) {
1150
1685
  root = config.root;
1686
+ base = config.base ?? "/";
1687
+ command = config.command;
1151
1688
  },
1152
1689
  async configureServer(server) {
1153
1690
  currentServer = server;
1154
1691
  await refreshRoutes(server);
1155
- server.middlewares.use(playgroundMiddleware);
1156
- server.middlewares.use(createMiddleware(() => app, logger));
1692
+ addMiddlewareFirst(server, playgroundMiddleware);
1693
+ const swPath = swConfig ? resolveSwRequestPath(swConfig.path) : null;
1694
+ if (swPath && hasSwRoutes()) {
1695
+ server.middlewares.use(async (req, res, next) => {
1696
+ const requestUrl = req.url ?? "/";
1697
+ const parsed = new URL(requestUrl, "http://mokup.local");
1698
+ if (parsed.pathname !== swPath) {
1699
+ return next();
1700
+ }
1701
+ try {
1702
+ const code = buildSwScript({
1703
+ routes: swRoutes,
1704
+ root,
1705
+ runtimeImportPath: resolveSwRuntimeImportPath(base),
1706
+ basePaths: swConfig?.basePaths ?? []
1707
+ });
1708
+ res.statusCode = 200;
1709
+ res.setHeader("Content-Type", "application/javascript; charset=utf-8");
1710
+ res.setHeader("Cache-Control", "no-cache");
1711
+ res.end(code);
1712
+ } catch (error) {
1713
+ res.statusCode = 500;
1714
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
1715
+ res.end("Failed to generate mokup service worker.");
1716
+ logger.error("SW generation failed:", error);
1717
+ }
1718
+ });
1719
+ }
1720
+ if (serverRoutes.length > 0) {
1721
+ server.middlewares.use(createMiddleware(() => app, logger));
1722
+ }
1157
1723
  if (!watchEnabled) {
1158
1724
  return;
1159
1725
  }
@@ -1179,8 +1745,36 @@ function createMokupPlugin(options = {}) {
1179
1745
  async configurePreviewServer(server) {
1180
1746
  currentServer = server;
1181
1747
  await refreshRoutes(server);
1182
- server.middlewares.use(playgroundMiddleware);
1183
- server.middlewares.use(createMiddleware(() => app, logger));
1748
+ addMiddlewareFirst(server, playgroundMiddleware);
1749
+ const swPath = swConfig ? resolveSwRequestPath(swConfig.path) : null;
1750
+ if (swPath && hasSwRoutes()) {
1751
+ server.middlewares.use(async (req, res, next) => {
1752
+ const requestUrl = req.url ?? "/";
1753
+ const parsed = new URL(requestUrl, "http://mokup.local");
1754
+ if (parsed.pathname !== swPath) {
1755
+ return next();
1756
+ }
1757
+ try {
1758
+ const code = buildSwScript({
1759
+ routes: swRoutes,
1760
+ root,
1761
+ basePaths: swConfig?.basePaths ?? []
1762
+ });
1763
+ res.statusCode = 200;
1764
+ res.setHeader("Content-Type", "application/javascript; charset=utf-8");
1765
+ res.setHeader("Cache-Control", "no-cache");
1766
+ res.end(code);
1767
+ } catch (error) {
1768
+ res.statusCode = 500;
1769
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
1770
+ res.end("Failed to generate mokup service worker.");
1771
+ logger.error("SW generation failed:", error);
1772
+ }
1773
+ });
1774
+ }
1775
+ if (serverRoutes.length > 0) {
1776
+ server.middlewares.use(createMiddleware(() => app, logger));
1777
+ }
1184
1778
  if (!watchEnabled) {
1185
1779
  return;
1186
1780
  }