create-fullstack-scaffold 0.1.1 → 0.2.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 (155) hide show
  1. package/dist/cli/index.js +1673 -696
  2. package/dist/cli/index.js.map +1 -1
  3. package/package.json +16 -10
  4. package/template/.husky/pre-commit +1 -1
  5. package/template/modules.config.ts +29 -2
  6. package/template/package.json +12 -12
  7. package/template/patches/{typescript+5.8.3.patch → typescript+5.9.3.patch} +2 -4
  8. package/template/playwright.config.ts +7 -1
  9. package/template/src/admin/App.tsx +2 -2
  10. package/template/src/admin/components/CaptchaModal.tsx +7 -9
  11. package/template/src/admin/layouts/Header.tsx +56 -22
  12. package/template/src/admin/layouts/Layout.tsx +14 -9
  13. package/template/src/admin/layouts/Sidebar.tsx +122 -105
  14. package/template/src/admin/pages/CategoryManagementPage.tsx +241 -0
  15. package/template/src/admin/pages/ContentPage.tsx +2 -0
  16. package/template/src/admin/pages/DashboardPage.tsx +2 -0
  17. package/template/src/admin/pages/DisputesPage.tsx +2 -0
  18. package/template/src/admin/pages/MediaTestPage.tsx +1 -1
  19. package/template/src/admin/pages/OrdersPage.tsx +2 -0
  20. package/template/src/admin/pages/PluginDashboardPage.tsx +297 -0
  21. package/template/src/admin/pages/PluginManagementPage.tsx +340 -0
  22. package/template/src/admin/pages/PluginReviewPage.tsx +255 -0
  23. package/template/src/admin/pages/SystemLogsPage.tsx +11 -2
  24. package/template/src/admin/pages/TicketsPage.tsx +2 -0
  25. package/template/src/admin/pages/UsersPage.tsx +3 -2
  26. package/template/src/admin/stores/adminStore.ts +5 -0
  27. package/template/src/cli/index.ts +17 -19
  28. package/template/src/cli/modules/auth/index.ts +65 -0
  29. package/template/src/cli/modules/config/index.ts +74 -77
  30. package/template/src/cli/modules/index.ts +28 -6
  31. package/template/src/cli/modules/notification/index.ts +95 -79
  32. package/template/src/cli/modules/plugin/index.ts +111 -0
  33. package/template/src/cli/modules/todo/index.ts +99 -51
  34. package/template/src/cli/utils/auto-command.ts +7 -25
  35. package/template/src/cli/utils/index.ts +3 -1
  36. package/template/src/client/App.tsx +36 -16
  37. package/template/src/client/Layout.tsx +67 -10
  38. package/template/src/client/components/AuthButton.tsx +25 -18
  39. package/template/src/client/components/BottomTabBar.tsx +107 -0
  40. package/template/src/client/components/Navigation.tsx +162 -52
  41. package/template/src/client/components/__tests__/App.test.tsx +48 -32
  42. package/template/src/client/components/__tests__/AuthButton.test.tsx +78 -77
  43. package/template/src/client/components/__tests__/Navigation.test.tsx +31 -20
  44. package/template/src/client/components/index.ts +1 -0
  45. package/template/src/client/contexts/PresetContext.tsx +10 -0
  46. package/template/src/client/main.tsx +63 -8
  47. package/template/src/client/pages/CartPage.tsx +244 -0
  48. package/template/src/client/pages/CategoriesPage.tsx +100 -0
  49. package/template/src/client/pages/ContentDetailPage.tsx +9 -14
  50. package/template/src/client/pages/ContentListPage.tsx +4 -11
  51. package/template/src/client/pages/DashboardPage.tsx +261 -0
  52. package/template/src/client/pages/DeveloperDashboardPage.tsx +211 -0
  53. package/template/src/client/pages/LoginPage.tsx +127 -0
  54. package/template/src/client/pages/OrdersPage.tsx +196 -0
  55. package/template/src/client/pages/PluginDetailPage.tsx +345 -0
  56. package/template/src/client/pages/PluginsPage.tsx +223 -0
  57. package/template/src/client/pages/ProfilePage.tsx +206 -0
  58. package/template/src/client/pages/PublishPage.tsx +336 -0
  59. package/template/src/client/pages/RegisterPage.tsx +136 -0
  60. package/template/src/client/pages/SearchPage.tsx +204 -0
  61. package/template/src/client/pages/SettingsPage.tsx +220 -0
  62. package/template/src/client/pages/TopicsPage.tsx +180 -0
  63. package/template/src/client/pages/__tests__/LoginPage.test.tsx +170 -0
  64. package/template/src/client/pages/__tests__/RegisterPage.test.tsx +168 -0
  65. package/template/src/client/preset-ui-config.ts +492 -0
  66. package/template/src/client/services/apiClient.ts +14 -4
  67. package/template/src/client/stores/__tests__/authStore.test.ts +293 -38
  68. package/template/src/client/stores/__tests__/todoStore.test.ts +7 -17
  69. package/template/src/client/stores/authStore.ts +58 -5
  70. package/template/src/client/stores/chatWSStore.ts +9 -0
  71. package/template/src/client/stores/notificationStore.ts +4 -0
  72. package/template/src/client/stores/pluginStore.ts +279 -0
  73. package/template/src/client/stores/todoStore.ts +2 -6
  74. package/template/src/server/core/__tests__/isr-cache.test.ts +117 -0
  75. package/template/src/server/core/__tests__/isr-invalidation.test.ts +72 -0
  76. package/template/src/server/core/__tests__/ssr-renderer.test.ts +89 -0
  77. package/template/src/server/core/isr-cache.ts +239 -0
  78. package/template/src/server/core/isr-invalidation.ts +45 -0
  79. package/template/src/server/core/module-loader.ts +14 -7
  80. package/template/src/server/core/ssr-renderer.ts +240 -0
  81. package/template/src/server/db/init.ts +257 -10
  82. package/template/src/server/db/schema/developers.ts +20 -0
  83. package/template/src/server/db/schema/index.ts +2 -0
  84. package/template/src/server/db/schema/plugins.ts +114 -0
  85. package/template/src/server/db/test-setup.ts +91 -0
  86. package/template/src/server/entries/cloudflare.ts +79 -7
  87. package/template/src/server/entries/node.ts +48 -5
  88. package/template/src/server/middleware/__tests__/captcha.test.ts +23 -13
  89. package/template/src/server/middleware/auth.ts +8 -1
  90. package/template/src/server/middleware/captcha.ts +14 -4
  91. package/template/src/server/middleware/rate-limit.ts +7 -2
  92. package/template/src/server/module-admin/module.ts +9 -5
  93. package/template/src/server/module-admin/routes/admin-notification-routes.ts +43 -14
  94. package/template/src/server/module-admin/routes/admin-routes.ts +0 -2
  95. package/template/src/server/module-admin/routes/client-auth-routes.ts +90 -0
  96. package/template/src/server/module-admin/routes/dashboard-routes.ts +79 -0
  97. package/template/src/server/module-admin/services/admin-service.ts +68 -11
  98. package/template/src/server/module-auth/__tests__/auth-service.test.ts +239 -0
  99. package/template/src/server/module-auth/index.ts +7 -0
  100. package/template/src/server/module-auth/module.ts +40 -0
  101. package/template/src/server/module-auth/routes/auth-routes.ts +94 -0
  102. package/template/src/server/module-auth/routes/profile-routes.ts +31 -0
  103. package/template/src/server/module-auth/services/auth-service.ts +100 -0
  104. package/template/src/server/module-content/module.ts +10 -4
  105. package/template/src/server/module-content/routes/public-content-routes.ts +2 -2
  106. package/template/src/server/module-content/routes/topics-routes.ts +205 -0
  107. package/template/src/server/module-content/services/content-service.ts +18 -2
  108. package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -1
  109. package/template/src/server/module-dispute/services/dispute-service.ts +1 -1
  110. package/template/src/server/module-notifications/__tests__/sse-rpc.test.ts +14 -14
  111. package/template/src/server/module-notifications/routes/notification-routes.ts +34 -8
  112. package/template/src/server/module-order/__tests__/order-route.test.ts +22 -2
  113. package/template/src/server/module-order/module.ts +15 -0
  114. package/template/src/server/module-order/routes/cart-routes.ts +103 -0
  115. package/template/src/server/module-order/routes/orders-mock-routes.ts +67 -0
  116. package/template/src/server/module-order/services/order-service.ts +1 -1
  117. package/template/src/server/module-plugin/__tests__/plugin-query-service.test.ts +203 -0
  118. package/template/src/server/module-plugin/__tests__/plugin-service.test.ts +234 -0
  119. package/template/src/server/module-plugin/index.ts +2 -0
  120. package/template/src/server/module-plugin/module.ts +52 -0
  121. package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +261 -0
  122. package/template/src/server/module-plugin/routes/plugin-routes.ts +354 -0
  123. package/template/src/server/module-plugin/services/admin-category-service.ts +99 -0
  124. package/template/src/server/module-plugin/services/admin-plugin-service.ts +170 -0
  125. package/template/src/server/module-plugin/services/admin-stats-service.ts +41 -0
  126. package/template/src/server/module-plugin/services/plugin-query-service.ts +360 -0
  127. package/template/src/server/module-plugin/services/plugin-review-service.ts +95 -0
  128. package/template/src/server/module-plugin/services/plugin-service.ts +163 -0
  129. package/template/src/server/module-ticket/services/ticket-service.ts +1 -1
  130. package/template/src/server/module-todos/routes/todos-routes.ts +0 -4
  131. package/template/src/server/route-registry.ts +16 -1
  132. package/template/src/server/test-utils/test-client.ts +1 -2
  133. package/template/src/server/utils/auth.ts +6 -0
  134. package/template/src/server/utils/json.ts +13 -0
  135. package/template/src/shared/core/module-manifest.ts +3 -6
  136. package/template/src/shared/modules/auth/index.ts +12 -0
  137. package/template/src/shared/modules/auth/schemas.ts +50 -0
  138. package/template/src/shared/modules/cart/index.ts +1 -0
  139. package/template/src/shared/modules/cart/schemas.ts +41 -0
  140. package/template/src/shared/modules/community/index.ts +1 -0
  141. package/template/src/shared/modules/community/schemas.ts +58 -0
  142. package/template/src/shared/modules/dashboard/index.ts +1 -0
  143. package/template/src/shared/modules/dashboard/schemas.ts +35 -0
  144. package/template/src/shared/modules/index.ts +41 -0
  145. package/template/src/shared/modules/order/schemas.ts +29 -0
  146. package/template/src/shared/modules/plugins/index.ts +48 -0
  147. package/template/src/shared/modules/plugins/schemas.ts +227 -0
  148. package/template/src/shared/schemas/index.ts +130 -0
  149. package/template/tests/e2e/todo.spec.ts +23 -18
  150. package/template/tests/e2e/visual-screenshots.spec.ts +1824 -0
  151. package/template/uploads/.gitkeep +0 -0
  152. package/template/vite.config.ts +2 -1
  153. package/template/vitest.setup.ts +11 -0
  154. package/template/wrangler.toml +4 -3
  155. package/template/package-lock.json +0 -14554
@@ -1,10 +1,7 @@
1
1
  import { getDb } from './driver'
2
2
  import { permissions, roles, rolePermissions } from './schema'
3
3
  import { logger } from '../utils/logger'
4
- import { seedOrdersIfEmpty } from '../module-order/services/order-service'
5
- import { seedTicketsIfEmpty } from '../module-ticket/services/ticket-service'
6
- import { seedDisputesIfEmpty } from '../module-dispute/services/dispute-service'
7
- import { seedContentsIfEmpty } from '../module-content/services/content-service'
4
+ import { generateUUID } from '../utils/uuid'
8
5
 
9
6
  const log = logger.db()
10
7
 
@@ -355,6 +352,254 @@ const initialRolePermissions = [
355
352
  { roleId: 'role_user', permissionId: 'perm_order_view' },
356
353
  ]
357
354
 
355
+ async function seedPluginsIfEmpty() {
356
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- dynamic import for presets that may not have plugin module
357
+ let plugins: any
358
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
359
+ let pluginVersions: any
360
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
361
+ let pluginCategories: any
362
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
363
+ let pluginCategoryMappings: any
364
+ try {
365
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
366
+ const schema = (await import('./schema')) as Record<string, any>
367
+ plugins = schema.plugins
368
+ pluginVersions = schema.pluginVersions
369
+ pluginCategories = schema.pluginCategories
370
+ pluginCategoryMappings = schema.pluginCategoryMappings
371
+ if (!plugins) return
372
+ } catch {
373
+ return
374
+ }
375
+
376
+ const db = await getDb()
377
+
378
+ const existing = await db.select().from(plugins)
379
+ if (existing.length > 0) return
380
+
381
+ log.info({}, 'Seeding plugins...')
382
+
383
+ const sampleCategories = [
384
+ {
385
+ id: generateUUID(),
386
+ name: 'UI & Design',
387
+ slug: 'ui-design',
388
+ description: 'Visual customization and theming',
389
+ icon: 'palette',
390
+ sortOrder: 1,
391
+ },
392
+ {
393
+ id: generateUUID(),
394
+ name: 'Analytics',
395
+ slug: 'analytics',
396
+ description: 'Data tracking and reporting',
397
+ icon: 'chart',
398
+ sortOrder: 2,
399
+ },
400
+ {
401
+ id: generateUUID(),
402
+ name: 'SEO & Marketing',
403
+ slug: 'seo-marketing',
404
+ description: 'Search optimization and growth tools',
405
+ icon: 'trending-up',
406
+ sortOrder: 3,
407
+ },
408
+ {
409
+ id: generateUUID(),
410
+ name: 'Developer Tools',
411
+ slug: 'developer-tools',
412
+ description: 'Utilities for developers',
413
+ icon: 'code',
414
+ sortOrder: 4,
415
+ },
416
+ {
417
+ id: generateUUID(),
418
+ name: 'Integration',
419
+ slug: 'integration',
420
+ description: 'Third-party service integrations',
421
+ icon: 'plug',
422
+ sortOrder: 5,
423
+ },
424
+ ]
425
+
426
+ await db.insert(pluginCategories).values(sampleCategories)
427
+
428
+ const samplePlugins = [
429
+ {
430
+ id: generateUUID(),
431
+ name: 'Theme Engine',
432
+ slug: 'theme-engine',
433
+ description:
434
+ 'Advanced theming system with dark mode, custom color palettes, and responsive layout controls.',
435
+ readme:
436
+ '# Theme Engine\n\nA powerful theming plugin for customizing the look and feel of your application.',
437
+ authorId: '1',
438
+ authorName: 'superadmin',
439
+ repositoryUrl: 'https://github.com/example/theme-engine',
440
+ homepageUrl: 'https://theme-engine.example.com',
441
+ license: 'MIT',
442
+ version: '2.1.0',
443
+ status: 'approved' as const,
444
+ downloadCount: 1520,
445
+ viewCount: 4320,
446
+ featured: true,
447
+ screenshotUrl: 'https://picsum.photos/seed/theme/800/400',
448
+ tags: '["theme","dark-mode","customization"]',
449
+ },
450
+ {
451
+ id: generateUUID(),
452
+ slug: 'analytics-dashboard',
453
+ name: 'Analytics Dashboard',
454
+ description:
455
+ 'Real-time analytics dashboard with charts, heatmaps, and user behavior tracking.',
456
+ readme: '# Analytics Dashboard\n\nTrack user engagement and visualize data in real time.',
457
+ authorId: '1',
458
+ authorName: 'superadmin',
459
+ repositoryUrl: 'https://github.com/example/analytics-dashboard',
460
+ homepageUrl: 'https://analytics.example.com',
461
+ license: 'Apache-2.0',
462
+ version: '1.5.3',
463
+ status: 'approved' as const,
464
+ downloadCount: 980,
465
+ viewCount: 2100,
466
+ featured: true,
467
+ screenshotUrl: 'https://picsum.photos/seed/analytics/800/400',
468
+ tags: '["analytics","dashboard","charts"]',
469
+ },
470
+ {
471
+ id: generateUUID(),
472
+ slug: 'seo-optimizer',
473
+ name: 'SEO Optimizer',
474
+ description:
475
+ 'Automated SEO optimization with meta tags, sitemap generation, and structured data support.',
476
+ authorId: '2',
477
+ authorName: 'customerservice',
478
+ repositoryUrl: 'https://github.com/example/seo-optimizer',
479
+ license: 'MIT',
480
+ version: '1.2.0',
481
+ status: 'approved' as const,
482
+ downloadCount: 740,
483
+ viewCount: 1800,
484
+ featured: false,
485
+ screenshotUrl: 'https://picsum.photos/seed/seo/800/400',
486
+ tags: '["seo","meta","sitemap"]',
487
+ },
488
+ {
489
+ id: generateUUID(),
490
+ slug: 'api-toolkit',
491
+ name: 'API Toolkit',
492
+ description:
493
+ 'Developer toolkit for building and testing REST APIs with auto-documentation and mock servers.',
494
+ authorId: '2',
495
+ authorName: 'customerservice',
496
+ repositoryUrl: 'https://github.com/example/api-toolkit',
497
+ homepageUrl: 'https://api-toolkit.example.com',
498
+ license: 'MIT',
499
+ version: '3.0.1',
500
+ status: 'approved' as const,
501
+ downloadCount: 2100,
502
+ viewCount: 5600,
503
+ featured: true,
504
+ screenshotUrl: 'https://picsum.photos/seed/apitoolkit/800/400',
505
+ tags: '["api","developer","testing","documentation"]',
506
+ },
507
+ {
508
+ id: generateUUID(),
509
+ slug: 'notification-hub',
510
+ name: 'Notification Hub',
511
+ description:
512
+ 'Unified notification management supporting email, SMS, push, and in-app notifications.',
513
+ authorId: '3',
514
+ authorName: 'user1',
515
+ repositoryUrl: 'https://github.com/example/notification-hub',
516
+ license: 'ISC',
517
+ version: '1.0.0',
518
+ status: 'pending' as const,
519
+ downloadCount: 0,
520
+ viewCount: 50,
521
+ featured: false,
522
+ screenshotUrl: 'https://picsum.photos/seed/notify/800/400',
523
+ tags: '["notification","email","push"]',
524
+ },
525
+ ]
526
+
527
+ await db.insert(plugins).values(samplePlugins)
528
+
529
+ const sampleVersions = [
530
+ {
531
+ id: generateUUID(),
532
+ pluginId: samplePlugins[0].id,
533
+ version: '2.1.0',
534
+ changelog: 'Added responsive layout controls',
535
+ status: 'approved' as const,
536
+ },
537
+ {
538
+ id: generateUUID(),
539
+ pluginId: samplePlugins[0].id,
540
+ version: '2.0.0',
541
+ changelog: 'Major rewrite with dark mode support',
542
+ status: 'approved' as const,
543
+ },
544
+ {
545
+ id: generateUUID(),
546
+ pluginId: samplePlugins[1].id,
547
+ version: '1.5.3',
548
+ changelog: 'Fixed heatmap rendering',
549
+ status: 'approved' as const,
550
+ },
551
+ {
552
+ id: generateUUID(),
553
+ pluginId: samplePlugins[1].id,
554
+ version: '1.5.0',
555
+ changelog: 'Added real-time tracking',
556
+ status: 'approved' as const,
557
+ },
558
+ {
559
+ id: generateUUID(),
560
+ pluginId: samplePlugins[2].id,
561
+ version: '1.2.0',
562
+ changelog: 'Structured data support',
563
+ status: 'approved' as const,
564
+ },
565
+ {
566
+ id: generateUUID(),
567
+ pluginId: samplePlugins[3].id,
568
+ version: '3.0.1',
569
+ changelog: 'Auto-documentation improvements',
570
+ status: 'approved' as const,
571
+ },
572
+ {
573
+ id: generateUUID(),
574
+ pluginId: samplePlugins[3].id,
575
+ version: '3.0.0',
576
+ changelog: 'Major version with mock server',
577
+ status: 'approved' as const,
578
+ },
579
+ {
580
+ id: generateUUID(),
581
+ pluginId: samplePlugins[4].id,
582
+ version: '1.0.0',
583
+ changelog: 'Initial release',
584
+ status: 'pending' as const,
585
+ },
586
+ ]
587
+
588
+ await db.insert(pluginVersions).values(sampleVersions)
589
+
590
+ const sampleMappings = [
591
+ { pluginId: samplePlugins[0].id, categoryId: sampleCategories[0].id },
592
+ { pluginId: samplePlugins[1].id, categoryId: sampleCategories[1].id },
593
+ { pluginId: samplePlugins[2].id, categoryId: sampleCategories[2].id },
594
+ { pluginId: samplePlugins[3].id, categoryId: sampleCategories[3].id },
595
+ { pluginId: samplePlugins[4].id, categoryId: sampleCategories[4].id },
596
+ ]
597
+
598
+ await db.insert(pluginCategoryMappings).values(sampleMappings)
599
+
600
+ log.info({}, 'Plugin seeding complete!')
601
+ }
602
+
358
603
  export async function initializeDatabase() {
359
604
  const db = await getDb()
360
605
 
@@ -402,11 +647,13 @@ export async function initializeDatabase() {
402
647
  log.info({}, 'Database initialization complete!')
403
648
 
404
649
  log.info({}, 'Seeding module data...')
405
- await Promise.all([
406
- seedOrdersIfEmpty(),
407
- seedTicketsIfEmpty(),
408
- seedDisputesIfEmpty(),
409
- seedContentsIfEmpty(),
410
- ])
650
+ const moduleSeeders = [
651
+ () => import('../module-order/services/order-service').then(m => m.seedOrdersIfEmpty()),
652
+ () => import('../module-ticket/services/ticket-service').then(m => m.seedTicketsIfEmpty()),
653
+ () => import('../module-dispute/services/dispute-service').then(m => m.seedDisputesIfEmpty()),
654
+ () => import('../module-content/services/content-service').then(m => m.seedContentsIfEmpty()),
655
+ () => seedPluginsIfEmpty(),
656
+ ]
657
+ await Promise.all(moduleSeeders.map(fn => fn().catch(() => {})))
411
658
  log.info({}, 'Module data seeding complete!')
412
659
  }
@@ -0,0 +1,20 @@
1
+ import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
2
+ import { sql } from 'drizzle-orm'
3
+
4
+ export const developers = sqliteTable('developers', {
5
+ id: text('id').primaryKey(),
6
+ username: text('username').notNull().unique(),
7
+ email: text('email').notNull().unique(),
8
+ passwordHash: text('password_hash').notNull(),
9
+ role: text('role').notNull().default('developer'),
10
+ apiKey: text('api_key').notNull().unique(),
11
+ createdAt: integer('created_at', { mode: 'timestamp' })
12
+ .notNull()
13
+ .default(sql`(unixepoch() * 1000)`),
14
+ updatedAt: integer('updated_at', { mode: 'timestamp' })
15
+ .notNull()
16
+ .default(sql`(unixepoch() * 1000)`),
17
+ })
18
+
19
+ export type DeveloperTable = typeof developers.$inferSelect
20
+ export type NewDeveloper = typeof developers.$inferInsert
@@ -12,3 +12,5 @@ export * from './orders'
12
12
  export * from './tickets'
13
13
  export * from './disputes'
14
14
  export * from './contents'
15
+ export * from './developers'
16
+ export * from './plugins'
@@ -0,0 +1,114 @@
1
+ import { sqliteTable, text, integer, uniqueIndex } from 'drizzle-orm/sqlite-core'
2
+ import { sql } from 'drizzle-orm'
3
+
4
+ export const pluginStatus = ['pending', 'approved', 'rejected'] as const
5
+ export type PluginStatus = (typeof pluginStatus)[number]
6
+
7
+ export const plugins = sqliteTable('plugins', {
8
+ id: text('id').primaryKey(),
9
+ name: text('name').notNull(),
10
+ slug: text('slug').notNull().unique(),
11
+ description: text('description').notNull().default(''),
12
+ readme: text('readme'),
13
+ authorId: text('author_id').notNull(),
14
+ authorName: text('author_name').notNull(),
15
+ repositoryUrl: text('repository_url'),
16
+ homepageUrl: text('homepage_url'),
17
+ npmPackage: text('npm_package'),
18
+ license: text('license'),
19
+ version: text('version').notNull().default('0.0.1'),
20
+ status: text('status', { enum: pluginStatus }).notNull().default('pending'),
21
+ downloadCount: integer('download_count').notNull().default(0),
22
+ viewCount: integer('view_count').notNull().default(0),
23
+ featured: integer('featured', { mode: 'boolean' }).notNull().default(false),
24
+ screenshotUrl: text('screenshot_url'),
25
+ siteUrls: text('site_urls'),
26
+ tags: text('tags'),
27
+ commands: text('commands'),
28
+ rejectReason: text('reject_reason'),
29
+ createdAt: integer('created_at', { mode: 'timestamp' })
30
+ .notNull()
31
+ .default(sql`(unixepoch() * 1000)`),
32
+ updatedAt: integer('updated_at', { mode: 'timestamp' })
33
+ .notNull()
34
+ .default(sql`(unixepoch() * 1000)`),
35
+ })
36
+
37
+ export const pluginVersions = sqliteTable(
38
+ 'plugin_versions',
39
+ {
40
+ id: text('id').primaryKey(),
41
+ pluginId: text('plugin_id')
42
+ .notNull()
43
+ .references(() => plugins.id, { onDelete: 'cascade' }),
44
+ version: text('version').notNull(),
45
+ changelog: text('changelog'),
46
+ packageUrl: text('package_url'),
47
+ fileSize: integer('file_size'),
48
+ checksum: text('checksum'),
49
+ status: text('status', { enum: pluginStatus }).notNull().default('pending'),
50
+ publishedAt: integer('published_at', { mode: 'timestamp' })
51
+ .notNull()
52
+ .default(sql`(unixepoch() * 1000)`),
53
+ },
54
+ table => ({
55
+ pluginVersionUnique: uniqueIndex('plugin_version_unique').on(table.pluginId, table.version),
56
+ })
57
+ )
58
+
59
+ export const pluginReviews = sqliteTable(
60
+ 'plugin_reviews',
61
+ {
62
+ id: text('id').primaryKey(),
63
+ pluginId: text('plugin_id')
64
+ .notNull()
65
+ .references(() => plugins.id, { onDelete: 'cascade' }),
66
+ userId: text('user_id').notNull(),
67
+ userName: text('user_name').notNull(),
68
+ rating: integer('rating').notNull(),
69
+ title: text('title'),
70
+ content: text('content'),
71
+ createdAt: integer('created_at', { mode: 'timestamp' })
72
+ .notNull()
73
+ .default(sql`(unixepoch() * 1000)`),
74
+ },
75
+ table => ({
76
+ pluginUserUnique: uniqueIndex('plugin_review_user_unique').on(table.pluginId, table.userId),
77
+ })
78
+ )
79
+
80
+ export const pluginCategories = sqliteTable('plugin_categories', {
81
+ id: text('id').primaryKey(),
82
+ name: text('name').notNull().unique(),
83
+ slug: text('slug').notNull().unique(),
84
+ description: text('description'),
85
+ icon: text('icon'),
86
+ sortOrder: integer('sort_order').notNull().default(0),
87
+ })
88
+
89
+ export const pluginCategoryMappings = sqliteTable(
90
+ 'plugin_category_mappings',
91
+ {
92
+ pluginId: text('plugin_id')
93
+ .notNull()
94
+ .references(() => plugins.id, { onDelete: 'cascade' }),
95
+ categoryId: text('category_id')
96
+ .notNull()
97
+ .references(() => pluginCategories.id, { onDelete: 'cascade' }),
98
+ },
99
+ table => ({
100
+ mappingUnique: uniqueIndex('plugin_category_mapping_unique').on(
101
+ table.pluginId,
102
+ table.categoryId
103
+ ),
104
+ })
105
+ )
106
+
107
+ export type PluginTable = typeof plugins.$inferSelect
108
+ export type NewPlugin = typeof plugins.$inferInsert
109
+ export type PluginVersionTable = typeof pluginVersions.$inferSelect
110
+ export type NewPluginVersion = typeof pluginVersions.$inferInsert
111
+ export type PluginReviewTable = typeof pluginReviews.$inferSelect
112
+ export type NewPluginReview = typeof pluginReviews.$inferInsert
113
+ export type PluginCategoryTable = typeof pluginCategories.$inferSelect
114
+ export type NewPluginCategory = typeof pluginCategories.$inferInsert
@@ -191,6 +191,87 @@ export async function setupTestDatabase(): Promise<void> {
191
191
  created_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL,
192
192
  updated_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL
193
193
  );
194
+
195
+ CREATE TABLE IF NOT EXISTS developers (
196
+ id TEXT PRIMARY KEY NOT NULL,
197
+ username TEXT NOT NULL UNIQUE,
198
+ email TEXT NOT NULL UNIQUE,
199
+ password_hash TEXT NOT NULL,
200
+ role TEXT DEFAULT 'developer' NOT NULL,
201
+ api_key TEXT NOT NULL UNIQUE,
202
+ created_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL,
203
+ updated_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL
204
+ );
205
+
206
+ CREATE TABLE IF NOT EXISTS plugins (
207
+ id TEXT PRIMARY KEY NOT NULL,
208
+ name TEXT NOT NULL,
209
+ slug TEXT NOT NULL UNIQUE,
210
+ description TEXT NOT NULL DEFAULT '',
211
+ readme TEXT,
212
+ author_id TEXT NOT NULL,
213
+ author_name TEXT NOT NULL,
214
+ repository_url TEXT,
215
+ homepage_url TEXT,
216
+ npm_package TEXT,
217
+ license TEXT,
218
+ version TEXT NOT NULL DEFAULT '0.0.1',
219
+ status TEXT NOT NULL DEFAULT 'pending',
220
+ download_count INTEGER NOT NULL DEFAULT 0,
221
+ view_count INTEGER NOT NULL DEFAULT 0,
222
+ featured INTEGER NOT NULL DEFAULT 0,
223
+ screenshot_url TEXT,
224
+ site_urls TEXT,
225
+ tags TEXT,
226
+ commands TEXT,
227
+ reject_reason TEXT,
228
+ created_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL,
229
+ updated_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL
230
+ );
231
+
232
+ CREATE TABLE IF NOT EXISTS plugin_versions (
233
+ id TEXT PRIMARY KEY NOT NULL,
234
+ plugin_id TEXT NOT NULL,
235
+ version TEXT NOT NULL,
236
+ changelog TEXT,
237
+ package_url TEXT,
238
+ file_size INTEGER,
239
+ checksum TEXT,
240
+ status TEXT NOT NULL DEFAULT 'pending',
241
+ published_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL,
242
+ FOREIGN KEY (plugin_id) REFERENCES plugins(id) ON DELETE CASCADE,
243
+ UNIQUE(plugin_id, version)
244
+ );
245
+
246
+ CREATE TABLE IF NOT EXISTS plugin_reviews (
247
+ id TEXT PRIMARY KEY NOT NULL,
248
+ plugin_id TEXT NOT NULL,
249
+ user_id TEXT NOT NULL,
250
+ user_name TEXT NOT NULL,
251
+ rating INTEGER NOT NULL,
252
+ title TEXT,
253
+ content TEXT,
254
+ created_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL,
255
+ FOREIGN KEY (plugin_id) REFERENCES plugins(id) ON DELETE CASCADE,
256
+ UNIQUE(plugin_id, user_id)
257
+ );
258
+
259
+ CREATE TABLE IF NOT EXISTS plugin_categories (
260
+ id TEXT PRIMARY KEY NOT NULL,
261
+ name TEXT NOT NULL UNIQUE,
262
+ slug TEXT NOT NULL UNIQUE,
263
+ description TEXT,
264
+ icon TEXT,
265
+ sort_order INTEGER NOT NULL DEFAULT 0
266
+ );
267
+
268
+ CREATE TABLE IF NOT EXISTS plugin_category_mappings (
269
+ plugin_id TEXT NOT NULL,
270
+ category_id TEXT NOT NULL,
271
+ FOREIGN KEY (plugin_id) REFERENCES plugins(id) ON DELETE CASCADE,
272
+ FOREIGN KEY (category_id) REFERENCES plugin_categories(id) ON DELETE CASCADE,
273
+ UNIQUE(plugin_id, category_id)
274
+ );
194
275
  `
195
276
 
196
277
  const statements = migrationSQL.split(';').filter(s => s.trim())
@@ -643,5 +724,15 @@ export async function cleanupTestDatabase(): Promise<void> {
643
724
  await client.execute('DELETE FROM orders')
644
725
  await client.execute('DELETE FROM disputes')
645
726
  await client.execute('DELETE FROM contents')
727
+ try {
728
+ await client.execute('DELETE FROM plugin_category_mappings')
729
+ await client.execute('DELETE FROM plugin_reviews')
730
+ await client.execute('DELETE FROM plugin_versions')
731
+ await client.execute('DELETE FROM plugins')
732
+ await client.execute('DELETE FROM plugin_categories')
733
+ await client.execute('DELETE FROM developers')
734
+ } catch {
735
+ // Plugin tables may not exist in all test environments
736
+ }
646
737
  }
647
738
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * @framework-baseline e350401421193896
3
3
  * @framework-modify
4
- * @reason 统一错误响应格式为 JSON,确保所有错误都返回结构化数据;移除冗余的 globalThis 设置
5
- * @impact 影响 Cloudflare Workers 环境的错误响应格式
4
+ * @reason 集成 ISR 缓存层,为页面路由提供增量静态再生能力
5
+ * @impact 影响 Cloudflare Workers 环境的页面响应流程,新增 ISR 缓存查找/存储/失效逻辑
6
6
  *
7
7
  * Note: In Cloudflare Workers, each request runs in its own isolate,
8
8
  * so globalThis is request-scoped and there's no race condition risk.
@@ -15,6 +15,9 @@ import { getDb } from '../db/driver-cloudflare'
15
15
  import { RealtimeDurableObject } from '@server/core'
16
16
  import { setRuntimeAdapter } from '@server/core/runtime'
17
17
  import { getCloudflareRuntimeAdapter } from '@server/core/runtime-cloudflare'
18
+ import { createISRCache, isISRRoute } from '@server/core/isr-cache'
19
+ import { renderPage } from '@server/core/ssr-renderer'
20
+ import { setISRCache } from '@server/core/isr-invalidation'
18
21
 
19
22
  export interface CloudflareBindings extends AppBindings {
20
23
  DB: D1Database
@@ -24,8 +27,15 @@ export interface CloudflareBindings extends AppBindings {
24
27
  const runtimeAdapter = getCloudflareRuntimeAdapter()
25
28
  setRuntimeAdapter(runtimeAdapter)
26
29
 
30
+ runtimeAdapter.handleWS('/api/chat/ws')
31
+ runtimeAdapter.handleSSE('/api/notifications/stream')
32
+ runtimeAdapter.handleSSE('/api/admin/notifications/stream')
33
+
27
34
  const app = createApp<CloudflareBindings>()
28
35
 
36
+ const isrCache = createISRCache()
37
+ setISRCache(isrCache)
38
+
29
39
  const wrappedApp = app
30
40
  .use('*', async (c, next) => {
31
41
  ;(globalThis as unknown as { DB: D1Database }).DB = c.env.DB
@@ -41,21 +51,73 @@ const wrappedApp = app
41
51
  .onError((err, c) => {
42
52
  console.error('Server error:', err)
43
53
  c.res.headers.set('Content-Type', 'application/json')
44
- const statusCode =
45
- err instanceof Error && 'status' in err ? (err as { status: number }).status : 500
54
+ let statusCode = 500
55
+ if ('statusCode' in err && typeof (err as { statusCode: unknown }).statusCode === 'number') {
56
+ statusCode = (err as { statusCode: number }).statusCode
57
+ } else if ('status' in err && typeof (err as { status: unknown }).status === 'number') {
58
+ statusCode = (err as { status: number }).status
59
+ }
46
60
  const message = err.message || 'Internal server error'
47
- const responseStatus = statusCode || 500
48
- return c.json({ success: false as const, error: message, status: responseStatus }, responseStatus as 500)
61
+ return c.json(
62
+ { success: false as const, error: message, status: statusCode },
63
+ statusCode as 500
64
+ )
49
65
  })
50
66
 
51
67
  export default {
52
68
  fetch: async (request: Request, env: CloudflareBindings, ctx: ExecutionContext) => {
69
+ ;(globalThis as unknown as { DB: D1Database }).DB = env.DB
70
+
53
71
  const url = new URL(request.url)
72
+ const pathname = url.pathname
54
73
 
55
- if (url.pathname.startsWith('/api/') || url.pathname === '/health') {
74
+ if (pathname.startsWith('/api/') || pathname === '/health') {
56
75
  return wrappedApp.fetch(request, env, ctx)
57
76
  }
58
77
 
78
+ if (env.ASSETS) {
79
+ if (
80
+ pathname.startsWith('/assets/') ||
81
+ pathname.endsWith('.js') ||
82
+ pathname.endsWith('.css') ||
83
+ pathname.endsWith('.svg') ||
84
+ pathname.endsWith('.png') ||
85
+ pathname.endsWith('.ico') ||
86
+ pathname === '/vite.svg'
87
+ ) {
88
+ const assetResponse = await env.ASSETS.fetch(request)
89
+ if (assetResponse.status !== 404) {
90
+ return assetResponse
91
+ }
92
+ }
93
+ }
94
+
95
+ if (isISRRoute(pathname)) {
96
+ const result = await isrCache.lookup(pathname)
97
+
98
+ if (result.status === 'fresh' && result.html) {
99
+ return new Response(result.html, {
100
+ headers: { 'Content-Type': 'text/html;charset=UTF-8', 'X-ISR-Status': 'fresh' },
101
+ })
102
+ }
103
+
104
+ if (result.status === 'stale' && result.html) {
105
+ ctx.waitUntil(regeneratePage(pathname, env))
106
+ return new Response(result.html, {
107
+ headers: { 'Content-Type': 'text/html;charset=UTF-8', 'X-ISR-Status': 'stale' },
108
+ })
109
+ }
110
+
111
+ const rendered = await renderPage(pathname)
112
+
113
+ ctx.waitUntil(isrCache.store(pathname, rendered.html))
114
+
115
+ return new Response(rendered.html, {
116
+ status: rendered.status,
117
+ headers: { ...rendered.headers, 'X-ISR-Status': 'miss' },
118
+ })
119
+ }
120
+
59
121
  if (env.ASSETS) {
60
122
  const assetResponse = await env.ASSETS.fetch(request)
61
123
  if (assetResponse.status !== 404) {
@@ -67,5 +129,15 @@ export default {
67
129
  },
68
130
  }
69
131
 
132
+ async function regeneratePage(pathname: string, _env: CloudflareBindings): Promise<void> {
133
+ try {
134
+ const rendered = await renderPage(pathname)
135
+ await isrCache.store(pathname, rendered.html)
136
+ } catch (error) {
137
+ console.error('ISR regeneration failed:', error)
138
+ }
139
+ }
140
+
141
+ export { isrCache }
70
142
  export { RealtimeDurableObject, getDb }
71
143
  export type AppType = typeof wrappedApp