@atomservice/app 0.1.5

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/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@atomservice/app",
3
+ "version": "0.1.5",
4
+ "description": "通用应用容器原子服务",
5
+ "type": "module",
6
+ "author": "openorson",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/openorson/atomservice.git",
10
+ "directory": "services/app"
11
+ },
12
+ "bugs": "https://github.com/openorson/atomservice/issues",
13
+ "homepage": "https://github.com/openorson/atomservice/tree/main/services/app#readme",
14
+ "keywords": [
15
+ "atomservice",
16
+ "container",
17
+ "app",
18
+ "self-hosted",
19
+ "podman"
20
+ ],
21
+ "engines": {
22
+ "bun": ">=1.3.14"
23
+ },
24
+ "files": [
25
+ "src"
26
+ ],
27
+ "exports": {
28
+ ".": "./src/index.ts"
29
+ },
30
+ "dependencies": {
31
+ "@atomservice/gateway": "0.1.5"
32
+ },
33
+ "peerDependencies": {
34
+ "@atomservice/core": "0.1.5"
35
+ }
36
+ }
@@ -0,0 +1,30 @@
1
+ import { tmpl } from "@atomservice/core"
2
+
3
+ export function appCompose(
4
+ id: string,
5
+ contextPath: string,
6
+ containerfilePath: string | undefined,
7
+ port: number,
8
+ hostPort?: number,
9
+ ): string {
10
+ const portsBlock = hostPort ? `\n ports:\n - "${hostPort}:${port}"` : ""
11
+ const dockerfileBlock = containerfilePath ? `\n dockerfile: ${containerfilePath}` : ""
12
+
13
+ return tmpl`\
14
+ services:
15
+ ${id}:
16
+ build:
17
+ context: ${contextPath}${dockerfileBlock}
18
+ container_name: ${id}
19
+ expose:
20
+ - "${port}"${portsBlock}
21
+ networks:
22
+ - atomservice
23
+ restart: unless-stopped
24
+
25
+ networks:
26
+ atomservice:
27
+ name: atomservice
28
+ external: true
29
+ `
30
+ }
@@ -0,0 +1,20 @@
1
+ import type { CallableService } from "@atomservice/core"
2
+ import type { GatewayInstance } from "@atomservice/gateway"
3
+
4
+ export type GitConfig = boolean | { branch?: string }
5
+
6
+ export interface ProjectConfig {
7
+ root: { path: string }
8
+ package?: { path: string }
9
+ }
10
+
11
+ export interface AppOptions {
12
+ id?: string
13
+ project: ProjectConfig
14
+ git?: GitConfig
15
+ port?: number
16
+ hostPort?: number
17
+ healthPath?: string
18
+ domain?: string
19
+ gateway?: CallableService<GatewayInstance>
20
+ }
@@ -0,0 +1,89 @@
1
+ import path from "node:path"
2
+ import type { CallableService } from "@atomservice/core"
3
+ import { defineService, onDown, onHealth, onUp, useConfig, useLogger, useShell } from "@atomservice/core"
4
+ import { appCompose } from "./app.compose.ts"
5
+ import type { AppOptions, GitConfig } from "./app.options.ts"
6
+
7
+ export function app(opts: AppOptions): CallableService {
8
+ const serviceId = opts.id && opts.id !== "default" ? `app-${opts.id}` : "app"
9
+ const port = opts.port ?? 3000
10
+
11
+ return defineService({
12
+ name: "app",
13
+ id: opts.id,
14
+ setup: () => {
15
+ const logger = useLogger()
16
+ const $ = useShell()
17
+ const config = useConfig()
18
+ const gw = opts.gateway?.()
19
+
20
+ const composePath = path.join(config.root, serviceId, "compose.yml")
21
+
22
+ function resolveProjectPaths() {
23
+ const rootPath = opts.project.root.path
24
+ const pkg = opts.project.package
25
+ const packagePath = pkg ? (pkg.path.startsWith("/") ? pkg.path : path.join(rootPath, pkg.path)) : undefined
26
+ const containerfilePath = packagePath ? path.join(packagePath, "Containerfile") : undefined
27
+ return { rootPath, containerfilePath }
28
+ }
29
+
30
+ function resolveGitBranch(git: GitConfig | undefined): string | null {
31
+ if (!git) return null
32
+ if (git === true) return "main"
33
+ return git.branch ?? "main"
34
+ }
35
+
36
+ onUp(async () => {
37
+ const { rootPath, containerfilePath } = resolveProjectPaths()
38
+ const branch = resolveGitBranch(opts.git)
39
+
40
+ if (branch) {
41
+ logger.info(`拉取代码 ${branch}…`)
42
+ await $`git -C ${rootPath} pull origin ${branch}`
43
+ }
44
+
45
+ await $`mkdir -p ${path.join(config.root, serviceId)}`
46
+ await Bun.write(composePath, appCompose(serviceId, rootPath, containerfilePath, port, opts.hostPort))
47
+
48
+ logger.info(`构建并启动 ${serviceId}…`)
49
+ await $`podman compose -f ${composePath} up -d --build`
50
+
51
+ if (opts.domain && gw) {
52
+ logger.info(`注册路由 ${opts.domain} → ${serviceId}:${port}`)
53
+ await gw.registerRoute(opts.domain, `${serviceId}:${port}`)
54
+ }
55
+
56
+ logger.success(`${serviceId} 已启动`)
57
+ })
58
+
59
+ onDown(async () => {
60
+ logger.info(`停止 ${serviceId}…`)
61
+ await $`podman compose -f ${composePath} down`
62
+
63
+ if (opts.domain && gw) {
64
+ await gw.unregisterRoute(opts.domain)
65
+ }
66
+
67
+ logger.success(`${serviceId} 已停止`)
68
+ })
69
+
70
+ onHealth(async () => {
71
+ if (opts.hostPort) {
72
+ try {
73
+ const res = await fetch(`http://localhost:${opts.hostPort}${opts.healthPath ?? "/"}`)
74
+ if (res.ok || res.status < 500) return { status: "healthy" }
75
+ return { status: "unhealthy", message: `HTTP ${res.status}` }
76
+ } catch (e) {
77
+ return { status: "unhealthy", message: String(e) }
78
+ }
79
+ }
80
+
81
+ const result = await $`podman inspect --format {{.State.Status}} ${serviceId}`.quiet().nothrow()
82
+ const containerStatus = result.text().trim()
83
+ return containerStatus === "running"
84
+ ? { status: "healthy" }
85
+ : { status: "unhealthy", message: `容器状态: ${containerStatus}` }
86
+ })
87
+ },
88
+ })
89
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export type { AppOptions, GitConfig, ProjectConfig } from "./app.options.ts"
2
+ export { app } from "./app.service.ts"