@hexclave/shared 1.0.2 → 1.0.5
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/dist/ai/unified-prompts/reminders.js +4 -3
- package/dist/ai/unified-prompts/reminders.js.map +1 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts +3 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts.map +1 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js +217 -30
- package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js.map +1 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-index.d.ts.map +1 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-index.js +1 -0
- package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-index.js.map +1 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.d.ts +18 -6
- package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js +23 -8
- package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js.map +1 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.js +1 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.js.map +1 -1
- package/dist/config/schema.d.ts +63 -63
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +5 -5
- package/dist/config/schema.js.map +1 -1
- package/dist/config-authoring.d.ts +4 -4
- package/dist/config-authoring.d.ts.map +1 -1
- package/dist/config-authoring.js +2 -2
- package/dist/config-authoring.js.map +1 -1
- package/dist/config-rendering.d.ts +4 -4
- package/dist/config-rendering.d.ts.map +1 -1
- package/dist/config-rendering.js +19 -18
- package/dist/config-rendering.js.map +1 -1
- package/dist/esm/ai/unified-prompts/reminders.js +4 -3
- package/dist/esm/ai/unified-prompts/reminders.js.map +1 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts +3 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts.map +1 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js +216 -31
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js.map +1 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-index.d.ts.map +1 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-index.js +1 -0
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-index.js.map +1 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.d.ts +18 -6
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js +23 -8
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js.map +1 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.js +1 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.js.map +1 -1
- package/dist/esm/config/schema.d.ts +63 -63
- package/dist/esm/config/schema.d.ts.map +1 -1
- package/dist/esm/config/schema.js +5 -5
- package/dist/esm/config/schema.js.map +1 -1
- package/dist/esm/config-authoring.d.ts +4 -4
- package/dist/esm/config-authoring.d.ts.map +1 -1
- package/dist/esm/config-authoring.js +2 -2
- package/dist/esm/config-authoring.js.map +1 -1
- package/dist/esm/config-rendering.d.ts +4 -4
- package/dist/esm/config-rendering.d.ts.map +1 -1
- package/dist/esm/config-rendering.js +15 -14
- package/dist/esm/config-rendering.js.map +1 -1
- package/dist/esm/helpers/vault/client-side.d.ts.map +1 -1
- package/dist/esm/helpers/vault/client-side.js.map +1 -1
- package/dist/esm/helpers/vault/server-side.d.ts.map +1 -1
- package/dist/esm/helpers/vault/server-side.js.map +1 -1
- package/dist/esm/hexclave-config-file.d.ts +15 -0
- package/dist/esm/hexclave-config-file.d.ts.map +1 -0
- package/dist/esm/{stack-config-file.js → hexclave-config-file.js} +7 -7
- package/dist/esm/hexclave-config-file.js.map +1 -0
- package/dist/esm/hooks/use-async-callback.js +1 -1
- package/dist/esm/hooks/use-async-external-store.js +1 -1
- package/dist/esm/hooks/use-strict-memo.js +1 -1
- package/dist/esm/interface/admin-metrics.d.ts +8 -8
- package/dist/esm/interface/admin-metrics.js.map +1 -1
- package/dist/esm/interface/client-interface.js +2 -2
- package/dist/esm/interface/client-interface.js.map +1 -1
- package/dist/esm/interface/conversations.d.ts +32 -32
- package/dist/esm/interface/crud/current-user.d.ts +9 -9
- package/dist/esm/interface/crud/email-outbox.d.ts +200 -200
- package/dist/esm/interface/crud/products.d.ts +13 -13
- package/dist/esm/interface/crud/products.d.ts.map +1 -1
- package/dist/esm/interface/crud/project-api-keys.d.ts +4 -4
- package/dist/esm/interface/crud/project-permissions.js +5 -5
- package/dist/esm/interface/crud/project-permissions.js.map +1 -1
- package/dist/esm/interface/crud/projects.d.ts +36 -36
- package/dist/esm/interface/crud/team-member-profiles.d.ts +18 -18
- package/dist/esm/interface/crud/team-permissions.js +5 -5
- package/dist/esm/interface/crud/team-permissions.js.map +1 -1
- package/dist/esm/interface/crud/transactions.d.ts +6 -6
- package/dist/esm/interface/crud/transactions.d.ts.map +1 -1
- package/dist/esm/interface/crud/users.d.ts +12 -12
- package/dist/esm/interface/page-component-versions.js +107 -107
- package/dist/esm/interface/page-component-versions.js.map +1 -1
- package/dist/esm/interface/webhooks.d.ts +2 -2
- package/dist/esm/known-errors.d.ts +2 -2
- package/dist/esm/known-errors.js +4 -4
- package/dist/esm/known-errors.js.map +1 -1
- package/dist/esm/schema-fields.d.ts +17 -7
- package/dist/esm/schema-fields.d.ts.map +1 -1
- package/dist/esm/schema-fields.js +26 -16
- package/dist/esm/schema-fields.js.map +1 -1
- package/dist/esm/sessions.d.ts +7 -7
- package/dist/esm/utils/crypto.d.ts.map +1 -1
- package/dist/esm/utils/crypto.js.map +1 -1
- package/dist/esm/utils/currency-constants.d.ts +6 -0
- package/dist/esm/utils/currency-constants.d.ts.map +1 -1
- package/dist/esm/utils/currency-constants.js.map +1 -1
- package/dist/esm/utils/dev-env-state-path.d.ts +3 -3
- package/dist/esm/utils/dev-env-state-path.d.ts.map +1 -1
- package/dist/esm/utils/dev-env-state-path.js +4 -4
- package/dist/esm/utils/dev-env-state-path.js.map +1 -1
- package/dist/esm/utils/errors.js +2 -2
- package/dist/esm/utils/errors.js.map +1 -1
- package/dist/esm/utils/esbuild.d.ts.map +1 -1
- package/dist/esm/utils/esbuild.js +0 -1
- package/dist/esm/utils/esbuild.js.map +1 -1
- package/dist/esm/utils/featurebase.d.ts +3 -3
- package/dist/esm/utils/featurebase.d.ts.map +1 -1
- package/dist/esm/utils/featurebase.js +15 -15
- package/dist/esm/utils/featurebase.js.map +1 -1
- package/dist/esm/utils/fs.js +6 -6
- package/dist/esm/utils/fs.js.map +1 -1
- package/dist/esm/utils/globals.d.ts.map +1 -1
- package/dist/esm/utils/globals.js +10 -10
- package/dist/esm/utils/globals.js.map +1 -1
- package/dist/esm/utils/jwt.d.ts.map +1 -1
- package/dist/esm/utils/jwt.js +3 -3
- package/dist/esm/utils/jwt.js.map +1 -1
- package/dist/esm/utils/redirect-urls.d.ts +3 -3
- package/dist/esm/utils/redirect-urls.d.ts.map +1 -1
- package/dist/esm/utils/redirect-urls.js +5 -5
- package/dist/esm/utils/redirect-urls.js.map +1 -1
- package/dist/esm/utils/urls.js.map +1 -1
- package/dist/helpers/vault/client-side.d.ts.map +1 -1
- package/dist/helpers/vault/client-side.js.map +1 -1
- package/dist/helpers/vault/server-side.d.ts.map +1 -1
- package/dist/helpers/vault/server-side.js.map +1 -1
- package/dist/hexclave-config-file.d.ts +15 -0
- package/dist/hexclave-config-file.d.ts.map +1 -0
- package/dist/{stack-config-file.js → hexclave-config-file.js} +8 -8
- package/dist/hexclave-config-file.js.map +1 -0
- package/dist/hooks/use-async-callback.js +1 -1
- package/dist/hooks/use-async-external-store.js +1 -1
- package/dist/hooks/use-strict-memo.js +1 -1
- package/dist/interface/admin-metrics.d.ts +8 -8
- package/dist/interface/admin-metrics.js.map +1 -1
- package/dist/interface/client-interface.js +2 -2
- package/dist/interface/client-interface.js.map +1 -1
- package/dist/interface/conversations.d.ts +32 -32
- package/dist/interface/crud/current-user.d.ts +9 -9
- package/dist/interface/crud/email-outbox.d.ts +200 -200
- package/dist/interface/crud/products.d.ts +13 -13
- package/dist/interface/crud/products.d.ts.map +1 -1
- package/dist/interface/crud/project-api-keys.d.ts +4 -4
- package/dist/interface/crud/project-permissions.js +5 -5
- package/dist/interface/crud/project-permissions.js.map +1 -1
- package/dist/interface/crud/projects.d.ts +36 -36
- package/dist/interface/crud/team-member-profiles.d.ts +18 -18
- package/dist/interface/crud/team-permissions.js +5 -5
- package/dist/interface/crud/team-permissions.js.map +1 -1
- package/dist/interface/crud/transactions.d.ts +6 -6
- package/dist/interface/crud/transactions.d.ts.map +1 -1
- package/dist/interface/crud/users.d.ts +12 -12
- package/dist/interface/page-component-versions.js +107 -107
- package/dist/interface/page-component-versions.js.map +1 -1
- package/dist/interface/webhooks.d.ts +2 -2
- package/dist/known-errors.d.ts +2 -2
- package/dist/known-errors.js +4 -4
- package/dist/known-errors.js.map +1 -1
- package/dist/schema-fields.d.ts +17 -7
- package/dist/schema-fields.d.ts.map +1 -1
- package/dist/schema-fields.js +26 -16
- package/dist/schema-fields.js.map +1 -1
- package/dist/sessions.d.ts +7 -7
- package/dist/utils/crypto.d.ts.map +1 -1
- package/dist/utils/crypto.js.map +1 -1
- package/dist/utils/currency-constants.d.ts +6 -0
- package/dist/utils/currency-constants.d.ts.map +1 -1
- package/dist/utils/currency-constants.js.map +1 -1
- package/dist/utils/dev-env-state-path.d.ts +3 -3
- package/dist/utils/dev-env-state-path.d.ts.map +1 -1
- package/dist/utils/dev-env-state-path.js +5 -5
- package/dist/utils/dev-env-state-path.js.map +1 -1
- package/dist/utils/errors.js +2 -2
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/esbuild.d.ts.map +1 -1
- package/dist/utils/esbuild.js +0 -1
- package/dist/utils/esbuild.js.map +1 -1
- package/dist/utils/featurebase.d.ts +3 -3
- package/dist/utils/featurebase.d.ts.map +1 -1
- package/dist/utils/featurebase.js +15 -15
- package/dist/utils/featurebase.js.map +1 -1
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/globals.d.ts.map +1 -1
- package/dist/utils/globals.js +10 -10
- package/dist/utils/globals.js.map +1 -1
- package/dist/utils/jwt.d.ts.map +1 -1
- package/dist/utils/jwt.js +3 -3
- package/dist/utils/jwt.js.map +1 -1
- package/dist/utils/redirect-urls.d.ts +3 -3
- package/dist/utils/redirect-urls.d.ts.map +1 -1
- package/dist/utils/redirect-urls.js +5 -5
- package/dist/utils/redirect-urls.js.map +1 -1
- package/dist/utils/urls.js.map +1 -1
- package/package.json +2 -1
- package/src/ai/image-limits.ts +62 -0
- package/src/ai/llms/llms.ts +36 -0
- package/src/ai/unified-prompts/reminders.ts +27 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.ts +1047 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/brain.ts +5 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/cli-help.ts +5 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/config-docs.ts +5 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/custom-components-instructions.ts +5 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/dashboard-instructions.ts +5 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/docs-index.ts +80 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.ts +322 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.ts +5 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/sdk-interface-source.ts +5 -0
- package/src/ai/unified-prompts/skill-site-prompt.ts +72 -0
- package/src/apps/apps-config.ts +177 -0
- package/src/apps/apps-ui.tsx +112 -0
- package/src/config/README.md +154 -0
- package/src/config/db-sync-mappings.ts +2293 -0
- package/src/config/format.ts +388 -0
- package/src/config/migrate-catalogs-to-product-lines.ts +475 -0
- package/src/config/schema-fuzzer.test.ts +377 -0
- package/src/config/schema.ts +1507 -0
- package/src/config-authoring.test.ts +52 -0
- package/src/config-authoring.ts +37 -0
- package/src/config-rendering.ts +142 -0
- package/src/crud.tsx +275 -0
- package/src/global.d.ts +1 -0
- package/src/helpers/emails.ts +250 -0
- package/src/helpers/init-prompt.ts +39 -0
- package/src/helpers/password.ts +16 -0
- package/src/helpers/production-mode.ts +58 -0
- package/src/helpers/support-sla.test.ts +124 -0
- package/src/helpers/support-sla.ts +93 -0
- package/src/helpers/vault/client-side.ts +92 -0
- package/src/helpers/vault/server-side.ts +153 -0
- package/src/hexclave-config-file.ts +120 -0
- package/src/hooks/use-async-callback.tsx +43 -0
- package/src/hooks/use-async-external-store.tsx +24 -0
- package/src/hooks/use-hash.tsx +14 -0
- package/src/hooks/use-hover.tsx +88 -0
- package/src/hooks/use-strict-memo.tsx +157 -0
- package/src/index.ts +17 -0
- package/src/interface/admin-interface.ts +1043 -0
- package/src/interface/admin-metrics.ts +214 -0
- package/src/interface/client-interface.test.ts +765 -0
- package/src/interface/client-interface.ts +2586 -0
- package/src/interface/conversations.ts +109 -0
- package/src/interface/crud/analytics.ts +11 -0
- package/src/interface/crud/connected-accounts.ts +52 -0
- package/src/interface/crud/contact-channels.ts +74 -0
- package/src/interface/crud/current-user.ts +72 -0
- package/src/interface/crud/email-outbox.ts +263 -0
- package/src/interface/crud/email-templates.ts +50 -0
- package/src/interface/crud/emails.ts +22 -0
- package/src/interface/crud/internal-api-keys.ts +69 -0
- package/src/interface/crud/invoices.ts +36 -0
- package/src/interface/crud/items.ts +24 -0
- package/src/interface/crud/notification-preferences.ts +33 -0
- package/src/interface/crud/oauth-providers.ts +91 -0
- package/src/interface/crud/products.ts +42 -0
- package/src/interface/crud/project-api-keys.ts +100 -0
- package/src/interface/crud/project-permissions.ts +118 -0
- package/src/interface/crud/projects.ts +229 -0
- package/src/interface/crud/session-replays.ts +85 -0
- package/src/interface/crud/sessions.ts +66 -0
- package/src/interface/crud/sign-up-rules.ts +12 -0
- package/src/interface/crud/svix-token.ts +20 -0
- package/src/interface/crud/team-invitation-details.ts +22 -0
- package/src/interface/crud/team-invitation.ts +35 -0
- package/src/interface/crud/team-member-profiles.ts +61 -0
- package/src/interface/crud/team-memberships.ts +56 -0
- package/src/interface/crud/team-permissions.ts +119 -0
- package/src/interface/crud/teams.ts +142 -0
- package/src/interface/crud/transactions.ts +131 -0
- package/src/interface/crud/users.ts +185 -0
- package/src/interface/handler-urls.ts +74 -0
- package/src/interface/page-component-versions.ts +1702 -0
- package/src/interface/server-interface.ts +1146 -0
- package/src/interface/webhooks.ts +28 -0
- package/src/known-errors.tsx +2031 -0
- package/src/local-emulator.ts +5 -0
- package/src/payments/payment-methods.ts +167 -0
- package/src/payments/stripe-limits.ts +18 -0
- package/src/plans.ts +107 -0
- package/src/schema-fields.ts +944 -0
- package/src/sessions.ts +310 -0
- package/src/utils/api-keys.tsx +122 -0
- package/src/utils/arrays.tsx +248 -0
- package/src/utils/auth-methods.ts +8 -0
- package/src/utils/base64.tsx +24 -0
- package/src/utils/booleans.tsx +34 -0
- package/src/utils/browser-compat.tsx +17 -0
- package/src/utils/bytes.tsx +280 -0
- package/src/utils/caches.tsx +280 -0
- package/src/utils/cel-fields.ts +92 -0
- package/src/utils/cloud-hosts.tsx +92 -0
- package/src/utils/compile-time.tsx +11 -0
- package/src/utils/country-codes.ts +39 -0
- package/src/utils/crypto.tsx +174 -0
- package/src/utils/currencies.tsx +53 -0
- package/src/utils/currency-constants.tsx +51 -0
- package/src/utils/dates.tsx +246 -0
- package/src/utils/dev-env-state-path.ts +14 -0
- package/src/utils/dom.tsx +7 -0
- package/src/utils/env.tsx +117 -0
- package/src/utils/errors.tsx +284 -0
- package/src/utils/esbuild.tsx +235 -0
- package/src/utils/featurebase.tsx +247 -0
- package/src/utils/fs.tsx +33 -0
- package/src/utils/functions.tsx +22 -0
- package/src/utils/geo.tsx +16 -0
- package/src/utils/globals.tsx +48 -0
- package/src/utils/hashes.tsx +52 -0
- package/src/utils/html.tsx +60 -0
- package/src/utils/http.tsx +91 -0
- package/src/utils/ips.tsx +47 -0
- package/src/utils/json.tsx +200 -0
- package/src/utils/jsx-editable-transpiler.test.ts +156 -0
- package/src/utils/jsx-editable-transpiler.tsx +401 -0
- package/src/utils/jwt.tsx +170 -0
- package/src/utils/locks.tsx +71 -0
- package/src/utils/maps.tsx +407 -0
- package/src/utils/math.tsx +18 -0
- package/src/utils/monkey-patch.tsx +29 -0
- package/src/utils/node-http.tsx +47 -0
- package/src/utils/numbers.tsx +82 -0
- package/src/utils/oauth.tsx +9 -0
- package/src/utils/objects.tsx +581 -0
- package/src/utils/paginated-lists.test.ts +1039 -0
- package/src/utils/paginated-lists.tsx +479 -0
- package/src/utils/passkey.tsx +1 -0
- package/src/utils/promises.tsx +540 -0
- package/src/utils/proxies.tsx +212 -0
- package/src/utils/react.tsx +308 -0
- package/src/utils/redirect-urls.tsx +299 -0
- package/src/utils/regex.tsx +10 -0
- package/src/utils/results.tsx +411 -0
- package/src/utils/sentry.tsx +20 -0
- package/src/utils/stores.tsx +238 -0
- package/src/utils/strings.nicify.test.ts +253 -0
- package/src/utils/strings.tsx +723 -0
- package/src/utils/telemetry.tsx +37 -0
- package/src/utils/turnstile-browser.ts +106 -0
- package/src/utils/turnstile-flow.test.ts +101 -0
- package/src/utils/turnstile-flow.ts +266 -0
- package/src/utils/turnstile.ts +34 -0
- package/src/utils/typed-arrays.tsx +17 -0
- package/src/utils/types.tsx +208 -0
- package/src/utils/unicode.tsx +24 -0
- package/src/utils/urls.tsx +411 -0
- package/src/utils/uuids.tsx +47 -0
- package/dist/esm/stack-config-file.d.ts +0 -15
- package/dist/esm/stack-config-file.d.ts.map +0 -1
- package/dist/esm/stack-config-file.js.map +0 -1
- package/dist/stack-config-file.d.ts +0 -15
- package/dist/stack-config-file.d.ts.map +0 -1
- package/dist/stack-config-file.js.map +0 -1
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { intervalSchema } from "../schema-fields";
|
|
2
|
+
import { HexclaveAssertionError } from "./errors";
|
|
3
|
+
import { remainder } from "./math";
|
|
4
|
+
|
|
5
|
+
export function isWeekend(date: Date): boolean {
|
|
6
|
+
return date.getDay() === 0 || date.getDay() === 6;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
import.meta.vitest?.test("isWeekend", ({ expect }) => {
|
|
10
|
+
// Sunday (day 0)
|
|
11
|
+
expect(isWeekend(new Date(2023, 0, 1))).toBe(true);
|
|
12
|
+
// Saturday (day 6)
|
|
13
|
+
expect(isWeekend(new Date(2023, 0, 7))).toBe(true);
|
|
14
|
+
// Monday (day 1)
|
|
15
|
+
expect(isWeekend(new Date(2023, 0, 2))).toBe(false);
|
|
16
|
+
// Friday (day 5)
|
|
17
|
+
expect(isWeekend(new Date(2023, 0, 6))).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const agoUnits = [
|
|
21
|
+
[60, 'second'],
|
|
22
|
+
[60, 'minute'],
|
|
23
|
+
[24, 'hour'],
|
|
24
|
+
[7, 'day'],
|
|
25
|
+
[5, 'week'],
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
export function fromNow(date: Date): string {
|
|
29
|
+
return fromNowDetailed(date).result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
import.meta.vitest?.test("fromNow", ({ expect }) => {
|
|
33
|
+
// Set a fixed date for testing
|
|
34
|
+
const fixedDate = new Date("2023-01-15T12:00:00.000Z");
|
|
35
|
+
|
|
36
|
+
// Use Vitest's fake timers
|
|
37
|
+
import.meta.vitest?.vi.useFakeTimers();
|
|
38
|
+
import.meta.vitest?.vi.setSystemTime(fixedDate);
|
|
39
|
+
|
|
40
|
+
// Test past times
|
|
41
|
+
expect(fromNow(new Date("2023-01-15T11:59:50.000Z"))).toBe("just now");
|
|
42
|
+
expect(fromNow(new Date("2023-01-15T11:59:00.000Z"))).toBe("1 minute ago");
|
|
43
|
+
expect(fromNow(new Date("2023-01-15T11:00:00.000Z"))).toBe("1 hour ago");
|
|
44
|
+
expect(fromNow(new Date("2023-01-14T12:00:00.000Z"))).toBe("1 day ago");
|
|
45
|
+
expect(fromNow(new Date("2023-01-08T12:00:00.000Z"))).toBe("1 week ago");
|
|
46
|
+
|
|
47
|
+
// Test future times
|
|
48
|
+
expect(fromNow(new Date("2023-01-15T12:00:10.000Z"))).toBe("just now");
|
|
49
|
+
expect(fromNow(new Date("2023-01-15T12:01:00.000Z"))).toBe("in 1 minute");
|
|
50
|
+
expect(fromNow(new Date("2023-01-15T13:00:00.000Z"))).toBe("in 1 hour");
|
|
51
|
+
expect(fromNow(new Date("2023-01-16T12:00:00.000Z"))).toBe("in 1 day");
|
|
52
|
+
expect(fromNow(new Date("2023-01-22T12:00:00.000Z"))).toBe("in 1 week");
|
|
53
|
+
|
|
54
|
+
// Test very old dates (should use date format)
|
|
55
|
+
expect(fromNow(new Date("2022-01-15T12:00:00.000Z"))).toMatch(/Jan 15, 2022/);
|
|
56
|
+
|
|
57
|
+
// Restore real timers
|
|
58
|
+
import.meta.vitest?.vi.useRealTimers();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export function fromNowDetailed(date: Date): {
|
|
62
|
+
result: string,
|
|
63
|
+
/**
|
|
64
|
+
* May be Infinity if the result will never change.
|
|
65
|
+
*/
|
|
66
|
+
secondsUntilChange: number,
|
|
67
|
+
} {
|
|
68
|
+
if (!(date instanceof Date)) {
|
|
69
|
+
throw new Error(`fromNow only accepts Date objects (received: ${date})`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const now = new Date();
|
|
73
|
+
const elapsed = now.getTime() - date.getTime();
|
|
74
|
+
|
|
75
|
+
let remainingInUnit = Math.abs(elapsed) / 1000;
|
|
76
|
+
if (remainingInUnit < 15) {
|
|
77
|
+
return {
|
|
78
|
+
result: 'just now',
|
|
79
|
+
secondsUntilChange: 15 - remainingInUnit,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
let unitInSeconds = 1;
|
|
83
|
+
for (const [nextUnitSize, unitName] of agoUnits) {
|
|
84
|
+
const rounded = Math.round(remainingInUnit);
|
|
85
|
+
if (rounded < nextUnitSize) {
|
|
86
|
+
if (elapsed < 0) {
|
|
87
|
+
return {
|
|
88
|
+
result: `in ${rounded} ${unitName}${rounded === 1 ? '' : 's'}`,
|
|
89
|
+
secondsUntilChange: remainder((remainingInUnit - rounded + 0.5) * unitInSeconds, unitInSeconds),
|
|
90
|
+
};
|
|
91
|
+
} else {
|
|
92
|
+
return {
|
|
93
|
+
result: `${rounded} ${unitName}${rounded === 1 ? '' : 's'} ago`,
|
|
94
|
+
secondsUntilChange: remainder((rounded - remainingInUnit - 0.5) * unitInSeconds, unitInSeconds),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
unitInSeconds *= nextUnitSize;
|
|
99
|
+
remainingInUnit /= nextUnitSize;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
result: date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }),
|
|
104
|
+
secondsUntilChange: Infinity,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns a string representation of the given date in the format expected by the `datetime-local` input type.
|
|
110
|
+
*/
|
|
111
|
+
export function getInputDatetimeLocalString(date: Date): string {
|
|
112
|
+
date = new Date(date);
|
|
113
|
+
date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
|
|
114
|
+
return date.toISOString().slice(0, 19);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
import.meta.vitest?.test("getInputDatetimeLocalString", ({ expect }) => {
|
|
118
|
+
// Use Vitest's fake timers to ensure consistent timezone behavior
|
|
119
|
+
import.meta.vitest?.vi.useFakeTimers();
|
|
120
|
+
|
|
121
|
+
// Test with a specific date
|
|
122
|
+
const mockDate = new Date("2023-01-15T12:30:45.000Z");
|
|
123
|
+
const result = getInputDatetimeLocalString(mockDate);
|
|
124
|
+
|
|
125
|
+
// The result should be in the format YYYY-MM-DDTHH:MM:SS
|
|
126
|
+
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/);
|
|
127
|
+
|
|
128
|
+
// Test with different dates
|
|
129
|
+
const dates = [
|
|
130
|
+
new Date("2023-01-01T00:00:00.000Z"),
|
|
131
|
+
new Date("2023-06-15T23:59:59.000Z"),
|
|
132
|
+
new Date("2023-12-31T12:34:56.000Z"),
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
for (const date of dates) {
|
|
136
|
+
const result = getInputDatetimeLocalString(date);
|
|
137
|
+
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Restore real timers
|
|
141
|
+
import.meta.vitest?.vi.useRealTimers();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
export type Interval = [number, 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'];
|
|
146
|
+
export type DayInterval = [number, 'day' | 'week' | 'month' | 'year'];
|
|
147
|
+
|
|
148
|
+
function applyInterval(date: Date, times: number, interval: Interval): Date {
|
|
149
|
+
if (!intervalSchema.isValidSync(interval)) {
|
|
150
|
+
throw new HexclaveAssertionError(`Invalid interval`, { interval });
|
|
151
|
+
}
|
|
152
|
+
const [amount, unit] = interval;
|
|
153
|
+
switch (unit) {
|
|
154
|
+
case 'millisecond': {
|
|
155
|
+
date.setMilliseconds(date.getMilliseconds() + amount * times);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case 'second': {
|
|
159
|
+
date.setSeconds(date.getSeconds() + amount * times);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'minute': {
|
|
163
|
+
date.setMinutes(date.getMinutes() + amount * times);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case 'hour': {
|
|
167
|
+
date.setHours(date.getHours() + amount * times);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case 'day': {
|
|
171
|
+
date.setDate(date.getDate() + amount * times);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case 'week': {
|
|
175
|
+
date.setDate(date.getDate() + amount * times * 7);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case 'month': {
|
|
179
|
+
date.setMonth(date.getMonth() + amount * times);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case 'year': {
|
|
183
|
+
date.setFullYear(date.getFullYear() + amount * times);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
default: {
|
|
187
|
+
throw new HexclaveAssertionError(`Invalid interval despite schema validation`, { interval });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return date;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function subtractInterval(date: Date, interval: Interval): Date {
|
|
194
|
+
return applyInterval(date, -1, interval);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function addInterval(date: Date, interval: Interval): Date {
|
|
198
|
+
return applyInterval(date, 1, interval);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export const FAR_FUTURE_DATE = new Date(8640000000000000); // 13 Sep 275760 00:00:00 UTC
|
|
202
|
+
|
|
203
|
+
function getMsPerDayIntervalUnit(unit: 'day' | 'week'): number {
|
|
204
|
+
if (unit === 'day') {
|
|
205
|
+
return 24 * 60 * 60 * 1000;
|
|
206
|
+
}
|
|
207
|
+
return 7 * 24 * 60 * 60 * 1000;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
export function getIntervalsElapsed(anchor: Date, to: Date, repeat: DayInterval): number {
|
|
212
|
+
const [amount, unit] = repeat;
|
|
213
|
+
if (to <= anchor) return 0;
|
|
214
|
+
if (unit === 'day' || unit === 'week') {
|
|
215
|
+
const msPerUnit = getMsPerDayIntervalUnit(unit);
|
|
216
|
+
const diffMs = to.getTime() - anchor.getTime();
|
|
217
|
+
return Math.floor(diffMs / (msPerUnit * amount));
|
|
218
|
+
}
|
|
219
|
+
if (["month", "year"].includes(unit)) {
|
|
220
|
+
let count = 0;
|
|
221
|
+
let current = new Date(anchor);
|
|
222
|
+
for (; ;) {
|
|
223
|
+
const next = addInterval(new Date(current), [amount, unit]);
|
|
224
|
+
if (next > to) break;
|
|
225
|
+
current = next;
|
|
226
|
+
count += 1;
|
|
227
|
+
}
|
|
228
|
+
return count;
|
|
229
|
+
}
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
import.meta.vitest?.test("getIntervalsElapsed", ({ expect }) => {
|
|
234
|
+
const anchor = new Date('2025-01-01T00:00:00.000Z');
|
|
235
|
+
const to = new Date('2025-01-15T00:00:00.000Z');
|
|
236
|
+
expect(getIntervalsElapsed(anchor, to, [1, 'week'])).toBe(2);
|
|
237
|
+
expect(getIntervalsElapsed(anchor, to, [3, 'day'])).toBe(4);
|
|
238
|
+
|
|
239
|
+
const mAnchor = new Date('2023-01-31T00:00:00.000Z');
|
|
240
|
+
const mTo = new Date('2023-03-01T00:00:00.000Z');
|
|
241
|
+
expect(getIntervalsElapsed(mAnchor, mTo, [1, 'month'])).toBe(0);
|
|
242
|
+
|
|
243
|
+
const yAnchor = new Date('2020-01-01T00:00:00.000Z');
|
|
244
|
+
const yTo = new Date('2022-06-01T00:00:00.000Z');
|
|
245
|
+
expect(getIntervalsElapsed(yAnchor, yTo, [1, 'year'])).toBe(2);
|
|
246
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { homedir } from "os";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
export function defaultHexclaveDevEnvStatePath(): string {
|
|
5
|
+
if (process.platform === "win32") {
|
|
6
|
+
const localAppData = process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local");
|
|
7
|
+
return join(localAppData, "Hexclave", "dev-envs.json");
|
|
8
|
+
}
|
|
9
|
+
return join(homedir(), ".stack", "dev-envs.json");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function hexclaveDevEnvStatePath(): string {
|
|
13
|
+
return process.env.STACK_DEV_ENVS_PATH ?? defaultHexclaveDevEnvStatePath();
|
|
14
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { HexclaveAssertionError, throwErr } from "./errors";
|
|
2
|
+
import { deindent } from "./strings";
|
|
3
|
+
|
|
4
|
+
export function isBrowserLike() {
|
|
5
|
+
return typeof window !== "undefined" && typeof document !== "undefined" && typeof document.createElement !== "undefined";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// newName: oldName
|
|
9
|
+
const ENV_VAR_RENAME: Record<string, string[]> = {
|
|
10
|
+
NEXT_PUBLIC_STACK_API_URL: ['STACK_BASE_URL', 'NEXT_PUBLIC_STACK_URL'],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Hexclave rebrand: compute the `HEXCLAVE_*`-prefixed equivalent of a `STACK_*`
|
|
15
|
+
* env var name by replacing the first `STACK_` occurrence with `HEXCLAVE_`.
|
|
16
|
+
* Covers `STACK_FOO`, `NEXT_PUBLIC_STACK_FOO`, `NEXT_PUBLIC_BROWSER_STACK_FOO`,
|
|
17
|
+
* `NEXT_PUBLIC_SERVER_STACK_FOO`, `VITE_STACK_FOO`. Returns `undefined` when the
|
|
18
|
+
* name has no `STACK_` segment (caller should behave exactly as before).
|
|
19
|
+
*/
|
|
20
|
+
function getHexclaveEnvVarName(name: string): string | undefined {
|
|
21
|
+
if (!name.includes("STACK_")) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return name.replace("STACK_", "HEXCLAVE_");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns the environment variable with the given name, returning the default (if given) or throwing an error (otherwise) if it's undefined or the empty string.
|
|
29
|
+
*/
|
|
30
|
+
export function getEnvVariable(name: string, defaultValue?: string | undefined): string {
|
|
31
|
+
if (isBrowserLike()) {
|
|
32
|
+
throw new Error(deindent`
|
|
33
|
+
Can't use getEnvVariable on the client because Next.js transpiles expressions of the kind process.env.XYZ at build-time on the client.
|
|
34
|
+
|
|
35
|
+
Use process.env.XYZ directly instead.
|
|
36
|
+
`);
|
|
37
|
+
}
|
|
38
|
+
if (name === "NEXT_RUNTIME") {
|
|
39
|
+
throw new Error(deindent`
|
|
40
|
+
Can't use getEnvVariable to access the NEXT_RUNTIME environment variable because it's compiled into the client bundle.
|
|
41
|
+
|
|
42
|
+
Use getNextRuntime() instead.
|
|
43
|
+
`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// throw error if the old name is used as the retrieve key
|
|
47
|
+
for (const [newName, oldNames] of Object.entries(ENV_VAR_RENAME)) {
|
|
48
|
+
if (oldNames.includes(name)) {
|
|
49
|
+
throwErr(`Environment variable ${name} has been renamed to ${newName}. Please update your configuration to use the new name.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Hexclave rebrand: prefer the HEXCLAVE_*-prefixed equivalent, fall back to the STACK_* name.
|
|
54
|
+
const hexclaveName = getHexclaveEnvVarName(name);
|
|
55
|
+
let value = (hexclaveName ? process.env[hexclaveName] : undefined) ?? process.env[name];
|
|
56
|
+
|
|
57
|
+
// check the key under the old name if the new name is not found
|
|
58
|
+
if (!value && ENV_VAR_RENAME[name] as any) {
|
|
59
|
+
for (const oldName of ENV_VAR_RENAME[name]) {
|
|
60
|
+
// Hexclave rebrand: also accept the HEXCLAVE_*-prefixed equivalent of each old alias.
|
|
61
|
+
const hexclaveOldName = getHexclaveEnvVarName(oldName);
|
|
62
|
+
value = (hexclaveOldName ? process.env[hexclaveOldName] : undefined) ?? process.env[oldName];
|
|
63
|
+
if (value) break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!value) {
|
|
68
|
+
if (defaultValue !== undefined) {
|
|
69
|
+
value = defaultValue;
|
|
70
|
+
} else {
|
|
71
|
+
throwErr(`Missing environment variable: ${name}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getEnvBoolean(name: string): boolean {
|
|
79
|
+
const value = getEnvVariable(name, "false");
|
|
80
|
+
if (value === "true") {
|
|
81
|
+
return true;
|
|
82
|
+
} else if (value === "false") {
|
|
83
|
+
return false;
|
|
84
|
+
} else {
|
|
85
|
+
throw new HexclaveAssertionError(`Environment variable ${name} must be either "true" or "false": found ${JSON.stringify(value)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getNextRuntime() {
|
|
90
|
+
// This variable is compiled into the client bundle, so we can't use getEnvVariable here.
|
|
91
|
+
return process.env.NEXT_RUNTIME || throwErr("Missing environment variable: NEXT_RUNTIME");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getNodeEnvironment() {
|
|
95
|
+
return getEnvVariable("NODE_ENV", "");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Browser-safe access to `process.env` for server-only or genuinely dynamic
|
|
100
|
+
* env-var lookups. Returns `undefined` when `process` is not defined (e.g. in
|
|
101
|
+
* a Vite browser bundle without a `process` shim).
|
|
102
|
+
*
|
|
103
|
+
* Note: uses `process.env[name]` (bracket form), which is NOT recognized by
|
|
104
|
+
* Next.js / webpack DefinePlugin for compile-time inlining. If you need
|
|
105
|
+
* build-time inlining for a `NEXT_PUBLIC_*` var, use the literal dot-form at
|
|
106
|
+
* the call site, guarded with `typeof process`:
|
|
107
|
+
*
|
|
108
|
+
* const value = (typeof process !== "undefined" ? process.env.NEXT_PUBLIC_FOO : undefined);
|
|
109
|
+
*/
|
|
110
|
+
export function getProcessEnv(name: string): string | undefined {
|
|
111
|
+
if (typeof process === "undefined" || typeof process.env === "undefined") {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
// Hexclave rebrand: prefer the HEXCLAVE_*-prefixed equivalent, fall back to the STACK_* name.
|
|
115
|
+
const hexclaveName = getHexclaveEnvVarName(name);
|
|
116
|
+
return (hexclaveName ? process.env[hexclaveName] : undefined) ?? process.env[name];
|
|
117
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { globalVar } from "./globals";
|
|
2
|
+
import { Json } from "./json";
|
|
3
|
+
import { pick } from "./objects";
|
|
4
|
+
import { nicify } from "./strings";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export function throwErr(errorMessage: string, extraData?: any): never;
|
|
8
|
+
export function throwErr(error: Error): never;
|
|
9
|
+
export function throwErr(...args: StatusErrorConstructorParameters): never;
|
|
10
|
+
export function throwErr(...args: any[]): never {
|
|
11
|
+
if (typeof args[0] === "string") {
|
|
12
|
+
throw new HexclaveAssertionError(args[0], args[1]);
|
|
13
|
+
} else if (args[0] instanceof Error) {
|
|
14
|
+
throw args[0];
|
|
15
|
+
} else {
|
|
16
|
+
// @ts-expect-error
|
|
17
|
+
throw new StatusError(...args);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function removeStacktraceNameLine(stack: string): string {
|
|
22
|
+
// some browsers (eg. Chrome) prepend the stack with an extra line with the error name
|
|
23
|
+
const addsNameLine = new Error().stack?.startsWith("Error\n");
|
|
24
|
+
return stack.split("\n").slice(addsNameLine ? 1 : 0).join("\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Concatenates the (original) stacktraces of the given errors onto the first.
|
|
30
|
+
*
|
|
31
|
+
* Note: Very often, the concatStacktracesIfRejected function in promises.tsx is an easier way to use this function.
|
|
32
|
+
*
|
|
33
|
+
* Useful when you invoke an async function to receive a promise without awaiting it immediately. Browsers are smart
|
|
34
|
+
* enough to keep track of the call stack in async function calls when you invoke `.then` within the same async tick,
|
|
35
|
+
* but if you don't, the stacktrace will be lost.
|
|
36
|
+
*
|
|
37
|
+
* Here's an example of the unwanted behavior:
|
|
38
|
+
*
|
|
39
|
+
* ```tsx
|
|
40
|
+
* async function log() {
|
|
41
|
+
* await wait(0); // put the task on the event loop
|
|
42
|
+
* console.log(new Error().stack);
|
|
43
|
+
* }
|
|
44
|
+
*
|
|
45
|
+
* async function main() {
|
|
46
|
+
* await log(); // good; prints both "log" and "main" on the stacktrace
|
|
47
|
+
* log(); // bad; prints only "log" on the stacktrace
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function concatStacktraces(first: Error, ...errors: Error[]): void {
|
|
52
|
+
// some browsers (eg. Firefox) add an extra empty line at the end
|
|
53
|
+
const addsEmptyLineAtEnd = first.stack?.endsWith("\n");
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// Add a reference to this function itself so that we know that stacktraces were concatenated
|
|
57
|
+
// If you are coming here from a stacktrace, please know that the two parts before and after this line are different
|
|
58
|
+
// stacktraces that were concatenated with concatStacktraces
|
|
59
|
+
const separator = removeStacktraceNameLine(new Error().stack ?? "").split("\n")[0];
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
for (const error of errors) {
|
|
63
|
+
const toAppend = removeStacktraceNameLine(error.stack ?? "");
|
|
64
|
+
first.stack += (addsEmptyLineAtEnd ? "" : "\n") + separator + "\n" + toAppend;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
export class HexclaveAssertionError extends Error {
|
|
70
|
+
constructor(message: string, public readonly extraData?: Record<string, any> & ErrorOptions) {
|
|
71
|
+
const disclaimer = `\n\nThis is likely an error in Hexclave. Please make sure you are running the newest version and report it.`;
|
|
72
|
+
super(`${message}${message.endsWith(disclaimer) ? "" : disclaimer}`, pick(extraData ?? {}, ["cause"]));
|
|
73
|
+
|
|
74
|
+
Object.defineProperty(this, "customCaptureExtraArgs", {
|
|
75
|
+
get() {
|
|
76
|
+
return [this.extraData];
|
|
77
|
+
},
|
|
78
|
+
enumerable: false,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Use literal dot-form (guarded with `typeof process`) so Next.js / webpack
|
|
82
|
+
// DefinePlugin can inline the value at build time. See getProcessEnv in ./env.
|
|
83
|
+
if ((typeof process !== "undefined" ? process.env.NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR : undefined) === "true") {
|
|
84
|
+
debugger;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
HexclaveAssertionError.prototype.name = "HexclaveAssertionError";
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
export function errorToNiceString(error: unknown): string {
|
|
92
|
+
if (!(error instanceof Error)) return `${typeof error}<${nicify(error)}>`;
|
|
93
|
+
return nicify(error, { maxDepth: 8 });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
export type CaptureLevel = "error" | "warning";
|
|
98
|
+
|
|
99
|
+
type ErrorSink = (location: string, error: unknown, level: CaptureLevel, ...extraArgs: unknown[]) => void;
|
|
100
|
+
|
|
101
|
+
const errorSinks = new Set<ErrorSink>();
|
|
102
|
+
export function registerErrorSink(sink: ErrorSink): void {
|
|
103
|
+
if (errorSinks.has(sink)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
errorSinks.add(sink);
|
|
107
|
+
}
|
|
108
|
+
registerErrorSink((location, error, level, ...extraArgs) => {
|
|
109
|
+
const logger = level === "warning" ? console.warn : console.error;
|
|
110
|
+
const colorCode = level === "warning" ? "\x1b[43m" : "\x1b[41m";
|
|
111
|
+
const label = level === "warning" ? "warning" : "error";
|
|
112
|
+
logger(
|
|
113
|
+
`${colorCode}Captured ${label} in ${location}:`,
|
|
114
|
+
// HACK: Log a nicified version of the error to get around buggy Next.js pretty-printing
|
|
115
|
+
// https://www.reddit.com/r/nextjs/comments/1gkxdqe/comment/m19kxgn/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button
|
|
116
|
+
errorToNiceString(error),
|
|
117
|
+
...extraArgs,
|
|
118
|
+
"\x1b[0m",
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
registerErrorSink((location, error, level, ...extraArgs) => {
|
|
122
|
+
globalVar.hexclaveCapturedErrors = globalVar.hexclaveCapturedErrors ?? [];
|
|
123
|
+
globalVar.hexclaveCapturedErrors.push({ location, error, level, extraArgs });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
function dispatchToSinks(location: string, error: unknown, level: CaptureLevel): void {
|
|
127
|
+
for (const sink of errorSinks) {
|
|
128
|
+
sink(
|
|
129
|
+
location,
|
|
130
|
+
error,
|
|
131
|
+
level,
|
|
132
|
+
...error && (typeof error === 'object' || typeof error === 'function') && "customCaptureExtraArgs" in error && Array.isArray(error.customCaptureExtraArgs) ? (error.customCaptureExtraArgs as any[]) : [],
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Captures an error and sends it to the error sinks (most notably, Sentry). Errors caught with captureError are
|
|
139
|
+
* supposed to be seen by an engineer, so they should be actionable and important.
|
|
140
|
+
*
|
|
141
|
+
* The location string is a machine-readable ID, and should hence not contain spaces or anything like that. Good
|
|
142
|
+
* examples are: "api-route-handler", "renderPart()", etc.
|
|
143
|
+
*
|
|
144
|
+
* Errors that bubble up to the top of runAsynchronously or a route handler are already captured with captureError.
|
|
145
|
+
*/
|
|
146
|
+
export function captureError(location: string, error: unknown): void {
|
|
147
|
+
dispatchToSinks(location, error, "error");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Like captureError, but logs at warning level. Use for issues that an engineer should know about but that aren't
|
|
152
|
+
* severe enough to be treated as an error (e.g. recoverable failures in background tasks).
|
|
153
|
+
*/
|
|
154
|
+
export function captureWarning(location: string, error: unknown): void {
|
|
155
|
+
dispatchToSinks(location, error, "warning");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
type Status = {
|
|
160
|
+
statusCode: number,
|
|
161
|
+
message: string,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
type StatusErrorConstructorParameters =
|
|
165
|
+
| [
|
|
166
|
+
status: Status,
|
|
167
|
+
message?: string
|
|
168
|
+
]
|
|
169
|
+
| [
|
|
170
|
+
statusCode: number | Status,
|
|
171
|
+
message: string,
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
export class StatusError extends Error {
|
|
175
|
+
private readonly __stackStatusErrorBrand = "stack-status-error-brand-sentinel" as const;
|
|
176
|
+
public name = "StatusError";
|
|
177
|
+
public readonly statusCode: number;
|
|
178
|
+
|
|
179
|
+
public static BadRequest = { statusCode: 400, message: "Bad Request" };
|
|
180
|
+
public static Unauthorized = { statusCode: 401, message: "Unauthorized" };
|
|
181
|
+
public static PaymentRequired = { statusCode: 402, message: "Payment Required" };
|
|
182
|
+
public static Forbidden = { statusCode: 403, message: "Forbidden" };
|
|
183
|
+
public static NotFound = { statusCode: 404, message: "Not Found" };
|
|
184
|
+
public static MethodNotAllowed = { statusCode: 405, message: "Method Not Allowed" };
|
|
185
|
+
public static NotAcceptable = { statusCode: 406, message: "Not Acceptable" };
|
|
186
|
+
public static ProxyAuthenticationRequired = { statusCode: 407, message: "Proxy Authentication Required" };
|
|
187
|
+
public static RequestTimeout = { statusCode: 408, message: "Request Timeout" };
|
|
188
|
+
public static Conflict = { statusCode: 409, message: "Conflict" };
|
|
189
|
+
public static Gone = { statusCode: 410, message: "Gone" };
|
|
190
|
+
public static LengthRequired = { statusCode: 411, message: "Length Required" };
|
|
191
|
+
public static PreconditionFailed = { statusCode: 412, message: "Precondition Failed" };
|
|
192
|
+
public static PayloadTooLarge = { statusCode: 413, message: "Payload Too Large" };
|
|
193
|
+
public static URITooLong = { statusCode: 414, message: "URI Too Long" };
|
|
194
|
+
public static UnsupportedMediaType = { statusCode: 415, message: "Unsupported Media Type" };
|
|
195
|
+
public static RangeNotSatisfiable = { statusCode: 416, message: "Range Not Satisfiable" };
|
|
196
|
+
public static ExpectationFailed = { statusCode: 417, message: "Expectation Failed" };
|
|
197
|
+
public static ImATeapot = { statusCode: 418, message: "I'm a teapot" };
|
|
198
|
+
public static MisdirectedRequest = { statusCode: 421, message: "Misdirected Request" };
|
|
199
|
+
public static UnprocessableEntity = { statusCode: 422, message: "Unprocessable Entity" };
|
|
200
|
+
public static Locked = { statusCode: 423, message: "Locked" };
|
|
201
|
+
public static FailedDependency = { statusCode: 424, message: "Failed Dependency" };
|
|
202
|
+
public static TooEarly = { statusCode: 425, message: "Too Early" };
|
|
203
|
+
public static UpgradeRequired = { statusCode: 426, message: "Upgrade Required" };
|
|
204
|
+
public static PreconditionRequired = { statusCode: 428, message: "Precondition Required" };
|
|
205
|
+
public static TooManyRequests = { statusCode: 429, message: "Too Many Requests" };
|
|
206
|
+
public static RequestHeaderFieldsTooLarge = { statusCode: 431, message: "Request Header Fields Too Large" };
|
|
207
|
+
public static UnavailableForLegalReasons = { statusCode: 451, message: "Unavailable For Legal Reasons" };
|
|
208
|
+
|
|
209
|
+
public static InternalServerError = { statusCode: 500, message: "Internal Server Error" };
|
|
210
|
+
public static NotImplemented = { statusCode: 501, message: "Not Implemented" };
|
|
211
|
+
public static BadGateway = { statusCode: 502, message: "Bad Gateway" };
|
|
212
|
+
public static ServiceUnavailable = { statusCode: 503, message: "Service Unavailable" };
|
|
213
|
+
public static GatewayTimeout = { statusCode: 504, message: "Gateway Timeout" };
|
|
214
|
+
public static HTTPVersionNotSupported = { statusCode: 505, message: "HTTP Version Not Supported" };
|
|
215
|
+
public static VariantAlsoNegotiates = { statusCode: 506, message: "Variant Also Negotiates" };
|
|
216
|
+
public static InsufficientStorage = { statusCode: 507, message: "Insufficient Storage" };
|
|
217
|
+
public static LoopDetected = { statusCode: 508, message: "Loop Detected" };
|
|
218
|
+
public static NotExtended = { statusCode: 510, message: "Not Extended" };
|
|
219
|
+
public static NetworkAuthenticationRequired = { statusCode: 511, message: "Network Authentication Required" };
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
constructor(...args: StatusErrorConstructorParameters);
|
|
223
|
+
constructor(
|
|
224
|
+
status: number | Status,
|
|
225
|
+
message?: string,
|
|
226
|
+
) {
|
|
227
|
+
if (typeof status === "object") {
|
|
228
|
+
message ??= status.message;
|
|
229
|
+
status = status.statusCode;
|
|
230
|
+
}
|
|
231
|
+
super(message);
|
|
232
|
+
this.statusCode = status;
|
|
233
|
+
if (!message) {
|
|
234
|
+
throw new HexclaveAssertionError("StatusError always requires a message unless a Status object is passed", { cause: this });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
public static isStatusError(error: unknown): error is StatusError {
|
|
239
|
+
// like instanceof, but also works for errors thrown in other realms or by different versions of the same package
|
|
240
|
+
return typeof error === "object" && error !== null && "__stackStatusErrorBrand" in error && error.__stackStatusErrorBrand === "stack-status-error-brand-sentinel";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public isClientError() {
|
|
244
|
+
return this.statusCode >= 400 && this.statusCode < 500;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
public isServerError() {
|
|
248
|
+
return !this.isClientError();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
public getStatusCode(): number {
|
|
252
|
+
return this.statusCode;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
public getBody(): Uint8Array {
|
|
256
|
+
return new TextEncoder().encode(this.message);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
public getHeaders(): Record<string, string[]> {
|
|
260
|
+
return {
|
|
261
|
+
"Content-Type": ["text/plain; charset=utf-8"],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
public toDescriptiveJson(): Json {
|
|
266
|
+
return {
|
|
267
|
+
status_code: this.getStatusCode(),
|
|
268
|
+
message: this.message,
|
|
269
|
+
headers: this.getHeaders(),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* @deprecated this is not a good way to make status errors human-readable, use toDescriptiveJson instead
|
|
275
|
+
*/
|
|
276
|
+
public toHttpJson(): Json {
|
|
277
|
+
return {
|
|
278
|
+
status_code: this.statusCode,
|
|
279
|
+
body: this.message,
|
|
280
|
+
headers: this.getHeaders(),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
StatusError.prototype.name = "StatusError";
|