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.
- package/CHANGELOG.md +10 -0
- package/README.md +16 -7
- package/TYPESCRIPT_LAW.md +1946 -2073
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +2375 -258
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/devtools.js +1 -1
- package/build/client/dev/error-overlay.js +1 -1
- package/build/client/routing/mount.js +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/auth-emails.js +7 -3
- package/build/compiler/emails.js +1 -1
- package/build/compiler/index.d.ts +1 -0
- package/build/compiler/index.js +25 -4
- package/build/compiler/pages.js +2 -3
- package/build/compiler/prerender.d.ts +3 -8
- package/build/compiler/prerender.js +88 -127
- package/build/compiler/ssg.js +3 -4
- package/build/compiler/template-build.js +3 -4
- package/build/compiler/toil-docs.generated.js +6 -5
- package/build/compiler/vite.js +3 -6
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/db/database.js +7 -3
- package/build/devserver/db/types.js +1 -1
- package/build/devserver/production.js +4 -3
- package/build/devserver/runtime/crypto.d.ts +1 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/logger/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/examples/basic/client/routes/analytics.tsx +7 -5
- package/examples/basic/server/tsconfig.json +1 -3
- package/package.json +45 -44
- package/presets/no-uint8array-tostring.js +120 -153
- package/presets/oxlint.d.ts +5 -0
- package/presets/oxlint.js +224 -0
- package/server/runtime/tsconfig.json +1 -3
- package/tsconfig.json +2 -11
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
- package/.github/changelog-config.json +0 -45
- package/.github/dependabot.yml +0 -27
- package/.github/workflows/ci.yml +0 -222
- package/.vscode/settings.json +0 -10
- package/docs/README.md +0 -107
- package/docs/auth/README.md +0 -104
- package/docs/auth/configuration.md +0 -105
- package/docs/auth/emails.md +0 -92
- package/docs/auth/extending.md +0 -202
- package/docs/auth/how-it-works.md +0 -138
- package/docs/auth/usage.md +0 -188
- package/docs/backend/README.md +0 -143
- package/docs/backend/data.md +0 -351
- package/docs/backend/rest.md +0 -402
- package/docs/backend/rpc.md +0 -226
- package/docs/background/README.md +0 -114
- package/docs/background/daemons.md +0 -276
- package/docs/background/derive.md +0 -179
- package/docs/cli/README.md +0 -379
- package/docs/concepts/ai-guide.md +0 -132
- package/docs/concepts/config.md +0 -416
- package/docs/concepts/decorators.md +0 -127
- package/docs/concepts/security.md +0 -108
- package/docs/concepts/tiers.md +0 -166
- package/docs/concepts/types.md +0 -216
- package/docs/database/README.md +0 -143
- package/docs/database/capacity.md +0 -350
- package/docs/database/counters.md +0 -174
- package/docs/database/documents.md +0 -277
- package/docs/database/events.md +0 -307
- package/docs/database/membership.md +0 -246
- package/docs/database/setup.md +0 -155
- package/docs/database/unique.md +0 -216
- package/docs/database/views.md +0 -246
- package/docs/frontend/README.md +0 -104
- package/docs/frontend/components.md +0 -286
- package/docs/frontend/data-fetching.md +0 -260
- package/docs/frontend/images.md +0 -148
- package/docs/frontend/metadata.md +0 -344
- package/docs/frontend/navigation.md +0 -296
- package/docs/frontend/rendering.md +0 -236
- package/docs/frontend/routing.md +0 -346
- package/docs/frontend/scripts.md +0 -118
- package/docs/frontend/search.md +0 -191
- package/docs/frontend/styling.md +0 -147
- package/docs/frontend/toil-global.md +0 -216
- package/docs/getting-started/README.md +0 -81
- package/docs/getting-started/create-project.md +0 -131
- package/docs/getting-started/deploy.md +0 -101
- package/docs/getting-started/first-app.md +0 -215
- package/docs/getting-started/installation.md +0 -124
- package/docs/getting-started/migrating.md +0 -125
- package/docs/getting-started/project-structure.md +0 -163
- package/docs/introduction/README.md +0 -66
- package/docs/introduction/design-principles.md +0 -81
- package/docs/introduction/distributed.md +0 -91
- package/docs/introduction/how-it-works.md +0 -121
- package/docs/introduction/hyperscale.md +0 -70
- package/docs/introduction/modern-stack.md +0 -85
- package/docs/introduction/vs-other-frameworks.md +0 -64
- package/docs/introduction/why-toil.md +0 -99
- package/docs/llms.txt +0 -95
- package/docs/realtime/README.md +0 -102
- package/docs/realtime/channels.md +0 -211
- package/docs/realtime/streams.md +0 -369
- package/docs/services/README.md +0 -60
- package/docs/services/analytics.md +0 -279
- package/docs/services/caching.md +0 -175
- package/docs/services/cookies.md +0 -235
- package/docs/services/crypto.md +0 -209
- package/docs/services/email.md +0 -289
- package/docs/services/environment.md +0 -141
- package/docs/services/ratelimit.md +0 -174
- package/docs/services/time.md +0 -85
- package/eslint.config.js +0 -57
- package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
- package/presets/eslint.js +0 -88
- package/src/backend/index.ts +0 -204
- package/src/cli/configure.ts +0 -363
- package/src/cli/create.ts +0 -924
- package/src/cli/db.ts +0 -158
- package/src/cli/diagnostics.ts +0 -665
- package/src/cli/doctor.ts +0 -982
- package/src/cli/features.ts +0 -170
- package/src/cli/index.ts +0 -326
- package/src/cli/notify.ts +0 -120
- package/src/cli/proc.ts +0 -56
- package/src/cli/ui.ts +0 -169
- package/src/cli/update.ts +0 -248
- package/src/cli/updates.ts +0 -92
- package/src/cli/validate.ts +0 -31
- package/src/cli/version-check.ts +0 -125
- package/src/client/auth.ts +0 -869
- package/src/client/channel/channel.ts +0 -146
- package/src/client/components/Form.tsx +0 -65
- package/src/client/components/Image.tsx +0 -160
- package/src/client/components/Script.tsx +0 -113
- package/src/client/components/Slot.tsx +0 -21
- package/src/client/dev/devtools.tsx +0 -1089
- package/src/client/dev/error-overlay.tsx +0 -225
- package/src/client/errors.ts +0 -11
- package/src/client/head/head.ts +0 -182
- package/src/client/head/metadata.ts +0 -110
- package/src/client/index.ts +0 -130
- package/src/client/navigation/Link.tsx +0 -108
- package/src/client/navigation/NavLink.tsx +0 -86
- package/src/client/navigation/navigation.ts +0 -235
- package/src/client/navigation/prefetch.ts +0 -169
- package/src/client/navigation/scroll.ts +0 -53
- package/src/client/routing/Router.tsx +0 -216
- package/src/client/routing/action.ts +0 -122
- package/src/client/routing/error-boundary.tsx +0 -43
- package/src/client/routing/hooks.ts +0 -140
- package/src/client/routing/lazy.ts +0 -77
- package/src/client/routing/loader.ts +0 -381
- package/src/client/routing/match.ts +0 -47
- package/src/client/routing/mount.tsx +0 -90
- package/src/client/routing/params-context.ts +0 -10
- package/src/client/routing/slot-context.ts +0 -7
- package/src/client/rpc.ts +0 -97
- package/src/client/search/search.ts +0 -189
- package/src/client/search/use-page-search.ts +0 -73
- package/src/client/ssr/markers.tsx +0 -172
- package/src/client/stream/client.ts +0 -237
- package/src/client/types.ts +0 -73
- package/src/compiler/auth-emails.ts +0 -312
- package/src/compiler/config.ts +0 -369
- package/src/compiler/docs.ts +0 -127
- package/src/compiler/email-preview.ts +0 -314
- package/src/compiler/emails.ts +0 -404
- package/src/compiler/fonts.ts +0 -89
- package/src/compiler/generate.ts +0 -471
- package/src/compiler/image-blur.ts +0 -42
- package/src/compiler/image-report.ts +0 -88
- package/src/compiler/index.ts +0 -1362
- package/src/compiler/pages.ts +0 -49
- package/src/compiler/plugin.ts +0 -391
- package/src/compiler/prerender.ts +0 -263
- package/src/compiler/routes.ts +0 -170
- package/src/compiler/seo.ts +0 -415
- package/src/compiler/sri.ts +0 -144
- package/src/compiler/ssg.ts +0 -177
- package/src/compiler/ssr-codegen.ts +0 -85
- package/src/compiler/template-build.ts +0 -676
- package/src/compiler/template.ts +0 -262
- package/src/compiler/toil-docs.generated.ts +0 -77
- package/src/compiler/vite.ts +0 -245
- package/src/devserver/analytics/index.ts +0 -317
- package/src/devserver/config/dotenv.ts +0 -102
- package/src/devserver/config/env.ts +0 -30
- package/src/devserver/config/ratelimit.ts +0 -123
- package/src/devserver/daemon/catalog.ts +0 -120
- package/src/devserver/daemon/cron.ts +0 -87
- package/src/devserver/daemon/host.ts +0 -134
- package/src/devserver/daemon/index.ts +0 -369
- package/src/devserver/daemon/runtime.ts +0 -48
- package/src/devserver/db/catalog.ts +0 -109
- package/src/devserver/db/database.ts +0 -1834
- package/src/devserver/db/derives.ts +0 -121
- package/src/devserver/db/index.ts +0 -20
- package/src/devserver/db/routeKinds.ts +0 -191
- package/src/devserver/db/types.ts +0 -168
- package/src/devserver/email/caps.ts +0 -0
- package/src/devserver/email/config.ts +0 -141
- package/src/devserver/email/console.ts +0 -348
- package/src/devserver/email/index.ts +0 -155
- package/src/devserver/email/providers.ts +0 -131
- package/src/devserver/email/status.ts +0 -23
- package/src/devserver/email/validate.ts +0 -37
- package/src/devserver/email/wire.ts +0 -55
- package/src/devserver/http/cache.ts +0 -0
- package/src/devserver/http/envelope.ts +0 -157
- package/src/devserver/http/proxy.ts +0 -172
- package/src/devserver/http/runtime.ts +0 -387
- package/src/devserver/index.ts +0 -43
- package/src/devserver/production-ipc.ts +0 -63
- package/src/devserver/production-worker.ts +0 -83
- package/src/devserver/production.ts +0 -718
- package/src/devserver/runtime/crypto.ts +0 -587
- package/src/devserver/runtime/host.ts +0 -427
- package/src/devserver/runtime/module.ts +0 -442
- package/src/devserver/server.ts +0 -251
- package/src/devserver/ssr.ts +0 -208
- package/src/devserver/stream/catalog.ts +0 -106
- package/src/devserver/stream/host.ts +0 -42
- package/src/devserver/stream/index.ts +0 -301
- package/src/devserver/stream/manager.ts +0 -167
- package/src/devserver/stream/router.ts +0 -101
- package/src/devserver/stream/wire.ts +0 -58
- package/src/devserver/stream/ws.ts +0 -78
- package/src/devserver/wasm/sections.ts +0 -59
- package/src/devserver/wasm/surface.ts +0 -115
- package/src/io/FastMap.ts +0 -151
- package/src/io/FastSet.ts +0 -113
- package/src/io/codec.ts +0 -362
- package/src/io/index.ts +0 -10
- package/src/io/lengths.ts +0 -14
- package/src/io/types.ts +0 -19
- package/src/logger/index.ts +0 -22
- package/src/shared/index.ts +0 -46
- package/test/analytics-dev.test.ts +0 -113
- package/test/assembly/aspect-shim.ts +0 -33
- package/test/assembly/cookie.spec.ts +0 -303
- package/test/assembly/example.spec.ts +0 -22
- package/test/assembly/ssr.spec.ts +0 -95
- package/test/assembly.test.ts +0 -59
- package/test/auth-emails.test.ts +0 -118
- package/test/built-ssr.test.ts +0 -148
- package/test/channel.test.ts +0 -21
- package/test/config-host-port.test.ts +0 -59
- package/test/configure.test.ts +0 -94
- package/test/daemon-build.test.ts +0 -278
- package/test/daemon-catalog.test.ts +0 -349
- package/test/daemon-emulation.test.ts +0 -256
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +0 -1102
- package/test/devserver-email.test.ts +0 -306
- package/test/devserver-pqauth.test.ts +0 -93
- package/test/devserver-secrets.test.ts +0 -63
- package/test/devserver.test.ts +0 -259
- package/test/doctor.test.ts +0 -298
- package/test/dom/Image.test.tsx +0 -144
- package/test/dom/Link.test.tsx +0 -67
- package/test/dom/NavLink.test.tsx +0 -37
- package/test/dom/Script.test.tsx +0 -48
- package/test/dom/action.test.tsx +0 -146
- package/test/dom/error-overlay.test.tsx +0 -44
- package/test/dom/loader.test.tsx +0 -121
- package/test/dom/navigation.test.ts +0 -59
- package/test/dom/revalidate.test.tsx +0 -38
- package/test/dom/route-head.test.tsx +0 -56
- package/test/dom/router-loading.test.tsx +0 -44
- package/test/dom/scroll.test.ts +0 -56
- package/test/dom/slot.test.tsx +0 -131
- package/test/dom/use-metadata.test.tsx +0 -58
- package/test/dom/view-transitions.test.tsx +0 -53
- package/test/dynamic-routing.test.ts +0 -132
- package/test/email-console.test.ts +0 -86
- package/test/email-preview.test.ts +0 -73
- package/test/emails.test.ts +0 -58
- package/test/errors.test.ts +0 -21
- package/test/example-guestbook.test.ts +0 -123
- package/test/features.test.ts +0 -149
- package/test/fixtures/bignum-wire/spec.ts +0 -25
- package/test/fixtures/daemon-app.ts +0 -65
- package/test/fixtures/stream-echo.ts +0 -26
- package/test/fixtures/stream-gate.ts +0 -28
- package/test/fixtures/stream-trap.ts +0 -18
- package/test/fixtures/stream-typed.ts +0 -41
- package/test/fonts.test.ts +0 -28
- package/test/global-setup.ts +0 -17
- package/test/head.test.ts +0 -42
- package/test/image-blur.test.ts +0 -50
- package/test/io.test.ts +0 -117
- package/test/metadata.test.ts +0 -40
- package/test/navlink.test.ts +0 -28
- package/test/pages.test.ts +0 -105
- package/test/placeholder.test.ts +0 -9
- package/test/pqauth-e2e.test.ts +0 -216
- package/test/pqauth-email.test.ts +0 -801
- package/test/prerender.test.ts +0 -116
- package/test/prettier-plugin.test.ts +0 -46
- package/test/routes.test.ts +0 -76
- package/test/rpc-bignum-wire.test.ts +0 -164
- package/test/rpc-dispatch.test.ts +0 -132
- package/test/rpc-kinds.test.ts +0 -18
- package/test/rpc.test.ts +0 -66
- package/test/search.test.ts +0 -114
- package/test/seo.test.ts +0 -175
- package/test/slot-layouts.test.ts +0 -69
- package/test/sri.test.ts +0 -129
- package/test/ssg.test.ts +0 -36
- package/test/ssr-hydration.test.tsx +0 -122
- package/test/ssr-render.test.ts +0 -197
- package/test/ssr-template.test.tsx +0 -482
- package/test/stream-emulation.test.ts +0 -479
- package/test/ui.test.ts +0 -40
- package/test/update.test.ts +0 -58
- package/test/validate.test.ts +0 -42
- package/test/version-check.test.ts +0 -130
- package/tests/data-parity/generated-parity.ts +0 -99
- package/tests/data-parity/parity.ts +0 -80
- package/tests/data-parity/spec.ts +0 -46
package/src/compiler/vite.ts
DELETED
|
@@ -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
|
-
}
|