app.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import { Log } from "../util/log"
  2. import { Context } from "../util/context"
  3. import { Filesystem } from "../util/filesystem"
  4. import { Global } from "../global"
  5. import path from "path"
  6. import os from "os"
  7. import { z } from "zod"
  8. export namespace App {
  9. const log = Log.create({ service: "app" })
  10. export const Info = z
  11. .object({
  12. user: z.string(),
  13. git: z.boolean(),
  14. path: z.object({
  15. config: z.string(),
  16. data: z.string(),
  17. root: z.string(),
  18. cwd: z.string(),
  19. }),
  20. time: z.object({
  21. initialized: z.number().optional(),
  22. }),
  23. })
  24. .openapi({
  25. ref: "App.Info",
  26. })
  27. export type Info = z.infer<typeof Info>
  28. const ctx = Context.create<Awaited<ReturnType<typeof create>>>("app")
  29. const APP_JSON = "app.json"
  30. async function create(input: { cwd: string; version: string }) {
  31. log.info("creating", {
  32. cwd: input.cwd,
  33. version: input.version,
  34. })
  35. const git = await Filesystem.findUp(".git", input.cwd).then(([x]) =>
  36. x ? path.dirname(x) : undefined,
  37. )
  38. log.info("git", { git })
  39. const data = path.join(
  40. Global.Path.data,
  41. "project",
  42. git ? git.split(path.sep).join("-") : "global",
  43. )
  44. const stateFile = Bun.file(path.join(data, APP_JSON))
  45. const state = (await stateFile.json().catch(() => ({}))) as {
  46. initialized: number
  47. version: string
  48. }
  49. state.version = input.version
  50. await stateFile.write(JSON.stringify(state))
  51. const services = new Map<
  52. any,
  53. {
  54. state: any
  55. shutdown?: (input: any) => Promise<void>
  56. }
  57. >()
  58. const info: Info = {
  59. user: os.userInfo().username,
  60. time: {
  61. initialized: state.initialized,
  62. },
  63. git: git !== undefined,
  64. path: {
  65. config: Global.Path.config,
  66. data,
  67. root: git ?? input.cwd,
  68. cwd: input.cwd,
  69. },
  70. }
  71. const result = {
  72. version: input.version,
  73. services,
  74. info,
  75. }
  76. return result
  77. }
  78. export function state<State>(
  79. key: any,
  80. init: (app: Info) => State,
  81. shutdown?: (state: Awaited<State>) => Promise<void>,
  82. ) {
  83. return () => {
  84. const app = ctx.use()
  85. const services = app.services
  86. if (!services.has(key)) {
  87. log.info("registering service", { name: key })
  88. services.set(key, {
  89. state: init(app.info),
  90. shutdown: shutdown,
  91. })
  92. }
  93. return services.get(key)?.state as State
  94. }
  95. }
  96. export function info() {
  97. return ctx.use().info
  98. }
  99. export async function provide<T extends (app: Info) => any>(
  100. input: { cwd: string; version: string },
  101. cb: T,
  102. ) {
  103. const app = await create(input)
  104. return ctx.provide(app, async () => {
  105. const result = await cb(app.info)
  106. for (const [key, entry] of app.services.entries()) {
  107. log.info("shutdown", { name: key })
  108. await entry.shutdown?.(await entry.state)
  109. }
  110. return result
  111. })
  112. }
  113. export async function initialize() {
  114. const { info, version } = ctx.use()
  115. info.time.initialized = Date.now()
  116. await Bun.write(
  117. path.join(info.path.data, APP_JSON),
  118. JSON.stringify({
  119. version,
  120. initialized: Date.now(),
  121. }),
  122. )
  123. }
  124. }