create-fullstack-scaffold 0.4.24 → 0.5.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.
Files changed (146) hide show
  1. package/dist/cli/index.js +439 -15
  2. package/dist/cli/index.js.map +1 -1
  3. package/package.json +10 -13
  4. package/template/eslint-rules/__tests__/no-merged-api-type-export.test.ts +96 -0
  5. package/template/eslint-rules/no-cross-module-service-import.js +4 -0
  6. package/template/eslint-rules/no-merged-api-type-export.js +190 -0
  7. package/template/eslint.config.js +3 -0
  8. package/template/package.json +9 -6
  9. package/template/scripts/sync-agent-hooks.mjs +189 -0
  10. package/template/src/admin/components/__tests__/StatsCard.test.tsx +2 -2
  11. package/template/src/admin/pages/ContentPage.tsx +1 -1
  12. package/template/src/admin/pages/DisputesPage.tsx +1 -1
  13. package/template/src/admin/pages/OrdersPage.tsx +1 -1
  14. package/template/src/admin/pages/TicketsPage.tsx +1 -1
  15. package/template/src/admin/pages/__tests__/ContentPage.test.tsx +5 -1
  16. package/template/src/admin/pages/__tests__/DashboardPage.test.tsx +62 -8
  17. package/template/src/admin/pages/__tests__/DisputesPage.test.tsx +8 -1
  18. package/template/src/admin/pages/__tests__/OrdersPage.test.tsx +4 -2
  19. package/template/src/admin/pages/__tests__/RegisterPage.test.tsx +40 -43
  20. package/template/src/admin/pages/__tests__/SettingsPage.test.tsx +83 -21
  21. package/template/src/admin/pages/__tests__/TicketsPage.test.tsx +3 -1
  22. package/template/src/admin/services/apiClient.ts +2 -3
  23. package/template/src/cli/modules/content/index.ts +3 -3
  24. package/template/src/cli/modules/dispute/index.ts +3 -3
  25. package/template/src/cli/modules/ticket/index.ts +3 -3
  26. package/template/src/cli/modules/todo/index.ts +1 -1
  27. package/template/src/cli/rpc/client.ts +3 -4
  28. package/template/src/cli/rpc/index.ts +1 -1
  29. package/template/src/client/App.tsx +3 -39
  30. package/template/src/client/AppRoutes.tsx +53 -0
  31. package/template/src/client/entry-server.tsx +75 -0
  32. package/template/src/client/main.tsx +3 -1
  33. package/template/src/client/pages/SearchPage.tsx +1 -1
  34. package/template/src/client/services/apiClient.ts +12 -4
  35. package/template/src/client/stores/__tests__/todoStore.test.ts +12 -3
  36. package/template/src/client/stores/entry-stores.ts +36 -0
  37. package/template/src/client/stores/notificationStore.ts +4 -4
  38. package/template/src/client/stores/todoStore.ts +2 -2
  39. package/template/src/merchant/pages/DisputesPage.tsx +3 -3
  40. package/template/src/merchant/pages/OrdersPage.tsx +1 -1
  41. package/template/src/merchant/pages/ProductsPage.tsx +1 -1
  42. package/template/src/merchant/pages/SettingsPage.tsx +1 -1
  43. package/template/src/server/__tests__/integration/isr-full-flow.test.ts +251 -0
  44. package/template/src/server/__tests__/integration/todos-api.test.ts +7 -4
  45. package/template/src/server/app.ts +6 -4
  46. package/template/src/server/core/__tests__/isr-cache.test.ts +96 -92
  47. package/template/src/server/core/__tests__/isr-invalidation.test.ts +29 -4
  48. package/template/src/server/core/__tests__/isr-registry.test.ts +215 -0
  49. package/template/src/server/core/__tests__/isr-renderer.test.ts +121 -0
  50. package/template/src/server/core/isr-cache.ts +6 -12
  51. package/template/src/server/core/isr-invalidation.ts +6 -12
  52. package/template/src/server/core/isr-registry.ts +104 -0
  53. package/template/src/server/core/isr-renderer.ts +75 -0
  54. package/template/src/server/db/schema/contents.ts +28 -20
  55. package/template/src/server/db/schema/disputes.ts +30 -22
  56. package/template/src/server/db/schema/notifications.ts +20 -14
  57. package/template/src/server/db/schema/orders.ts +23 -16
  58. package/template/src/server/db/schema/plugins.ts +56 -38
  59. package/template/src/server/db/schema/products.ts +24 -17
  60. package/template/src/server/db/schema/tickets.ts +44 -31
  61. package/template/src/server/db/schema/todos.ts +26 -18
  62. package/template/src/server/entries/cloudflare.ts +82 -19
  63. package/template/src/server/entries/node.ts +0 -2
  64. package/template/src/server/index.ts +0 -1
  65. package/template/src/server/isr-modules.ts +10 -0
  66. package/template/src/server/module-admin/__tests__/admin-service.test.ts +36 -12
  67. package/template/src/server/module-admin/routes/admin-notification-routes.ts +2 -1
  68. package/template/src/server/module-admin/routes/admin-routes.ts +3 -0
  69. package/template/src/server/module-admin/routes/dashboard-routes.ts +3 -0
  70. package/template/src/server/module-admin/services/admin-service.ts +57 -13
  71. package/template/src/server/module-auth/routes/auth-routes.ts +3 -0
  72. package/template/src/server/module-auth/routes/profile-routes.ts +3 -0
  73. package/template/src/server/module-captcha/routes/captcha-routes.ts +3 -0
  74. package/template/src/server/module-chat/routes/chat-routes.ts +4 -1
  75. package/template/src/server/module-content/__tests__/content-route.test.ts +2 -1
  76. package/template/src/server/module-content/__tests__/content-service.test.ts +2 -1
  77. package/template/src/server/module-content/__tests__/isr.test.ts +138 -0
  78. package/template/src/server/module-content/isr.ts +63 -0
  79. package/template/src/server/module-content/routes/content-routes.ts +10 -10
  80. package/template/src/server/module-content/routes/public-content-routes.ts +8 -4
  81. package/template/src/server/module-content/routes/topics-routes.ts +3 -0
  82. package/template/src/server/module-content/services/content-service.ts +23 -10
  83. package/template/src/server/module-dispute/__tests__/dispute-route.test.ts +2 -1
  84. package/template/src/server/module-dispute/__tests__/dispute-service.test.ts +2 -1
  85. package/template/src/server/module-dispute/routes/dispute-routes.ts +11 -10
  86. package/template/src/server/module-dispute/services/dispute-service.ts +15 -3
  87. package/template/src/server/module-file/routes/file-routes.ts +3 -0
  88. package/template/src/server/module-merchant/routes/merchant-routes.ts +3 -0
  89. package/template/src/server/module-merchant/services/merchant-service.ts +8 -8
  90. package/template/src/server/module-notifications/routes/notification-routes.ts +5 -1
  91. package/template/src/server/module-order/__tests__/order-route.test.ts +8 -8
  92. package/template/src/server/module-order/__tests__/order-service.test.ts +4 -3
  93. package/template/src/server/module-order/routes/cart-routes.ts +3 -0
  94. package/template/src/server/module-order/routes/order-routes.ts +9 -4
  95. package/template/src/server/module-order/routes/orders-mock-routes.ts +3 -0
  96. package/template/src/server/module-order/services/order-service.ts +25 -13
  97. package/template/src/server/module-permission/__tests__/audit-log-service.test.ts +464 -0
  98. package/template/src/server/module-permission/__tests__/role-service.test.ts +348 -0
  99. package/template/src/server/module-permission/routes/audit-log-routes.ts +3 -0
  100. package/template/src/server/module-permission/routes/permission-routes.ts +3 -0
  101. package/template/src/server/module-permission/routes/role-routes.ts +3 -0
  102. package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +3 -0
  103. package/template/src/server/module-plugin/routes/plugin-routes.ts +3 -0
  104. package/template/src/server/module-plugin/services/admin-plugin-service.ts +10 -18
  105. package/template/src/server/module-plugin/services/admin-stats-service.ts +28 -9
  106. package/template/src/server/module-plugin/services/plugin-query-service.ts +42 -66
  107. package/template/src/server/module-tenant/routes/tenant-routes.ts +5 -1
  108. package/template/src/server/module-ticket/__tests__/ticket-route.test.ts +2 -1
  109. package/template/src/server/module-ticket/__tests__/ticket-service.test.ts +4 -3
  110. package/template/src/server/module-ticket/routes/ticket-routes.ts +11 -10
  111. package/template/src/server/module-ticket/services/ticket-service.ts +16 -5
  112. package/template/src/server/module-todos/__tests__/isr.test.ts +105 -0
  113. package/template/src/server/module-todos/__tests__/todo-service.test.ts +12 -9
  114. package/template/src/server/module-todos/__tests__/todos-route-rpc.test.ts +6 -6
  115. package/template/src/server/module-todos/isr.ts +41 -0
  116. package/template/src/server/module-todos/routes/todos-routes.ts +12 -5
  117. package/template/src/server/module-todos/services/todo-service.ts +31 -10
  118. package/template/src/server/route-registry.ts +0 -4
  119. package/template/src/server/rpc-merge.ts +27 -0
  120. package/template/src/server/rpc-surface.ts +117 -0
  121. package/template/src/server/rpc-type-canary.ts +80 -0
  122. package/template/src/server/test-utils/test-client.ts +7 -9
  123. package/template/src/server/test-utils/test-isr-helper.ts +140 -0
  124. package/template/src/shared/modules/content/schemas.ts +14 -0
  125. package/template/src/shared/modules/dispute/schemas.ts +14 -0
  126. package/template/src/shared/modules/order/schemas.ts +9 -1
  127. package/template/src/shared/modules/ticket/schemas.ts +14 -0
  128. package/template/src/shared/modules/todos/index.ts +4 -0
  129. package/template/src/shared/modules/todos/schemas.ts +14 -0
  130. package/template/src/shared/schemas/index.ts +18 -0
  131. package/template/tsup.config.ts +48 -1
  132. package/template/vitest.config.ts +19 -3
  133. package/template/vitest.setup.ts +68 -5
  134. package/template/drizzle/0000_rainy_boomer.sql +0 -101
  135. package/template/drizzle/0001_add_todo_attachments.sql +0 -12
  136. package/template/drizzle/0002_chilly_magneto.sql +0 -89
  137. package/template/drizzle/0003_ambiguous_magdalene.sql +0 -100
  138. package/template/drizzle/0004_add_merchants_products.sql +0 -30
  139. package/template/drizzle/meta/0000_snapshot.json +0 -652
  140. package/template/drizzle/meta/0001_snapshot.json +0 -735
  141. package/template/drizzle/meta/0002_snapshot.json +0 -1198
  142. package/template/drizzle/meta/0003_snapshot.json +0 -1837
  143. package/template/drizzle/meta/_journal.json +0 -41
  144. package/template/patches/typescript+5.9.3.patch +0 -24
  145. package/template/pnpm-lock.yaml +0 -7137
  146. /package/template/patches/{hono+4.12.16.patch → hono+4.12.34.patch} +0 -0
package/dist/cli/index.js CHANGED
@@ -149,6 +149,15 @@ function getAdminPages(resolved) {
149
149
  // src/generators/file-filter.ts
150
150
  function getExcludePatterns(resolved, allManifests) {
151
151
  const excludes = [];
152
+ if (!resolved.hasClient) {
153
+ excludes.push(
154
+ "playwright.config.ts",
155
+ "src/shared/hooks",
156
+ "src/server/entries/cloudflare.ts",
157
+ "tests/e2e"
158
+ );
159
+ }
160
+ excludes.push("src/server/rpc-type-canary.ts");
152
161
  for (const [name, manifest] of allManifests) {
153
162
  if (resolved.modules.has(name)) continue;
154
163
  excludes.push(`src/server/module-${name}`);
@@ -274,6 +283,9 @@ function getExcludePatterns(resolved, allManifests) {
274
283
  function getGeneratedFiles(resolved) {
275
284
  const files = [
276
285
  "src/server/route-registry.ts",
286
+ "src/server/rpc-surface.ts",
287
+ "src/server/isr-modules.ts",
288
+ "src/client/stores/entry-stores.ts",
277
289
  "src/server/db/schema/index.ts",
278
290
  "src/shared/modules/index.ts",
279
291
  "src/shared/schemas/index.ts",
@@ -358,9 +370,11 @@ function generateRouteRegistry(resolved) {
358
370
  let content = imports.join("\n") + "\n\n";
359
371
  content += `const apiRateLimit = rateLimitMiddleware({
360
372
  `;
361
- content += ` windowMs: 60 * 1000,
373
+ content += ` windowMs: 60_000,
362
374
  `;
363
375
  content += ` max: 100,
376
+ `;
377
+ content += ` message: 'Too many requests, please try again later',
364
378
  `;
365
379
  content += `})
366
380
 
@@ -393,9 +407,7 @@ function generateRouteRegistry(resolved) {
393
407
 
394
408
  `;
395
409
  }
396
- content += `export type ClientApiRoutes = typeof clientApiRoutes
397
- `;
398
- content += `export type AdminApiRoutes = typeof adminApiRoutes
410
+ content += `// \u7C7B\u578B\u51FA\u53E3\u5DF2\u79FB\u9664\uFF1A\u5DE8\u578B merge \u7C7B\u578B\u662F TS2589 \u6839\u6E90\uFF0C\u5BA2\u6237\u7AEF\u6539\u7528 rpc-surface.ts \u95E8\u9762
399
411
  `;
400
412
  return content;
401
413
  }
@@ -413,6 +425,211 @@ function getStandaloneRoutes(resolved) {
413
425
  return routes;
414
426
  }
415
427
 
428
+ // src/generators/rpc-surface.ts
429
+ function extractApiTypes(content) {
430
+ const re = /export type (\w+ApiType)\s*=\s*typeof\s+(\w+)/g;
431
+ const names = [];
432
+ let m;
433
+ while ((m = re.exec(content)) !== null) names.push(m[1]);
434
+ return names;
435
+ }
436
+ function extractSegments(content, readRouteFile, baseDir, depth = 0) {
437
+ const segs = /* @__PURE__ */ new Set();
438
+ const pathRe = /path:\s*'\/([a-zA-Z][a-zA-Z-]*)/g;
439
+ let m;
440
+ while ((m = pathRe.exec(content)) !== null) segs.add(m[1]);
441
+ if (depth < 3) {
442
+ const importRe = /import\s*\{([^}]+)\}\s*from\s*'(\.\/[a-zA-Z-]+)'/g;
443
+ while ((m = importRe.exec(content)) !== null) {
444
+ const names = m[1].split(",").map((s) => s.trim().split(" as ")[0]).filter(Boolean);
445
+ const subPath = m[2].replace(/^\.\//, "");
446
+ const isMounted = names.some(
447
+ (n) => new RegExp(`\\.route\\(\\s*'/'\\s*,\\s*${n}\\s*\\)`).test(content)
448
+ );
449
+ if (!isMounted) continue;
450
+ const sub = readRouteFile(`${baseDir}/${subPath}`);
451
+ if (sub) {
452
+ for (const s of extractSegments(sub, readRouteFile, baseDir, depth + 1)) segs.add(s);
453
+ }
454
+ }
455
+ }
456
+ return [...segs];
457
+ }
458
+ function generateRpcSurface(resolved, readRouteFile) {
459
+ const sources = [];
460
+ const usedNames = /* @__PURE__ */ new Set();
461
+ const collect = (moduleName, manifest, key2) => {
462
+ const raw = manifest.routes[key2];
463
+ if (!raw) return;
464
+ const list = Array.isArray(raw) ? raw : [raw];
465
+ for (const entry of list) {
466
+ const routePath = `module-${moduleName}/${entry.importPath.replace(/^\.\//, "").replace(/\.ts$/, "")}`;
467
+ let localName = usedNames.has(entry.exportName) ? `${routePath.split("/").pop().replace(
468
+ /-([a-z])/g,
469
+ (_, c) => c.toUpperCase()
470
+ )}${entry.exportName.charAt(0).toUpperCase()}${entry.exportName.slice(1)}` : entry.exportName;
471
+ while (usedNames.has(localName)) localName += "X";
472
+ usedNames.add(localName);
473
+ const content = readRouteFile(routePath);
474
+ if (!content) continue;
475
+ const typeNames = extractApiTypes(content);
476
+ const segments = extractSegments(
477
+ content,
478
+ readRouteFile,
479
+ routePath.split("/").slice(0, -1).join("/")
480
+ );
481
+ for (const typeName of typeNames) {
482
+ sources.push({ routePath, localName, typeName, segments });
483
+ }
484
+ }
485
+ };
486
+ for (const [moduleName, manifest] of [...resolved.modules.entries()]) {
487
+ collect(moduleName, manifest, "client");
488
+ collect(moduleName, manifest, "admin");
489
+ }
490
+ const segmentOwners = /* @__PURE__ */ new Map();
491
+ sources.forEach((s, idx) => {
492
+ for (const seg of s.segments) {
493
+ const list = segmentOwners.get(seg) ?? [];
494
+ list.push({ localName: s.localName, typeName: s.typeName, routePath: s.routePath, idx });
495
+ segmentOwners.set(seg, list);
496
+ }
497
+ });
498
+ const hasMergedSegments = [...segmentOwners.values()].some((owners) => owners.length > 1);
499
+ const imports = [
500
+ `import { hc } from 'hono/client'`,
501
+ ...hasMergedSegments ? [`import { mergeRpcObjects } from './rpc-merge'`] : []
502
+ ];
503
+ for (const s of sources) {
504
+ imports.push(`import type { ${s.typeName} } from './${s.routePath}'`);
505
+ }
506
+ const clients = [];
507
+ sources.forEach((s, i) => {
508
+ clients.push(` const ${s.localName}Client${i} = hc<${s.typeName}>(api, options)`);
509
+ });
510
+ const entries = [];
511
+ const key = (seg) => /^[a-zA-Z][a-zA-Z0-9]*$/.test(seg) ? seg : `'${seg}'`;
512
+ const access = (seg) => /^[a-zA-Z][a-zA-Z0-9]*$/.test(seg) ? `.${seg}` : `['${seg}']`;
513
+ for (const [seg, owners] of segmentOwners) {
514
+ owners.sort((a, b) => a.idx - b.idx);
515
+ const refs = owners.map((o) => `${o.localName}Client${o.idx}${access(seg)}`);
516
+ entries.push(
517
+ refs.length === 1 ? ` ${key(seg)}: ${refs[0]},` : ` ${key(seg)}: mergeRpcObjects(${refs.join(", ")}),`
518
+ );
519
+ }
520
+ return `/**
521
+ * @framework-baseline rpc-surface-v1
522
+ *
523
+ * \u6309\u6A21\u5757\u62C6\u5206\u7684\u7C7B\u578B\u5B89\u5168 RPC \u95E8\u9762\uFF08\u672C\u6587\u4EF6\u7531 CLI \u751F\u6210\uFF0C\u52FF\u624B\u6539\uFF09\u3002
524
+ * \u751F\u6210\u5668: src/generators/rpc-surface.ts
525
+ *
526
+ * \u6BCF\u4E2A\u6A21\u5757\u5355\u72EC\u5B9E\u4F8B\u5316\u7A84\u5BA2\u6237\u7AEF\uFF08\u6DF1\u5EA6 = 1 \u4E2A\u6A21\u5757\uFF09\uFF0C\u7EC4\u88C5\u6210\u4E0E\u65E7
527
+ * \`hc<MergedApiType>\` \u5B8C\u5168\u540C\u5F62\u7684\u95E8\u9762\u3002\u7981\u6B62\u5BFC\u51FA\u94FE\u5F0F merge \u7C7B\u578B
528
+ * \uFF08eslint: no-merged-api-type-export\uFF09\u3002
529
+ */
530
+
531
+ ${imports.join("\n")}
532
+
533
+ /** hono hc \u7684\u9009\u9879\u7C7B\u578B\uFF08fetch / webSocket / sse / headers \u7B49\uFF09 */
534
+ export type RpcClientOptions = NonNullable<Parameters<typeof hc>[1]>
535
+
536
+ /**
537
+ * \u521B\u5EFA\u6309\u6A21\u5757\u62C6\u5206\u7684 RPC \u95E8\u9762\u3002
538
+ * \u8C03\u7528\u5F62\u6001\u4E0E\u65E7 mega \u5BA2\u6237\u7AEF\u517C\u5BB9\uFF1Afacade.api.todos.$get()\u3002
539
+ */
540
+ export function createApiFacade(baseUrl: string, options: RpcClientOptions = {}) {
541
+ const api = \`\${baseUrl.replace(/\\/$/, '')}/api\`
542
+
543
+ ${clients.join("\n")}
544
+
545
+ return {
546
+ api: {
547
+ ${entries.join("\n")}
548
+ },
549
+ }
550
+ }
551
+
552
+ export type ApiFacade = ReturnType<typeof createApiFacade>
553
+ `;
554
+ }
555
+
556
+ // src/generators/isr-modules.ts
557
+ function generateIsrModules(resolved, moduleFileExists) {
558
+ const imports = [];
559
+ for (const [moduleName] of [...resolved.modules.entries()]) {
560
+ if (moduleFileExists(moduleName, "isr.ts")) {
561
+ imports.push(`import '@server/module-${moduleName}/isr'`);
562
+ }
563
+ }
564
+ return `/**
565
+ * ISR \u6A21\u5757\u6CE8\u518C\u6C47\u603B\uFF08\u672C\u6587\u4EF6\u7531 CLI \u751F\u6210\uFF0C\u52FF\u624B\u6539\uFF09\u3002
566
+ * \u751F\u6210\u5668: src/generators/isr-modules.ts
567
+ *
568
+ * \u6309 preset \u53EA\u5BFC\u5165\u9009\u4E2D\u6A21\u5757\u7684 ISR \u6CE8\u518C\u526F\u4F5C\u7528\uFF1B\u672A\u88AB\u88C1\u526A\u98CE\u9669\u7684\u5355\u4E00\u5165\u53E3
569
+ * \u7531 entries/cloudflare.ts import \u672C\u6587\u4EF6\u3002
570
+ */
571
+
572
+ ${imports.join("\n")}
573
+ `;
574
+ }
575
+
576
+ // src/generators/entry-stores.ts
577
+ function generateEntryStores(resolved, moduleFileExists) {
578
+ const hasTodos = resolved.modules.has("todos") && moduleFileExists("todos", "todoStore.ts");
579
+ if (!hasTodos) {
580
+ return `/**
581
+ * Entry SSR store \u79CD\u5B50\uFF08\u672C\u6587\u4EF6\u7531 CLI \u751F\u6210\uFF0C\u52FF\u624B\u6539\uFF09\u3002
582
+ * \u5F53\u524D preset \u4E0D\u542B todo \u6A21\u5757\uFF1A\u65E0 store \u9700\u8981\u9884\u586B\u5145\uFF0C\u5168\u90E8\u4E3A\u7A7A\u5B9E\u73B0\u3002
583
+ */
584
+
585
+ export interface SSRData {
586
+ [key: string]: unknown
587
+ }
588
+
589
+ export function snapshotEntryStores(): Record<string, unknown> {
590
+ return {}
591
+ }
592
+
593
+ export function seedEntryStores(_data: SSRData): void {}
594
+
595
+ export function restoreEntryStores(_snapshot: Record<string, unknown>): void {}
596
+ `;
597
+ }
598
+ return `/**
599
+ * Entry SSR store \u79CD\u5B50\uFF08\u672C\u6587\u4EF6\u7531 CLI \u751F\u6210\uFF0C\u52FF\u624B\u6539\uFF09\u3002
600
+ * todo \u6A21\u5757\u5728\u9009\u4E2D\u5217\u8868\u4E2D\uFF1A\u9884\u586B\u5145 todoStore \u4F9B ISR \u6E32\u67D3\u771F\u5B9E\u5185\u5BB9\u3002
601
+ */
602
+
603
+ import { useTodoStore } from './todoStore'
604
+ import type { Todo } from '@shared/schemas'
605
+
606
+ export interface SSRData {
607
+ todos?: Todo[]
608
+ [key: string]: unknown
609
+ }
610
+
611
+ export interface EntryStoreSnapshot {
612
+ todos: Todo[]
613
+ loading: boolean
614
+ }
615
+
616
+ export function snapshotEntryStores(): EntryStoreSnapshot {
617
+ const state = useTodoStore.getState()
618
+ return { todos: state.todos, loading: state.loading }
619
+ }
620
+
621
+ export function seedEntryStores(data: SSRData): void {
622
+ if (data.todos) {
623
+ useTodoStore.setState({ todos: data.todos, loading: false })
624
+ }
625
+ }
626
+
627
+ export function restoreEntryStores(snapshot: EntryStoreSnapshot): void {
628
+ useTodoStore.setState({ todos: snapshot.todos, loading: snapshot.loading })
629
+ }
630
+ `;
631
+ }
632
+
416
633
  // src/generators/client-navigation.ts
417
634
  var DEFAULT_ICON = "Circle";
418
635
  var ICON_MAP = {
@@ -589,7 +806,10 @@ describe('App Component', () => {
589
806
  describe('Initial Render', () => {
590
807
  it('should render navigation', () => {
591
808
  render(<App />)
592
- expect(screen.getByTestId('app-nav')).toBeInTheDocument()
809
+ // \u5E03\u5C40\u611F\u77E5\uFF1Atop-nav \u6709 app-nav\uFF1Bsidebar \u5E03\u5C40\u6CA1\u6709\u9876\u680F\uFF08\u6709 app-container/app-main\uFF09
810
+ expect(
811
+ document.querySelector('[data-testid="app-nav"], [data-testid="app-container"]')
812
+ ).toBeTruthy()
593
813
  })
594
814
 
595
815
  it('should render main content area', () => {
@@ -606,7 +826,11 @@ describe('App Component', () => {
606
826
  describe('Navigation Links', () => {
607
827
  it('should render footer', () => {
608
828
  render(<App />)
609
- expect(screen.getByTestId('app-footer')).toBeInTheDocument()
829
+ // \u9875\u811A\u4EC5 top-nav \u5E03\u5C40\u6E32\u67D3\uFF08Layout: showFooter = layout === 'top-nav'\uFF09
830
+ expect(
831
+ document.querySelector('[data-testid="app-footer"]') ||
832
+ document.querySelector('[data-testid="app-container"]')
833
+ ).toBeTruthy()
610
834
  })
611
835
  })
612
836
  })
@@ -668,7 +892,11 @@ describe('Navigation', () => {
668
892
  })
669
893
 
670
894
  it('should render nav items', () => {
671
- renderWithRouter(<Navigation />)
895
+ renderWithRouter(
896
+ <Navigation
897
+ items={[{ label: '${firstLabel}', icon: 'CheckSquare', path: '${firstPage.route}' }]}
898
+ />
899
+ )
672
900
  expect(screen.getByText('${firstLabel}')).toBeInTheDocument()
673
901
  })
674
902
  })
@@ -771,11 +999,10 @@ function generateAdminApiClient(resolved) {
771
999
  * @impact Captcha module excluded = no captchaStore import
772
1000
  */
773
1001
 
774
- import { hc } from 'hono/client'
775
1002
  import { WSClientImpl } from '@shared/core/ws-client'
776
1003
  import { SSEClientImpl } from '@shared/core/sse-client'
777
1004
  import { createRequestInterceptor } from './requestInterceptor'
778
- ${captchaImport}import type { AdminApiType } from '@server/index'
1005
+ ${captchaImport}import { createApiFacade } from '@server/rpc-surface'
779
1006
 
780
1007
  const baseUrl = import.meta.env.API_BASE_URL || window.location.origin
781
1008
 
@@ -807,7 +1034,7 @@ ${captchaFetchSetup} return createRequestInterceptor({
807
1034
  ${captchaHandler} })
808
1035
  }
809
1036
 
810
- export const apiClient = hc<AdminApiType>(baseUrl, {
1037
+ export const apiClient = createApiFacade(baseUrl, {
811
1038
  fetch: createCustomFetch() as typeof fetch,
812
1039
  webSocket: url => new WSClientImpl(url) as unknown as WebSocket,
813
1040
  sse: url => {
@@ -1371,7 +1598,10 @@ export function createApp<T extends AppBindings = AppBindings>(_options: CreateA
1371
1598
  return c.json({ status: 'ok', timestamp: new Date().toISOString(), db: 'not configured' })
1372
1599
  }
1373
1600
  })
1374
- .post('/api/__test__/cleanup', async c => {
1601
+ // \u6D4B\u8BD5\u8F85\u52A9\u7AEF\u70B9\uFF1A\u4EC5\u5F00\u53D1/\u6D4B\u8BD5\u73AF\u5883\u6CE8\u518C\uFF08Cloudflare \u6784\u5EFA\u65F6 NODE_ENV \u88AB\u66FF\u6362\u4E3A
1602
+ // "production"\uFF0C\u6B64\u7AEF\u70B9\u4E0D\u4F1A\u51FA\u73B0\u5728\u751F\u4EA7 bundle \u4E2D\uFF09
1603
+ if (process.env.NODE_ENV !== 'production') {
1604
+ app.post('/api/__test__/cleanup', async c => {
1375
1605
  try {
1376
1606
  const { cleanupTestDatabase } = await import('./db/test-setup')
1377
1607
  await cleanupTestDatabase()
@@ -1381,6 +1611,7 @@ export function createApp<T extends AppBindings = AppBindings>(_options: CreateA
1381
1611
  return c.json({ success: false as const, message: 'Failed to cleanup database' }, 500)
1382
1612
  }
1383
1613
  })
1614
+ }
1384
1615
 
1385
1616
  autoRegisterRealtime(app as unknown as Parameters<typeof autoRegisterRealtime>[0])
1386
1617
 
@@ -1427,9 +1658,6 @@ export function createApp<T extends AppBindings = AppBindings>(_options: CreateA
1427
1658
 
1428
1659
  return app
1429
1660
  }
1430
- export type AdminApiType = typeof adminApiRoutes
1431
- export type ClientApiType = typeof clientApiRoutes
1432
- export type AppType = ReturnType<typeof createApp>
1433
1661
  `;
1434
1662
  }
1435
1663
 
@@ -1562,6 +1790,39 @@ var MODULE_EXPORTS = {
1562
1790
  "type PluginListQuery"
1563
1791
  ]
1564
1792
  },
1793
+ plugins: {
1794
+ namedExports: [
1795
+ "PluginSchema",
1796
+ "PluginStatusSchema",
1797
+ "CreatePluginSchema",
1798
+ "UpdatePluginSchema",
1799
+ "PluginVersionStatusSchema",
1800
+ "VersionSchema",
1801
+ "CategorySchema",
1802
+ "ReviewSchema",
1803
+ "CreateReviewSchema",
1804
+ "MarketplaceStatsSchema",
1805
+ "PluginListResponseSchema",
1806
+ "AdminPluginSchema",
1807
+ "AdminDashboardStatsSchema",
1808
+ "PluginListQuerySchema",
1809
+ "PluginSlugSchema",
1810
+ "type Plugin",
1811
+ "type PluginStatus",
1812
+ "type CreatePluginInput",
1813
+ "type UpdatePluginInput",
1814
+ "type PluginVersionStatus",
1815
+ "type Version",
1816
+ "type Category",
1817
+ "type Review",
1818
+ "type CreateReviewInput",
1819
+ "type MarketplaceStats",
1820
+ "type PluginListResponse",
1821
+ "type AdminPlugin",
1822
+ "type AdminDashboardStats",
1823
+ "type PluginListQuery"
1824
+ ]
1825
+ },
1565
1826
  merchant: {
1566
1827
  namedExports: [
1567
1828
  "ProductSchema",
@@ -1773,6 +2034,7 @@ function generateSharedModulesIndex(resolved) {
1773
2034
  "permission",
1774
2035
  "auth",
1775
2036
  "plugin",
2037
+ "plugins",
1776
2038
  "merchant",
1777
2039
  "tenant",
1778
2040
  "order",
@@ -1863,6 +2125,10 @@ var MODULE_EXPORTS2 = {
1863
2125
  "TodoWithAttachmentsSchema",
1864
2126
  "UploadFileSchema",
1865
2127
  "AttachmentIdResponseSchema",
2128
+ "TodoListResponseSchema",
2129
+ "TodoListQuerySchema",
2130
+ "type TodoListResponse",
2131
+ "type TodoListQuery",
1866
2132
  "type Todo",
1867
2133
  "type TodoStatus",
1868
2134
  "type CreateTodoInput",
@@ -1958,6 +2224,56 @@ var MODULE_EXPORTS2 = {
1958
2224
  "type PluginListQuery"
1959
2225
  ]
1960
2226
  },
2227
+ plugins: {
2228
+ namedExports: [
2229
+ "PluginSchema",
2230
+ "PluginStatusSchema",
2231
+ "CreatePluginSchema",
2232
+ "UpdatePluginSchema",
2233
+ "PluginVersionStatusSchema",
2234
+ "VersionSchema",
2235
+ "CategorySchema",
2236
+ "ReviewSchema",
2237
+ "CreateReviewSchema",
2238
+ "MarketplaceStatsSchema",
2239
+ "PluginListResponseSchema",
2240
+ "AdminPluginSchema",
2241
+ "AdminDashboardStatsSchema",
2242
+ "PluginListQuerySchema",
2243
+ "PluginSlugSchema",
2244
+ "PluginSearchQuerySchema",
2245
+ "PluginDeleteResponseSchema",
2246
+ "ReviewIdParamsSchema",
2247
+ "ReviewDeleteResponseSchema",
2248
+ "CategorySlugParamsSchema",
2249
+ "CategoryPluginsQuerySchema",
2250
+ "PluginListAdminSchema",
2251
+ "AdminListQuerySchema",
2252
+ "AdminListAllQuerySchema",
2253
+ "RejectPluginBodySchema",
2254
+ "BulkApproveBodySchema",
2255
+ "BulkRejectBodySchema",
2256
+ "BulkResponseSchema",
2257
+ "CreateCategoryBodySchema",
2258
+ "UpdateCategoryBodySchema",
2259
+ "CategoryIdParamsSchema",
2260
+ "CategoryIdResponseSchema",
2261
+ "type Plugin",
2262
+ "type PluginStatus",
2263
+ "type CreatePluginInput",
2264
+ "type UpdatePluginInput",
2265
+ "type PluginVersionStatus",
2266
+ "type Version",
2267
+ "type Category",
2268
+ "type Review",
2269
+ "type CreateReviewInput",
2270
+ "type MarketplaceStats",
2271
+ "type PluginListResponse",
2272
+ "type AdminPlugin",
2273
+ "type AdminDashboardStats",
2274
+ "type PluginListQuery"
2275
+ ]
2276
+ },
1961
2277
  admin: {
1962
2278
  namedExports: [
1963
2279
  "SystemStatsSchema",
@@ -2256,6 +2572,7 @@ var moduleOrder = [
2256
2572
  "notifications",
2257
2573
  "auth",
2258
2574
  "plugin",
2575
+ "plugins",
2259
2576
  "admin",
2260
2577
  "audit",
2261
2578
  "captcha",
@@ -2438,6 +2755,34 @@ function extractToken(authHeader: string | undefined): string | null {
2438
2755
  return authHeader.slice(7)
2439
2756
  }
2440
2757
 
2758
+ // dev tokens\uFF1A\u4E0E\u6A21\u677F\u5B8C\u6574\u7248 auth.ts \u8BED\u4E49\u4E00\u81F4\uFF08\u5F00\u53D1/\u6D4B\u8BD5\u4FBF\u5229\u540E\u95E8\uFF0Cproduction \u9ED8\u8BA4\u7981\u7528\uFF09
2759
+ function isDevTokensEnabled(): boolean {
2760
+ if (process.env.ENABLE_DEV_TOKENS === 'true') return true
2761
+ if (process.env.ENABLE_DEV_TOKENS === 'false') return false
2762
+ if (process.env.NODE_ENV === 'production') return false
2763
+ return true
2764
+ }
2765
+
2766
+ function verifyDevToken(token: string): AuthUser | null {
2767
+ if (token === 'admin-token' || token === 'super-admin-token') {
2768
+ return {
2769
+ id: 'super-admin-1',
2770
+ username: 'superadmin',
2771
+ email: 'superadmin@example.com',
2772
+ role: 'admin',
2773
+ }
2774
+ }
2775
+ if (token === 'user-token') {
2776
+ return {
2777
+ id: 'user-1',
2778
+ username: 'Demo User',
2779
+ email: 'demo@example.com',
2780
+ role: 'user',
2781
+ }
2782
+ }
2783
+ return null
2784
+ }
2785
+
2441
2786
  export function authMiddleware(_options: AuthMiddlewareOptions = {}): MiddlewareHandler {
2442
2787
  const log = createModuleLoggerSync('auth')
2443
2788
 
@@ -2449,6 +2794,16 @@ export function authMiddleware(_options: AuthMiddlewareOptions = {}): Middleware
2449
2794
  return c.json({ success: false, error: 'Authentication required', status: 401 }, 401)
2450
2795
  }
2451
2796
 
2797
+ if (isDevTokensEnabled()) {
2798
+ const devUser = verifyDevToken(token)
2799
+ if (devUser) {
2800
+ c.set('authUser', devUser)
2801
+ log.warn({ module: 'auth' }, 'DEV TOKEN USED - This should not appear in production!')
2802
+ await next()
2803
+ return
2804
+ }
2805
+ }
2806
+
2452
2807
  try {
2453
2808
  const jwt = await import('jsonwebtoken')
2454
2809
  const secretKey = process.env.AUTH_SECRET_KEY || 'dev-secret-key-change-in-production'
@@ -2733,7 +3088,7 @@ var MODULE_PACKAGES = {
2733
3088
  admin: ["bcryptjs"],
2734
3089
  auth: ["bcryptjs"]
2735
3090
  };
2736
- var ADMIN_PANEL_PACKAGES = ["antd"];
3091
+ var ADMIN_PANEL_PACKAGES = ["antd", "@ant-design/icons"];
2737
3092
  var CLI_PACKAGES = ["commander"];
2738
3093
  var UNUSED_PACKAGES = ["lodash-es", "chalk", "mysql2"];
2739
3094
  var CLIENT_PACKAGES = [
@@ -2759,6 +3114,7 @@ var CLIENT_DEV_PACKAGES = [
2759
3114
  "playwright",
2760
3115
  "@prerenderer/renderer-jsdom",
2761
3116
  "@prerenderer/renderer-puppeteer",
3117
+ "puppeteer",
2762
3118
  "@prerenderer/rollup-plugin",
2763
3119
  "eventsource"
2764
3120
  ];
@@ -2855,6 +3211,7 @@ function generateViteConfig(resolved, templateDir) {
2855
3211
  entriesToRemove.push("merchant");
2856
3212
  aliasesToRemove.push("@merchant");
2857
3213
  }
3214
+ const hasAntdConsumer = resolved.modules.has("admin") || resolved.modules.has("tenant") || resolved.modules.has("merchant");
2858
3215
  for (const name of entriesToRemove) {
2859
3216
  const re = new RegExp(
2860
3217
  `^\\s*${name}:\\s*path\\.resolve\\(__dirname,\\s*['"]${name}\\.html['"]\\),\\s*$\\n?`,
@@ -2871,6 +3228,12 @@ function generateViteConfig(resolved, templateDir) {
2871
3228
  );
2872
3229
  content = content.replace(re, "");
2873
3230
  }
3231
+ if (!hasAntdConsumer) {
3232
+ const antdChunkRe = /^(\s*'vendor-antd':\s*\['antd',\s*'@ant-design\/icons'\],\s*\n)/gm;
3233
+ content = content.replace(antdChunkRe, "");
3234
+ const onwarnRe = /^\s*\/\/ Suppress antd "use client" directive warnings[^\n]*\n\s*if \(warning\.code === 'MODULE_LEVEL_DIRECTIVE'\) return\n/gm;
3235
+ content = content.replace(onwarnRe, "");
3236
+ }
2874
3237
  return content;
2875
3238
  }
2876
3239
 
@@ -3826,6 +4189,50 @@ async function createProject(projectNameOrOptions, useCurrentDir = false, preset
3826
4189
  const genSpinner = ora("Generating module-specific files...").start();
3827
4190
  const routeRegistryContent = generateRouteRegistry(resolved);
3828
4191
  await fs.writeFile(path.join(targetDir, "src/server/route-registry.ts"), routeRegistryContent);
4192
+ const rpcSurfaceContent = generateRpcSurface(resolved, (relPath) => {
4193
+ const full = path.join(targetDir, "src/server", `${relPath}.ts`);
4194
+ return fs.existsSync(full) ? fs.readFileSync(full, "utf-8") : null;
4195
+ });
4196
+ await fs.writeFile(path.join(targetDir, "src/server/rpc-surface.ts"), rpcSurfaceContent);
4197
+ for (const [rel, from, to] of [
4198
+ ["src/client/App.tsx", "presetId = 'todo'", `presetId = '${selectedPreset.id}'`],
4199
+ [
4200
+ "src/client/entry-server.tsx",
4201
+ "const preset = 'todo'",
4202
+ `const preset = '${selectedPreset.id}'`
4203
+ ]
4204
+ ]) {
4205
+ const p = path.join(targetDir, rel);
4206
+ if (await fs.pathExists(p)) {
4207
+ let content = await fs.readFile(p, "utf-8");
4208
+ content = content.split(from).join(to);
4209
+ await fs.writeFile(p, content);
4210
+ }
4211
+ }
4212
+ if (!resolved.hasClient) {
4213
+ const barrelPath = path.join(targetDir, "src/shared/index.ts");
4214
+ if (await fs.pathExists(barrelPath)) {
4215
+ let barrel = await fs.readFile(barrelPath, "utf-8");
4216
+ barrel = barrel.replaceAll("export * from './hooks'\n", "");
4217
+ await fs.writeFile(barrelPath, barrel);
4218
+ }
4219
+ }
4220
+ const isrModulesContent = generateIsrModules(
4221
+ resolved,
4222
+ (moduleName, relPath) => fs.existsSync(path.join(targetDir, "src/server", `module-${moduleName}`, relPath))
4223
+ );
4224
+ await fs.writeFile(path.join(targetDir, "src/server/isr-modules.ts"), isrModulesContent);
4225
+ if (resolved.hasClient) {
4226
+ const entryStoresContent = generateEntryStores(
4227
+ resolved,
4228
+ (_moduleName, relPath) => fs.existsSync(path.join(targetDir, "src/client/stores", relPath))
4229
+ );
4230
+ await fs.ensureDir(path.join(targetDir, "src/client/stores"));
4231
+ await fs.writeFile(
4232
+ path.join(targetDir, "src/client/stores/entry-stores.ts"),
4233
+ entryStoresContent
4234
+ );
4235
+ }
3829
4236
  const dbSchemaContent = generateDbSchemaBarrel(resolved);
3830
4237
  await fs.writeFile(path.join(targetDir, "src/server/db/schema/index.ts"), dbSchemaContent);
3831
4238
  if (resolved.hasClient) {
@@ -3932,6 +4339,7 @@ async function createProject(projectNameOrOptions, useCurrentDir = false, preset
3932
4339
  try {
3933
4340
  const { execSync } = await import('child_process');
3934
4341
  execSync("npm install --legacy-peer-deps", {
4342
+ env: { ...process.env, PUPPETEER_SKIP_DOWNLOAD: "1" },
3935
4343
  cwd: targetDir,
3936
4344
  stdio: "pipe",
3937
4345
  timeout: 3e5
@@ -3990,6 +4398,22 @@ async function createProject(projectNameOrOptions, useCurrentDir = false, preset
3990
4398
  }
3991
4399
  console.log("");
3992
4400
  console.log(chalk.gray(" Happy coding! \u{1F41F}"));
4401
+ const home = process.env.HOME || "";
4402
+ const agents = [];
4403
+ if (home) {
4404
+ if (await fs.pathExists(path.join(home, ".zcode"))) agents.push("ZCode");
4405
+ if (await fs.pathExists(path.join(home, ".codex"))) agents.push("Codex");
4406
+ if (await fs.pathExists(path.join(home, ".claude"))) agents.push("Claude Code");
4407
+ if (await fs.pathExists(path.join(home, ".cursor"))) agents.push("Cursor");
4408
+ }
4409
+ if (agents.length > 0) {
4410
+ console.log("");
4411
+ console.log(chalk.cyan(` \u{1FA9D} Agent hooks (\u68C0\u6D4B\u5230 ${agents.join(" / ")}):`));
4412
+ console.log(
4413
+ chalk.gray(" \u5DE5\u4F5C\u533A\u94A9\u5B50\u5DF2\u751F\u6210\uFF08.zcode/\uFF0C\u7981\u6B62 --no-verify \u63D0\u4EA4\uFF09\uFF0CZCode \u81EA\u52A8\u751F\u6548")
4414
+ );
4415
+ console.log(chalk.gray(" \u540C\u6B65\u5230\u5168\u5C40\uFF08Codex \u7B49\uFF09\uFF1Anpm run hooks:sync -- --global"));
4416
+ }
3993
4417
  console.log("");
3994
4418
  } catch (error) {
3995
4419
  if (error instanceof ScaffoldError) throw error;