fix: when auth is disabled, the users entity doesn't exist

This commit is contained in:
dswbx
2024-11-21 08:23:16 +01:00
parent 4e7c1e6e9f
commit 0df5c761ec
14 changed files with 212 additions and 74 deletions

View File

@@ -0,0 +1,35 @@
import type { ComponentPropsWithoutRef } from "react";
import { twMerge } from "tailwind-merge";
export type AlertProps = ComponentPropsWithoutRef<"div"> & {
className?: string;
visible?: boolean;
title?: string;
message?: string;
};
const Base: React.FC<AlertProps> = ({ visible = true, title, message, className, ...props }) =>
visible ? (
<div
{...props}
className={twMerge("flex flex-row dark:bg-amber-300/20 bg-amber-200 p-4", className)}
>
<div>
{title && <b className="mr-2">{title}:</b>}
{message}
</div>
</div>
) : null;
const Warning: React.FC<AlertProps> = (props) => (
<Base {...props} className="dark:bg-amber-300/20 bg-amber-200" />
);
const Exception: React.FC<AlertProps> = (props) => (
<Base {...props} className="dark:bg-red-950 bg-red-100" />
);
export const Alert = {
Warning,
Exception
};

View File

@@ -1,6 +1,6 @@
import { Button } from "../buttons/Button"; import { Button } from "../buttons/Button";
type EmptyProps = { export type EmptyProps = {
Icon?: any; Icon?: any;
title?: string; title?: string;
description?: string; description?: string;

View File

@@ -0,0 +1,7 @@
import { Empty, type EmptyProps } from "./Empty";
const NotFound = (props: Partial<EmptyProps>) => <Empty title="Not Found" {...props} />;
export const Message = {
NotFound
};

View File

@@ -194,7 +194,7 @@ export const SidebarLink = <E extends React.ElementType = "a">({
"flex flex-row px-4 py-2.5 items-center gap-2", "flex flex-row px-4 py-2.5 items-center gap-2",
!disabled && !disabled &&
"cursor-pointer rounded-md [&.active]:bg-primary/10 [&.active]:hover:bg-primary/15 [&.active]:font-medium hover:bg-primary/5 link", "cursor-pointer rounded-md [&.active]:bg-primary/10 [&.active]:hover:bg-primary/15 [&.active]:font-medium hover:bg-primary/5 link",
disabled && "opacity-50 cursor-not-allowed", disabled && "opacity-50 cursor-not-allowed pointer-events-none",
className className
)} )}
> >

View File

@@ -33,16 +33,22 @@ export function AuthRoot({ children }) {
<AppShell.SidebarLink <AppShell.SidebarLink
as={Link} as={Link}
href={app.getAbsolutePath("/data/" + routes.data.entity.list(users_entity))} href={app.getAbsolutePath("/data/" + routes.data.entity.list(users_entity))}
disabled={!config.auth.enabled}
> >
Users Users
</AppShell.SidebarLink> </AppShell.SidebarLink>
<AppShell.SidebarLink as={Link} href={routes.auth.roles.list()}> <AppShell.SidebarLink
as={Link}
href={routes.auth.roles.list()}
disabled={!config.auth.enabled}
>
Roles & Permissions Roles & Permissions
</AppShell.SidebarLink> </AppShell.SidebarLink>
<AppShell.SidebarLink as={Link} href={routes.auth.strategies()} disabled> <AppShell.SidebarLink as={Link} href={routes.auth.strategies()} disabled>
Strategies Strategies
</AppShell.SidebarLink> </AppShell.SidebarLink>
<AppShell.SidebarLink as={Link} href={routes.auth.settings()}> {/*<AppShell.SidebarLink as={Link} href={routes.auth.settings()}>*/}
<AppShell.SidebarLink as={Link} href={app.getSettingsPath(["auth"])}>
Settings Settings
</AppShell.SidebarLink> </AppShell.SidebarLink>
</nav> </nav>

View File

@@ -1,4 +1,6 @@
import { useBknd, useClient } from "ui/client"; import { useBknd, useClient } from "ui/client";
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
import { Alert } from "ui/components/display/Alert";
import { routes } from "ui/lib/routes"; import { routes } from "ui/lib/routes";
import { import {
Button, Button,
@@ -10,15 +12,13 @@ import * as AppShell from "../../layouts/AppShell/AppShell";
export function AuthIndex() { export function AuthIndex() {
const client = useClient(); const client = useClient();
const { app, config } = useBknd(); const { app } = useBknd();
const users_entity = config.auth.entity_name; const {
config: { roles, strategies, entity_name, enabled }
} = useBkndAuth();
const users_entity = entity_name;
const query = client.query().data.entity("users").count(); const query = client.query().data.entity("users").count();
const usersTotal = query.data?.body.count ?? 0; const usersTotal = query.data?.body.count ?? 0;
const {
config: {
auth: { roles, strategies }
}
} = useBknd();
const rolesTotal = Object.keys(roles ?? {}).length ?? 0; const rolesTotal = Object.keys(roles ?? {}).length ?? 0;
const strategiesTotal = Object.keys(strategies ?? {}).length ?? 0; const strategiesTotal = Object.keys(strategies ?? {}).length ?? 0;
@@ -30,11 +30,16 @@ export function AuthIndex() {
<> <>
<AppShell.SectionHeader>Overview</AppShell.SectionHeader> <AppShell.SectionHeader>Overview</AppShell.SectionHeader>
<AppShell.Scrollable> <AppShell.Scrollable>
<Alert.Warning
visible={!enabled}
title="Auth not enabled"
message="To use authentication features, please enable it in the settings."
/>
<div className="flex flex-col flex-grow p-3 gap-3"> <div className="flex flex-col flex-grow p-3 gap-3">
<div className="grid xs:grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-5"> <div className="grid xs:grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-5">
<KpiCard <KpiCard
title="Users registered" title="Users registered"
value={usersTotal} value={!enabled ? 0 : usersTotal}
actions={[ actions={[
{ {
label: "View all", label: "View all",
@@ -45,7 +50,7 @@ export function AuthIndex() {
/> />
<KpiCard <KpiCard
title="Roles" title="Roles"
value={rolesTotal} value={!enabled ? 0 : rolesTotal}
actions={[ actions={[
{ label: "View all", href: rolesLink }, { label: "View all", href: rolesLink },
{ label: "Add new", variant: "default", href: rolesLink } { label: "Add new", variant: "default", href: rolesLink }
@@ -53,7 +58,7 @@ export function AuthIndex() {
/> />
<KpiCard <KpiCard
title="Strategies enabled" title="Strategies enabled"
value={strategiesTotal} value={!enabled ? 0 : strategiesTotal}
actions={[ actions={[
{ label: "View all", href: strategiesLink }, { label: "View all", href: strategiesLink },
{ label: "Add new", variant: "default", href: strategiesLink } { label: "Add new", variant: "default", href: strategiesLink }

View File

@@ -1,12 +1,7 @@
import { Modal, TextInput } from "@mantine/core";
import { useDisclosure, useFocusTrap } from "@mantine/hooks";
import { StringIdentifier, transformObject, ucFirstAllSnakeToPascalWithSpaces } from "core/utils"; import { StringIdentifier, transformObject, ucFirstAllSnakeToPascalWithSpaces } from "core/utils";
import { useRef } from "react";
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth"; import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
import { JsonSchemaForm } from "ui/components/form/json-schema/JsonSchemaForm"; import { Alert } from "ui/components/display/Alert";
import { bkndModals } from "ui/modals"; import { bkndModals } from "ui/modals";
import { SchemaFormModal } from "ui/modals/debug/SchemaFormModal";
import { useBknd } from "../../client/BkndProvider";
import { Button } from "../../components/buttons/Button"; import { Button } from "../../components/buttons/Button";
import { CellValue, DataTable } from "../../components/table/DataTable"; import { CellValue, DataTable } from "../../components/table/DataTable";
import * as AppShell from "../../layouts/AppShell/AppShell"; import * as AppShell from "../../layouts/AppShell/AppShell";
@@ -14,9 +9,6 @@ import { routes, useNavigate } from "../../lib/routes";
export function AuthRolesList() { export function AuthRolesList() {
const [navigate] = useNavigate(); const [navigate] = useNavigate();
const [modalOpen, modalHandler] = useDisclosure(false);
const focusRef = useFocusTrap();
const inputRef = useRef<HTMLInputElement>(null);
const { config, actions } = useBkndAuth(); const { config, actions } = useBkndAuth();
const data = Object.values( const data = Object.values(
@@ -64,27 +56,6 @@ export function AuthRolesList() {
return ( return (
<> <>
{/*<Modal
ref={focusRef}
opened={modalOpen}
onClose={modalHandler.close}
title={"New Role"}
classNames={{
root: "bknd-admin",
header: "!bg-primary/5 border-b border-b-muted !py-3 px-5 !h-auto !min-h-px",
content: "rounded-lg select-none",
title: "font-bold !text-md",
body: "pt-3 pb-3 px-3 gap-4 flex flex-col"
}}
>
<TextInput ref={inputRef} data-autofocus size="md" placeholder="Enter role name" />
<div className="flex flex-row justify-end gap-2">
<Button onClick={() => modalHandler.close()}>Cancel</Button>
<Button variant="primary" onClick={handleClickAdd}>
Create
</Button>
</div>
</Modal>*/}
<AppShell.SectionHeader <AppShell.SectionHeader
right={ right={
<Button variant="primary" onClick={openCreateModal}> <Button variant="primary" onClick={openCreateModal}>
@@ -95,6 +66,11 @@ export function AuthRolesList() {
Roles & Permissions Roles & Permissions
</AppShell.SectionHeader> </AppShell.SectionHeader>
<AppShell.Scrollable> <AppShell.Scrollable>
<Alert.Warning
visible={!config.enabled}
title="Auth not enabled"
message="To use authentication features, please enable it in the settings."
/>
<div className="flex flex-col flex-grow p-3 gap-3"> <div className="flex flex-col flex-grow p-3 gap-3">
<DataTable <DataTable
data={data} data={data}

View File

@@ -1,13 +1,28 @@
import { cloneDeep, omit } from "lodash-es"; import { cloneDeep, omit } from "lodash-es";
import { useEffect, useRef } from "react";
import { useBknd } from "ui/client"; import { useBknd } from "ui/client";
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
import { Button } from "ui/components/buttons/Button"; import { Button } from "ui/components/buttons/Button";
import { JsonSchemaForm } from "ui/components/form/json-schema/JsonSchemaForm"; import { Alert } from "ui/components/display/Alert";
import {
JsonSchemaForm,
type JsonSchemaFormRef
} from "ui/components/form/json-schema/JsonSchemaForm";
import * as AppShell from "ui/layouts/AppShell/AppShell"; import * as AppShell from "ui/layouts/AppShell/AppShell";
import { useNavigate } from "ui/lib/routes";
import { extractSchema } from "../settings/utils/schema"; import { extractSchema } from "../settings/utils/schema";
// @todo: improve the inline editing expierence, for now redirect to settings
export function AuthSettingsList() { export function AuthSettingsList() {
useBknd({ withSecrets: true }); const { app } = useBknd();
return <AuthSettingsListInternal />; const [navigate] = useNavigate();
useEffect(() => {
navigate(app.getSettingsPath(["auth"]));
}, []);
/*useBknd({ withSecrets: true });
return <AuthSettingsListInternal />;*/
} }
const uiSchema = { const uiSchema = {
@@ -21,9 +36,11 @@ const uiSchema = {
}; };
function AuthSettingsListInternal() { function AuthSettingsListInternal() {
const s = useBknd(); const $auth = useBkndAuth();
const config = s.config.auth; const { entities } = useBkndData();
const schema = cloneDeep(omit(s.schema.auth, ["title"])); const formRef = useRef<JsonSchemaFormRef>(null);
const config = $auth.config;
const schema = cloneDeep(omit($auth.schema, ["title"]));
const [generalSchema, generalConfig, extracted] = extractSchema(schema as any, config, [ const [generalSchema, generalConfig, extracted] = extractSchema(schema as any, config, [
"jwt", "jwt",
"roles", "roles",
@@ -32,7 +49,6 @@ function AuthSettingsListInternal() {
]); ]);
try { try {
const user_entity = config.entity_name ?? "users"; const user_entity = config.entity_name ?? "users";
const entities = s.config.data.entities ?? {};
const user_fields = Object.entries(entities[user_entity]?.fields ?? {}) const user_fields = Object.entries(entities[user_entity]?.fields ?? {})
.map(([name, field]) => (!field.config?.virtual ? name : undefined)) .map(([name, field]) => (!field.config?.virtual ? name : undefined))
.filter(Boolean); .filter(Boolean);
@@ -42,18 +58,34 @@ function AuthSettingsListInternal() {
extracted.jwt.schema.properties.fields.items.enum = user_fields; extracted.jwt.schema.properties.fields.items.enum = user_fields;
extracted.jwt.schema.properties.fields.uniqueItems = true; extracted.jwt.schema.properties.fields.uniqueItems = true;
uiSchema.jwt.fields["ui:widget"] = "checkboxes"; uiSchema.jwt.fields["ui:widget"] = "checkboxes";
} else {
uiSchema.jwt.fields["ui:widget"] = "hidden";
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
console.log({ generalSchema, generalConfig, extracted });
async function handleSubmit() {
console.log(formRef.current?.validateForm(), formRef.current?.formData());
}
return ( return (
<> <>
<AppShell.SectionHeader right={<Button variant="primary">Update</Button>}> <AppShell.SectionHeader
right={
<Button variant="primary" onClick={handleSubmit}>
Update
</Button>
}
>
Settings Settings
</AppShell.SectionHeader> </AppShell.SectionHeader>
<AppShell.Scrollable> <AppShell.Scrollable>
<Alert.Warning
visible={!config.enabled}
title="Auth not enabled"
message="Enable it by toggling the switch below. Please also make sure set a secure secret to sign JWT tokens."
/>
<div className="flex flex-col flex-grow px-5 py-4 gap-8"> <div className="flex flex-col flex-grow px-5 py-4 gap-8">
<div> <div>
<JsonSchemaForm <JsonSchemaForm
@@ -65,6 +97,7 @@ function AuthSettingsListInternal() {
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<h3 className="font-bold">JWT Settings</h3> <h3 className="font-bold">JWT Settings</h3>
<JsonSchemaForm <JsonSchemaForm
ref={formRef}
schema={extracted.jwt.schema} schema={extracted.jwt.schema}
uiSchema={uiSchema.jwt} uiSchema={uiSchema.jwt}
className="legacy hide-required-mark fieldset-alternative mute-root" className="legacy hide-required-mark fieldset-alternative mute-root"

View File

@@ -3,6 +3,7 @@ import type { Entity, EntityData } from "data";
import type { EntityRelation } from "data"; import type { EntityRelation } from "data";
import { Fragment, memo, useState } from "react"; import { Fragment, memo, useState } from "react";
import { TbArrowLeft, TbDots } from "react-icons/tb"; import { TbArrowLeft, TbDots } from "react-icons/tb";
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
import { EntityForm } from "ui/modules/data/components/EntityForm"; import { EntityForm } from "ui/modules/data/components/EntityForm";
import { EntityTable2 } from "ui/modules/data/components/EntityTable2"; import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm"; import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
@@ -20,17 +21,17 @@ import { routes, useNavigate } from "../../lib/routes";
import { bkndModals } from "../../modals"; import { bkndModals } from "../../modals";
export function DataEntityUpdate({ params }) { export function DataEntityUpdate({ params }) {
const { app } = useBknd(); const { $data, relations } = useBkndData();
const entity = app.entity(params.entity as string)!; const entity = $data.entity(params.entity as string)!;
const entityId = Number.parseInt(params.id as string); const entityId = Number.parseInt(params.id as string);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [navigate] = useNavigate(); const [navigate] = useNavigate();
useBrowserTitle(["Data", entity.label, `#${entityId}`]); useBrowserTitle(["Data", entity.label, `#${entityId}`]);
const targetRelations = app.relations.listableRelationsOf(entity); const targetRelations = relations.listableRelationsOf(entity);
console.log("targetRelations", targetRelations, app.relations.relationsOf(entity)); //console.log("targetRelations", targetRelations, relations.relationsOf(entity));
// filter out polymorphic for now // filter out polymorphic for now
//.filter((r) => r.type() !== "poly"); //.filter((r) => r.type() !== "poly");
const local_relation_refs = app.relations const local_relation_refs = relations
.sourceRelationsOf(entity) .sourceRelationsOf(entity)
?.map((r) => r.other(entity).reference); ?.map((r) => r.other(entity).reference);

View File

@@ -1,6 +1,9 @@
import { Type } from "core/utils"; import { Type } from "core/utils";
import { querySchema } from "data"; import { querySchema } from "data";
import { TbDots } from "react-icons/tb"; import { TbDots } from "react-icons/tb";
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
import { Empty } from "ui/components/display/Empty";
import { Message } from "ui/components/display/Message";
import { EntityTable2 } from "ui/modules/data/components/EntityTable2"; import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
import { useBknd } from "../../client"; import { useBknd } from "../../client";
import { Button } from "../../components/buttons/Button"; import { Button } from "../../components/buttons/Button";
@@ -25,22 +28,21 @@ const searchSchema = Type.Composite(
); );
export function DataEntityList({ params }) { export function DataEntityList({ params }) {
console.log("params", params); const { $data, relations } = useBkndData();
const { app } = useBknd(); const entity = $data.entity(params.entity as string);
const entity = app.entity(params.entity as string)!;
const [navigate] = useNavigate(); const [navigate] = useNavigate();
const search = useSearch(searchSchema, { const search = useSearch(searchSchema, {
select: entity.getSelect(undefined, "table"), select: entity?.getSelect(undefined, "table") ?? [],
sort: entity.getDefaultSort() sort: entity?.getDefaultSort()
}); });
console.log("search", search.value); console.log("search", search.value);
useBrowserTitle(["Data", entity.label]); useBrowserTitle(["Data", entity?.label ?? params.entity]);
const PER_PAGE_OPTIONS = [5, 10, 25]; const PER_PAGE_OPTIONS = [5, 10, 25];
//console.log("search", search.value); //console.log("search", search.value);
function handleClickRow(row: Record<string, any>) { function handleClickRow(row: Record<string, any>) {
navigate(routes.data.entity.edit(entity.name, row.id)); if (entity) navigate(routes.data.entity.edit(entity.name, row.id));
} }
function handleClickPage(page: number) { function handleClickPage(page: number) {
@@ -61,6 +63,10 @@ export function DataEntityList({ params }) {
search.set("perPage", perPage); search.set("perPage", perPage);
} }
if (!entity) {
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
}
return ( return (
<> <>
<AppShell.SectionHeader <AppShell.SectionHeader

View File

@@ -4,6 +4,7 @@ import { omit } from "lodash-es";
import { type ReactNode, useMemo, useRef, useState } from "react"; import { type ReactNode, useMemo, useRef, useState } from "react";
import { TbSettings } from "react-icons/tb"; import { TbSettings } from "react-icons/tb";
import { useAuth } from "ui"; import { useAuth } from "ui";
import { Alert } from "ui/components/display/Alert";
import { Link, Route, useLocation } from "wouter"; import { Link, Route, useLocation } from "wouter";
import { useBknd } from "../../../client/BkndProvider"; import { useBknd } from "../../../client/BkndProvider";
import { Button } from "../../../components/buttons/Button"; import { Button } from "../../../components/buttons/Button";
@@ -36,6 +37,7 @@ export type SettingProps<
allowDelete?: (config: any) => boolean; allowDelete?: (config: any) => boolean;
allowEdit?: (config: any) => boolean; allowEdit?: (config: any) => boolean;
showAlert?: (config: any) => string | ReactNode | undefined; showAlert?: (config: any) => string | ReactNode | undefined;
reloadOnSave?: boolean;
}; };
properties?: { properties?: {
[key in keyof Partial<Props>]: { [key in keyof Partial<Props>]: {
@@ -151,6 +153,9 @@ export function Setting<Schema extends TObject = any>({
console.log("save:success", success); console.log("save:success", success);
if (success) { if (success) {
if (options?.reloadOnSave) {
window.location.reload();
}
//window.location.reload(); //window.location.reload();
} else { } else {
setSubmitting(false); setSubmitting(false);
@@ -229,11 +234,7 @@ export function Setting<Schema extends TObject = any>({
<Breadcrumbs path={path} /> <Breadcrumbs path={path} />
</AppShell.SectionHeader> </AppShell.SectionHeader>
<AppShell.Scrollable key={path.join("-")}> <AppShell.Scrollable key={path.join("-")}>
{showAlert && ( {typeof showAlert === "string" && <Alert.Warning message={showAlert} />}
<div className="flex flex-row dark:bg-amber-300/20 bg-amber-200 p-4">
{showAlert}
</div>
)}
<div className="flex flex-col flex-grow p-3 gap-3"> <div className="flex flex-col flex-grow p-3 gap-3">
<JsonSchemaForm <JsonSchemaForm

View File

@@ -70,9 +70,11 @@ export const SettingNewModal = ({
const [module, ...restOfPath] = path; const [module, ...restOfPath] = path;
const addPath = [...restOfPath, newKey].join("."); const addPath = [...restOfPath, newKey].join(".");
if (await actions.add(module as any, addPath, data)) { if (await actions.add(module as any, addPath, data)) {
setTimeout(() => {
navigate(prefixPath + newKey, { navigate(prefixPath + newKey, {
replace: true replace: true
}); });
}, 500);
} else { } else {
setSubmitting(false); setSubmitting(false);
} }

View File

@@ -115,6 +115,15 @@ export const AuthSettings = ({ schema: _unsafe_copy, config }) => {
schema={_schema} schema={_schema}
uiSchema={uiSchema} uiSchema={uiSchema}
config={config} config={config}
options={{
showAlert: (config: any) => {
if (!config.enabled) {
return "Auth is disabled. Enable it by toggling the switch below. Please also make sure set a secure secret to sign JWT tokens.";
}
return;
},
reloadOnSave: true
}}
properties={{ properties={{
strategies: { strategies: {
extract: true, extract: true,

View File

@@ -1,9 +1,66 @@
import { readFile } from "node:fs/promises";
import { serveStatic } from "@hono/node-server/serve-static";
import { createClient } from "@libsql/client/node"; import { createClient } from "@libsql/client/node";
import { serveFresh } from "./src/adapter/vite"; import { App, type BkndConfig } from "./src";
import { LibsqlConnection } from "./src/data"; import { LibsqlConnection } from "./src/data";
import { StorageLocalAdapter } from "./src/media/storage/adapters/StorageLocalAdapter"; import { StorageLocalAdapter } from "./src/media/storage/adapters/StorageLocalAdapter";
import { registries } from "./src/modules/registries"; import { registries } from "./src/modules/registries";
async function getHtml() {
return readFile("index.html", "utf8");
}
function addViteScripts(html: string) {
return html.replace(
"<head>",
`<script type="module">
import RefreshRuntime from "/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
<script type="module" src="/@vite/client"></script>
`
);
}
function createApp(config: BkndConfig, env: any) {
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
return App.create(create_config);
}
function setAppBuildListener(app: App, config: BkndConfig, html: string) {
app.emgr.on(
"app-built",
async () => {
await config.onBuilt?.(app);
app.module.server.setAdminHtml(html);
app.module.server.client.get("/assets/!*", serveStatic({ root: "./" }));
},
"sync"
);
}
export async function serveFresh(config: BkndConfig, _html?: string) {
let html = _html;
if (!html) {
html = await getHtml();
}
html = addViteScripts(html);
return {
async fetch(request: Request, env: any) {
const app = createApp(config, env);
setAppBuildListener(app, config, html);
await app.build();
return app.fetch(request, env);
}
};
}
registries.media.add("local", { registries.media.add("local", {
cls: StorageLocalAdapter, cls: StorageLocalAdapter,
schema: StorageLocalAdapter.prototype.getSchema() schema: StorageLocalAdapter.prototype.getSchema()