@h-ai/cli 0.1.0-alpha.15 → 0.1.0-alpha.18
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/{chunk-LANIDJC7.js → chunk-BPO34NRL.js} +3 -3
- package/dist/chunk-BPO34NRL.js.map +1 -0
- package/dist/{chunk-YV7WPUI6.js → chunk-GSMXGUYF.js} +6 -2
- package/dist/chunk-GSMXGUYF.js.map +1 -0
- package/dist/cli-config-templates-JCS7T3NU.js +5 -0
- package/dist/cli-config-templates-JCS7T3NU.js.map +1 -0
- package/dist/cli-skill-templates-IMYZPQRG.js +5 -0
- package/dist/cli-skill-templates-IMYZPQRG.js.map +1 -0
- package/dist/index.js +391 -58
- package/dist/index.js.map +1 -1
- package/dist/{src-RZ4DPYNJ.js → src-N4NPS7AG.js} +1239 -493
- package/dist/src-N4NPS7AG.js.map +1 -0
- package/package.json +8 -7
- package/templates/README.md +10 -3
- package/templates/apps/api/tests/health.test.ts.hbs +26 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +21 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/eslint.config.js.hbs +23 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/messages/en-US.json.hbs +34 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/messages/zh-CN.json.hbs +34 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +50 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/project.inlang/settings.json.hbs +17 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/app.css.hbs +18 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/app.d.ts.hbs +9 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/app.html.hbs +14 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/routes/+layout.svelte.hbs +18 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/routes/+page.svelte.hbs +162 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/svelte.config.js.hbs +20 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/tests/api.test.ts.hbs +28 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/tsconfig.json.hbs +17 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +29 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +21 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/eslint.config.js.hbs +23 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/en-US.json.hbs +34 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/zh-CN.json.hbs +34 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +50 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/project.inlang/settings.json.hbs +17 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/app.css.hbs +18 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/app.d.ts.hbs +9 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/app.html.hbs +14 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/routes/+layout.svelte.hbs +18 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/routes/+page.svelte.hbs +162 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/svelte.config.js.hbs +20 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/tests/api.test.ts.hbs +28 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/tsconfig.json.hbs +17 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +29 -0
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +13 -0
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/messages/en-US.json.hbs +12 -0
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/messages/zh-CN.json.hbs +12 -0
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/project.inlang/settings.json.hbs +17 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +21 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/eslint.config.js.hbs +23 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/messages/en-US.json.hbs +34 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/messages/zh-CN.json.hbs +34 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +50 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/project.inlang/settings.json.hbs +17 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/app.css.hbs +18 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/app.d.ts.hbs +9 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/app.html.hbs +14 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/routes/+layout.svelte.hbs +18 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/routes/+page.svelte.hbs +162 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/svelte.config.js.hbs +20 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/tests/api.test.ts.hbs +28 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/tsconfig.json.hbs +17 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +29 -0
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +30 -0
- package/templates/apps/fullstack/eslint.config.js +44 -0
- package/templates/apps/fullstack/package.json.hbs +42 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +15 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/package.json.hbs +27 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/app-contract.ts.hbs +21 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/app-routes.ts.hbs +27 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/app-schemas.ts.hbs +34 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/index.ts.hbs +4 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +9 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/tests/contract.test.ts.hbs +48 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/tsconfig.json.hbs +9 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/tsup.config.ts.hbs +9 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +14 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +29 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/index.ts.hbs +15 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/app-procedures.ts.hbs +38 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +13 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server-app.ts.hbs +42 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/server-app.test.ts.hbs +49 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tsconfig.json.hbs +10 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tsup.config.ts.hbs +9 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/.gitignore.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +39 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/en-US.json.hbs +24 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/zh-CN.json.hbs +24 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/package.json.hbs +44 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/project.inlang/settings.json.hbs +11 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/index.ts.hbs +8 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +49 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +60 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/LanguageSwitcher.svelte.hbs +27 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/ThemeSwitcher.svelte.hbs +18 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/theme.svelte.ts.hbs +55 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tsconfig.json.hbs +17 -0
- package/templates/apps/fullstack/playwright.config.ts.hbs +34 -0
- package/templates/apps/fullstack/pnpm-workspace.yaml +7 -0
- package/templates/apps/fullstack/tsconfig.base.json +15 -0
- package/templates/base/e2e/app.spec.ts.hbs +7 -0
- package/templates/base/eslint.config.js +6 -0
- package/templates/base/package.json.hbs +42 -36
- package/templates/base/src/app.css.hbs +1 -0
- package/templates/base/src/lib/server/init.ts.hbs +3 -3
- package/templates/base/tests/scaffold.test.ts.hbs +27 -0
- package/templates/skills/hai-serv/SKILL.md +24 -11
- package/dist/chunk-LANIDJC7.js.map +0 -1
- package/dist/chunk-YV7WPUI6.js.map +0 -1
- package/dist/config-templates-S5RI2MC6.js +0 -5
- package/dist/config-templates-S5RI2MC6.js.map +0 -1
- package/dist/skill-templates-N5DU5FNX.js +0 -5
- package/dist/skill-templates-N5DU5FNX.js.map +0 -1
- package/dist/src-RZ4DPYNJ.js.map +0 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { apiContract } from '@h-ai/api-contract'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
export const AppFrontendTargetSchema = z.enum(['web', 'app', 'miniapp', 'desktop'])
|
|
5
|
+
|
|
6
|
+
/** 服务信息响应数据。 */
|
|
7
|
+
export const AppInfoOutputDataSchema = z.object({
|
|
8
|
+
name: z.string(),
|
|
9
|
+
version: z.string(),
|
|
10
|
+
startedAt: z.string().datetime(),
|
|
11
|
+
frontends: z.array(AppFrontendTargetSchema),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
/** Echo 请求体。 */
|
|
15
|
+
export const AppEchoInputSchema = z.object({
|
|
16
|
+
message: z.string().min(1).max(200),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
/** Echo 响应数据。 */
|
|
20
|
+
export const AppEchoOutputDataSchema = z.object({
|
|
21
|
+
message: z.string(),
|
|
22
|
+
echoedAt: z.string().datetime(),
|
|
23
|
+
requestId: z.string(),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const AppInfoOutputSchema = apiContract.haiResultSchema(AppInfoOutputDataSchema)
|
|
27
|
+
export const AppEchoOutputSchema = apiContract.haiResultSchema(AppEchoOutputDataSchema)
|
|
28
|
+
|
|
29
|
+
export type AppFrontendTarget = z.infer<typeof AppFrontendTargetSchema>
|
|
30
|
+
export type AppInfoOutputData = z.infer<typeof AppInfoOutputDataSchema>
|
|
31
|
+
export type AppInfoOutput = z.infer<typeof AppInfoOutputSchema>
|
|
32
|
+
export type AppEchoInput = z.infer<typeof AppEchoInputSchema>
|
|
33
|
+
export type AppEchoOutputData = z.infer<typeof AppEchoOutputDataSchema>
|
|
34
|
+
export type AppEchoOutput = z.infer<typeof AppEchoOutputSchema>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { apiContract } from '@h-ai/api-contract'
|
|
2
|
+
import { appContract } from './app-contract.js'
|
|
3
|
+
|
|
4
|
+
/** 工程级 API contract,前端 typed client 与后端 service 共用。 */
|
|
5
|
+
export const {{fullstack.contractExportName}} = apiContract.create({
|
|
6
|
+
app: appContract,
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export type FullstackContract = typeof {{fullstack.contractExportName}}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
APP_API_ROUTES,
|
|
4
|
+
appContract,
|
|
5
|
+
AppEchoInputSchema,
|
|
6
|
+
AppInfoOutputDataSchema,
|
|
7
|
+
AppInfoOutputSchema,
|
|
8
|
+
ENABLED_FRONTENDS,
|
|
9
|
+
{{fullstack.contractExportName}},
|
|
10
|
+
} from '../src/index.js'
|
|
11
|
+
|
|
12
|
+
describe('{{fullstack.contractPackageName}}', () => {
|
|
13
|
+
it('defines app contract routes', () => {
|
|
14
|
+
expect({{fullstack.contractExportName}}.app).toBeDefined()
|
|
15
|
+
expect(appContract.info).toBeDefined()
|
|
16
|
+
expect(APP_API_ROUTES.info).toBe('/api/v1/app/info')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('validates app info payload', () => {
|
|
20
|
+
const result = AppInfoOutputDataSchema.safeParse({
|
|
21
|
+
name: '{{fullstack.servPackageName}}',
|
|
22
|
+
version: '0.1.0',
|
|
23
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
24
|
+
frontends: [...ENABLED_FRONTENDS],
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
expect(result.success).toBe(true)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('validates echo input boundaries', () => {
|
|
31
|
+
expect(AppEchoInputSchema.safeParse({ message: 'hello' }).success).toBe(true)
|
|
32
|
+
expect(AppEchoInputSchema.safeParse({ message: '' }).success).toBe(false)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('validates HaiResult app info response', () => {
|
|
36
|
+
const result = AppInfoOutputSchema.safeParse({
|
|
37
|
+
success: true,
|
|
38
|
+
data: {
|
|
39
|
+
name: '{{fullstack.servPackageName}}',
|
|
40
|
+
version: '0.1.0',
|
|
41
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
42
|
+
frontends: [...ENABLED_FRONTENDS],
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
expect(result.success).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# {{fullstack.servPackageName}}
|
|
2
|
+
|
|
3
|
+
后端 API Service,基于 `@h-ai/serv` 将共享 contract 装配为最小 HTTP App 抽象,不在应用代码中直接暴露 Hono。
|
|
4
|
+
|
|
5
|
+
## 骨架说明
|
|
6
|
+
|
|
7
|
+
- `src/server-app.ts`:服务装配入口,挂载健康检查、OpenAPI、docs 与 procedures。
|
|
8
|
+
- `src/server/procedures/app-procedures.ts`:app 领域 procedures,对应 contract 中的 `app.info` / `app.echo` 路由。
|
|
9
|
+
- `tests/server-app.test.ts`:通过 `ServerApp.request()` 验证健康检查与业务路由。
|
|
10
|
+
|
|
11
|
+
## 开发
|
|
12
|
+
|
|
13
|
+
- `pnpm --filter {{fullstack.servPackageName}} dev`
|
|
14
|
+
- `pnpm --filter {{fullstack.servPackageName}} test`
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{fullstack.servPackageName}}",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"private": true,
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"lint": "eslint .",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"clean": "rimraf dist .turbo",
|
|
21
|
+
"dev": "pnpm build && node dist/index.js",
|
|
22
|
+
"start": "node dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@h-ai/core": "{{fullstack.versions.core}}",
|
|
26
|
+
"@h-ai/serv": "{{fullstack.versions.serv}}",
|
|
27
|
+
"{{fullstack.contractPackageName}}": "workspace:*"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
import { core } from '@h-ai/core'
|
|
3
|
+
import { serv } from '@h-ai/serv'
|
|
4
|
+
import { createServerApp } from './server-app.js'
|
|
5
|
+
|
|
6
|
+
const app = createServerApp()
|
|
7
|
+
const port = process.env.PORT ? Number(process.env.PORT) : 3000
|
|
8
|
+
const host = process.env.HOST ?? '127.0.0.1'
|
|
9
|
+
const listenApp = app as Parameters<typeof serv.listen>[0]
|
|
10
|
+
|
|
11
|
+
serv.listen(listenApp, {
|
|
12
|
+
port,
|
|
13
|
+
host,
|
|
14
|
+
onListening: info => core.logger.info('{{fullstack.servPackageName}} listening', { host, port: info.port }),
|
|
15
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ServContext } from '@h-ai/serv'
|
|
2
|
+
import type { AppEchoOutputData, AppFrontendTarget, AppInfoOutputData } from '{{fullstack.contractPackageName}}'
|
|
3
|
+
import { ok } from '@h-ai/core'
|
|
4
|
+
import { serv } from '@h-ai/serv'
|
|
5
|
+
import { appContract } from '{{fullstack.contractPackageName}}'
|
|
6
|
+
|
|
7
|
+
export interface CreateAppProceduresOptions {
|
|
8
|
+
readonly name: string
|
|
9
|
+
readonly version: string
|
|
10
|
+
readonly startedAt: string
|
|
11
|
+
readonly frontends: readonly AppFrontendTarget[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const p = serv.implement(appContract).$context<ServContext>()
|
|
15
|
+
|
|
16
|
+
/** app 领域 procedures:把共享 contract 绑定到真实业务处理函数。 */
|
|
17
|
+
export function createAppProcedures(options: CreateAppProceduresOptions) {
|
|
18
|
+
return p.router({
|
|
19
|
+
info: p.info.handler(() => ok(createAppInfo(options))),
|
|
20
|
+
echo: p.echo.handler(({ input, context }) => {
|
|
21
|
+
const data: AppEchoOutputData = {
|
|
22
|
+
message: input.message,
|
|
23
|
+
echoedAt: new Date().toISOString(),
|
|
24
|
+
requestId: context.requestId,
|
|
25
|
+
}
|
|
26
|
+
return ok(data)
|
|
27
|
+
}),
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createAppInfo(options: CreateAppProceduresOptions): AppInfoOutputData {
|
|
32
|
+
return {
|
|
33
|
+
name: options.name,
|
|
34
|
+
version: options.version,
|
|
35
|
+
startedAt: options.startedAt,
|
|
36
|
+
frontends: [...options.frontends],
|
|
37
|
+
}
|
|
38
|
+
}
|
package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CreateAppProceduresOptions } from './app-procedures.js'
|
|
2
|
+
import { createAppProcedures } from './app-procedures.js'
|
|
3
|
+
|
|
4
|
+
export interface CreateServerProceduresOptions {
|
|
5
|
+
readonly app: CreateAppProceduresOptions
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** 工程级 procedures 聚合入口;后续增加领域时在这里追加即可。 */
|
|
9
|
+
export function createServerProcedures(options: CreateServerProceduresOptions) {
|
|
10
|
+
return {
|
|
11
|
+
app: createAppProcedures(options.app),
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { serv } from '@h-ai/serv'
|
|
2
|
+
import { APP_HTTP_ROUTES, ENABLED_FRONTENDS, {{fullstack.contractExportName}} } from '{{fullstack.contractPackageName}}'
|
|
3
|
+
import { createServerProcedures } from './server/procedures/index.js'
|
|
4
|
+
|
|
5
|
+
const STARTED_AT = new Date().toISOString()
|
|
6
|
+
|
|
7
|
+
/** 应用侧最小 HTTP App 抽象;不把底层 Hono 类型暴露给项目代码。 */
|
|
8
|
+
export interface ServerApp {
|
|
9
|
+
readonly fetch: (request: Request) => Response | Promise<Response>
|
|
10
|
+
readonly request: (input: RequestInfo | URL, init?: RequestInit) => Response | Promise<Response>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 创建后端 HTTP App;与 @h-ai/serv 的 createApp 返回语义保持一致。 */
|
|
14
|
+
export function createServerApp(): ServerApp {
|
|
15
|
+
return serv.createApp({
|
|
16
|
+
contract: {{fullstack.contractExportName}},
|
|
17
|
+
procedures: createServerProcedures({
|
|
18
|
+
app: {
|
|
19
|
+
name: '{{fullstack.servPackageName}}',
|
|
20
|
+
version: '0.1.0',
|
|
21
|
+
startedAt: STARTED_AT,
|
|
22
|
+
frontends: [...ENABLED_FRONTENDS],
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
http: {
|
|
26
|
+
apiPrefix: APP_HTTP_ROUTES.apiPrefix,
|
|
27
|
+
health: {
|
|
28
|
+
path: APP_HTTP_ROUTES.health,
|
|
29
|
+
readyPath: APP_HTTP_ROUTES.ready,
|
|
30
|
+
},
|
|
31
|
+
openapi: { path: APP_HTTP_ROUTES.openapi },
|
|
32
|
+
docs: { path: APP_HTTP_ROUTES.docs },
|
|
33
|
+
rpc: false,
|
|
34
|
+
},
|
|
35
|
+
middlewares: [{
|
|
36
|
+
middleware: serv.cors({
|
|
37
|
+
origin: '*',
|
|
38
|
+
allowedHeaders: ['Authorization', 'Content-Type', 'X-Requested-With'],
|
|
39
|
+
}),
|
|
40
|
+
}],
|
|
41
|
+
})
|
|
42
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { APP_API_ROUTES } from '{{fullstack.contractPackageName}}'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { createServerApp } from '../src/server-app.js'
|
|
4
|
+
|
|
5
|
+
describe('{{fullstack.servPackageName}}', () => {
|
|
6
|
+
it('returns health status', async () => {
|
|
7
|
+
const app = createServerApp()
|
|
8
|
+
const response = await app.request(APP_API_ROUTES.health)
|
|
9
|
+
|
|
10
|
+
expect(response.status).toBe(200)
|
|
11
|
+
await expect(response.json()).resolves.toEqual({ status: 'ok' })
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('returns app info as HaiResult', async () => {
|
|
15
|
+
const app = createServerApp()
|
|
16
|
+
const response = await app.request(APP_API_ROUTES.info, { method: 'POST' })
|
|
17
|
+
const body = await response.json()
|
|
18
|
+
|
|
19
|
+
expect(response.status).toBe(200)
|
|
20
|
+
expect(body.success).toBe(true)
|
|
21
|
+
expect(body.data.name).toBe('{{fullstack.servPackageName}}')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('validates echo input', async () => {
|
|
25
|
+
const app = createServerApp()
|
|
26
|
+
const response = await app.request(APP_API_ROUTES.echo, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'content-type': 'application/json' },
|
|
29
|
+
body: JSON.stringify({ message: '' }),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
expect(response.status).toBe(400)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('returns echo result as HaiResult', async () => {
|
|
36
|
+
const app = createServerApp()
|
|
37
|
+
const response = await app.request(APP_API_ROUTES.echo, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'content-type': 'application/json' },
|
|
40
|
+
body: JSON.stringify({ message: 'hello unit' }),
|
|
41
|
+
})
|
|
42
|
+
const body = await response.json()
|
|
43
|
+
|
|
44
|
+
expect(response.status).toBe(200)
|
|
45
|
+
expect(body.success).toBe(true)
|
|
46
|
+
expect(body.data.message).toBe('hello unit')
|
|
47
|
+
expect(body.data.requestId).toBeDefined()
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# {{fullstack.sharedPackageName}}
|
|
2
|
+
|
|
3
|
+
Frontend shared package for **{{projectName}}**. Provides:
|
|
4
|
+
|
|
5
|
+
- `AppShell` — common layout (header / footer / theme + language switchers)
|
|
6
|
+
- `ThemeSwitcher`, `LanguageSwitcher`
|
|
7
|
+
- `themeStore` — daisyUI theme persistence
|
|
8
|
+
- `fetchAppInfo` — typed API client wrapping `@h-ai/api-client`
|
|
9
|
+
- Shared Inlang Paraglide messages compiled from this package's `project.inlang/` + `messages/`
|
|
10
|
+
|
|
11
|
+
Consumed by `{{projectName}}-web`, `{{projectName}}-app`, `{{projectName}}-desktop`, and the reserved `{{projectName}}-miniapp` guidance.
|
|
12
|
+
|
|
13
|
+
## i18n collaboration
|
|
14
|
+
|
|
15
|
+
- `packages/{{fullstack.sharedPackageName}}/messages/` stores **shared UI copy** used by reusable components such as `AppShell`, `ThemeSwitcher`, and `LanguageSwitcher`.
|
|
16
|
+
- `apps/{{projectName}}-web/messages/`, `apps/{{projectName}}-app/messages/`, and `apps/{{projectName}}-desktop/messages/` store **frontend-specific product copy**.
|
|
17
|
+
- `apps/{{projectName}}-miniapp/messages/` is prepared for mini-app-specific copy even though the mini target is currently a placeholder.
|
|
18
|
+
- Keep `baseLocale` and `locales` aligned across shared and every frontend `project.inlang/settings.json`. The language switcher updates shared and the host app runtime together.
|
|
19
|
+
|
|
20
|
+
Rule of thumb: if a string appears inside reusable shared components, put it here; if it belongs to one product surface, put it in that frontend's `messages/` directory.
|
|
21
|
+
|
|
22
|
+
## Scripts
|
|
23
|
+
|
|
24
|
+
- `pnpm paraglide:compile` — compile messages → `src/lib/paraglide/`
|
|
25
|
+
- `pnpm paraglide:watch` — watch + recompile on message edits
|
|
26
|
+
- `pnpm typecheck`, `pnpm lint`, `pnpm test`
|
|
27
|
+
|
|
28
|
+
## Adding a locale
|
|
29
|
+
|
|
30
|
+
1. Add the locale to `project.inlang/settings.json` `locales` array.
|
|
31
|
+
2. Add the same locale to every enabled frontend `project.inlang/settings.json`.
|
|
32
|
+
3. Create `messages/{locale}.json` in shared and each enabled frontend.
|
|
33
|
+
4. Run root `pnpm i18n:compile` to compile shared and every enabled frontend Paraglide output. You can also run `pnpm paraglide:compile` inside one package when editing that package only.
|
|
34
|
+
|
|
35
|
+
## Customizing theme and shell
|
|
36
|
+
|
|
37
|
+
- Theme names come from the scaffold `fullstack.theme` config and are rendered into each frontend `src/app.css` daisyUI plugin block.
|
|
38
|
+
- `themeStore` persists the selected theme in local storage under `{{projectName}}:theme`.
|
|
39
|
+
- To customize the global chrome, edit `src/lib/components/AppShell.svelte`; to customize a single frontend page, edit that frontend's route and messages.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{{! @skipIfEmpty }}
|
|
2
|
+
{
|
|
3
|
+
"$schema": "https://inlang.com/schema/inlang-message-format",
|
|
4
|
+
"app_name": "{{projectName}}",
|
|
5
|
+
"nav_home": "Home",
|
|
6
|
+
"nav_settings": "Settings",
|
|
7
|
+
"settings_title": "Settings",
|
|
8
|
+
"settings_language": "Language",
|
|
9
|
+
"settings_theme": "Theme",
|
|
10
|
+
"shell_footer": "Hai Framework · {platform}",
|
|
11
|
+
"shell_tagline": "Shared shell · Theme · Locale",
|
|
12
|
+
"home_welcome": "Welcome to {name}",
|
|
13
|
+
"home_subtitle": "Hai Framework fullstack starter for web / app / desktop / mini collaboration",
|
|
14
|
+
"home_status_title": "Service status",
|
|
15
|
+
"home_refresh": "Refresh",
|
|
16
|
+
"home_loading": "Loading…",
|
|
17
|
+
"home_error": "Unable to reach the API service",
|
|
18
|
+
"home_started_at": "Started at: {time}",
|
|
19
|
+
"home_frontends": "Enabled frontends: {list}",
|
|
20
|
+
"platform_web": "Web",
|
|
21
|
+
"platform_app": "App",
|
|
22
|
+
"platform_desktop": "Desktop",
|
|
23
|
+
"platform_miniapp": "Mini"
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{{! @skipIfEmpty }}
|
|
2
|
+
{
|
|
3
|
+
"$schema": "https://inlang.com/schema/inlang-message-format",
|
|
4
|
+
"app_name": "{{projectName}}",
|
|
5
|
+
"nav_home": "首页",
|
|
6
|
+
"nav_settings": "设置",
|
|
7
|
+
"settings_title": "设置",
|
|
8
|
+
"settings_language": "语言",
|
|
9
|
+
"settings_theme": "主题",
|
|
10
|
+
"shell_footer": "Hai Framework · {platform}",
|
|
11
|
+
"shell_tagline": "共享 Shell · 主题 · 语言",
|
|
12
|
+
"home_welcome": "欢迎使用 {name}",
|
|
13
|
+
"home_subtitle": "Hai Framework 全栈样板(web / app / desktop / mini 协同)",
|
|
14
|
+
"home_status_title": "服务状态",
|
|
15
|
+
"home_refresh": "刷新",
|
|
16
|
+
"home_loading": "加载中…",
|
|
17
|
+
"home_error": "无法连接到 API 服务",
|
|
18
|
+
"home_started_at": "启动时间:{time}",
|
|
19
|
+
"home_frontends": "已启用前端:{list}",
|
|
20
|
+
"platform_web": "Web",
|
|
21
|
+
"platform_app": "App",
|
|
22
|
+
"platform_desktop": "Desktop",
|
|
23
|
+
"platform_miniapp": "Mini"
|
|
24
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{fullstack.sharedPackageName}}",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"private": true,
|
|
6
|
+
"description": "Shared frontend code (AppShell / theme / i18n / API) for {{projectName}} web/app/desktop/mini.",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./components/*": "./src/lib/components/*",
|
|
10
|
+
"./stores/*": "./src/lib/stores/*",
|
|
11
|
+
"./api": "./src/lib/api/api-client.ts",
|
|
12
|
+
"./project.inlang/settings.json": "./project.inlang/settings.json",
|
|
13
|
+
"./messages/*": "./messages/*",
|
|
14
|
+
"./paraglide/messages": "./src/lib/paraglide/messages.js",
|
|
15
|
+
"./paraglide/runtime": "./src/lib/paraglide/runtime.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"project.inlang",
|
|
20
|
+
"messages"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"paraglide:compile": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide",
|
|
24
|
+
"paraglide:watch": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide --watch",
|
|
25
|
+
"prepare": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide",
|
|
26
|
+
"typecheck": "pnpm paraglide:compile && svelte-check --tsconfig ./tsconfig.json",
|
|
27
|
+
"lint": "eslint .",
|
|
28
|
+
"test": "vitest run --passWithNoTests",
|
|
29
|
+
"clean": "rimraf .turbo src/lib/paraglide"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@h-ai/api-client": "{{fullstack.versions.apiClient}}",
|
|
33
|
+
"@h-ai/ui": "{{fullstack.versions.ui}}",
|
|
34
|
+
"{{fullstack.contractPackageName}}": "workspace:*"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@inlang/paraglide-js": "{{fullstack.versions.inlangParaglide}}",
|
|
38
|
+
"@inlang/plugin-message-format": "{{fullstack.versions.inlangMessageFormat}}",
|
|
39
|
+
"svelte": "{{fullstack.versions.svelte}}",
|
|
40
|
+
"svelte-check": "{{fullstack.versions.svelteCheck}}",
|
|
41
|
+
"typescript": "{{fullstack.versions.typescript}}",
|
|
42
|
+
"vitest": "{{fullstack.versions.vitest}}"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/templates/apps/fullstack/packages/{{projectName}}-shared/project.inlang/settings.json.hbs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://inlang.com/schema/project-settings",
|
|
3
|
+
"baseLocale": "{{fullstack.i18n.baseLocale}}",
|
|
4
|
+
"locales": [{{#each fullstack.i18n.locales}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}],
|
|
5
|
+
"modules": [
|
|
6
|
+
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js"
|
|
7
|
+
],
|
|
8
|
+
"plugin.inlang.messageFormat": {
|
|
9
|
+
"pathPattern": "./messages/{locale}.json"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { default as AppShell } from './lib/components/AppShell.svelte'
|
|
2
|
+
export { default as LanguageSwitcher } from './lib/components/LanguageSwitcher.svelte'
|
|
3
|
+
export { default as ThemeSwitcher } from './lib/components/ThemeSwitcher.svelte'
|
|
4
|
+
export { AVAILABLE_THEMES, themeStore } from './lib/stores/theme.svelte.js'
|
|
5
|
+
export type { AppInfo, FetchAppInfoOptions } from './lib/api/api-client.js'
|
|
6
|
+
export { fetchAppInfo } from './lib/api/api-client.js'
|
|
7
|
+
export * as messages from './lib/paraglide/messages.js'
|
|
8
|
+
export { getLocale, locales, setLocale } from './lib/paraglide/runtime.js'
|
package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AppInfoOutputData } from '{{fullstack.contractPackageName}}'
|
|
2
|
+
import { apiClient } from '@h-ai/api-client'
|
|
3
|
+
import { APP_HTTP_ROUTES, {{fullstack.contractExportName}} } from '{{fullstack.contractPackageName}}'
|
|
4
|
+
|
|
5
|
+
export type AppInfo = AppInfoOutputData
|
|
6
|
+
|
|
7
|
+
const DEFAULT_API_ORIGIN = 'http://127.0.0.1:3000'
|
|
8
|
+
|
|
9
|
+
export interface FetchAppInfoOptions {
|
|
10
|
+
fetchFn?: typeof fetch
|
|
11
|
+
baseUrl?: string
|
|
12
|
+
clientName?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function fetchAppInfo(options: FetchAppInfoOptions = {}): Promise<AppInfo> {
|
|
16
|
+
const fetchFn = options.fetchFn ?? fetch
|
|
17
|
+
const baseUrl = options.baseUrl ?? DEFAULT_API_ORIGIN
|
|
18
|
+
const clientName = options.clientName ?? '{{projectName}}-shared'
|
|
19
|
+
|
|
20
|
+
const client = apiClient.create({{fullstack.contractExportName}})
|
|
21
|
+
const init = await client.init({
|
|
22
|
+
baseUrl: normalizeApiBaseUrl(baseUrl),
|
|
23
|
+
clientName,
|
|
24
|
+
fetch: fetchFn,
|
|
25
|
+
})
|
|
26
|
+
if (!init.success)
|
|
27
|
+
throw new Error(init.error?.message ?? 'Failed to initialize API client')
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const result = await client.app.info()
|
|
31
|
+
if (!result.success)
|
|
32
|
+
throw new Error(result.error?.message ?? 'Failed to fetch app info')
|
|
33
|
+
return result.data
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await client.close()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeApiBaseUrl(value: string): string {
|
|
41
|
+
const trimmed = trimTrailingSlash(value)
|
|
42
|
+
return trimmed.endsWith(APP_HTTP_ROUTES.apiPrefix)
|
|
43
|
+
? trimmed
|
|
44
|
+
: `${trimmed}${APP_HTTP_ROUTES.apiPrefix}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function trimTrailingSlash(value: string): string {
|
|
48
|
+
return value.replace(/\/+$/, '')
|
|
49
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import * as m from '../paraglide/messages.js'
|
|
4
|
+
import LanguageSwitcher from './LanguageSwitcher.svelte'
|
|
5
|
+
import ThemeSwitcher from './ThemeSwitcher.svelte'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** 当前运行的前端平台 */
|
|
9
|
+
platform: 'web' | 'app' | 'desktop' | 'miniapp'
|
|
10
|
+
/** 宿主应用本地 Paraglide runtime 的同步回调 */
|
|
11
|
+
onLocaleChange?: (locale: string) => void
|
|
12
|
+
/** 主内容 */
|
|
13
|
+
children: Snippet
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { platform, onLocaleChange, children }: Props = $props()
|
|
17
|
+
|
|
18
|
+
function getPlatformLabel(value: Props['platform']): string {
|
|
19
|
+
if (value === 'web')
|
|
20
|
+
return m.platform_web()
|
|
21
|
+
if (value === 'app')
|
|
22
|
+
return m.platform_app()
|
|
23
|
+
if (value === 'desktop')
|
|
24
|
+
return m.platform_desktop()
|
|
25
|
+
return m.platform_miniapp()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const platformLabel = $derived(getPlatformLabel(platform))
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<div class="min-h-screen flex flex-col overflow-hidden bg-base-100 text-base-content">
|
|
32
|
+
<div class="pointer-events-none fixed inset-0 -z-10 bg-[radial-gradient(circle_at_top_left,var(--color-primary)_0,transparent_34%),radial-gradient(circle_at_bottom_right,var(--color-secondary)_0,transparent_30%)] opacity-10"></div>
|
|
33
|
+
|
|
34
|
+
<header class="navbar sticky top-0 z-20 border-b border-base-300/70 bg-base-100/80 px-4 shadow-sm backdrop-blur-xl lg:px-8">
|
|
35
|
+
<div class="flex-1 flex items-center gap-3">
|
|
36
|
+
<div class="grid size-10 place-items-center rounded-2xl bg-primary text-primary-content shadow-lg shadow-primary/20">
|
|
37
|
+
<span class="icon-[tabler--sparkles] size-5"></span>
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<span class="block text-lg font-semibold leading-tight">{m.app_name()}</span>
|
|
41
|
+
<span class="text-xs text-base-content/55">{m.shell_tagline()}</span>
|
|
42
|
+
</div>
|
|
43
|
+
<span class="badge badge-primary badge-outline ml-1 hidden sm:inline-flex">{platformLabel}</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="flex-none flex items-end gap-2">
|
|
46
|
+
<ThemeSwitcher />
|
|
47
|
+
<LanguageSwitcher {onLocaleChange} />
|
|
48
|
+
</div>
|
|
49
|
+
</header>
|
|
50
|
+
|
|
51
|
+
<main class="flex-1 px-4 py-8 lg:px-8 lg:py-10">
|
|
52
|
+
<div class="mx-auto w-full max-w-6xl">
|
|
53
|
+
{@render children()}
|
|
54
|
+
</div>
|
|
55
|
+
</main>
|
|
56
|
+
|
|
57
|
+
<footer class="footer footer-center border-t border-base-300/70 bg-base-100/70 p-4 text-sm text-base-content/60 backdrop-blur-xl">
|
|
58
|
+
<span>{m.shell_footer({ platform: platformLabel })}</span>
|
|
59
|
+
</footer>
|
|
60
|
+
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as m from '../paraglide/messages.js'
|
|
3
|
+
import { getLocale, locales, setLocale } from '../paraglide/runtime.js'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** 同步宿主应用自己的 Paraglide runtime */
|
|
7
|
+
onLocaleChange?: (locale: string) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { onLocaleChange }: Props = $props()
|
|
11
|
+
|
|
12
|
+
function handleChange(event: Event) {
|
|
13
|
+
const target = event.target as HTMLSelectElement
|
|
14
|
+
const locale = target.value as (typeof locales)[number]
|
|
15
|
+
setLocale(locale)
|
|
16
|
+
onLocaleChange?.(locale)
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<label class="form-control w-32 sm:w-40">
|
|
21
|
+
<span class="label-text text-xs">{m.settings_language()}</span>
|
|
22
|
+
<select class="select select-bordered select-sm bg-base-100/80" value={getLocale()} onchange={handleChange}>
|
|
23
|
+
{#each locales as locale (locale)}
|
|
24
|
+
<option value={locale}>{locale}</option>
|
|
25
|
+
{/each}
|
|
26
|
+
</select>
|
|
27
|
+
</label>
|