@flaredev/cli 0.3.0 → 0.4.1
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/index.js +498 -220
- package/package.json +2 -2
- package/templates/app/app/dashboard/account/layout.tsx +30 -0
- package/templates/app/app/dashboard/account/page.tsx +11 -33
- package/templates/app/app/dashboard/account/password/page.tsx +22 -0
- package/templates/app/app/dashboard/account/security/page.tsx +34 -0
- package/templates/app/app/dashboard/account/sessions/page.tsx +9 -0
- package/templates/app/app/dashboard/actions.ts +73 -5
- package/templates/app/app/dashboard/layout.tsx +28 -4
- package/templates/app/app/dashboard/observability/page.tsx +4 -3
- package/templates/app/app/dashboard/page.tsx +2 -2
- package/templates/app/app/globals.css +0 -6
- package/templates/app/app/layout.tsx +37 -28
- package/templates/app/components/account/account-nav.tsx +39 -0
- package/templates/app/components/account/{security-settings.tsx → sections.tsx} +507 -522
- package/templates/app/components/dashboard/column-menu.tsx +66 -0
- package/templates/app/components/dashboard/dashboard-header.tsx +115 -78
- package/templates/app/components/dashboard/dashboard-sidebar.tsx +86 -24
- package/templates/app/components/dashboard/dropzone.tsx +92 -0
- package/templates/app/components/dashboard/editable.ts +12 -0
- package/templates/app/components/dashboard/fields/file-field.tsx +310 -302
- package/templates/app/components/dashboard/fields/relation-field.tsx +25 -3
- package/templates/app/components/dashboard/import-dialog.tsx +62 -9
- package/templates/app/components/dashboard/inline-cell.tsx +165 -0
- package/templates/app/components/dashboard/local-time.tsx +77 -0
- package/templates/app/components/dashboard/query.ts +2 -0
- package/templates/app/components/dashboard/record-actions.tsx +113 -0
- package/templates/app/components/dashboard/record-detail.tsx +203 -0
- package/templates/app/components/dashboard/resource-chart.tsx +91 -0
- package/templates/app/components/dashboard/resource-form-dialog.tsx +3 -3
- package/templates/app/components/dashboard/resource-form-page.tsx +64 -61
- package/templates/app/components/dashboard/resource-form.tsx +165 -165
- package/templates/app/components/dashboard/resource-stats.tsx +6 -11
- package/templates/app/components/dashboard/resource-table-toolbar.tsx +2 -2
- package/templates/app/components/dashboard/resource-table.tsx +408 -331
- package/templates/app/components/dashboard/row-actions.tsx +156 -144
- package/templates/app/components/dashboard/save-view-button.tsx +86 -0
- package/templates/app/components/dashboard/user-menu.tsx +91 -0
- package/templates/app/components/ui/sonner.tsx +33 -13
- package/templates/app/db/flare-schema.ts +22 -0
- package/templates/app/lib/api.ts +12 -0
- package/templates/app/lib/dashboard.ts +165 -138
- package/templates/app/lib/views.ts +29 -0
- package/templates/app/migrations/0000_init.sql +11 -0
- package/templates/app/migrations/meta/0000_snapshot.json +63 -1
- package/templates/app/vite.config.ts +41 -31
- package/templates/locks/package-lock.json +9 -9
- package/templates/locks/pnpm-lock.yaml +12 -12
- 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 {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (!
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
* the
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
.
|
|
109
|
-
.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
[
|
|
114
|
-
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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": "
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
}));
|
|
@@ -4973,9 +4973,9 @@
|
|
|
4973
4973
|
}
|
|
4974
4974
|
},
|
|
4975
4975
|
"node_modules/@vitejs/plugin-rsc/node_modules/magic-string": {
|
|
4976
|
-
"version": "1.4.
|
|
4977
|
-
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.
|
|
4978
|
-
"integrity": "sha512-
|
|
4976
|
+
"version": "1.4.2",
|
|
4977
|
+
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
|
|
4978
|
+
"integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==",
|
|
4979
4979
|
"devOptional": true,
|
|
4980
4980
|
"license": "MIT",
|
|
4981
4981
|
"dependencies": {
|
|
@@ -6253,9 +6253,9 @@
|
|
|
6253
6253
|
}
|
|
6254
6254
|
},
|
|
6255
6255
|
"node_modules/electron-to-chromium": {
|
|
6256
|
-
"version": "1.5.
|
|
6257
|
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.
|
|
6258
|
-
"integrity": "sha512-
|
|
6256
|
+
"version": "1.5.438",
|
|
6257
|
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.438.tgz",
|
|
6258
|
+
"integrity": "sha512-AN9xMU1hJiT65LkCUPL1DZm5TCbOPb2Qsm5pYwFLEXp6/qj9SdT4yMiqs7Qqstwvkn1zF7l36SRGt+s6XcJ0FA==",
|
|
6259
6259
|
"license": "ISC",
|
|
6260
6260
|
"peer": true
|
|
6261
6261
|
},
|
|
@@ -7124,9 +7124,9 @@
|
|
|
7124
7124
|
"license": "MIT"
|
|
7125
7125
|
},
|
|
7126
7126
|
"node_modules/node-releases": {
|
|
7127
|
-
"version": "2.0.
|
|
7128
|
-
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.
|
|
7129
|
-
"integrity": "sha512-
|
|
7127
|
+
"version": "2.0.57",
|
|
7128
|
+
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz",
|
|
7129
|
+
"integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==",
|
|
7130
7130
|
"license": "MIT",
|
|
7131
7131
|
"peer": true,
|
|
7132
7132
|
"engines": {
|
|
@@ -2640,8 +2640,8 @@ packages:
|
|
|
2640
2640
|
sqlite3:
|
|
2641
2641
|
optional: true
|
|
2642
2642
|
|
|
2643
|
-
electron-to-chromium@1.5.
|
|
2644
|
-
resolution: {integrity: sha512-
|
|
2643
|
+
electron-to-chromium@1.5.438:
|
|
2644
|
+
resolution: {integrity: sha512-AN9xMU1hJiT65LkCUPL1DZm5TCbOPb2Qsm5pYwFLEXp6/qj9SdT4yMiqs7Qqstwvkn1zF7l36SRGt+s6XcJ0FA==}
|
|
2645
2645
|
|
|
2646
2646
|
emoji-regex-xs@2.0.1:
|
|
2647
2647
|
resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==}
|
|
@@ -2950,8 +2950,8 @@ packages:
|
|
|
2950
2950
|
magic-string@0.30.21:
|
|
2951
2951
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
|
2952
2952
|
|
|
2953
|
-
magic-string@1.4.
|
|
2954
|
-
resolution: {integrity: sha512-
|
|
2953
|
+
magic-string@1.4.2:
|
|
2954
|
+
resolution: {integrity: sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==}
|
|
2955
2955
|
|
|
2956
2956
|
merge-stream@2.0.0:
|
|
2957
2957
|
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
|
@@ -3039,8 +3039,8 @@ packages:
|
|
|
3039
3039
|
neo-async@2.6.2:
|
|
3040
3040
|
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
|
3041
3041
|
|
|
3042
|
-
node-releases@2.0.
|
|
3043
|
-
resolution: {integrity: sha512-
|
|
3042
|
+
node-releases@2.0.57:
|
|
3043
|
+
resolution: {integrity: sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==}
|
|
3044
3044
|
engines: {node: '>=18'}
|
|
3045
3045
|
|
|
3046
3046
|
obug@3.0.0:
|
|
@@ -5214,7 +5214,7 @@ snapshots:
|
|
|
5214
5214
|
'@rolldown/pluginutils': 1.0.1
|
|
5215
5215
|
es-module-lexer: 2.3.2
|
|
5216
5216
|
estree-walker: 3.0.3
|
|
5217
|
-
magic-string: 1.4.
|
|
5217
|
+
magic-string: 1.4.2
|
|
5218
5218
|
obug: 3.0.0
|
|
5219
5219
|
react: 19.3.0
|
|
5220
5220
|
react-dom: 19.3.0(react@19.3.0)
|
|
@@ -5388,8 +5388,8 @@ 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.
|
|
5392
|
-
node-releases: 2.0.
|
|
5391
|
+
electron-to-chromium: 1.5.438
|
|
5392
|
+
node-releases: 2.0.57
|
|
5393
5393
|
update-browserslist-db: 1.3.3(browserslist@4.29.0)
|
|
5394
5394
|
|
|
5395
5395
|
buffer-from@1.1.2: {}
|
|
@@ -5459,7 +5459,7 @@ snapshots:
|
|
|
5459
5459
|
optionalDependencies:
|
|
5460
5460
|
kysely: 0.29.6
|
|
5461
5461
|
|
|
5462
|
-
electron-to-chromium@1.5.
|
|
5462
|
+
electron-to-chromium@1.5.438: {}
|
|
5463
5463
|
|
|
5464
5464
|
emoji-regex-xs@2.0.1: {}
|
|
5465
5465
|
|
|
@@ -5780,7 +5780,7 @@ snapshots:
|
|
|
5780
5780
|
dependencies:
|
|
5781
5781
|
'@jridgewell/sourcemap-codec': 1.6.0
|
|
5782
5782
|
|
|
5783
|
-
magic-string@1.4.
|
|
5783
|
+
magic-string@1.4.2:
|
|
5784
5784
|
dependencies:
|
|
5785
5785
|
'@jridgewell/sourcemap-codec': 1.6.0
|
|
5786
5786
|
|
|
@@ -5827,7 +5827,7 @@ snapshots:
|
|
|
5827
5827
|
|
|
5828
5828
|
neo-async@2.6.2: {}
|
|
5829
5829
|
|
|
5830
|
-
node-releases@2.0.
|
|
5830
|
+
node-releases@2.0.57: {}
|
|
5831
5831
|
|
|
5832
5832
|
obug@3.0.0: {}
|
|
5833
5833
|
|