app.ts 3.3 KB

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