@supatype/cli 0.1.5 → 0.1.7

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.
Files changed (247) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +143 -93
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/deno.d.ts +52 -0
  5. package/dist/augmentation-generator.d.ts +22 -0
  6. package/dist/augmentation-generator.d.ts.map +1 -1
  7. package/dist/augmentation-generator.js +43 -7
  8. package/dist/augmentation-generator.js.map +1 -1
  9. package/dist/binary-cache.d.ts +14 -0
  10. package/dist/binary-cache.d.ts.map +1 -1
  11. package/dist/binary-cache.js +33 -13
  12. package/dist/binary-cache.js.map +1 -1
  13. package/dist/cache-clean.d.ts +15 -0
  14. package/dist/cache-clean.d.ts.map +1 -0
  15. package/dist/cache-clean.js +76 -0
  16. package/dist/cache-clean.js.map +1 -0
  17. package/dist/cache-pins.d.ts +51 -0
  18. package/dist/cache-pins.d.ts.map +1 -0
  19. package/dist/cache-pins.js +184 -0
  20. package/dist/cache-pins.js.map +1 -0
  21. package/dist/cli-install-method.d.ts +19 -0
  22. package/dist/cli-install-method.d.ts.map +1 -0
  23. package/dist/cli-install-method.js +60 -0
  24. package/dist/cli-install-method.js.map +1 -0
  25. package/dist/cli.d.ts.map +1 -1
  26. package/dist/cli.js +6 -0
  27. package/dist/cli.js.map +1 -1
  28. package/dist/cloud-credentials.d.ts +34 -0
  29. package/dist/cloud-credentials.d.ts.map +1 -0
  30. package/dist/cloud-credentials.js +139 -0
  31. package/dist/cloud-credentials.js.map +1 -0
  32. package/dist/commands/admin.d.ts +11 -0
  33. package/dist/commands/admin.d.ts.map +1 -1
  34. package/dist/commands/admin.js +131 -22
  35. package/dist/commands/admin.js.map +1 -1
  36. package/dist/commands/cache.d.ts.map +1 -1
  37. package/dist/commands/cache.js +67 -71
  38. package/dist/commands/cache.js.map +1 -1
  39. package/dist/commands/cloud.d.ts.map +1 -1
  40. package/dist/commands/cloud.js +32 -8
  41. package/dist/commands/cloud.js.map +1 -1
  42. package/dist/commands/db.d.ts +1 -0
  43. package/dist/commands/db.d.ts.map +1 -1
  44. package/dist/commands/db.js +147 -1
  45. package/dist/commands/db.js.map +1 -1
  46. package/dist/commands/dev.d.ts.map +1 -1
  47. package/dist/commands/dev.js +50 -10
  48. package/dist/commands/dev.js.map +1 -1
  49. package/dist/commands/doctor.d.ts +28 -0
  50. package/dist/commands/doctor.d.ts.map +1 -1
  51. package/dist/commands/doctor.js +60 -3
  52. package/dist/commands/doctor.js.map +1 -1
  53. package/dist/commands/functions.d.ts +7 -0
  54. package/dist/commands/functions.d.ts.map +1 -1
  55. package/dist/commands/functions.js +40 -6
  56. package/dist/commands/functions.js.map +1 -1
  57. package/dist/commands/generate.d.ts.map +1 -1
  58. package/dist/commands/generate.js +5 -1
  59. package/dist/commands/generate.js.map +1 -1
  60. package/dist/commands/hooks.d.ts +12 -0
  61. package/dist/commands/hooks.d.ts.map +1 -0
  62. package/dist/commands/hooks.js +118 -0
  63. package/dist/commands/hooks.js.map +1 -0
  64. package/dist/commands/init.d.ts.map +1 -1
  65. package/dist/commands/init.js +41 -7
  66. package/dist/commands/init.js.map +1 -1
  67. package/dist/commands/internal.d.ts +6 -0
  68. package/dist/commands/internal.d.ts.map +1 -0
  69. package/dist/commands/internal.js +22 -0
  70. package/dist/commands/internal.js.map +1 -0
  71. package/dist/commands/keys.d.ts.map +1 -1
  72. package/dist/commands/keys.js +2 -0
  73. package/dist/commands/keys.js.map +1 -1
  74. package/dist/commands/link-helpers.d.ts.map +1 -1
  75. package/dist/commands/link-helpers.js +8 -2
  76. package/dist/commands/link-helpers.js.map +1 -1
  77. package/dist/commands/login.d.ts +7 -0
  78. package/dist/commands/login.d.ts.map +1 -0
  79. package/dist/commands/login.js +110 -0
  80. package/dist/commands/login.js.map +1 -0
  81. package/dist/commands/push.d.ts.map +1 -1
  82. package/dist/commands/push.js +110 -7
  83. package/dist/commands/push.js.map +1 -1
  84. package/dist/commands/self-update.d.ts +7 -0
  85. package/dist/commands/self-update.d.ts.map +1 -1
  86. package/dist/commands/self-update.js +18 -11
  87. package/dist/commands/self-update.js.map +1 -1
  88. package/dist/config.d.ts.map +1 -1
  89. package/dist/config.js +18 -3
  90. package/dist/config.js.map +1 -1
  91. package/dist/db-preflight.d.ts +78 -0
  92. package/dist/db-preflight.d.ts.map +1 -0
  93. package/dist/db-preflight.js +452 -0
  94. package/dist/db-preflight.js.map +1 -0
  95. package/dist/dev-compose.d.ts +1 -1
  96. package/dist/dev-compose.d.ts.map +1 -1
  97. package/dist/dev-compose.js +129 -72
  98. package/dist/dev-compose.js.map +1 -1
  99. package/dist/env-file.d.ts +40 -2
  100. package/dist/env-file.d.ts.map +1 -1
  101. package/dist/env-file.js +132 -11
  102. package/dist/env-file.js.map +1 -1
  103. package/dist/field-masking-tier.d.ts +50 -0
  104. package/dist/field-masking-tier.d.ts.map +1 -0
  105. package/dist/field-masking-tier.js +91 -0
  106. package/dist/field-masking-tier.js.map +1 -0
  107. package/dist/functions-deno-types.d.ts +24 -0
  108. package/dist/functions-deno-types.d.ts.map +1 -0
  109. package/dist/functions-deno-types.js +155 -0
  110. package/dist/functions-deno-types.js.map +1 -0
  111. package/dist/hook-upload.d.ts +30 -0
  112. package/dist/hook-upload.d.ts.map +1 -0
  113. package/dist/hook-upload.js +86 -0
  114. package/dist/hook-upload.js.map +1 -0
  115. package/dist/hooks-generator.d.ts +8 -0
  116. package/dist/hooks-generator.d.ts.map +1 -0
  117. package/dist/hooks-generator.js +332 -0
  118. package/dist/hooks-generator.js.map +1 -0
  119. package/dist/link.d.ts +4 -0
  120. package/dist/link.d.ts.map +1 -1
  121. package/dist/link.js +2 -0
  122. package/dist/link.js.map +1 -1
  123. package/dist/local-secrets.d.ts +71 -0
  124. package/dist/local-secrets.d.ts.map +1 -0
  125. package/dist/local-secrets.js +100 -0
  126. package/dist/local-secrets.js.map +1 -0
  127. package/dist/model-hooks.d.ts +73 -0
  128. package/dist/model-hooks.d.ts.map +1 -0
  129. package/dist/model-hooks.js +211 -0
  130. package/dist/model-hooks.js.map +1 -0
  131. package/dist/postgres-ctl.d.ts +11 -0
  132. package/dist/postgres-ctl.d.ts.map +1 -1
  133. package/dist/postgres-ctl.js +25 -2
  134. package/dist/postgres-ctl.js.map +1 -1
  135. package/dist/project-config.d.ts +121 -1
  136. package/dist/project-config.d.ts.map +1 -1
  137. package/dist/project-config.js +150 -1
  138. package/dist/project-config.js.map +1 -1
  139. package/dist/resolve-target.d.ts +4 -0
  140. package/dist/resolve-target.d.ts.map +1 -1
  141. package/dist/resolve-target.js +69 -73
  142. package/dist/resolve-target.js.map +1 -1
  143. package/dist/schema-ast-v2.d.ts +2 -1
  144. package/dist/schema-ast-v2.d.ts.map +1 -1
  145. package/dist/schema-ast-v2.js +4 -2
  146. package/dist/schema-ast-v2.js.map +1 -1
  147. package/dist/schema-push-lock.d.ts.map +1 -1
  148. package/dist/schema-push-lock.js +43 -16
  149. package/dist/schema-push-lock.js.map +1 -1
  150. package/dist/self-host-compose.d.ts +24 -0
  151. package/dist/self-host-compose.d.ts.map +1 -1
  152. package/dist/self-host-compose.js +301 -69
  153. package/dist/self-host-compose.js.map +1 -1
  154. package/dist/service-role-check.d.ts +21 -0
  155. package/dist/service-role-check.d.ts.map +1 -0
  156. package/dist/service-role-check.js +85 -0
  157. package/dist/service-role-check.js.map +1 -0
  158. package/dist/target-client.d.ts +17 -0
  159. package/dist/target-client.d.ts.map +1 -1
  160. package/dist/target-client.js +69 -17
  161. package/dist/target-client.js.map +1 -1
  162. package/dist/type-extractor.d.ts.map +1 -1
  163. package/dist/type-extractor.js +736 -32
  164. package/dist/type-extractor.js.map +1 -1
  165. package/dist/ui/runtime/command-chrome.js +1 -1
  166. package/dist/ui/runtime/command-chrome.js.map +1 -1
  167. package/dist/ui/runtime/stdin-after-ink.d.ts.map +1 -1
  168. package/dist/ui/runtime/stdin-after-ink.js +3 -1
  169. package/dist/ui/runtime/stdin-after-ink.js.map +1 -1
  170. package/package.json +4 -1
  171. package/scripts/generate-homebrew-formula.mjs +121 -0
  172. package/src/augmentation-generator.ts +46 -9
  173. package/src/binary-cache.ts +47 -15
  174. package/src/cache-clean.ts +81 -0
  175. package/src/cache-pins.ts +243 -0
  176. package/src/cli-install-method.ts +67 -0
  177. package/src/cli.ts +6 -0
  178. package/src/cloud-credentials.ts +183 -0
  179. package/src/commands/admin.ts +153 -28
  180. package/src/commands/cache.ts +90 -75
  181. package/src/commands/cloud.ts +32 -8
  182. package/src/commands/db.ts +162 -1
  183. package/src/commands/dev.ts +57 -8
  184. package/src/commands/doctor.ts +69 -3
  185. package/src/commands/functions.ts +42 -5
  186. package/src/commands/generate.ts +5 -1
  187. package/src/commands/hooks.ts +138 -0
  188. package/src/commands/init.ts +42 -8
  189. package/src/commands/internal.ts +23 -0
  190. package/src/commands/keys.ts +2 -0
  191. package/src/commands/link-helpers.ts +9 -2
  192. package/src/commands/login.ts +127 -0
  193. package/src/commands/push.ts +124 -7
  194. package/src/commands/self-update.ts +85 -13
  195. package/src/config.ts +22 -3
  196. package/src/db-preflight.ts +575 -0
  197. package/src/dev-compose.ts +144 -73
  198. package/src/env-file.ts +157 -11
  199. package/src/field-masking-tier.ts +107 -0
  200. package/src/functions-deno-types.ts +188 -0
  201. package/src/hook-upload.ts +107 -0
  202. package/src/hooks-generator.ts +352 -0
  203. package/src/link.ts +7 -0
  204. package/src/local-secrets.ts +104 -0
  205. package/src/model-hooks.ts +260 -0
  206. package/src/postgres-ctl.ts +29 -2
  207. package/src/project-config.ts +236 -2
  208. package/src/resolve-target.ts +113 -68
  209. package/src/schema-ast-v2.ts +5 -2
  210. package/src/schema-push-lock.ts +56 -17
  211. package/src/self-host-compose.ts +344 -68
  212. package/src/service-role-check.ts +111 -0
  213. package/src/target-client.ts +105 -18
  214. package/src/type-extractor.ts +883 -28
  215. package/src/ui/runtime/command-chrome.ts +1 -1
  216. package/src/ui/runtime/stdin-after-ink.ts +3 -1
  217. package/tests/access-composition.test.ts +667 -0
  218. package/tests/admin-studio-membership.test.ts +121 -0
  219. package/tests/api-schemas.test.ts +83 -0
  220. package/tests/archive-name-alias.test.ts +42 -0
  221. package/tests/augmentation-generator.test.ts +72 -4
  222. package/tests/cache-clean.test.ts +206 -0
  223. package/tests/cli-help.test.ts +3 -0
  224. package/tests/cli-install-method.test.ts +66 -0
  225. package/tests/command-chrome-coverage.test.ts +31 -26
  226. package/tests/command-chrome-wrap.test.ts +1 -0
  227. package/tests/db-preflight.test.ts +72 -0
  228. package/tests/doctor-render.test.ts +62 -0
  229. package/tests/doctor-service-role.test.ts +55 -0
  230. package/tests/env-file.test.ts +121 -0
  231. package/tests/external-database-compose.test.ts +184 -0
  232. package/tests/external-database-config.test.ts +154 -0
  233. package/tests/field-masking-tier.test.ts +184 -0
  234. package/tests/functions-deno-types.test.ts +197 -0
  235. package/tests/homebrew-formula.test.ts +58 -0
  236. package/tests/hook-upload.test.ts +125 -0
  237. package/tests/hooks-generator.test.ts +212 -0
  238. package/tests/init.test.ts +3 -0
  239. package/tests/local-secrets.test.ts +92 -0
  240. package/tests/model-hooks.test.ts +322 -0
  241. package/tests/runtime-contract.test.ts +113 -1
  242. package/tests/service-role-check.test.ts +109 -0
  243. package/tests/service-role-routes.test.ts +51 -0
  244. package/tests/target-client-refresh.test.ts +94 -0
  245. package/tests/type-extractor.test.ts +187 -0
  246. package/tsconfig.tsbuildinfo +1 -1
  247. package/tests/engine-distribution.test.ts +0 -418
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Which mechanism will enforce this project's per-column rules — decided offline.
3
+ *
4
+ * The engine makes the same decision at push time by asking the database whether `supatype_mask` is
5
+ * installed. The CLI has to answer earlier: it writes `PGRST_DB_SCHEMA` when it generates compose,
6
+ * before any database necessarily exists. It can, because the extension needs
7
+ * `shared_preload_libraries` and a compiled library on the host, so exactly one path ships it —
8
+ * `supatype/postgres`. Everything else gets the view tier:
9
+ *
10
+ * | Config | Ships the extension | Tier |
11
+ * | -------------------------------------------- | ------------------- | --------- |
12
+ * | `database.provider: "docker"` | yes | extension |
13
+ * | `provider: native` | no | views |
14
+ * | `database.external` | no | views |
15
+ * | `provider: docker` + `database.image` override | no | views |
16
+ *
17
+ * Deliberately **not** "is the database external": a native `supatype dev` has no extension either,
18
+ * and treating it as tier 1 would point PostgREST at a schema whose tables the API roles no longer
19
+ * hold privileges on — every request denied.
20
+ *
21
+ * The one case this cannot call is a self-managed Postgres where the operator compiled and installed
22
+ * the extension themselves and points at it with `database.external`. They set `schema.api_schemas`
23
+ * explicitly; `supatype db check` reports the mismatch either way.
24
+ */
25
+
26
+ import { existsSync } from "node:fs"
27
+ // Statically imported: these packages are ESM, where `require` does not exist. The first attempt used
28
+ // it for a lazy import and every command that generates compose would have thrown at runtime — caught
29
+ // by the tests that run the built binary rather than the source.
30
+ import { loadSchemaAst } from "./config.js"
31
+ export { nativeMaskLibraryPresent } from "./postgres-ctl.js"
32
+ import type { SupatypeProjectConfig } from "./project-config.js"
33
+ import {
34
+ resolveRuntimeProvider,
35
+ schemaPathFromProject,
36
+ usesExternalDatabase,
37
+ } from "./project-config.js"
38
+
39
+ export type FieldMaskingTier = "none" | "extension" | "views"
40
+
41
+
42
+ /** Postgres image this project would run, when it runs one at all. */
43
+ function postgresImage(config: SupatypeProjectConfig): string | undefined {
44
+ if (usesExternalDatabase(config)) return undefined
45
+ if (config.database.provider !== "docker" && resolveRuntimeProvider(config) !== "docker") {
46
+ return undefined
47
+ }
48
+ return config.database.image?.trim() || "supatype/postgres:latest"
49
+ }
50
+
51
+ /** True when the Postgres this project runs is one that carries `supatype_mask`. */
52
+ export function imageShipsMaskExtension(config: SupatypeProjectConfig): boolean {
53
+ const image = postgresImage(config)
54
+ return image !== undefined && /^supatype\/postgres(:|$)/.test(image)
55
+ }
56
+
57
+ /**
58
+ * Whether any model masks a column.
59
+ *
60
+ * Read from the extracted AST rather than the config, because that is where field rules live. Shaped
61
+ * loosely on purpose: the CLI's AST type carries `platform.access` as an opaque record, and this only
62
+ * needs to know whether a `fields` map is present and non-empty.
63
+ */
64
+ export function schemaHasFieldRules(ast: unknown): boolean {
65
+ const models = (ast as { models?: unknown[] } | null)?.models
66
+ if (!Array.isArray(models)) return false
67
+ return models.some((model) => {
68
+ const access = (
69
+ model as { annotations?: { platform?: { access?: Record<string, unknown> } } } | null
70
+ )?.annotations?.platform?.access
71
+ const fields = access?.["fields"]
72
+ return typeof fields === "object" && fields !== null && Object.keys(fields).length > 0
73
+ })
74
+ }
75
+
76
+ /** The tier this project's next push will use. */
77
+ export function fieldMaskingTier(
78
+ config: SupatypeProjectConfig,
79
+ ast: unknown,
80
+ /** Set on the native path once the Postgres install is known — see [`nativeMaskLibraryPresent`]. */
81
+ maskLibraryPresent?: boolean,
82
+ ): FieldMaskingTier {
83
+ if (!schemaHasFieldRules(ast)) return "none"
84
+ if (maskLibraryPresent === true) return "extension"
85
+ return imageShipsMaskExtension(config) ? "extension" : "views"
86
+ }
87
+
88
+ /**
89
+ * The tier for a project on disk, loading its schema to see whether any column is masked.
90
+ *
91
+ * A schema that fails to load falls back to `"none"` — the default exposed-schema list. Deciding the
92
+ * tier is the wrong moment to report a syntax error, and `push`/`dev` do it properly moments later
93
+ * with the file and line.
94
+ */
95
+ export function fieldMaskingTierFromProject(
96
+ cwd: string,
97
+ config: SupatypeProjectConfig,
98
+ maskLibraryPresent?: boolean,
99
+ ): FieldMaskingTier {
100
+ try {
101
+ const schemaPath = schemaPathFromProject(config, cwd)
102
+ if (!existsSync(schemaPath)) return "none"
103
+ return fieldMaskingTier(config, loadSchemaAst(schemaPath, cwd), maskLibraryPresent)
104
+ } catch {
105
+ return "none"
106
+ }
107
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Scaffold Deno IDE types for edge functions so tsserver / Cursor stop
3
+ * reporting "Cannot find name 'Deno'" under the project root tsconfig.
4
+ *
5
+ * We ship a curated ambient declaration (not `@types/deno` at the app root):
6
+ * installing `@types/deno` as a project dependency auto-includes it in the
7
+ * whole app when root `tsconfig` omits `"types"`, which pollutes React/Next.
8
+ */
9
+
10
+ import {
11
+ existsSync,
12
+ mkdirSync,
13
+ readFileSync,
14
+ writeFileSync,
15
+ } from "node:fs"
16
+ import { join, relative } from "node:path"
17
+ import ts from "typescript"
18
+
19
+ const MARKER = "Supatype edge functions (IDE / tsserver)"
20
+
21
+ /** Ambient Deno types written into `functions/deno.d.ts`. */
22
+ export function functionsDenoAmbientSource(): string {
23
+ return `/**
24
+ * ${MARKER}.
25
+ * Runtime is Deno; this file covers APIs edge functions commonly use.
26
+ * Do not add \`@types/deno\` to the app package.json — it leaks into the
27
+ * whole project when root tsconfig has no \`"types"\` field.
28
+ *
29
+ * Optional: for the full Deno API, install the Deno VS Code/Cursor extension
30
+ * and open \`functions/\` as a Deno-enabled workspace folder.
31
+ */
32
+ /* eslint-disable @typescript-eslint/no-unused-vars */
33
+
34
+ interface DenoEnv {
35
+ /** Returns the value of the environment variable, or \`undefined\` if unset. */
36
+ get(key: string): string | undefined
37
+ set(key: string, value: string): void
38
+ delete(key: string): void
39
+ has(key: string): boolean
40
+ toObject(): { [key: string]: string }
41
+ }
42
+
43
+ declare namespace Deno {
44
+ const env: DenoEnv
45
+
46
+ namespace errors {
47
+ class NotFound extends Error {}
48
+ class PermissionDenied extends Error {}
49
+ }
50
+
51
+ interface DirEntry {
52
+ name: string
53
+ isFile: boolean
54
+ isDirectory: boolean
55
+ isSymlink: boolean
56
+ }
57
+
58
+ interface FileInfo {
59
+ isFile: boolean
60
+ isDirectory: boolean
61
+ isSymlink: boolean
62
+ size: number
63
+ mtime: Date | null
64
+ }
65
+
66
+ function readDir(path: string): AsyncIterable<DirEntry>
67
+ function readTextFile(path: string): Promise<string>
68
+ function stat(path: string): Promise<FileInfo>
69
+
70
+ function serve(
71
+ options: { port: number; hostname?: string; onListen?: (params: { hostname: string; port: number }) => void },
72
+ handler: (req: Request) => Response | Promise<Response>,
73
+ ): void
74
+ }
75
+ `
76
+ }
77
+
78
+ /** `functions/tsconfig.json` — owns function files so the app config can exclude them. */
79
+ export function functionsTsConfigSource(): string {
80
+ return `{
81
+ "compilerOptions": {
82
+ "target": "ES2022",
83
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
84
+ "strict": true,
85
+ "noEmit": true,
86
+ "module": "ESNext",
87
+ "moduleResolution": "bundler",
88
+ "isolatedModules": true,
89
+ "skipLibCheck": true,
90
+ "allowImportingTsExtensions": true,
91
+ "types": []
92
+ },
93
+ "include": ["./**/*.ts", "./deno.d.ts"]
94
+ }
95
+ `
96
+ }
97
+
98
+ export type FunctionsDenoTypesResult = {
99
+ wroteDenoDts: boolean
100
+ wroteTsconfig: boolean
101
+ rootExclude: "updated" | "already" | "missing" | "manual"
102
+ hints: string[]
103
+ }
104
+
105
+ /**
106
+ * Ensure `functions/deno.d.ts` + `functions/tsconfig.json` exist, and try to
107
+ * exclude `functions` from the project-root `tsconfig.json`.
108
+ */
109
+ export function ensureFunctionsDenoTypes(
110
+ projectRoot: string,
111
+ functionsDir: string,
112
+ ): FunctionsDenoTypesResult {
113
+ mkdirSync(functionsDir, { recursive: true })
114
+
115
+ const denoDtsPath = join(functionsDir, "deno.d.ts")
116
+ const tsconfigPath = join(functionsDir, "tsconfig.json")
117
+
118
+ let wroteDenoDts = false
119
+ if (!existsSync(denoDtsPath)) {
120
+ writeFileSync(denoDtsPath, functionsDenoAmbientSource(), "utf8")
121
+ wroteDenoDts = true
122
+ }
123
+
124
+ let wroteTsconfig = false
125
+ if (!existsSync(tsconfigPath)) {
126
+ writeFileSync(tsconfigPath, functionsTsConfigSource(), "utf8")
127
+ wroteTsconfig = true
128
+ }
129
+
130
+ const rootExclude = ensureRootTsconfigExcludesFunctions(projectRoot)
131
+ const hints: string[] = []
132
+ const fnRel = toPosix(relative(projectRoot, functionsDir)) || "functions"
133
+
134
+ if (rootExclude === "manual") {
135
+ hints.push(
136
+ `Add "functions" to the root tsconfig.json "exclude" array so the app checker skips ${fnRel}/.`,
137
+ )
138
+ } else if (rootExclude === "missing") {
139
+ hints.push(
140
+ `If you add a root tsconfig.json, exclude "${fnRel}" (or "functions") so app typecheck stays separate.`,
141
+ )
142
+ }
143
+
144
+ return { wroteDenoDts, wroteTsconfig, rootExclude, hints }
145
+ }
146
+
147
+ function ensureRootTsconfigExcludesFunctions(
148
+ projectRoot: string,
149
+ ): FunctionsDenoTypesResult["rootExclude"] {
150
+ const path = join(projectRoot, "tsconfig.json")
151
+ if (!existsSync(path)) return "missing"
152
+
153
+ const raw = readFileSync(path, "utf8")
154
+ const parsed = ts.parseConfigFileTextToJson(path, raw)
155
+ if (parsed.error || parsed.config === undefined || typeof parsed.config !== "object") {
156
+ return "manual"
157
+ }
158
+
159
+ const cfg = parsed.config as { exclude?: unknown }
160
+ const exclude = Array.isArray(cfg.exclude) ? cfg.exclude.map(String) : []
161
+ if (exclude.some(isFunctionsExcludeEntry)) return "already"
162
+
163
+ try {
164
+ const asJson = JSON.parse(raw) as { exclude?: string[] }
165
+ asJson.exclude = [...(Array.isArray(asJson.exclude) ? asJson.exclude : []), "functions"]
166
+ writeFileSync(path, `${JSON.stringify(asJson, null, 2)}\n`, "utf8")
167
+ return "updated"
168
+ } catch {
169
+ // JSONC / trailing commas — try a conservative string splice into an existing exclude array.
170
+ if (/"exclude"\s*:\s*\[/.test(raw)) {
171
+ const next = raw.replace(/("exclude"\s*:\s*\[)/, `$1\n "functions",`)
172
+ if (next !== raw) {
173
+ writeFileSync(path, next, "utf8")
174
+ return "updated"
175
+ }
176
+ }
177
+ return "manual"
178
+ }
179
+ }
180
+
181
+ function isFunctionsExcludeEntry(entry: string): boolean {
182
+ const n = entry.replace(/\\/g, "/").replace(/^\.\//, "")
183
+ return n === "functions" || n === "functions/**" || n.startsWith("functions/")
184
+ }
185
+
186
+ function toPosix(path: string): string {
187
+ return path.replace(/\\/g, "/")
188
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Package a project's hooks for a managed stack.
3
+ *
4
+ * Self-hosted stacks need none of this: the worker mounts the project directory, so `hooks/` is already
5
+ * where it looks. A managed project has no directory — its worker reads a ConfigMap, whose keys may only
6
+ * contain alphanumerics, `-`, `_` and `.`. So the tree is flattened here, where it can be checked and
7
+ * explained, rather than in the deploy route where a dropped file would be silent.
8
+ */
9
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"
10
+ import { join, relative } from "node:path"
11
+
12
+ import { declaredHooks, manifestHooks, type ManifestHookEntry } from "./model-hooks.js"
13
+
14
+ /** The generated adapter, flattened. `_`-prefixed, so the worker never routes to it. */
15
+ export const ADAPTER_KEY = "_supatype-hooks.ts"
16
+
17
+ /** What the handlers import locally, and what has to be rewritten to reach the flattened name. */
18
+ const ADAPTER_IMPORT = "../_supatype/hooks.ts"
19
+
20
+ export interface HookUpload {
21
+ /** Handler name → its source, flattened and import-rewritten. */
22
+ handlers: Array<{ name: string; source: string }>
23
+ /** table → event → config, exactly as the manifest would carry it. */
24
+ map: Record<string, Record<string, ManifestHookEntry>>
25
+ }
26
+
27
+ /**
28
+ * Rewrite the one import a hook handler is generated to make.
29
+ *
30
+ * `../_supatype/hooks.ts` is a path that cannot survive flattening, and it is the specifier our own
31
+ * scaffold writes — so it is rewritten rather than refused. Any *other* relative import is refused
32
+ * below: it would resolve to nothing on the worker, and a hook that fails to import is a hooked table
33
+ * whose writes all start failing.
34
+ */
35
+ function rewriteAdapterImport(source: string): string {
36
+ return source.split(ADAPTER_IMPORT).join(`./${ADAPTER_KEY}`)
37
+ }
38
+
39
+ /** Relative specifiers a flattened layout cannot honour, so the caller can refuse with a real reason. */
40
+ function unsupportedImports(source: string): string[] {
41
+ const found = new Set<string>()
42
+ // Matches `from "…"` and `import("…")`, which is every form that resolves against the file's own path.
43
+ for (const match of source.matchAll(/(?:from|import)\s*\(?\s*["'](\.[^"']*)["']/g)) {
44
+ const specifier = match[1]!
45
+ if (specifier === ADAPTER_IMPORT || specifier === `./${ADAPTER_KEY}`) continue
46
+ found.add(specifier)
47
+ }
48
+ return [...found].sort()
49
+ }
50
+
51
+ /**
52
+ * Read the hooks a schema actually names, ready to upload.
53
+ *
54
+ * **Named-only**, not every directory under `hooks/`: an unnamed handler would sit on the worker
55
+ * holding the service-role key with nothing able to call it. Uploading it buys nothing and widens what
56
+ * runs with that credential.
57
+ *
58
+ * Throws with a message meant for a terminal when a handler cannot be flattened faithfully — the
59
+ * alternative is a deploy that succeeds and a hook that fails to import, which reads to the caller as
60
+ * the table's writes being broken for no visible reason.
61
+ */
62
+ export function readHookUpload(cwd: string, hooksDir: string, ast: unknown): HookUpload | null {
63
+ const declared = declaredHooks(ast)
64
+ if (declared.length === 0) return null
65
+
66
+ const names = [...new Set(declared.map((hook) => hook.function))].sort()
67
+ const where = relative(cwd, hooksDir) || hooksDir
68
+ const handlers: Array<{ name: string; source: string }> = []
69
+
70
+ for (const name of names) {
71
+ const dir = join(hooksDir, name)
72
+ const entry = join(dir, "index.ts")
73
+ if (!existsSync(entry)) {
74
+ throw new Error(`Hook "${name}" has no ${where}/${name}/index.ts`)
75
+ }
76
+
77
+ const extra = readdirSync(dir)
78
+ .filter((file) => file !== "index.ts")
79
+ .filter((file) => statSync(join(dir, file)).isFile() || !file.startsWith("."))
80
+ if (extra.length > 0) {
81
+ throw new Error(
82
+ `Hook "${name}" has more than one file (${extra.join(", ")}).\n` +
83
+ `A managed stack serves each hook as a single module today, so the others would not be ` +
84
+ `uploaded and the hook would fail to import.\nMove what it needs into index.ts, or run this ` +
85
+ `hook on a self-hosted stack, which mounts the directory as it is.`,
86
+ )
87
+ }
88
+
89
+ const source = readFileSync(entry, "utf8")
90
+ const unsupported = unsupportedImports(source)
91
+ if (unsupported.length > 0) {
92
+ throw new Error(
93
+ `Hook "${name}" imports ${unsupported.join(", ")}, which a managed stack cannot resolve.\n` +
94
+ `Only the generated ${ADAPTER_IMPORT} survives upload; bare specifiers and URLs are fine.`,
95
+ )
96
+ }
97
+
98
+ handlers.push({ name, source: rewriteAdapterImport(source) })
99
+ }
100
+
101
+ return { handlers, map: manifestHooks(ast) }
102
+ }
103
+
104
+ /** The generated adapter as a handler-shaped entry, so it travels with them. */
105
+ export function adapterEntry(module: string): { name: string; source: string } {
106
+ return { name: ADAPTER_KEY.replace(/\.ts$/, ""), source: module }
107
+ }