create-fullstack-scaffold 0.4.25 → 0.5.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/cli/index.js +432 -15
- package/dist/cli/index.js.map +1 -1
- package/package.json +10 -13
- package/template/drizzle.config.ts +10 -4
- package/template/eslint-rules/__tests__/no-merged-api-type-export.test.ts +96 -0
- package/template/eslint-rules/no-cross-module-service-import.js +4 -0
- package/template/eslint-rules/no-merged-api-type-export.js +190 -0
- package/template/eslint.config.js +3 -0
- package/template/package.json +9 -6
- package/template/scripts/sync-agent-hooks.mjs +189 -0
- package/template/src/admin/components/__tests__/StatsCard.test.tsx +2 -2
- package/template/src/admin/pages/ContentPage.tsx +1 -1
- package/template/src/admin/pages/DisputesPage.tsx +1 -1
- package/template/src/admin/pages/OrdersPage.tsx +1 -1
- package/template/src/admin/pages/TicketsPage.tsx +1 -1
- package/template/src/admin/pages/__tests__/ContentPage.test.tsx +5 -1
- package/template/src/admin/pages/__tests__/DashboardPage.test.tsx +62 -8
- package/template/src/admin/pages/__tests__/DisputesPage.test.tsx +8 -1
- package/template/src/admin/pages/__tests__/OrdersPage.test.tsx +4 -2
- package/template/src/admin/pages/__tests__/RegisterPage.test.tsx +40 -43
- package/template/src/admin/pages/__tests__/SettingsPage.test.tsx +83 -21
- package/template/src/admin/pages/__tests__/TicketsPage.test.tsx +3 -1
- package/template/src/admin/services/apiClient.ts +2 -3
- package/template/src/cli/modules/content/index.ts +3 -3
- package/template/src/cli/modules/dispute/index.ts +3 -3
- package/template/src/cli/modules/ticket/index.ts +3 -3
- package/template/src/cli/modules/todo/index.ts +1 -1
- package/template/src/cli/rpc/client.ts +3 -4
- package/template/src/cli/rpc/index.ts +1 -1
- package/template/src/client/App.tsx +3 -39
- package/template/src/client/AppRoutes.tsx +53 -0
- package/template/src/client/entry-server.tsx +75 -0
- package/template/src/client/main.tsx +3 -1
- package/template/src/client/pages/SearchPage.tsx +1 -1
- package/template/src/client/services/apiClient.ts +12 -4
- package/template/src/client/stores/__tests__/todoStore.test.ts +12 -3
- package/template/src/client/stores/entry-stores.ts +36 -0
- package/template/src/client/stores/notificationStore.ts +4 -4
- package/template/src/client/stores/todoStore.ts +2 -2
- package/template/src/merchant/pages/DisputesPage.tsx +3 -3
- package/template/src/merchant/pages/OrdersPage.tsx +1 -1
- package/template/src/merchant/pages/ProductsPage.tsx +1 -1
- package/template/src/merchant/pages/SettingsPage.tsx +1 -1
- package/template/src/server/__tests__/integration/isr-full-flow.test.ts +251 -0
- package/template/src/server/__tests__/integration/todos-api.test.ts +7 -4
- package/template/src/server/app.ts +6 -4
- package/template/src/server/core/__tests__/isr-cache-cf.test.ts +96 -0
- package/template/src/server/core/__tests__/isr-cache.test.ts +96 -92
- package/template/src/server/core/__tests__/isr-invalidation.test.ts +29 -4
- package/template/src/server/core/__tests__/isr-registry.test.ts +215 -0
- package/template/src/server/core/__tests__/isr-renderer.test.ts +121 -0
- package/template/src/server/core/isr-cache.ts +61 -18
- package/template/src/server/core/isr-invalidation.ts +6 -12
- package/template/src/server/core/isr-registry.ts +104 -0
- package/template/src/server/core/isr-renderer.ts +75 -0
- package/template/src/server/db/schema/contents.ts +28 -20
- package/template/src/server/db/schema/disputes.ts +30 -22
- package/template/src/server/db/schema/notifications.ts +20 -14
- package/template/src/server/db/schema/orders.ts +23 -16
- package/template/src/server/db/schema/plugins.ts +56 -38
- package/template/src/server/db/schema/products.ts +24 -17
- package/template/src/server/db/schema/tickets.ts +44 -31
- package/template/src/server/db/schema/todos.ts +26 -18
- package/template/src/server/entries/cloudflare.ts +97 -21
- package/template/src/server/entries/node.ts +0 -2
- package/template/src/server/index.ts +0 -1
- package/template/src/server/isr-modules.ts +10 -0
- package/template/src/server/module-admin/__tests__/admin-service.test.ts +36 -12
- package/template/src/server/module-admin/routes/admin-notification-routes.ts +2 -1
- package/template/src/server/module-admin/routes/admin-routes.ts +3 -0
- package/template/src/server/module-admin/routes/dashboard-routes.ts +3 -0
- package/template/src/server/module-admin/services/admin-service.ts +57 -13
- package/template/src/server/module-auth/routes/auth-routes.ts +3 -0
- package/template/src/server/module-auth/routes/profile-routes.ts +3 -0
- package/template/src/server/module-captcha/routes/captcha-routes.ts +3 -0
- package/template/src/server/module-chat/routes/chat-routes.ts +4 -1
- package/template/src/server/module-content/__tests__/content-route.test.ts +2 -1
- package/template/src/server/module-content/__tests__/content-service.test.ts +2 -1
- package/template/src/server/module-content/__tests__/isr.test.ts +138 -0
- package/template/src/server/module-content/isr.ts +63 -0
- package/template/src/server/module-content/routes/content-routes.ts +10 -10
- package/template/src/server/module-content/routes/public-content-routes.ts +8 -4
- package/template/src/server/module-content/routes/topics-routes.ts +3 -0
- package/template/src/server/module-content/services/content-service.ts +23 -10
- package/template/src/server/module-dispute/__tests__/dispute-route.test.ts +2 -1
- package/template/src/server/module-dispute/__tests__/dispute-service.test.ts +2 -1
- package/template/src/server/module-dispute/routes/dispute-routes.ts +11 -10
- package/template/src/server/module-dispute/services/dispute-service.ts +15 -3
- package/template/src/server/module-file/routes/file-routes.ts +3 -0
- package/template/src/server/module-merchant/routes/merchant-routes.ts +3 -0
- package/template/src/server/module-merchant/services/merchant-service.ts +8 -8
- package/template/src/server/module-notifications/routes/notification-routes.ts +5 -1
- package/template/src/server/module-order/__tests__/order-route.test.ts +8 -8
- package/template/src/server/module-order/__tests__/order-service.test.ts +4 -3
- package/template/src/server/module-order/routes/cart-routes.ts +3 -0
- package/template/src/server/module-order/routes/order-routes.ts +9 -4
- package/template/src/server/module-order/routes/orders-mock-routes.ts +3 -0
- package/template/src/server/module-order/services/order-service.ts +25 -13
- package/template/src/server/module-permission/__tests__/audit-log-service.test.ts +464 -0
- package/template/src/server/module-permission/__tests__/role-service.test.ts +348 -0
- package/template/src/server/module-permission/routes/audit-log-routes.ts +3 -0
- package/template/src/server/module-permission/routes/permission-routes.ts +3 -0
- package/template/src/server/module-permission/routes/role-routes.ts +3 -0
- package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +3 -0
- package/template/src/server/module-plugin/routes/plugin-routes.ts +3 -0
- package/template/src/server/module-plugin/services/admin-plugin-service.ts +10 -18
- package/template/src/server/module-plugin/services/admin-stats-service.ts +28 -9
- package/template/src/server/module-plugin/services/plugin-query-service.ts +42 -66
- package/template/src/server/module-tenant/routes/tenant-routes.ts +5 -1
- package/template/src/server/module-ticket/__tests__/ticket-route.test.ts +2 -1
- package/template/src/server/module-ticket/__tests__/ticket-service.test.ts +4 -3
- package/template/src/server/module-ticket/routes/ticket-routes.ts +11 -10
- package/template/src/server/module-ticket/services/ticket-service.ts +16 -5
- package/template/src/server/module-todos/__tests__/isr.test.ts +105 -0
- package/template/src/server/module-todos/__tests__/todo-service.test.ts +12 -9
- package/template/src/server/module-todos/__tests__/todos-route-rpc.test.ts +6 -6
- package/template/src/server/module-todos/isr.ts +41 -0
- package/template/src/server/module-todos/routes/todos-routes.ts +12 -5
- package/template/src/server/module-todos/services/todo-service.ts +31 -10
- package/template/src/server/route-registry.ts +0 -4
- package/template/src/server/rpc-merge.ts +27 -0
- package/template/src/server/rpc-surface.ts +117 -0
- package/template/src/server/rpc-type-canary.ts +80 -0
- package/template/src/server/test-utils/test-client.ts +7 -9
- package/template/src/server/test-utils/test-isr-helper.ts +140 -0
- package/template/src/shared/modules/content/schemas.ts +14 -0
- package/template/src/shared/modules/dispute/schemas.ts +14 -0
- package/template/src/shared/modules/order/schemas.ts +9 -1
- package/template/src/shared/modules/ticket/schemas.ts +14 -0
- package/template/src/shared/modules/todos/index.ts +4 -0
- package/template/src/shared/modules/todos/schemas.ts +14 -0
- package/template/src/shared/schemas/index.ts +18 -0
- package/template/tsup.config.ts +48 -1
- package/template/vitest.config.ts +19 -3
- package/template/vitest.setup.ts +68 -5
- package/template/patches/typescript+5.9.3.patch +0 -24
- /package/template/patches/{hono+4.12.16.patch → hono+4.12.34.patch} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
|
|
1
|
+
import { sqliteTable, integer, text, index } from 'drizzle-orm/sqlite-core'
|
|
2
2
|
import { sql } from 'drizzle-orm'
|
|
3
3
|
|
|
4
4
|
export const orderStatuses = [
|
|
@@ -10,21 +10,28 @@ export const orderStatuses = [
|
|
|
10
10
|
] as const
|
|
11
11
|
export type OrderStatus = (typeof orderStatuses)[number]
|
|
12
12
|
|
|
13
|
-
export const orders = sqliteTable(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.notNull()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
13
|
+
export const orders = sqliteTable(
|
|
14
|
+
'orders',
|
|
15
|
+
{
|
|
16
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
17
|
+
orderNo: text('order_no').notNull(),
|
|
18
|
+
customerName: text('customer_name').notNull(),
|
|
19
|
+
customerEmail: text('customer_email').notNull(),
|
|
20
|
+
productName: text('product_name').notNull(),
|
|
21
|
+
amount: integer('amount').notNull(),
|
|
22
|
+
status: text('status', { enum: orderStatuses }).notNull().default('pending'),
|
|
23
|
+
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
24
|
+
.notNull()
|
|
25
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
26
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' })
|
|
27
|
+
.notNull()
|
|
28
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
29
|
+
},
|
|
30
|
+
table => ({
|
|
31
|
+
statusIdx: index('orders_status_idx').on(table.status),
|
|
32
|
+
createdAtIdx: index('orders_created_at_idx').on(table.createdAt),
|
|
33
|
+
})
|
|
34
|
+
)
|
|
28
35
|
|
|
29
36
|
export type OrderTable = typeof orders.$inferSelect
|
|
30
37
|
export type NewOrder = typeof orders.$inferInsert
|
|
@@ -1,38 +1,47 @@
|
|
|
1
|
-
import { sqliteTable, text, integer, uniqueIndex } from 'drizzle-orm/sqlite-core'
|
|
1
|
+
import { sqliteTable, text, integer, uniqueIndex, index } from 'drizzle-orm/sqlite-core'
|
|
2
2
|
import { sql } from 'drizzle-orm'
|
|
3
3
|
|
|
4
4
|
export const pluginStatus = ['pending', 'approved', 'rejected'] as const
|
|
5
5
|
export type PluginStatus = (typeof pluginStatus)[number]
|
|
6
6
|
|
|
7
|
-
export const plugins = sqliteTable(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
7
|
+
export const plugins = sqliteTable(
|
|
8
|
+
'plugins',
|
|
9
|
+
{
|
|
10
|
+
id: text('id').primaryKey(),
|
|
11
|
+
name: text('name').notNull(),
|
|
12
|
+
slug: text('slug').notNull().unique(),
|
|
13
|
+
description: text('description').notNull().default(''),
|
|
14
|
+
readme: text('readme'),
|
|
15
|
+
authorId: text('author_id').notNull(),
|
|
16
|
+
authorName: text('author_name').notNull(),
|
|
17
|
+
repositoryUrl: text('repository_url'),
|
|
18
|
+
homepageUrl: text('homepage_url'),
|
|
19
|
+
npmPackage: text('npm_package'),
|
|
20
|
+
license: text('license'),
|
|
21
|
+
version: text('version').notNull().default('0.0.1'),
|
|
22
|
+
status: text('status', { enum: pluginStatus }).notNull().default('pending'),
|
|
23
|
+
downloadCount: integer('download_count').notNull().default(0),
|
|
24
|
+
viewCount: integer('view_count').notNull().default(0),
|
|
25
|
+
featured: integer('featured', { mode: 'boolean' }).notNull().default(false),
|
|
26
|
+
screenshotUrl: text('screenshot_url'),
|
|
27
|
+
siteUrls: text('site_urls'),
|
|
28
|
+
tags: text('tags'),
|
|
29
|
+
commands: text('commands'),
|
|
30
|
+
rejectReason: text('reject_reason'),
|
|
31
|
+
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
32
|
+
.notNull()
|
|
33
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
34
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' })
|
|
35
|
+
.notNull()
|
|
36
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
37
|
+
},
|
|
38
|
+
table => ({
|
|
39
|
+
statusIdx: index('plugins_status_idx').on(table.status),
|
|
40
|
+
slugIdx: index('plugins_slug_idx').on(table.slug),
|
|
41
|
+
authorIdIdx: index('plugins_author_id_idx').on(table.authorId),
|
|
42
|
+
createdAtIdx: index('plugins_created_at_idx').on(table.createdAt),
|
|
43
|
+
})
|
|
44
|
+
)
|
|
36
45
|
|
|
37
46
|
export const pluginVersions = sqliteTable(
|
|
38
47
|
'plugin_versions',
|
|
@@ -53,6 +62,7 @@ export const pluginVersions = sqliteTable(
|
|
|
53
62
|
},
|
|
54
63
|
table => ({
|
|
55
64
|
pluginVersionUnique: uniqueIndex('plugin_version_unique').on(table.pluginId, table.version),
|
|
65
|
+
pluginIdIdx: index('plugin_versions_plugin_id_idx').on(table.pluginId),
|
|
56
66
|
})
|
|
57
67
|
)
|
|
58
68
|
|
|
@@ -77,14 +87,20 @@ export const pluginReviews = sqliteTable(
|
|
|
77
87
|
})
|
|
78
88
|
)
|
|
79
89
|
|
|
80
|
-
export const pluginCategories = sqliteTable(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
export const pluginCategories = sqliteTable(
|
|
91
|
+
'plugin_categories',
|
|
92
|
+
{
|
|
93
|
+
id: text('id').primaryKey(),
|
|
94
|
+
name: text('name').notNull().unique(),
|
|
95
|
+
slug: text('slug').notNull().unique(),
|
|
96
|
+
description: text('description'),
|
|
97
|
+
icon: text('icon'),
|
|
98
|
+
sortOrder: integer('sort_order').notNull().default(0),
|
|
99
|
+
},
|
|
100
|
+
table => ({
|
|
101
|
+
slugIdx: index('plugin_categories_slug_idx').on(table.slug),
|
|
102
|
+
})
|
|
103
|
+
)
|
|
88
104
|
|
|
89
105
|
export const pluginCategoryMappings = sqliteTable(
|
|
90
106
|
'plugin_category_mappings',
|
|
@@ -101,6 +117,8 @@ export const pluginCategoryMappings = sqliteTable(
|
|
|
101
117
|
table.pluginId,
|
|
102
118
|
table.categoryId
|
|
103
119
|
),
|
|
120
|
+
categoryIdIdx: index('plugin_category_mappings_category_id_idx').on(table.categoryId),
|
|
121
|
+
pluginIdIdx: index('plugin_category_mappings_plugin_id_idx').on(table.pluginId),
|
|
104
122
|
})
|
|
105
123
|
)
|
|
106
124
|
|
|
@@ -1,25 +1,32 @@
|
|
|
1
|
-
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
|
|
1
|
+
import { sqliteTable, integer, text, index } from 'drizzle-orm/sqlite-core'
|
|
2
2
|
import { sql } from 'drizzle-orm'
|
|
3
3
|
|
|
4
4
|
export const productStatuses = ['active', 'inactive', 'out_of_stock'] as const
|
|
5
5
|
export type ProductStatus = (typeof productStatuses)[number]
|
|
6
6
|
|
|
7
|
-
export const products = sqliteTable(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.notNull()
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
export const products = sqliteTable(
|
|
8
|
+
'products',
|
|
9
|
+
{
|
|
10
|
+
id: text('id').primaryKey(),
|
|
11
|
+
name: text('name').notNull(),
|
|
12
|
+
description: text('description'),
|
|
13
|
+
price: integer('price').notNull(), // 单位:分
|
|
14
|
+
status: text('status', { enum: productStatuses }).notNull().default('active'),
|
|
15
|
+
stock: integer('stock').notNull().default(0),
|
|
16
|
+
imageUrl: text('image_url'),
|
|
17
|
+
merchantId: integer('merchant_id').notNull(), // Changed to integer to match merchants.id
|
|
18
|
+
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
19
|
+
.notNull()
|
|
20
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
21
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' })
|
|
22
|
+
.notNull()
|
|
23
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
24
|
+
},
|
|
25
|
+
table => ({
|
|
26
|
+
statusIdx: index('products_status_idx').on(table.status),
|
|
27
|
+
merchantIdIdx: index('products_merchant_id_idx').on(table.merchantId),
|
|
28
|
+
})
|
|
29
|
+
)
|
|
23
30
|
|
|
24
31
|
export type Product = typeof products.$inferSelect
|
|
25
32
|
export type NewProduct = typeof products.$inferInsert
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
|
|
1
|
+
import { sqliteTable, integer, text, index } from 'drizzle-orm/sqlite-core'
|
|
2
2
|
import { sql } from 'drizzle-orm'
|
|
3
3
|
|
|
4
4
|
export const ticketStatuses = [
|
|
@@ -24,37 +24,50 @@ export type TicketCategory = (typeof ticketCategories)[number]
|
|
|
24
24
|
|
|
25
25
|
export const ticketReplyAuthorRoles = ['customer', 'admin', 'system'] as const
|
|
26
26
|
|
|
27
|
-
export const tickets = sqliteTable(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
27
|
+
export const tickets = sqliteTable(
|
|
28
|
+
'tickets',
|
|
29
|
+
{
|
|
30
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
31
|
+
ticketNo: text('ticket_no').notNull(),
|
|
32
|
+
customerName: text('customer_name').notNull(),
|
|
33
|
+
customerEmail: text('customer_email').notNull(),
|
|
34
|
+
subject: text('subject').notNull(),
|
|
35
|
+
description: text('description').notNull(),
|
|
36
|
+
status: text('status', { enum: ticketStatuses }).notNull().default('open'),
|
|
37
|
+
priority: text('priority', { enum: ticketPriorities }).notNull().default('medium'),
|
|
38
|
+
category: text('category', { enum: ticketCategories }).notNull(),
|
|
39
|
+
assignedTo: text('assigned_to'),
|
|
40
|
+
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
41
|
+
.notNull()
|
|
42
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
43
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' })
|
|
44
|
+
.notNull()
|
|
45
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
46
|
+
},
|
|
47
|
+
table => ({
|
|
48
|
+
statusIdx: index('tickets_status_idx').on(table.status),
|
|
49
|
+
createdAtIdx: index('tickets_created_at_idx').on(table.createdAt),
|
|
50
|
+
})
|
|
51
|
+
)
|
|
45
52
|
|
|
46
|
-
export const ticketReplies = sqliteTable(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.notNull()
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
export const ticketReplies = sqliteTable(
|
|
54
|
+
'ticket_replies',
|
|
55
|
+
{
|
|
56
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
57
|
+
ticketId: integer('ticket_id')
|
|
58
|
+
.notNull()
|
|
59
|
+
.references(() => tickets.id, { onDelete: 'cascade' }),
|
|
60
|
+
content: text('content').notNull(),
|
|
61
|
+
author: text('author').notNull(),
|
|
62
|
+
isCustomer: integer('is_customer', { mode: 'boolean' }).notNull().default(false),
|
|
63
|
+
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
64
|
+
.notNull()
|
|
65
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
66
|
+
},
|
|
67
|
+
table => ({
|
|
68
|
+
ticketIdIdx: index('ticket_replies_ticket_id_idx').on(table.ticketId),
|
|
69
|
+
})
|
|
70
|
+
)
|
|
58
71
|
|
|
59
72
|
export type TicketTable = typeof tickets.$inferSelect
|
|
60
73
|
export type NewTicket = typeof tickets.$inferInsert
|
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
|
|
2
|
-
import { sql } from 'drizzle-orm'
|
|
1
|
+
import { sqliteTable, integer, text, index } from 'drizzle-orm/sqlite-core'
|
|
2
|
+
import { sql } from 'drizzle-orm'
|
|
3
3
|
|
|
4
|
-
export const todoStatus = ['pending', 'in_progress', 'completed'] as const
|
|
5
|
-
export type TodoStatus = (typeof todoStatus)[number]
|
|
4
|
+
export const todoStatus = ['pending', 'in_progress', 'completed'] as const
|
|
5
|
+
export type TodoStatus = (typeof todoStatus)[number]
|
|
6
6
|
|
|
7
|
-
export const todos = sqliteTable(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.notNull()
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
export const todos = sqliteTable(
|
|
8
|
+
'todos',
|
|
9
|
+
{
|
|
10
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
11
|
+
title: text('title').notNull(),
|
|
12
|
+
description: text('description'),
|
|
13
|
+
status: text('status', { enum: todoStatus }).notNull().default('pending'),
|
|
14
|
+
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
15
|
+
.notNull()
|
|
16
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
17
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' })
|
|
18
|
+
.notNull()
|
|
19
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
20
|
+
},
|
|
21
|
+
table => ({
|
|
22
|
+
statusIdx: index('todos_status_idx').on(table.status),
|
|
23
|
+
createdAtIdx: index('todos_created_at_idx').on(table.createdAt),
|
|
24
|
+
updatedAtIdx: index('todos_updated_at_idx').on(table.updatedAt),
|
|
25
|
+
})
|
|
26
|
+
)
|
|
19
27
|
|
|
20
|
-
export type TodoTable = typeof todos.$inferSelect
|
|
21
|
-
export type NewTodo = typeof todos.$inferInsert
|
|
28
|
+
export type TodoTable = typeof todos.$inferSelect
|
|
29
|
+
export type NewTodo = typeof todos.$inferInsert
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @framework-baseline e350401421193896
|
|
3
3
|
* @framework-modify
|
|
4
|
-
* @reason
|
|
5
|
-
* @impact
|
|
6
|
-
*
|
|
7
|
-
* Note: In Cloudflare Workers, each request runs in its own isolate,
|
|
8
|
-
* so globalThis is request-scoped and there's no race condition risk.
|
|
9
|
-
* The middleware sets the DB binding for each request.
|
|
4
|
+
* @reason 模块化 ISR 改造 + SSR 渲染:调用 renderSSR 生成 React body,注入到 ISR 模板
|
|
5
|
+
* @impact CF 入口集成 React SSR,ISR 同时负责 SEO meta 标签和 body 渲染
|
|
10
6
|
*/
|
|
11
7
|
|
|
12
8
|
import { createApp } from '../app'
|
|
@@ -16,8 +12,15 @@ import { RealtimeDurableObject } from '@server/core'
|
|
|
16
12
|
import { setRuntimeAdapter } from '@server/core/runtime'
|
|
17
13
|
import { getCloudflareRuntimeAdapter } from '@server/core/runtime-cloudflare'
|
|
18
14
|
import { createISRCache, isISRRoute } from '@server/core/isr-cache'
|
|
19
|
-
import { renderPage } from '@server/core/ssr-renderer'
|
|
20
15
|
import { setISRCache } from '@server/core/isr-invalidation'
|
|
16
|
+
import { isrRegistry, type ISRRouterContext } from '@server/core/isr-registry'
|
|
17
|
+
import { renderISRPage } from '@server/core/isr-renderer'
|
|
18
|
+
import { renderSSR } from '@client/entry-server'
|
|
19
|
+
|
|
20
|
+
// Import module ISR registrations (side-effect: registers routes).
|
|
21
|
+
// 由 CLI 按 preset 生成的汇总文件(src/server/isr-modules.ts),
|
|
22
|
+
// 避免 content 等模块被裁剪后悬空导入导致 CF 构建/tsc 失败。
|
|
23
|
+
import '@server/isr-modules'
|
|
21
24
|
|
|
22
25
|
export interface CloudflareBindings extends AppBindings {
|
|
23
26
|
DB: D1Database
|
|
@@ -36,6 +39,8 @@ const app = createApp<CloudflareBindings>()
|
|
|
36
39
|
const isrCache = createISRCache()
|
|
37
40
|
setISRCache(isrCache)
|
|
38
41
|
|
|
42
|
+
let cachedTemplate: string | null = null
|
|
43
|
+
|
|
39
44
|
const wrappedApp = app
|
|
40
45
|
.use('*', async (c, next) => {
|
|
41
46
|
;(globalThis as unknown as { DB: D1Database }).DB = c.env.DB
|
|
@@ -102,19 +107,20 @@ export default {
|
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
if (result.status === 'stale' && result.html) {
|
|
105
|
-
ctx.waitUntil(regeneratePage(pathname, env))
|
|
110
|
+
ctx.waitUntil(regeneratePage(pathname, env, request))
|
|
106
111
|
return new Response(result.html, {
|
|
107
112
|
headers: { 'Content-Type': 'text/html;charset=UTF-8', 'X-ISR-Status': 'stale' },
|
|
108
113
|
})
|
|
109
114
|
}
|
|
110
115
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
const html = await renderISRForRoute(pathname, env, request)
|
|
117
|
+
ctx.waitUntil(isrCache.store(pathname, html))
|
|
118
|
+
return new Response(html, {
|
|
119
|
+
headers: {
|
|
120
|
+
'Content-Type': 'text/html;charset=UTF-8',
|
|
121
|
+
'X-ISR-Status': 'miss',
|
|
122
|
+
'X-ISR-Rendered': 'true',
|
|
123
|
+
},
|
|
118
124
|
})
|
|
119
125
|
}
|
|
120
126
|
|
|
@@ -129,15 +135,85 @@ export default {
|
|
|
129
135
|
},
|
|
130
136
|
}
|
|
131
137
|
|
|
132
|
-
|
|
138
|
+
// 单飞:同 isolate 内并发 stale 请求共享同一次重建,防缓存击穿
|
|
139
|
+
const inflightRevalidations = new Map<string, Promise<void>>()
|
|
140
|
+
|
|
141
|
+
function regeneratePage(
|
|
142
|
+
pathname: string,
|
|
143
|
+
env: CloudflareBindings,
|
|
144
|
+
request: Request
|
|
145
|
+
): Promise<void> {
|
|
146
|
+
const existing = inflightRevalidations.get(pathname)
|
|
147
|
+
if (existing) return existing
|
|
148
|
+
|
|
149
|
+
const task = (async () => {
|
|
150
|
+
try {
|
|
151
|
+
const html = await renderISRForRoute(pathname, env, request)
|
|
152
|
+
await isrCache.store(pathname, html)
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error('ISR regeneration failed:', error)
|
|
155
|
+
} finally {
|
|
156
|
+
inflightRevalidations.delete(pathname)
|
|
157
|
+
}
|
|
158
|
+
})()
|
|
159
|
+
|
|
160
|
+
inflightRevalidations.set(pathname, task)
|
|
161
|
+
return task
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function renderISRForRoute(
|
|
165
|
+
pathname: string,
|
|
166
|
+
env: CloudflareBindings,
|
|
167
|
+
request: Request
|
|
168
|
+
): Promise<string> {
|
|
169
|
+
const entry = isrRegistry.match(pathname)
|
|
170
|
+
if (!entry) {
|
|
171
|
+
throw new Error(`No ISR handler for ${pathname}`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const ctx: ISRRouterContext = { db: env.DB, env }
|
|
175
|
+
let data: unknown = {}
|
|
176
|
+
let meta = { title: 'Biomimic App', description: 'A full-stack application template' }
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
data = await entry.fetch(pathname, ctx)
|
|
180
|
+
meta = entry.meta(data, pathname)
|
|
181
|
+
} catch {
|
|
182
|
+
// DB error — fall through with default meta
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!cachedTemplate && env.ASSETS) {
|
|
186
|
+
try {
|
|
187
|
+
const indexUrl = new URL('/index.html', request.url).href
|
|
188
|
+
const resp = await env.ASSETS.fetch(new Request(indexUrl))
|
|
189
|
+
if (resp.ok) {
|
|
190
|
+
cachedTemplate = await resp.text()
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
// fallback below
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Render React SSR body
|
|
198
|
+
let body = ''
|
|
133
199
|
try {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
200
|
+
const ssrResult = renderSSR(pathname, data as Parameters<typeof renderSSR>[1])
|
|
201
|
+
body = ssrResult.html
|
|
202
|
+
// Helmet takes priority for title/meta
|
|
203
|
+
const helmetTitle = ssrResult.helmet.title
|
|
204
|
+
?.replace(/<title[^>]*>/, '')
|
|
205
|
+
?.replace(/<\/title>/, '')
|
|
206
|
+
?.trim()
|
|
207
|
+
if (helmetTitle) {
|
|
208
|
+
meta = { ...meta, title: helmetTitle }
|
|
209
|
+
}
|
|
210
|
+
} catch (e) {
|
|
211
|
+
console.error('SSR render failed:', e)
|
|
212
|
+
// Fallback: empty body, SPA will hydrate
|
|
138
213
|
}
|
|
214
|
+
|
|
215
|
+
return renderISRPage({ template: cachedTemplate, body, meta })
|
|
139
216
|
}
|
|
140
217
|
|
|
141
218
|
export { isrCache }
|
|
142
219
|
export { RealtimeDurableObject, getDb }
|
|
143
|
-
export type AppType = typeof wrappedApp
|
|
@@ -59,4 +59,3 @@ export { createApp } from './app'
|
|
|
59
59
|
export { type AppBindings, type CreateAppOptions } from './types/bindings'
|
|
60
60
|
export { getAppConfig, getDatabaseConfig, type AppConfig, type DatabaseConfig } from './config'
|
|
61
61
|
export { createServer, startServer } from './entries/node'
|
|
62
|
-
export type { ClientApiType, AdminApiType, AppType } from './app'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ISR 模块注册汇总(单一入口,供 entries/cloudflare.ts 引用)。
|
|
3
|
+
*
|
|
4
|
+
* 本文件在模板仓库中为全量版本;CLI 脚手架时会按 preset 重新生成
|
|
5
|
+
* (src/generators/isr-modules.ts),只保留选中模块的导入,
|
|
6
|
+
* 避免 content 等模块被裁剪后悬空导入导致 CF 构建/tsc 失败。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import '@server/module-todos/isr'
|
|
10
|
+
import '@server/module-content/isr'
|