@flaredev/cli 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/index.js +385 -209
  2. package/package.json +2 -2
  3. package/templates/app/app/dashboard/account/layout.tsx +30 -0
  4. package/templates/app/app/dashboard/account/page.tsx +11 -33
  5. package/templates/app/app/dashboard/account/password/page.tsx +22 -0
  6. package/templates/app/app/dashboard/account/security/page.tsx +34 -0
  7. package/templates/app/app/dashboard/account/sessions/page.tsx +9 -0
  8. package/templates/app/app/dashboard/actions.ts +35 -0
  9. package/templates/app/app/dashboard/layout.tsx +28 -4
  10. package/templates/app/app/dashboard/observability/page.tsx +4 -3
  11. package/templates/app/app/dashboard/page.tsx +2 -2
  12. package/templates/app/components/account/account-nav.tsx +39 -0
  13. package/templates/app/components/account/{security-settings.tsx → sections.tsx} +507 -522
  14. package/templates/app/components/dashboard/column-menu.tsx +66 -0
  15. package/templates/app/components/dashboard/dashboard-header.tsx +115 -78
  16. package/templates/app/components/dashboard/dashboard-sidebar.tsx +86 -24
  17. package/templates/app/components/dashboard/dropzone.tsx +92 -0
  18. package/templates/app/components/dashboard/editable.ts +12 -0
  19. package/templates/app/components/dashboard/fields/file-field.tsx +306 -302
  20. package/templates/app/components/dashboard/import-dialog.tsx +60 -7
  21. package/templates/app/components/dashboard/inline-cell.tsx +165 -0
  22. package/templates/app/components/dashboard/local-time.tsx +77 -0
  23. package/templates/app/components/dashboard/query.ts +2 -0
  24. package/templates/app/components/dashboard/record-actions.tsx +113 -0
  25. package/templates/app/components/dashboard/record-detail.tsx +200 -0
  26. package/templates/app/components/dashboard/resource-chart.tsx +91 -0
  27. package/templates/app/components/dashboard/resource-stats.tsx +6 -11
  28. package/templates/app/components/dashboard/resource-table.tsx +405 -331
  29. package/templates/app/components/dashboard/row-actions.tsx +156 -144
  30. package/templates/app/components/dashboard/save-view-button.tsx +86 -0
  31. package/templates/app/components/dashboard/user-menu.tsx +91 -0
  32. package/templates/app/components/ui/sonner.tsx +33 -13
  33. package/templates/app/db/flare-schema.ts +22 -0
  34. package/templates/app/lib/api.ts +12 -0
  35. package/templates/app/lib/dashboard.ts +165 -138
  36. package/templates/app/lib/views.ts +29 -0
  37. package/templates/app/migrations/0000_init.sql +11 -0
  38. package/templates/app/migrations/meta/0000_snapshot.json +63 -1
  39. package/templates/app/vite.config.ts +41 -31
  40. package/templates/locks/package-lock.json +3 -3
  41. package/templates/locks/pnpm-lock.yaml +4 -4
  42. package/templates/locks/yarn.lock +3548 -0
@@ -1,138 +1,165 @@
1
- import { headers } from "next/headers";
2
- import { notFound, redirect } from "next/navigation";
3
- import { allowedActions, can, type Policy, type PolicyAction, type Resource } from "@flaredev/core";
4
- import { and, count, gte, lt } from "drizzle-orm";
5
- import type { SQLiteTable } from "drizzle-orm/sqlite-core";
6
- import { createResourceStore, type ResourceStore } from "@flaredev/core/server";
7
- import { getDb } from "@/db";
8
- import { policies } from "@/policies";
9
- import { resourceTables } from "@/resources/server";
10
- import { auth } from "./auth";
11
- import { cached, resourceTag, revalidateResource, TTL } from "./cache";
12
-
13
- /**
14
- * Roles that always reach the dashboard, whatever the policies say. Other roles get in when a
15
- * policy grants them read access to at least one resource.
16
- * Register roles with `flare role:add`, assign with `flare user:role`.
17
- */
18
- export const ADMIN_ROLES = ["admin", "staff"];
19
-
20
- export function policyFor(resourceName: string): Policy | undefined {
21
- return (policies as Record<string, Policy | undefined>)[resourceName];
22
- }
23
-
24
- export async function dashboardSession() {
25
- const session = await auth.api.getSession({ headers: await headers() });
26
- if (!session) return { session: null, role: null, allowed: false } as const;
27
- const role = (session.user as { role?: string | null }).role ?? null;
28
- const readable = allResources().some((resource) => can(policyFor(resource.name), role, "read"));
29
- return { session, role, allowed: (role !== null && ADMIN_ROLES.includes(role)) || readable } as const;
30
- }
31
-
32
- /** For admin pages: redirects signed-out visitors to sign-in and users without access to the home page. */
33
- export async function requireDashboard(returnTo = "/dashboard") {
34
- const { session, role, allowed } = await dashboardSession();
35
- if (!session) redirect(`/sign-in?next=${encodeURIComponent(returnTo)}`);
36
- if (!allowed) redirect("/?error=forbidden");
37
- return { session, role };
38
- }
39
-
40
- /**
41
- * For resource pages: also requires the policy to allow `action` on this resource.
42
- * Sends the user somewhere they can actually go — the list when they may read it,
43
- * the dashboard when they may not.
44
- */
45
- export async function requireAccess(resource: Resource, action: PolicyAction) {
46
- const { session, role } = await requireDashboard(resourcePath(resource));
47
- const policy = policyFor(resource.name);
48
- if (!can(policy, role, action)) {
49
- redirect(action !== "read" && can(policy, role, "read") ? resourcePath(resource) : "/dashboard?error=forbidden");
50
- }
51
- return { session, role };
52
- }
53
-
54
- /** What the current user may do with a resource, for hiding actions they can't use. */
55
- export async function adminPermissions(resourceName: string) {
56
- const { role } = await dashboardSession();
57
- return allowedActions(policyFor(resourceName), role);
58
- }
59
-
60
- const stores = new Map<string, ResourceStore>();
61
-
62
- /** CRUD store for a resource by name (404 for unknown names). */
63
- export function dashboardStore(name: string): ResourceStore {
64
- const entry = (resourceTables as unknown as Record<string, { resource: Resource; table: SQLiteTable } | undefined>)[name];
65
- if (!entry) notFound();
66
- let store = stores.get(name);
67
- if (!store) {
68
- store = createResourceStore({ resource: entry.resource, table: entry.table, getDb, onChange: revalidateResource });
69
- stores.set(name, store);
70
- }
71
- return store;
72
- }
73
-
74
- /**
75
- * How many records a resource has. Cached in the data cache under the resource's tag,
76
- * so the dashboard does not count every table on every visit, and a write through the
77
- * admin or the API drops the count on its way out.
78
- */
79
- export function recordCount(name: string): Promise<number> {
80
- const count = cached(
81
- async (resourceName: string) => {
82
- const result = await dashboardStore(resourceName).list(new URLSearchParams({ perPage: "1" }));
83
- return result.ok ? result.data.meta.total : 0;
84
- },
85
- ["flare", "record-count"],
86
- { tags: [resourceTag(name)], revalidate: TTL.short },
87
- );
88
- return count(name);
89
- }
90
-
91
- /**
92
- * How many records a resource gained in the last `days` days, and in the `days` before
93
- * that, so a stat can show which way it's going. Cached like the totals.
94
- */
95
- export function recentCounts(name: string, days = 7): Promise<{ current: number; previous: number }> {
96
- const counts = cached(
97
- async (resourceName: string, windowDays: number) => {
98
- const entry = (resourceTables as unknown as Record<string, { table: SQLiteTable } | undefined>)[resourceName];
99
- const createdAt = entry ? (entry.table as unknown as Record<string, unknown>).createdAt : undefined;
100
- // A resource whose table has no createdAt (a custom one) simply has no trend.
101
- if (!entry || !createdAt) return { current: 0, previous: 0 };
102
- const window = windowDays * 86_400_000;
103
- const since = new Date(Date.now() - window);
104
- const before = new Date(Date.now() - window * 2);
105
- const column = createdAt as never;
106
- const [current] = await getDb().select({ total: count() }).from(entry.table).where(gte(column, since));
107
- const [previous] = await getDb()
108
- .select({ total: count() })
109
- .from(entry.table)
110
- .where(and(gte(column, before), lt(column, since)));
111
- return { current: current?.total ?? 0, previous: previous?.total ?? 0 };
112
- },
113
- ["flare", "recent-count"],
114
- { tags: [resourceTag(name)], revalidate: TTL.short },
115
- );
116
- return counts(name, days);
117
- }
118
-
119
- /** Percentage change between two periods, or undefined when there's nothing to compare. */
120
- export function trend(current: number, previous: number): number | undefined {
121
- if (previous === 0) return current === 0 ? undefined : 100;
122
- return ((current - previous) / previous) * 100;
123
- }
124
-
125
- /** Every resource descriptor (unfiltered): navigation uses `visibleResources()` instead. */
126
- export function allResources(): Resource[] {
127
- return Object.values(resourceTables as unknown as Record<string, { resource: Resource }>).map((entry) => entry.resource);
128
- }
129
-
130
- /** Resources the current user may read, for the sidebar and dashboard. */
131
- export async function visibleResources(): Promise<Resource[]> {
132
- const { role } = await dashboardSession();
133
- return allResources().filter((resource) => can(policyFor(resource.name), role, "read"));
134
- }
135
-
136
- export function resourcePath(resource: Resource, ...parts: string[]) {
137
- return ["/dashboard", resource.slug, ...parts].join("/");
138
- }
1
+ import { headers } from "next/headers";
2
+ import { notFound, redirect } from "next/navigation";
3
+ import { allowedActions, can, type Policy, type PolicyAction, type Resource } from "@flaredev/core";
4
+ import { and, count, gte, lt } from "drizzle-orm";
5
+ import type { SQLiteTable } from "drizzle-orm/sqlite-core";
6
+ import { createResourceStore, type ResourceStore } from "@flaredev/core/server";
7
+ import { getDb } from "@/db";
8
+ import { policies } from "@/policies";
9
+ import { resourceTables } from "@/resources/server";
10
+ import { currentUser } from "./api";
11
+ import { auth } from "./auth";
12
+ import { cached, resourceTag, revalidateResource, TTL } from "./cache";
13
+
14
+ /**
15
+ * Roles that always reach the dashboard, whatever the policies say. Other roles get in when a
16
+ * policy grants them read access to at least one resource.
17
+ * Register roles with `flare role:add`, assign with `flare user:role`.
18
+ */
19
+ export const ADMIN_ROLES = ["admin", "staff"];
20
+
21
+ export function policyFor(resourceName: string): Policy | undefined {
22
+ return (policies as Record<string, Policy | undefined>)[resourceName];
23
+ }
24
+
25
+ export async function dashboardSession() {
26
+ const session = await auth.api.getSession({ headers: await headers() });
27
+ if (!session) return { session: null, role: null, allowed: false } as const;
28
+ const role = (session.user as { role?: string | null }).role ?? null;
29
+ const readable = allResources().some((resource) => can(policyFor(resource.name), role, "read"));
30
+ return { session, role, allowed: (role !== null && ADMIN_ROLES.includes(role)) || readable } as const;
31
+ }
32
+
33
+ /** For admin pages: redirects signed-out visitors to sign-in and users without access to the home page. */
34
+ export async function requireDashboard(returnTo = "/dashboard") {
35
+ const { session, role, allowed } = await dashboardSession();
36
+ if (!session) redirect(`/sign-in?next=${encodeURIComponent(returnTo)}`);
37
+ if (!allowed) redirect("/?error=forbidden");
38
+ return { session, role };
39
+ }
40
+
41
+ /**
42
+ * For resource pages: also requires the policy to allow `action` on this resource.
43
+ * Sends the user somewhere they can actually go — the list when they may read it,
44
+ * the dashboard when they may not.
45
+ */
46
+ export async function requireAccess(resource: Resource, action: PolicyAction) {
47
+ const { session, role } = await requireDashboard(resourcePath(resource));
48
+ const policy = policyFor(resource.name);
49
+ if (!can(policy, role, action)) {
50
+ redirect(action !== "read" && can(policy, role, "read") ? resourcePath(resource) : "/dashboard?error=forbidden");
51
+ }
52
+ return { session, role };
53
+ }
54
+
55
+ /** What the current user may do with a resource, for hiding actions they can't use. */
56
+ export async function adminPermissions(resourceName: string) {
57
+ const { role } = await dashboardSession();
58
+ return allowedActions(policyFor(resourceName), role);
59
+ }
60
+
61
+ const stores = new Map<string, ResourceStore>();
62
+
63
+ /** CRUD store for a resource by name (404 for unknown names). */
64
+ export function dashboardStore(name: string): ResourceStore {
65
+ const entry = (resourceTables as unknown as Record<string, { resource: Resource; table: SQLiteTable } | undefined>)[name];
66
+ if (!entry) notFound();
67
+ let store = stores.get(name);
68
+ if (!store) {
69
+ store = createResourceStore({ resource: entry.resource, table: entry.table, getDb, onChange: revalidateResource, currentUser });
70
+ stores.set(name, store);
71
+ }
72
+ return store;
73
+ }
74
+
75
+ /**
76
+ * How many records a resource has. Cached in the data cache under the resource's tag,
77
+ * so the dashboard does not count every table on every visit, and a write through the
78
+ * admin or the API drops the count on its way out.
79
+ */
80
+ export function recordCount(name: string): Promise<number> {
81
+ const counted = cached(
82
+ async (resourceName: string) => {
83
+ // Counted straight off the table, not through the store: a list stops counting at
84
+ // 10,000 so that paging stays quick, and a stat wants the real number.
85
+ const entry = (resourceTables as unknown as Record<string, { table: SQLiteTable } | undefined>)[resourceName];
86
+ if (!entry) return 0;
87
+ const [row] = await getDb().select({ total: count() }).from(entry.table);
88
+ return row?.total ?? 0;
89
+ },
90
+ ["flare", "record-count"],
91
+ { tags: [resourceTag(name)], revalidate: TTL.short },
92
+ );
93
+ return counted(name);
94
+ }
95
+
96
+ /**
97
+ * How many records hold each value of a field, in one grouped query rather than one
98
+ * count per option. Used by the stats above a resource's table.
99
+ */
100
+ export function valueCounts(name: string, field: string): Promise<Record<string, number>> {
101
+ const counted = cached(
102
+ async (resourceName: string, key: string) => {
103
+ const entry = (resourceTables as unknown as Record<string, { table: SQLiteTable } | undefined>)[resourceName];
104
+ const column = entry ? (entry.table as unknown as Record<string, unknown>)[key] : undefined;
105
+ if (!entry || !column) return {};
106
+ const rows = (await getDb()
107
+ .select({ value: column as never, total: count() })
108
+ .from(entry.table)
109
+ .groupBy(column as never)) as { value: unknown; total: number }[];
110
+ return Object.fromEntries(rows.map((row) => [String(row.value ?? ""), row.total]));
111
+ },
112
+ ["flare", "value-counts"],
113
+ { tags: [resourceTag(name)], revalidate: TTL.short },
114
+ );
115
+ return counted(name, field);
116
+ }
117
+
118
+ /**
119
+ * How many records a resource gained in the last `days` days, and in the `days` before
120
+ * that, so a stat can show which way it's going. Cached like the totals.
121
+ */
122
+ export function recentCounts(name: string, days = 7): Promise<{ current: number; previous: number }> {
123
+ const counts = cached(
124
+ async (resourceName: string, windowDays: number) => {
125
+ const entry = (resourceTables as unknown as Record<string, { table: SQLiteTable } | undefined>)[resourceName];
126
+ const createdAt = entry ? (entry.table as unknown as Record<string, unknown>).createdAt : undefined;
127
+ // A resource whose table has no createdAt (a custom one) simply has no trend.
128
+ if (!entry || !createdAt) return { current: 0, previous: 0 };
129
+ const window = windowDays * 86_400_000;
130
+ const since = new Date(Date.now() - window);
131
+ const before = new Date(Date.now() - window * 2);
132
+ const column = createdAt as never;
133
+ const [current] = await getDb().select({ total: count() }).from(entry.table).where(gte(column, since));
134
+ const [previous] = await getDb()
135
+ .select({ total: count() })
136
+ .from(entry.table)
137
+ .where(and(gte(column, before), lt(column, since)));
138
+ return { current: current?.total ?? 0, previous: previous?.total ?? 0 };
139
+ },
140
+ ["flare", "recent-count"],
141
+ { tags: [resourceTag(name)], revalidate: TTL.short },
142
+ );
143
+ return counts(name, days);
144
+ }
145
+
146
+ /** Percentage change between two periods, or undefined when there's nothing to compare. */
147
+ export function trend(current: number, previous: number): number | undefined {
148
+ if (previous === 0) return current === 0 ? undefined : 100;
149
+ return ((current - previous) / previous) * 100;
150
+ }
151
+
152
+ /** Every resource descriptor (unfiltered): navigation uses `visibleResources()` instead. */
153
+ export function allResources(): Resource[] {
154
+ return Object.values(resourceTables as unknown as Record<string, { resource: Resource }>).map((entry) => entry.resource);
155
+ }
156
+
157
+ /** Resources the current user may read, for the sidebar and dashboard. */
158
+ export async function visibleResources(): Promise<Resource[]> {
159
+ const { role } = await dashboardSession();
160
+ return allResources().filter((resource) => can(policyFor(resource.name), role, "read"));
161
+ }
162
+
163
+ export function resourcePath(resource: Resource, ...parts: string[]) {
164
+ return ["/dashboard", resource.slug, ...parts].join("/");
165
+ }
@@ -0,0 +1,29 @@
1
+ import { and, asc, eq } from "drizzle-orm";
2
+ import { getDb } from "@/db";
3
+ import { savedView } from "@/db/flare-schema";
4
+ import { dashboardSession } from "./dashboard";
5
+
6
+ export interface SavedView {
7
+ id: string;
8
+ resource: string;
9
+ name: string;
10
+ query: string;
11
+ }
12
+
13
+ /** How many views one person may keep per resource, so the sidebar stays a sidebar. */
14
+ export const MAX_VIEWS = 12;
15
+
16
+ /** This person's saved views, oldest first so the sidebar doesn't reshuffle. */
17
+ export async function listViews(resource?: string): Promise<SavedView[]> {
18
+ const { session } = await dashboardSession();
19
+ if (!session) return [];
20
+ const where = resource
21
+ ? and(eq(savedView.userId, session.user.id), eq(savedView.resource, resource))
22
+ : eq(savedView.userId, session.user.id);
23
+ const rows = await getDb()
24
+ .select({ id: savedView.id, resource: savedView.resource, name: savedView.name, query: savedView.query })
25
+ .from(savedView)
26
+ .where(where)
27
+ .orderBy(asc(savedView.createdAt));
28
+ return rows;
29
+ }
@@ -107,3 +107,14 @@ CREATE TABLE `role` (
107
107
  `label` text NOT NULL,
108
108
  `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
109
109
  );
110
+ --> statement-breakpoint
111
+ CREATE TABLE `saved_view` (
112
+ `id` text PRIMARY KEY NOT NULL,
113
+ `user_id` text NOT NULL,
114
+ `resource` text NOT NULL,
115
+ `name` text NOT NULL,
116
+ `query` text NOT NULL,
117
+ `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
118
+ );
119
+ --> statement-breakpoint
120
+ CREATE INDEX `saved_view_user_idx` ON `saved_view` (`user_id`,`resource`);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "6",
3
3
  "dialect": "sqlite",
4
- "id": "b98aab68-42f4-4832-ac5d-de8a121d8eae",
4
+ "id": "06909070-5897-4787-87c1-4e86bf5f98d3",
5
5
  "prevId": "00000000-0000-0000-0000-000000000000",
6
6
  "tables": {
7
7
  "account": {
@@ -733,6 +733,68 @@
733
733
  "compositePrimaryKeys": {},
734
734
  "uniqueConstraints": {},
735
735
  "checkConstraints": {}
736
+ },
737
+ "saved_view": {
738
+ "name": "saved_view",
739
+ "columns": {
740
+ "id": {
741
+ "name": "id",
742
+ "type": "text",
743
+ "primaryKey": true,
744
+ "notNull": true,
745
+ "autoincrement": false
746
+ },
747
+ "user_id": {
748
+ "name": "user_id",
749
+ "type": "text",
750
+ "primaryKey": false,
751
+ "notNull": true,
752
+ "autoincrement": false
753
+ },
754
+ "resource": {
755
+ "name": "resource",
756
+ "type": "text",
757
+ "primaryKey": false,
758
+ "notNull": true,
759
+ "autoincrement": false
760
+ },
761
+ "name": {
762
+ "name": "name",
763
+ "type": "text",
764
+ "primaryKey": false,
765
+ "notNull": true,
766
+ "autoincrement": false
767
+ },
768
+ "query": {
769
+ "name": "query",
770
+ "type": "text",
771
+ "primaryKey": false,
772
+ "notNull": true,
773
+ "autoincrement": false
774
+ },
775
+ "created_at": {
776
+ "name": "created_at",
777
+ "type": "integer",
778
+ "primaryKey": false,
779
+ "notNull": true,
780
+ "autoincrement": false,
781
+ "default": "(cast(unixepoch('subsecond') * 1000 as integer))"
782
+ }
783
+ },
784
+ "indexes": {
785
+ "saved_view_user_idx": {
786
+ "name": "saved_view_user_idx",
787
+ "columns": [
788
+ "user_id",
789
+ "resource"
790
+ ],
791
+ "isUnique": false
792
+ }
793
+ },
794
+ "foreignKeys": {},
795
+ "compositePrimaryKeys": {},
796
+ "uniqueConstraints": {},
797
+ "checkConstraints": {}
736
798
  }
737
799
  },
738
800
  "views": {},
@@ -1,31 +1,41 @@
1
- import { defineConfig, loadEnv } from "vite";
2
- import vinext from "vinext";
3
- import { cloudflare } from "@cloudflare/vite-plugin";
4
- import { kvDataAdapter } from "@vinext/cloudflare/cache/kv-data-adapter";
5
-
6
- export default defineConfig(({ mode }) => ({
7
- // FLARE_THEME (shell or .env) picks the theme for this build; see lib/theme.ts.
8
- define: { __FLARE_THEME__: JSON.stringify(loadEnv(mode, process.cwd(), "FLARE_").FLARE_THEME ?? "") },
9
- // `flare dev --tunnel` serves the dev server on a https://*.trycloudflare.com URL.
10
- server: { allowedHosts: [".trycloudflare.com"] },
11
- plugins: [
12
- vinext({
13
- cache: {
14
- // Data cache (`cached()`, fetch, "use cache") in Workers KV, shared by every colo.
15
- // Entries live a day unless a tag revalidates them first; a colo may reuse its own
16
- // copy for a minute after a write elsewhere.
17
- data: kvDataAdapter({ ttlSeconds: 86_400, entryCacheTtlSeconds: 60 }),
18
- // No `cdn` adapter yet. vinext's Workers Cache adapter (cdnAdapter) is experimental
19
- // in the 1.0 beta: behind it, any page that redirects fails with "Too many
20
- // redirects", and nothing is cached without an experimental two-stage deploy.
21
- // See "Caching" in the Flare docs before turning it on.
22
- },
23
- }),
24
- cloudflare({
25
- viteEnvironment: {
26
- name: "rsc",
27
- childEnvironments: ["ssr"],
28
- },
29
- }),
30
- ],
31
- }));
1
+ import { defineConfig, loadEnv } from "vite";
2
+ import vinext from "vinext";
3
+ import { cloudflare } from "@cloudflare/vite-plugin";
4
+ import { kvDataAdapter } from "@vinext/cloudflare/cache/kv-data-adapter";
5
+
6
+ export default defineConfig(({ mode }) => ({
7
+ // FLARE_THEME (shell or .env) picks the theme for this build; see lib/theme.ts.
8
+ define: { __FLARE_THEME__: JSON.stringify(loadEnv(mode, process.cwd(), "FLARE_").FLARE_THEME ?? "") },
9
+ // `flare dev --tunnel` serves the dev server on a https://*.trycloudflare.com URL.
10
+ server: { allowedHosts: [".trycloudflare.com"] },
11
+ optimizeDeps: {
12
+ /*
13
+ * Packages that ship their own "use client" components are left out of dependency
14
+ * pre-bundling. Bundled, the browser and the server each end up with their own copy
15
+ * of the same module, and a client-side navigation lands on the wrong one — which is
16
+ * the "Cannot read properties of null (reading 'useSyncExternalStore')" you get once
17
+ * and never again after a reload.
18
+ */
19
+ exclude: ["lucide-react", "vinext", "radix-ui", "@radix-ui/react-slot", "sonner", "cmdk", "react-day-picker", "@flaredev/core"],
20
+ },
21
+ plugins: [
22
+ vinext({
23
+ cache: {
24
+ // Data cache (`cached()`, fetch, "use cache") in Workers KV, shared by every colo.
25
+ // Entries live a day unless a tag revalidates them first; a colo may reuse its own
26
+ // copy for a minute after a write elsewhere.
27
+ data: kvDataAdapter({ ttlSeconds: 86_400, entryCacheTtlSeconds: 60 }),
28
+ // No `cdn` adapter yet. vinext's Workers Cache adapter (cdnAdapter) is experimental
29
+ // in the 1.0 beta: behind it, any page that redirects fails with "Too many
30
+ // redirects", and nothing is cached without an experimental two-stage deploy.
31
+ // See "Caching" in the Flare docs before turning it on.
32
+ },
33
+ }),
34
+ cloudflare({
35
+ viteEnvironment: {
36
+ name: "rsc",
37
+ childEnvironments: ["ssr"],
38
+ },
39
+ }),
40
+ ],
41
+ }));
@@ -6253,9 +6253,9 @@
6253
6253
  }
6254
6254
  },
6255
6255
  "node_modules/electron-to-chromium": {
6256
- "version": "1.5.436",
6257
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.436.tgz",
6258
- "integrity": "sha512-iG79/xCF3iThADicbgbES2+liFYmTvelo7jWcEfg6lQ+Adrh7zShrXSg2ZdEDLbKjJ298kGAKMnnzVWobp0L+g==",
6256
+ "version": "1.5.437",
6257
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.437.tgz",
6258
+ "integrity": "sha512-1z60R0Nb+kYPnGUrQ6PvMjTVINDGMfqQZ/mI5FJIHvTotlA6noI+YJWakbjaFM+qhLXg9WnK3RUeYSO5ixSHJA==",
6259
6259
  "license": "ISC",
6260
6260
  "peer": true
6261
6261
  },
@@ -2640,8 +2640,8 @@ packages:
2640
2640
  sqlite3:
2641
2641
  optional: true
2642
2642
 
2643
- electron-to-chromium@1.5.436:
2644
- resolution: {integrity: sha512-iG79/xCF3iThADicbgbES2+liFYmTvelo7jWcEfg6lQ+Adrh7zShrXSg2ZdEDLbKjJ298kGAKMnnzVWobp0L+g==}
2643
+ electron-to-chromium@1.5.437:
2644
+ resolution: {integrity: sha512-1z60R0Nb+kYPnGUrQ6PvMjTVINDGMfqQZ/mI5FJIHvTotlA6noI+YJWakbjaFM+qhLXg9WnK3RUeYSO5ixSHJA==}
2645
2645
 
2646
2646
  emoji-regex-xs@2.0.1:
2647
2647
  resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==}
@@ -5388,7 +5388,7 @@ snapshots:
5388
5388
  dependencies:
5389
5389
  baseline-browser-mapping: 2.11.25
5390
5390
  caniuse-lite: 1.0.30001810
5391
- electron-to-chromium: 1.5.436
5391
+ electron-to-chromium: 1.5.437
5392
5392
  node-releases: 2.0.56
5393
5393
  update-browserslist-db: 1.3.3(browserslist@4.29.0)
5394
5394
 
@@ -5459,7 +5459,7 @@ snapshots:
5459
5459
  optionalDependencies:
5460
5460
  kysely: 0.29.6
5461
5461
 
5462
- electron-to-chromium@1.5.436: {}
5462
+ electron-to-chromium@1.5.437: {}
5463
5463
 
5464
5464
  emoji-regex-xs@2.0.1: {}
5465
5465