toiljs 0.0.115 → 0.0.117

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 (326) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +16 -7
  3. package/TYPESCRIPT_LAW.md +1946 -2073
  4. package/build/backend/.tsbuildinfo +1 -1
  5. package/build/cli/.tsbuildinfo +1 -1
  6. package/build/cli/index.js +2375 -258
  7. package/build/client/.tsbuildinfo +1 -1
  8. package/build/client/dev/devtools.js +1 -1
  9. package/build/client/dev/error-overlay.js +1 -1
  10. package/build/client/routing/mount.js +1 -1
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/auth-emails.js +7 -3
  13. package/build/compiler/emails.js +1 -1
  14. package/build/compiler/index.d.ts +1 -0
  15. package/build/compiler/index.js +25 -4
  16. package/build/compiler/pages.js +2 -3
  17. package/build/compiler/prerender.d.ts +3 -8
  18. package/build/compiler/prerender.js +88 -127
  19. package/build/compiler/ssg.js +3 -4
  20. package/build/compiler/template-build.js +3 -4
  21. package/build/compiler/toil-docs.generated.js +6 -5
  22. package/build/compiler/vite.js +3 -6
  23. package/build/devserver/.tsbuildinfo +1 -1
  24. package/build/devserver/db/database.js +7 -3
  25. package/build/devserver/db/types.js +1 -1
  26. package/build/devserver/production.js +4 -3
  27. package/build/devserver/runtime/crypto.d.ts +1 -1
  28. package/build/io/.tsbuildinfo +1 -1
  29. package/build/logger/.tsbuildinfo +1 -1
  30. package/build/shared/.tsbuildinfo +1 -1
  31. package/examples/basic/client/routes/analytics.tsx +7 -5
  32. package/examples/basic/server/tsconfig.json +1 -3
  33. package/package.json +45 -44
  34. package/presets/no-uint8array-tostring.js +120 -153
  35. package/presets/oxlint.d.ts +5 -0
  36. package/presets/oxlint.js +224 -0
  37. package/server/runtime/tsconfig.json +1 -3
  38. package/tsconfig.json +2 -11
  39. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
  40. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
  41. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  42. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  43. package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
  44. package/.github/changelog-config.json +0 -45
  45. package/.github/dependabot.yml +0 -27
  46. package/.github/workflows/ci.yml +0 -222
  47. package/.vscode/settings.json +0 -10
  48. package/docs/README.md +0 -107
  49. package/docs/auth/README.md +0 -104
  50. package/docs/auth/configuration.md +0 -105
  51. package/docs/auth/emails.md +0 -92
  52. package/docs/auth/extending.md +0 -202
  53. package/docs/auth/how-it-works.md +0 -138
  54. package/docs/auth/usage.md +0 -188
  55. package/docs/backend/README.md +0 -143
  56. package/docs/backend/data.md +0 -351
  57. package/docs/backend/rest.md +0 -402
  58. package/docs/backend/rpc.md +0 -226
  59. package/docs/background/README.md +0 -114
  60. package/docs/background/daemons.md +0 -276
  61. package/docs/background/derive.md +0 -179
  62. package/docs/cli/README.md +0 -379
  63. package/docs/concepts/ai-guide.md +0 -132
  64. package/docs/concepts/config.md +0 -416
  65. package/docs/concepts/decorators.md +0 -127
  66. package/docs/concepts/security.md +0 -108
  67. package/docs/concepts/tiers.md +0 -166
  68. package/docs/concepts/types.md +0 -216
  69. package/docs/database/README.md +0 -143
  70. package/docs/database/capacity.md +0 -350
  71. package/docs/database/counters.md +0 -174
  72. package/docs/database/documents.md +0 -277
  73. package/docs/database/events.md +0 -307
  74. package/docs/database/membership.md +0 -246
  75. package/docs/database/setup.md +0 -155
  76. package/docs/database/unique.md +0 -216
  77. package/docs/database/views.md +0 -246
  78. package/docs/frontend/README.md +0 -104
  79. package/docs/frontend/components.md +0 -286
  80. package/docs/frontend/data-fetching.md +0 -260
  81. package/docs/frontend/images.md +0 -148
  82. package/docs/frontend/metadata.md +0 -344
  83. package/docs/frontend/navigation.md +0 -296
  84. package/docs/frontend/rendering.md +0 -236
  85. package/docs/frontend/routing.md +0 -346
  86. package/docs/frontend/scripts.md +0 -118
  87. package/docs/frontend/search.md +0 -191
  88. package/docs/frontend/styling.md +0 -147
  89. package/docs/frontend/toil-global.md +0 -216
  90. package/docs/getting-started/README.md +0 -81
  91. package/docs/getting-started/create-project.md +0 -131
  92. package/docs/getting-started/deploy.md +0 -101
  93. package/docs/getting-started/first-app.md +0 -215
  94. package/docs/getting-started/installation.md +0 -124
  95. package/docs/getting-started/migrating.md +0 -125
  96. package/docs/getting-started/project-structure.md +0 -163
  97. package/docs/introduction/README.md +0 -66
  98. package/docs/introduction/design-principles.md +0 -81
  99. package/docs/introduction/distributed.md +0 -91
  100. package/docs/introduction/how-it-works.md +0 -121
  101. package/docs/introduction/hyperscale.md +0 -70
  102. package/docs/introduction/modern-stack.md +0 -85
  103. package/docs/introduction/vs-other-frameworks.md +0 -64
  104. package/docs/introduction/why-toil.md +0 -99
  105. package/docs/llms.txt +0 -95
  106. package/docs/realtime/README.md +0 -102
  107. package/docs/realtime/channels.md +0 -211
  108. package/docs/realtime/streams.md +0 -369
  109. package/docs/services/README.md +0 -60
  110. package/docs/services/analytics.md +0 -279
  111. package/docs/services/caching.md +0 -175
  112. package/docs/services/cookies.md +0 -235
  113. package/docs/services/crypto.md +0 -209
  114. package/docs/services/email.md +0 -289
  115. package/docs/services/environment.md +0 -141
  116. package/docs/services/ratelimit.md +0 -174
  117. package/docs/services/time.md +0 -85
  118. package/eslint.config.js +0 -57
  119. package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
  120. package/presets/eslint.js +0 -88
  121. package/src/backend/index.ts +0 -204
  122. package/src/cli/configure.ts +0 -363
  123. package/src/cli/create.ts +0 -924
  124. package/src/cli/db.ts +0 -158
  125. package/src/cli/diagnostics.ts +0 -665
  126. package/src/cli/doctor.ts +0 -982
  127. package/src/cli/features.ts +0 -170
  128. package/src/cli/index.ts +0 -326
  129. package/src/cli/notify.ts +0 -120
  130. package/src/cli/proc.ts +0 -56
  131. package/src/cli/ui.ts +0 -169
  132. package/src/cli/update.ts +0 -248
  133. package/src/cli/updates.ts +0 -92
  134. package/src/cli/validate.ts +0 -31
  135. package/src/cli/version-check.ts +0 -125
  136. package/src/client/auth.ts +0 -869
  137. package/src/client/channel/channel.ts +0 -146
  138. package/src/client/components/Form.tsx +0 -65
  139. package/src/client/components/Image.tsx +0 -160
  140. package/src/client/components/Script.tsx +0 -113
  141. package/src/client/components/Slot.tsx +0 -21
  142. package/src/client/dev/devtools.tsx +0 -1089
  143. package/src/client/dev/error-overlay.tsx +0 -225
  144. package/src/client/errors.ts +0 -11
  145. package/src/client/head/head.ts +0 -182
  146. package/src/client/head/metadata.ts +0 -110
  147. package/src/client/index.ts +0 -130
  148. package/src/client/navigation/Link.tsx +0 -108
  149. package/src/client/navigation/NavLink.tsx +0 -86
  150. package/src/client/navigation/navigation.ts +0 -235
  151. package/src/client/navigation/prefetch.ts +0 -169
  152. package/src/client/navigation/scroll.ts +0 -53
  153. package/src/client/routing/Router.tsx +0 -216
  154. package/src/client/routing/action.ts +0 -122
  155. package/src/client/routing/error-boundary.tsx +0 -43
  156. package/src/client/routing/hooks.ts +0 -140
  157. package/src/client/routing/lazy.ts +0 -77
  158. package/src/client/routing/loader.ts +0 -381
  159. package/src/client/routing/match.ts +0 -47
  160. package/src/client/routing/mount.tsx +0 -90
  161. package/src/client/routing/params-context.ts +0 -10
  162. package/src/client/routing/slot-context.ts +0 -7
  163. package/src/client/rpc.ts +0 -97
  164. package/src/client/search/search.ts +0 -189
  165. package/src/client/search/use-page-search.ts +0 -73
  166. package/src/client/ssr/markers.tsx +0 -172
  167. package/src/client/stream/client.ts +0 -237
  168. package/src/client/types.ts +0 -73
  169. package/src/compiler/auth-emails.ts +0 -312
  170. package/src/compiler/config.ts +0 -369
  171. package/src/compiler/docs.ts +0 -127
  172. package/src/compiler/email-preview.ts +0 -314
  173. package/src/compiler/emails.ts +0 -404
  174. package/src/compiler/fonts.ts +0 -89
  175. package/src/compiler/generate.ts +0 -471
  176. package/src/compiler/image-blur.ts +0 -42
  177. package/src/compiler/image-report.ts +0 -88
  178. package/src/compiler/index.ts +0 -1362
  179. package/src/compiler/pages.ts +0 -49
  180. package/src/compiler/plugin.ts +0 -391
  181. package/src/compiler/prerender.ts +0 -263
  182. package/src/compiler/routes.ts +0 -170
  183. package/src/compiler/seo.ts +0 -415
  184. package/src/compiler/sri.ts +0 -144
  185. package/src/compiler/ssg.ts +0 -177
  186. package/src/compiler/ssr-codegen.ts +0 -85
  187. package/src/compiler/template-build.ts +0 -676
  188. package/src/compiler/template.ts +0 -262
  189. package/src/compiler/toil-docs.generated.ts +0 -77
  190. package/src/compiler/vite.ts +0 -245
  191. package/src/devserver/analytics/index.ts +0 -317
  192. package/src/devserver/config/dotenv.ts +0 -102
  193. package/src/devserver/config/env.ts +0 -30
  194. package/src/devserver/config/ratelimit.ts +0 -123
  195. package/src/devserver/daemon/catalog.ts +0 -120
  196. package/src/devserver/daemon/cron.ts +0 -87
  197. package/src/devserver/daemon/host.ts +0 -134
  198. package/src/devserver/daemon/index.ts +0 -369
  199. package/src/devserver/daemon/runtime.ts +0 -48
  200. package/src/devserver/db/catalog.ts +0 -109
  201. package/src/devserver/db/database.ts +0 -1834
  202. package/src/devserver/db/derives.ts +0 -121
  203. package/src/devserver/db/index.ts +0 -20
  204. package/src/devserver/db/routeKinds.ts +0 -191
  205. package/src/devserver/db/types.ts +0 -168
  206. package/src/devserver/email/caps.ts +0 -0
  207. package/src/devserver/email/config.ts +0 -141
  208. package/src/devserver/email/console.ts +0 -348
  209. package/src/devserver/email/index.ts +0 -155
  210. package/src/devserver/email/providers.ts +0 -131
  211. package/src/devserver/email/status.ts +0 -23
  212. package/src/devserver/email/validate.ts +0 -37
  213. package/src/devserver/email/wire.ts +0 -55
  214. package/src/devserver/http/cache.ts +0 -0
  215. package/src/devserver/http/envelope.ts +0 -157
  216. package/src/devserver/http/proxy.ts +0 -172
  217. package/src/devserver/http/runtime.ts +0 -387
  218. package/src/devserver/index.ts +0 -43
  219. package/src/devserver/production-ipc.ts +0 -63
  220. package/src/devserver/production-worker.ts +0 -83
  221. package/src/devserver/production.ts +0 -718
  222. package/src/devserver/runtime/crypto.ts +0 -587
  223. package/src/devserver/runtime/host.ts +0 -427
  224. package/src/devserver/runtime/module.ts +0 -442
  225. package/src/devserver/server.ts +0 -251
  226. package/src/devserver/ssr.ts +0 -208
  227. package/src/devserver/stream/catalog.ts +0 -106
  228. package/src/devserver/stream/host.ts +0 -42
  229. package/src/devserver/stream/index.ts +0 -301
  230. package/src/devserver/stream/manager.ts +0 -167
  231. package/src/devserver/stream/router.ts +0 -101
  232. package/src/devserver/stream/wire.ts +0 -58
  233. package/src/devserver/stream/ws.ts +0 -78
  234. package/src/devserver/wasm/sections.ts +0 -59
  235. package/src/devserver/wasm/surface.ts +0 -115
  236. package/src/io/FastMap.ts +0 -151
  237. package/src/io/FastSet.ts +0 -113
  238. package/src/io/codec.ts +0 -362
  239. package/src/io/index.ts +0 -10
  240. package/src/io/lengths.ts +0 -14
  241. package/src/io/types.ts +0 -19
  242. package/src/logger/index.ts +0 -22
  243. package/src/shared/index.ts +0 -46
  244. package/test/analytics-dev.test.ts +0 -113
  245. package/test/assembly/aspect-shim.ts +0 -33
  246. package/test/assembly/cookie.spec.ts +0 -303
  247. package/test/assembly/example.spec.ts +0 -22
  248. package/test/assembly/ssr.spec.ts +0 -95
  249. package/test/assembly.test.ts +0 -59
  250. package/test/auth-emails.test.ts +0 -118
  251. package/test/built-ssr.test.ts +0 -148
  252. package/test/channel.test.ts +0 -21
  253. package/test/config-host-port.test.ts +0 -59
  254. package/test/configure.test.ts +0 -94
  255. package/test/daemon-build.test.ts +0 -278
  256. package/test/daemon-catalog.test.ts +0 -349
  257. package/test/daemon-emulation.test.ts +0 -256
  258. package/test/db.test.ts +0 -0
  259. package/test/devserver-database.test.ts +0 -1102
  260. package/test/devserver-email.test.ts +0 -306
  261. package/test/devserver-pqauth.test.ts +0 -93
  262. package/test/devserver-secrets.test.ts +0 -63
  263. package/test/devserver.test.ts +0 -259
  264. package/test/doctor.test.ts +0 -298
  265. package/test/dom/Image.test.tsx +0 -144
  266. package/test/dom/Link.test.tsx +0 -67
  267. package/test/dom/NavLink.test.tsx +0 -37
  268. package/test/dom/Script.test.tsx +0 -48
  269. package/test/dom/action.test.tsx +0 -146
  270. package/test/dom/error-overlay.test.tsx +0 -44
  271. package/test/dom/loader.test.tsx +0 -121
  272. package/test/dom/navigation.test.ts +0 -59
  273. package/test/dom/revalidate.test.tsx +0 -38
  274. package/test/dom/route-head.test.tsx +0 -56
  275. package/test/dom/router-loading.test.tsx +0 -44
  276. package/test/dom/scroll.test.ts +0 -56
  277. package/test/dom/slot.test.tsx +0 -131
  278. package/test/dom/use-metadata.test.tsx +0 -58
  279. package/test/dom/view-transitions.test.tsx +0 -53
  280. package/test/dynamic-routing.test.ts +0 -132
  281. package/test/email-console.test.ts +0 -86
  282. package/test/email-preview.test.ts +0 -73
  283. package/test/emails.test.ts +0 -58
  284. package/test/errors.test.ts +0 -21
  285. package/test/example-guestbook.test.ts +0 -123
  286. package/test/features.test.ts +0 -149
  287. package/test/fixtures/bignum-wire/spec.ts +0 -25
  288. package/test/fixtures/daemon-app.ts +0 -65
  289. package/test/fixtures/stream-echo.ts +0 -26
  290. package/test/fixtures/stream-gate.ts +0 -28
  291. package/test/fixtures/stream-trap.ts +0 -18
  292. package/test/fixtures/stream-typed.ts +0 -41
  293. package/test/fonts.test.ts +0 -28
  294. package/test/global-setup.ts +0 -17
  295. package/test/head.test.ts +0 -42
  296. package/test/image-blur.test.ts +0 -50
  297. package/test/io.test.ts +0 -117
  298. package/test/metadata.test.ts +0 -40
  299. package/test/navlink.test.ts +0 -28
  300. package/test/pages.test.ts +0 -105
  301. package/test/placeholder.test.ts +0 -9
  302. package/test/pqauth-e2e.test.ts +0 -216
  303. package/test/pqauth-email.test.ts +0 -801
  304. package/test/prerender.test.ts +0 -116
  305. package/test/prettier-plugin.test.ts +0 -46
  306. package/test/routes.test.ts +0 -76
  307. package/test/rpc-bignum-wire.test.ts +0 -164
  308. package/test/rpc-dispatch.test.ts +0 -132
  309. package/test/rpc-kinds.test.ts +0 -18
  310. package/test/rpc.test.ts +0 -66
  311. package/test/search.test.ts +0 -114
  312. package/test/seo.test.ts +0 -175
  313. package/test/slot-layouts.test.ts +0 -69
  314. package/test/sri.test.ts +0 -129
  315. package/test/ssg.test.ts +0 -36
  316. package/test/ssr-hydration.test.tsx +0 -122
  317. package/test/ssr-render.test.ts +0 -197
  318. package/test/ssr-template.test.tsx +0 -482
  319. package/test/stream-emulation.test.ts +0 -479
  320. package/test/ui.test.ts +0 -40
  321. package/test/update.test.ts +0 -58
  322. package/test/validate.test.ts +0 -42
  323. package/test/version-check.test.ts +0 -130
  324. package/tests/data-parity/generated-parity.ts +0 -99
  325. package/tests/data-parity/parity.ts +0 -80
  326. package/tests/data-parity/spec.ts +0 -46
@@ -1,245 +0,0 @@
1
- import fs from 'node:fs';
2
- import { createRequire } from 'node:module';
3
- import path from 'node:path';
4
- import { pathToFileURL } from 'node:url';
5
-
6
- import pc from 'picocolors';
7
- import react from '@vitejs/plugin-react';
8
- import { imagetools } from 'vite-imagetools';
9
- import { nodePolyfills } from 'vite-plugin-node-polyfills';
10
- import { createLogger, type InlineConfig, type Logger, mergeConfig, type PluginOption } from 'vite';
11
-
12
- import { type ResolvedToilConfig } from './config.js';
13
- import { fontPreloadPlugin } from './fonts.js';
14
- import { imageReportPlugin } from './image-report.js';
15
- import { imageBlurPlugin } from './image-blur.js';
16
- import { toilPlugin } from './plugin.js';
17
- import { prerenderPlugin } from './prerender.js';
18
- import { sriPlugin } from './sri.js';
19
-
20
- // `vite-plugin-node-polyfills` rewrites react/react-dom to import its `vite-plugin-node-polyfills/
21
- // shims/*` modules. When a consumer links toiljs by symlink (`file:`/workspace), that package lives
22
- // only in toiljs's own node_modules, so resolving those bare specifiers from the consumer root
23
- // fails ("Failed to resolve import vite-plugin-node-polyfills/shims/process"). Alias them to
24
- // absolute paths resolved from toiljs's location so the build works however toiljs was installed.
25
- const polyfillPkgRoot = path.dirname(
26
- path.dirname(createRequire(import.meta.url).resolve('vite-plugin-node-polyfills')),
27
- );
28
- const polyfillShimAliases: Record<string, string> = {
29
- 'vite-plugin-node-polyfills/shims/buffer': path.join(
30
- polyfillPkgRoot,
31
- 'shims/buffer/dist/index.js',
32
- ),
33
- 'vite-plugin-node-polyfills/shims/global': path.join(
34
- polyfillPkgRoot,
35
- 'shims/global/dist/index.js',
36
- ),
37
- 'vite-plugin-node-polyfills/shims/process': path.join(
38
- polyfillPkgRoot,
39
- 'shims/process/dist/index.js',
40
- ),
41
- };
42
-
43
- /** Image extensions routed to `images/` in the build output. */
44
- const IMAGE_EXT = /^(png|jpe?g|svg|gif|tiff|bmp|ico|webp|avif)$/i;
45
- /** Font extensions routed to `fonts/`. */
46
- const FONT_EXT = /^(woff|woff2|eot|ttf|otf)$/i;
47
-
48
- /**
49
- * Resolves bare `shared/*` imports to the project's `shared/` folder (replacing a plain alias so it
50
- * can run early), and fails `shared/server` with an actionable message when the module has not been
51
- * generated yet (it comes from the server build, which must run first), instead of Vite's opaque
52
- * `UNLOADABLE_DEPENDENCY`.
53
- */
54
- function sharedResolverPlugin(cfg: ResolvedToilConfig): PluginOption {
55
- const sharedDir = path.join(cfg.root, 'shared');
56
- const serverModule = path.join(sharedDir, 'server.ts');
57
- // Posix-normalized: Vite hands `transform` forward-slash module ids while path.join is OS-native, so a
58
- // raw `===` would silently never match on Windows (the surface would regress to "client not loaded").
59
- const routesModule = path.join(cfg.toilDir, 'routes.ts').replace(/\\/g, '/');
60
- return {
61
- name: 'toiljs:shared-resolver',
62
- enforce: 'pre',
63
- resolveId(source: string) {
64
- if (source !== 'shared' && !source.startsWith('shared/')) return null;
65
- const rel = source === 'shared' ? 'index' : source.slice('shared/'.length);
66
- for (const ext of ['', '.ts', '.tsx', '.js', '.jsx', '.mjs']) {
67
- const candidate = path.join(sharedDir, rel + ext);
68
- if (fs.existsSync(candidate)) return candidate;
69
- }
70
- if (source === 'shared/server') {
71
- throw new Error(
72
- 'toiljs: "shared/server" is generated by the server build but is missing. ' +
73
- 'Run the server build first (it emits shared/server.ts from your @data/@remote code): ' +
74
- '`npm run build:server` (toilscript --target release --rpcModule shared/server.ts).',
75
- );
76
- }
77
- return null;
78
- },
79
- transform(code: string, id: string) {
80
- // Guarantee the generated client surface (shared/server.ts: __toilRest/__toilRpc/__toilStream
81
- // + globalThis.Server) is EVALUATED. Otherwise it is dead unless some route happens to
82
- // value-import a @data type - a type-only import is erased and never attaches, so
83
- // `Server.<rpc>()` / `Server.REST` / `Server.Stream` would throw "client not loaded". Inject a
84
- // side-effect import from the always-loaded route manifest; idempotent and a no-op for a
85
- // client-only project (no shared/server.ts).
86
- if ((id.split('?')[0] ?? id).replace(/\\/g, '/') === routesModule && fs.existsSync(serverModule)) {
87
- return { code: `import 'shared/server';\n${code}`, map: null };
88
- }
89
- return null;
90
- },
91
- };
92
- }
93
-
94
- /** Routes a built asset to a typed sub-folder (`images/`, `fonts/`, `css/`, else `assets/`). CSS is
95
- * content-hashed (`[name]-[hash]`) because we put Subresource Integrity on `<link rel=stylesheet>`:
96
- * a stable URL whose bytes change across deploys would make a client holding stale HTML fail the
97
- * integrity check and BLOCK the stylesheet. Hashing pins the URL to its bytes, so the baked integrity
98
- * can never falsely mismatch (identical to how JS chunks are hashed). Images/fonts stay stable-named:
99
- * they carry no SRI and are commonly referenced by fixed path. */
100
- function assetFileName(name: string): string {
101
- const ext = name.split('.').pop() ?? '';
102
- if (IMAGE_EXT.test(ext)) return 'images/[name][extname]';
103
- if (FONT_EXT.test(ext)) return 'fonts/[name][extname]';
104
- if (/^css$/i.test(ext)) return 'css/[name]-[hash][extname]';
105
- return 'assets/[name][extname]';
106
- }
107
-
108
- /**
109
- * Loads the Tailwind v4 Vite plugin if the project has `@tailwindcss/vite` installed (added by
110
- * `toiljs create`/`configure` when Tailwind is enabled). Resolved from the project root so it picks
111
- * up the project's copy; returns `undefined` when Tailwind is off, so the plugin simply isn't added.
112
- */
113
- async function tailwindPlugin(root: string): Promise<PluginOption | undefined> {
114
- let resolved: string;
115
- try {
116
- resolved = createRequire(path.join(root, 'package.json')).resolve('@tailwindcss/vite');
117
- } catch {
118
- return undefined;
119
- }
120
- const mod = (await import(pathToFileURL(resolved).href)) as { default?: () => PluginOption };
121
- return mod.default?.();
122
- }
123
-
124
- /** Splits React's runtime into its own long-lived chunk for better caching. */
125
- function manualChunks(id: string): string | undefined {
126
- if (!id.includes('node_modules')) return undefined;
127
- if (
128
- id.includes('node_modules/react-dom') ||
129
- id.includes('node_modules/react/') ||
130
- id.includes('node_modules/scheduler')
131
- ) {
132
- return 'react';
133
- }
134
- return undefined;
135
- }
136
-
137
- /**
138
- * Builds the framework-owned Vite config. Vite's `root` is the generated `.toil` dir so its
139
- * `index.html` (built from the project's `public/index.html` template) emits at the output root
140
- * with assets resolving correctly; static `public/` assets are mirrored to `.toil/public` and
141
- * picked up via Vite's default publicDir. `fs.allow` opens the project (for `client/`) and the
142
- * framework runtime. The opinionated default, Node polyfills
143
- * (Buffer/global/process), React plugin, toil route plugin, typed asset folders, React chunk
144
- * splitting and tuned build options, is applied here; `toiljs/client` is aliased to the
145
- * runtime, and the user's `client.vite` overrides deep-merge on top.
146
- */
147
- /**
148
- * Vite's `(ssr)` environment label is dark blue — nearly invisible on a black
149
- * terminal. Wrap the default logger to recolor `(ssr)` magenta (purple) and
150
- * `(client)` cyan, so the dev reload logs are readable.
151
- */
152
- function brandedLogger(): Logger {
153
- const logger = createLogger();
154
- const recolor = (msg: string): string =>
155
- msg
156
- .replace(/(?:\[[0-9;]*m)*\(ssr\)(?:\[[0-9;]*m)*/g, pc.magenta('(ssr)'))
157
- .replace(/(?:\[[0-9;]*m)*\(client\)(?:\[[0-9;]*m)*/g, pc.cyan('(client)'));
158
- const info = logger.info.bind(logger);
159
- const warn = logger.warn.bind(logger);
160
- logger.info = (msg, opts) => info(recolor(msg), opts);
161
- logger.warn = (msg, opts) => warn(recolor(msg), opts);
162
- return logger;
163
- }
164
-
165
- export async function createViteConfig(cfg: ResolvedToilConfig): Promise<InlineConfig> {
166
- const frameworkRoot = path.resolve(path.dirname(cfg.runtimePath), '..', '..');
167
- const tailwind = await tailwindPlugin(cfg.root);
168
-
169
- const base: InlineConfig = {
170
- root: cfg.toilDir,
171
- base: cfg.base,
172
- configFile: false,
173
- customLogger: brandedLogger(),
174
- plugins: [
175
- tailwind,
176
- // Auto-generate a tiny blurred base64 LQIP for `?toil` image imports (consumed by
177
- // `Toil.Image` placeholder="blur"). Runs before imagetools (enforce:'pre') so it owns the
178
- // `?toil` query; the bare re-imported src still flows through imagetools below.
179
- cfg.images ? imageBlurPlugin() : undefined,
180
- // Build-time image resize/optimization. Every *imported* raster image is compressed to
181
- // webp by default (so a plain `<img src={imported}>` is optimized too, not just
182
- // `Toil.Image`); add `?w=400;800&format=…` to resize or pick a format. `public/` assets
183
- // referenced by string path are served as-is. Disabled by `client.images: false`.
184
- cfg.images
185
- ? imagetools({
186
- defaultDirectives: () =>
187
- new URLSearchParams({ format: 'webp', quality: '80' }),
188
- })
189
- : undefined,
190
- cfg.images ? imageReportPlugin(cfg.root, cfg.toilDir) : undefined,
191
- // Subresource Integrity (build only): adds integrity="sha384-…" + crossorigin to every
192
- // local script/stylesheet/modulepreload in the shell. MUST precede prerenderPlugin so the
193
- // per-route/bracket/SSR pages baked from the shell inherit it (and the SSR .tmpl coherence
194
- // hash covers it).
195
- sriPlugin(cfg),
196
- // Static per-route SEO prerender (build only): bakes each route's metadata into its HTML.
197
- cfg.seo ? prerenderPlugin(cfg) : undefined,
198
- // Preload bundled fonts (build only). Disabled by `client.fonts: false`.
199
- cfg.fonts ? fontPreloadPlugin(cfg) : undefined,
200
- nodePolyfills({ globals: { Buffer: true, global: true, process: true } }),
201
- react(),
202
- sharedResolverPlugin(cfg),
203
- toilPlugin(cfg),
204
- ],
205
- resolve: {
206
- alias: {
207
- 'toiljs/client': cfg.runtimePath,
208
- 'toiljs/routes': path.join(cfg.toilDir, 'routes.ts'),
209
- // `client/*` -> the project's client source dir, so anything (pages, and notably
210
- // `emails/*.tsx`) can reuse existing client assets, e.g. `import 'client/styles/x.css'`.
211
- // Vite's string alias matches only `client` or `client/...`, never `toiljs/client`.
212
- client: cfg.clientAbsDir,
213
- // `shared/*` is resolved by sharedResolverPlugin (above) so a missing generated
214
- // shared/server.ts gives an actionable error instead of an opaque load failure.
215
- ...polyfillShimAliases,
216
- },
217
- dedupe: ['react', 'react-dom'],
218
- },
219
- server: {
220
- port: cfg.port,
221
- fs: { allow: [cfg.root, frameworkRoot] },
222
- },
223
- build: {
224
- outDir: path.resolve(cfg.root, cfg.outDir),
225
- emptyOutDir: true,
226
- target: 'es2020',
227
- modulePreload: false,
228
- cssCodeSplit: false,
229
- assetsInlineLimit: 10000,
230
- chunkSizeWarningLimit: 3000,
231
- commonjsOptions: {
232
- strictRequires: true,
233
- transformMixedEsModules: true,
234
- },
235
- rollupOptions: {
236
- output: {
237
- chunkFileNames: 'assets/[name]-[hash].js',
238
- assetFileNames: (assetInfo) => assetFileName(assetInfo.names[0] ?? ''),
239
- manualChunks,
240
- },
241
- },
242
- },
243
- };
244
- return mergeConfig(base, cfg.vite);
245
- }
@@ -1,317 +0,0 @@
1
- /**
2
- * Dev-server stand-in for the edge `env.analytics_read` host import
3
- * (toil-backend `src/wasm/host/import_functions/analytics.rs`). It returns a fixed
4
- * sample `TenantStats` frame so a tenant's wasm exercises the toilscript `Analytics`
5
- * API under `toiljs dev`. The frame is stashed into the SAME per-request result
6
- * buffer the `data.*` ops use (`db.lastResult`), so the guest drains it with the
7
- * existing `data.take_result`.
8
- *
9
- * Dev is intentionally PERMISSIVE: it returns sample data for ANY domain (own or
10
- * named) so a dashboard can be built locally. The real `dacely.com`-only
11
- * authorization is enforced host-side at the edge; the dev server has no
12
- * multi-tenant host resolution to authorize against.
13
- */
14
- import type { DbDevState } from '../db/index.js';
15
- import type { MemoryRef } from '../runtime/host.js';
16
-
17
- /** Frame version + counter count, kept in lockstep with the edge (`analytics.rs` / `metric_id.rs`). */
18
- const FRAME_VERSION = 3;
19
- const METRIC_COUNTERS = 42;
20
-
21
- /** The sustained-rate window: requests are metered over a 3-minute ROLLING window. A plan's
22
- * `sustained_rps` (X) allows `X * BURST_WINDOW_SECS` requests across it; instantaneous spikes above
23
- * that are NOT capped per-tier (only the box `firewall.global_pps` DDoS backstop applies). */
24
- export const BURST_WINDOW_SECS = 180;
25
-
26
- /** The plan's sustained rate in requests/second, derived from the rolling window's allowance.
27
- * `0` means unmetered (no sustained cap), and must NOT be divided into a rate. */
28
- export function sustainedRpsCap(reqBurstCap: number): number {
29
- return reqBurstCap === 0 ? 0 : reqBurstCap / BURST_WINDOW_SECS;
30
- }
31
- /** MetricId indices we seed with sample data (others default 0). Mirrors the AUTHORITATIVE edge wire
32
- * contract EXACTLY (`toil-backend/src/analytics/metric_id.rs`): counter ids 0..=41, gauge ids
33
- * ConnectedStreamsAvg=42, CommittedMemoryAvg=44. There is NO WasmDispatches (removed + reindexed); the
34
- * edge/guest are 42, so this must be 42 with matching indices or the dashboard mislabels every metric. */
35
- const MID = {
36
- Requests: 0,
37
- BytesOutL1: 1,
38
- BytesInL1: 2,
39
- Status2xx: 3,
40
- Status4xx: 5,
41
- StaticHits: 7,
42
- GasUsed: 11,
43
- DbOps: 12,
44
- DbReads: 13,
45
- DbWrites: 14,
46
- StreamBytesIn: 24,
47
- StreamBytesOut: 25,
48
- MemGrownBytes: 38,
49
- CacheHits: 40,
50
- CacheMisses: 41,
51
- } as const;
52
-
53
- interface DevTenantStats {
54
- life: bigint[]; // length METRIC_COUNTERS, indexed by MetricId
55
- connectedStreams: number;
56
- committedMemory: number;
57
- // Fleet-global requests in the trailing 3-minute ROLLING window, and its allowance
58
- // (= sustained_rps * BURST_WINDOW_SECS; 0 = unmetered). Spikes are not tier-capped.
59
- reqBurstUsed: number;
60
- reqBurstCap: number;
61
- // Fleet-global requests in the current 30-day tumbling bucket, and the 30-day quota (0 = unmetered).
62
- req30dUsed: number;
63
- req30dCap: number;
64
- // Derived (NOT part of the wire frame): the plan's sustained request rate, `reqBurstCap /
65
- // BURST_WINDOW_SECS` (0 = unmetered). Surfaced so a dev dashboard can show "X sustained rps".
66
- sustainedRpsCap: number;
67
- // LIVE per-second rates (f64), appended after the windows. Mirrors the edge `encode_stats`.
68
- rps: number;
69
- bytesInPerSec: number;
70
- bytesOutPerSec: number;
71
- streamBytesInPerSec: number;
72
- streamBytesOutPerSec: number;
73
- dbOpsPerSec: number;
74
- gasPerSec: number;
75
- nowMs: number;
76
- }
77
-
78
- /** A plausible sample so a guest sees non-zero analytics under `toiljs dev`. */
79
- function devStats(): DevTenantStats {
80
- const life = new Array<bigint>(METRIC_COUNTERS).fill(0n);
81
- life[MID.Requests] = 42n;
82
- life[MID.BytesOutL1] = 12345n;
83
- life[MID.BytesInL1] = 4096n;
84
- life[MID.Status2xx] = 40n;
85
- life[MID.Status4xx] = 2n;
86
- life[MID.StaticHits] = 8n;
87
- life[MID.GasUsed] = 1_000_000n;
88
- life[MID.DbOps] = 17n;
89
- life[MID.DbReads] = 12n;
90
- life[MID.DbWrites] = 5n;
91
- life[MID.StreamBytesIn] = 2048n;
92
- life[MID.StreamBytesOut] = 8192n;
93
- life[MID.MemGrownBytes] = 262144n;
94
- life[MID.CacheHits] = 900n;
95
- life[MID.CacheMisses] = 100n;
96
- // Free tier: sustained 1000 rps -> a 3-minute burst cap of 1000 * 180 = 180_000, plus a 30-day
97
- // quota of 10_000_000. `used` values are small so the dashboard shows plenty of headroom.
98
- const reqBurstCap: number = 180_000;
99
- return {
100
- life,
101
- connectedStreams: 3,
102
- committedMemory: 65536,
103
- reqBurstUsed: 1_200,
104
- reqBurstCap,
105
- req30dUsed: 250_000,
106
- req30dCap: 10_000_000,
107
- sustainedRpsCap: sustainedRpsCap(reqBurstCap),
108
- // Plausible non-zero live rates so a dev dashboard shows moving gauges.
109
- rps: 0.7,
110
- bytesInPerSec: 68.2,
111
- bytesOutPerSec: 205.75,
112
- streamBytesInPerSec: 34.1,
113
- streamBytesOutPerSec: 136.5,
114
- dbOpsPerSec: 0.28,
115
- gasPerSec: 16_666.67,
116
- nowMs: 1_700_000_000_000,
117
- };
118
- }
119
-
120
- /**
121
- * Encode the v3 snapshot frame BYTE-FOR-BYTE like the edge (`analytics.rs` `encode_stats`), FIXED
122
- * layout (no strings). The v2 -> v3 bump renamed two u64 pairs (req_minute_* -> req_burst_*,
123
- * req_day_* -> req_30d_*); the BYTE LAYOUT is UNCHANGED (same field count, order, and widths).
124
- * Counters/levels are u64; the 7 trailing LIVE per-second rates are f64:
125
- * u16 version | u64 now_ms | u32 count | u64 × count | u64 connectedStreams | u64 committedMemory |
126
- * u64 reqBurstUsed | u64 reqBurstCap | u64 req30dUsed | u64 req30dCap |
127
- * f64 rps | f64 bytesInPerSec | f64 bytesOutPerSec | f64 streamBytesInPerSec |
128
- * f64 streamBytesOutPerSec | f64 dbOpsPerSec | f64 gasPerSec
129
- * The rates are APPENDED (version-gated by length): the guest's bounds-checked reader yields 0.0 for a
130
- * pre-rates frame, so an older edge stays decodable.
131
- */
132
- function encodeStats(s: DevTenantStats): Buffer {
133
- const head = Buffer.alloc(2 + 8 + 4);
134
- head.writeUInt16LE(FRAME_VERSION, 0);
135
- head.writeBigUInt64LE(BigInt(s.nowMs), 2);
136
- head.writeUInt32LE(s.life.length, 10);
137
- // 6 window u64s (48 bytes) + 7 rate f64s (56 bytes).
138
- const body = Buffer.alloc(s.life.length * 8 + 48 + 56);
139
- let o = 0;
140
- for (const v of s.life) {
141
- body.writeBigUInt64LE(v, o);
142
- o += 8;
143
- }
144
- body.writeBigUInt64LE(BigInt(s.connectedStreams), o); o += 8;
145
- body.writeBigUInt64LE(BigInt(s.committedMemory), o); o += 8;
146
- body.writeBigUInt64LE(BigInt(s.reqBurstUsed), o); o += 8;
147
- body.writeBigUInt64LE(BigInt(s.reqBurstCap), o); o += 8;
148
- body.writeBigUInt64LE(BigInt(s.req30dUsed), o); o += 8;
149
- body.writeBigUInt64LE(BigInt(s.req30dCap), o); o += 8;
150
- body.writeDoubleLE(s.rps, o); o += 8;
151
- body.writeDoubleLE(s.bytesInPerSec, o); o += 8;
152
- body.writeDoubleLE(s.bytesOutPerSec, o); o += 8;
153
- body.writeDoubleLE(s.streamBytesInPerSec, o); o += 8;
154
- body.writeDoubleLE(s.streamBytesOutPerSec, o); o += 8;
155
- body.writeDoubleLE(s.dbOpsPerSec, o); o += 8;
156
- body.writeDoubleLE(s.gasPerSec, o);
157
- return Buffer.concat([head, body]);
158
- }
159
-
160
- /** The metric ids carried in the per-minute ring (mirrors the edge `MINUTE_RING_METRICS` in
161
- * `toil-backend/src/analytics/ring.rs` EXACTLY). Only these get minute resolution on the 1h/6h ranges;
162
- * every other metric falls back to the hour ring there. These are the AUTHORITATIVE edge ids
163
- * (Requests=0, BytesInL1=2, BytesOutL1=1, GasUsed=11, DbOps=12, StreamBytesIn=24, StreamBytesOut=25,
164
- * MemGrownBytes=38, ConnectedStreamsAvg=42, CommittedMemoryAvg=44) -- must match the reindexed layout or
165
- * the dev 1h/6h graphs pick the wrong ring for these metrics. */
166
- const MINUTE_RING_METRICS = new Set<number>([0, 1, 2, 11, 12, 24, 25, 38, 42, 44]);
167
-
168
- /**
169
- * Range id -> (bucketCount, bucketSecs), matching the edge `Range` + minute-ring fallback: 1h/6h are
170
- * per-minute ONLY for a minute-ring metric; otherwise they fall back to the hour ring (1h/6h buckets).
171
- */
172
- function rangeShape(range: number, metricId: number): { count: number; bucketSecs: number } {
173
- const isMinute = (range === 0 || range === 1) && MINUTE_RING_METRICS.has(metricId);
174
- if (isMinute) return range === 0 ? { count: 60, bucketSecs: 60 } : { count: 360, bucketSecs: 60 };
175
- switch (range) {
176
- case 0: return { count: 1, bucketSecs: 3600 }; // 1h on the hour ring
177
- case 1: return { count: 6, bucketSecs: 3600 }; // 6h on the hour ring
178
- case 2: return { count: 12, bucketSecs: 3600 };
179
- case 3: return { count: 24, bucketSecs: 3600 };
180
- case 4: return { count: 72, bucketSecs: 3600 };
181
- case 5: return { count: 168, bucketSecs: 3600 };
182
- case 6: return { count: 336, bucketSecs: 3600 };
183
- case 7: return { count: 720, bucketSecs: 3600 };
184
- case 8: return { count: 60, bucketSecs: 86_400 }; // D60 on the DAY ring
185
- case 9: return { count: 90, bucketSecs: 86_400 }; // D90 on the DAY ring
186
- default: return { count: 24, bucketSecs: 3600 };
187
- }
188
- }
189
-
190
- /**
191
- * Encode the v3 series frame BYTE-FOR-BYTE like the edge `encode_series` (points are u64 now):
192
- * u16 version | u16 metricId | u32 bucketSecs | u64 headMs | u32 count | u64 × count
193
- * A synthetic gentle ramp so a dev dashboard draws a non-flat line.
194
- */
195
- function encodeSeries(metricId: number, range: number): Buffer {
196
- const { count, bucketSecs } = rangeShape(range, metricId);
197
- const nowMs = 1_700_000_000_000;
198
- const head = Buffer.alloc(2 + 2 + 4 + 8 + 4);
199
- head.writeUInt16LE(FRAME_VERSION, 0);
200
- head.writeUInt16LE(metricId & 0xffff, 2);
201
- head.writeUInt32LE(bucketSecs, 4);
202
- head.writeBigUInt64LE(BigInt(nowMs), 8);
203
- head.writeUInt32LE(count, 16);
204
- const body = Buffer.alloc(count * 8);
205
- for (let i = 0; i < count; i++) {
206
- // A smooth ramp with a little variation, deterministic per (metric, index). Non-negative -> u64.
207
- const v = BigInt(((i * 7 + metricId) % 50) + i);
208
- body.writeBigUInt64LE(v, i * 8);
209
- }
210
- return Buffer.concat([head, body]);
211
- }
212
-
213
- /**
214
- * Encode a site page BYTE-FOR-BYTE like the edge (analytics.rs `encode_site_list`):
215
- * count u32 | (u32 nameLen, name bytes)* | has_more u8, all little-endian.
216
- */
217
- function encodeSiteList(names: string[], hasMore: boolean): Buffer {
218
- const parts: Buffer[] = [];
219
- const head = Buffer.alloc(4);
220
- head.writeUInt32LE(names.length, 0);
221
- parts.push(head);
222
- for (const name of names) {
223
- const nb = Buffer.from(name, 'utf8');
224
- const nl = Buffer.alloc(4);
225
- nl.writeUInt32LE(nb.length, 0);
226
- parts.push(nl, nb);
227
- }
228
- parts.push(Buffer.from([hasMore ? 1 : 0]));
229
- return Buffer.concat(parts);
230
- }
231
-
232
- // Mirror the edge ABI bounds (analytics.rs) so dev exercises the SAME negative-status paths as prod.
233
- const MAX_DOMAIN_LEN = 256;
234
- const MAX_CURSOR_LEN = 256;
235
- const ABSENT = -2; // the edge's ABSENT sentinel; the guest maps a negative status to empty stats/list.
236
- // Pre-sorted so cursor pagination is deterministic and matches the edge's sorted enumeration.
237
- const SITE_SAMPLE = ['demo.dacely.com', 'example.com', 'shop.test'];
238
-
239
- /** The `env.analytics_read` + `env.analytics_list_sites` dev imports. Mirrors `buildDatabaseImports`. */
240
- export function buildAnalyticsImports(
241
- ref: MemoryRef,
242
- db: DbDevState,
243
- ): Record<string, (...args: number[]) => number> {
244
- return {
245
- analytics_read: (domainPtr: number, domainLen: number): number => {
246
- // Over-long domain -> ABSENT, mirroring the edge (which returns a negative status, not a
247
- // trap); the guest maps it to empty stats. domainLen 0 = the caller's own stats.
248
- if (domainLen > MAX_DOMAIN_LEN) return ABSENT;
249
- if (domainLen > 0) {
250
- if (!ref.memory) throw new Error('analytics_read called before memory was bound');
251
- const m = Buffer.from(ref.memory.buffer);
252
- if (domainPtr < 0 || domainPtr + domainLen > m.length) {
253
- throw new Error('analytics_read: domain out of bounds');
254
- }
255
- }
256
- const frame = encodeStats(devStats());
257
- db.lastResult = frame;
258
- db.lastResultVersion = -1;
259
- return frame.length;
260
- },
261
-
262
- // `env.analytics_series`: one metric's time-series for a range. Dev returns a synthetic ramp
263
- // (real ring reads are edge-side). Mirrors the edge bounds + negative-status paths.
264
- analytics_series: (
265
- domainPtr: number,
266
- domainLen: number,
267
- metricId: number,
268
- range: number,
269
- ): number => {
270
- if (domainLen > MAX_DOMAIN_LEN) return ABSENT;
271
- // Valid metric ids are 0..=45 (counters 0..=41 + the 4 gauge avg/peak series 42..=45), matching
272
- // the edge `MetricId::from_u16` (v < N_METRICS=46); ranges 0..=9 (H1..D30 + D60/D90).
273
- if (metricId < 0 || metricId > 45 || range < 0 || range > 9) return ABSENT;
274
- if (domainLen > 0) {
275
- if (!ref.memory) throw new Error('analytics_series called before memory was bound');
276
- const m = Buffer.from(ref.memory.buffer);
277
- if (domainPtr < 0 || domainPtr + domainLen > m.length) {
278
- throw new Error('analytics_series: domain out of bounds');
279
- }
280
- }
281
- const frame = encodeSeries(metricId, range);
282
- db.lastResult = frame;
283
- db.lastResultVersion = -1;
284
- return frame.length;
285
- },
286
-
287
- // The dacely dashboard enumerate-all-sites stub. Dev returns a fixed SORTED sample for any
288
- // caller (the real dacely.com-only authz is the edge), but mirrors the edge ABI exactly:
289
- // over-long/non-utf8 cursor -> ABSENT, cursor = strictly-after, limit cap, and a REAL has_more.
290
- analytics_list_sites: (cursorPtr: number, cursorLen: number, limit: number): number => {
291
- if (cursorLen > MAX_CURSOR_LEN) return ABSENT;
292
- let cursor = '';
293
- if (cursorLen > 0) {
294
- if (!ref.memory) throw new Error('analytics_list_sites called before memory was bound');
295
- const m = Buffer.from(ref.memory.buffer);
296
- if (cursorPtr < 0 || cursorPtr + cursorLen > m.length) {
297
- throw new Error('analytics_list_sites: cursor out of bounds');
298
- }
299
- const cb = m.subarray(cursorPtr, cursorPtr + cursorLen);
300
- try {
301
- cursor = new TextDecoder('utf-8', { fatal: true }).decode(cb);
302
- } catch {
303
- return ABSENT; // non-utf8 cursor -> empty page, mirroring the edge
304
- }
305
- }
306
- const start = SITE_SAMPLE.findIndex((n) => n > cursor); // strictly after the cursor
307
- const from = start < 0 ? SITE_SAMPLE.length : start;
308
- const lim = limit > 0 ? Math.min(limit, 256) : 256;
309
- const page = SITE_SAMPLE.slice(from, from + lim);
310
- const hasMore = from + page.length < SITE_SAMPLE.length;
311
- const frame = encodeSiteList(page, hasMore);
312
- db.lastResult = frame;
313
- db.lastResultVersion = -1;
314
- return frame.length;
315
- },
316
- };
317
- }
@@ -1,102 +0,0 @@
1
- /**
2
- * Shared dotenv loader for the dev server (and the future Node self-host): reads
3
- * a project's `.env` (plain vars) + `.env.secrets` (secrets) and splits out the
4
- * framework-reserved `TOIL_*` keys (host-only), mirroring the edge's `env_store`.
5
- *
6
- * Vite/devserver-free on purpose — both the `Environment.get/getSecure` dev
7
- * source (`./env.ts`) and the email backend config (`./email/config.ts`) consume
8
- * this one loader, and the self-host will too.
9
- */
10
- import fs from 'node:fs';
11
- import path from 'node:path';
12
-
13
- /** Keys with this prefix are framework-reserved/host-only (never a tenant var/secret). */
14
- export const RESERVED_PREFIX = 'TOIL_';
15
-
16
- export interface LoadedEnv {
17
- /** Non-reserved `.env` entries + `process.env` overlay → `Environment.get`. */
18
- readonly vars: Map<string, string>;
19
- /** Non-reserved `.env.secrets` entries → `Environment.getSecure`. */
20
- readonly secrets: Map<string, string>;
21
- /** Reserved `TOIL_*` entries from either file (+ `process.env`) → host-only config. */
22
- readonly reserved: Map<string, string>;
23
- }
24
-
25
- const cache = new Map<string, LoadedEnv>();
26
-
27
- /** Parse one dotenv value: take inside matching quotes, else cut an inline ` #`. */
28
- function parseValue(rest: string): string {
29
- const q = rest[0];
30
- if (q === '"' || q === "'") {
31
- const end = rest.indexOf(q, 1);
32
- return end < 0 ? rest.slice(1) : rest.slice(1, end);
33
- }
34
- const hash = rest.indexOf(' #');
35
- return (hash < 0 ? rest : rest.slice(0, hash)).trimEnd();
36
- }
37
-
38
- /**
39
- * Parse dotenv text into `plain` (non-reserved) and `reserved` (`TOIL_*`):
40
- * `KEY=value`, `#` comments, optional `export`, optional surrounding quotes.
41
- */
42
- function parseDotenv(
43
- text: string,
44
- plain: Map<string, string>,
45
- reserved: Map<string, string>,
46
- ): void {
47
- for (const raw of text.split('\n')) {
48
- let line = raw.trim();
49
- if (line.length === 0 || line.startsWith('#')) continue;
50
- if (line.startsWith('export ')) line = line.slice('export '.length);
51
- const eq = line.indexOf('=');
52
- if (eq < 0) continue;
53
- const key = line.slice(0, eq).trim();
54
- if (key.length === 0) continue;
55
- const val = parseValue(line.slice(eq + 1).trim());
56
- (key.startsWith(RESERVED_PREFIX) ? reserved : plain).set(key, val);
57
- }
58
- }
59
-
60
- function readFileInto(
61
- file: string,
62
- plain: Map<string, string>,
63
- reserved: Map<string, string>,
64
- ): void {
65
- try {
66
- parseDotenv(fs.readFileSync(file, 'utf8'), plain, reserved);
67
- } catch {
68
- /* file absent: skip */
69
- }
70
- }
71
-
72
- /**
73
- * Load `<root>/.env` + `<root>/.env.secrets`, cached by resolved root. `process.env`
74
- * overlays first (non-reserved → vars, `TOIL_*` → reserved), then the files take
75
- * precedence. Secrets come only from `.env.secrets`.
76
- */
77
- export function loadEnvFiles(root: string): LoadedEnv {
78
- const key = path.resolve(root);
79
- const hit = cache.get(key);
80
- if (hit) return hit;
81
-
82
- const vars = new Map<string, string>();
83
- const secrets = new Map<string, string>();
84
- const reserved = new Map<string, string>();
85
-
86
- // process.env overlay: non-reserved as plain vars, TOIL_* as reserved config.
87
- for (const [k, v] of Object.entries(process.env)) {
88
- if (typeof v !== 'string') continue;
89
- (k.startsWith(RESERVED_PREFIX) ? reserved : vars).set(k, v);
90
- }
91
- readFileInto(path.join(key, '.env'), vars, reserved);
92
- readFileInto(path.join(key, '.env.secrets'), secrets, reserved);
93
-
94
- const out: LoadedEnv = { vars, secrets, reserved };
95
- cache.set(key, out);
96
- return out;
97
- }
98
-
99
- /** Drop the cache (tests). */
100
- export function clearEnvCache(): void {
101
- cache.clear();
102
- }