mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 04:46:05 +00:00
using vite for static bundling to solve external package's react resolution
This commit is contained in:
95
app/build.ts
95
app/build.ts
@@ -1,8 +1,5 @@
|
|||||||
import { $ } from "bun";
|
import { $ } from "bun";
|
||||||
import * as esbuild from "esbuild";
|
|
||||||
import postcss from "esbuild-postcss";
|
|
||||||
import * as tsup from "tsup";
|
import * as tsup from "tsup";
|
||||||
import { guessMimeType } from "./src/media/storage/mime-types";
|
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const watch = args.includes("--watch");
|
const watch = args.includes("--watch");
|
||||||
@@ -11,12 +8,9 @@ const types = args.includes("--types");
|
|||||||
const sourcemap = args.includes("--sourcemap");
|
const sourcemap = args.includes("--sourcemap");
|
||||||
const clean = args.includes("--clean");
|
const clean = args.includes("--clean");
|
||||||
|
|
||||||
// keep console logs if not minified
|
|
||||||
const debugging = minify;
|
|
||||||
|
|
||||||
if (clean) {
|
if (clean) {
|
||||||
console.log("Cleaning dist");
|
console.log("Cleaning dist (w/o static)");
|
||||||
await $`rm -rf dist`;
|
await $`find dist -mindepth 1 ! -path "dist/static/*" ! -path "dist/static" -exec rm -rf {} +`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let types_running = false;
|
let types_running = false;
|
||||||
@@ -52,70 +46,6 @@ if (types && !watch) {
|
|||||||
buildTypes();
|
buildTypes();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Build static assets
|
|
||||||
* Using esbuild because tsup doesn't include "react"
|
|
||||||
*/
|
|
||||||
const result = await esbuild.build({
|
|
||||||
minify,
|
|
||||||
sourcemap,
|
|
||||||
entryPoints: ["src/ui/main.tsx"],
|
|
||||||
entryNames: "[dir]/[name]-[hash]",
|
|
||||||
outdir: "dist/static/assets",
|
|
||||||
platform: "browser",
|
|
||||||
bundle: true,
|
|
||||||
splitting: true,
|
|
||||||
metafile: true,
|
|
||||||
drop: debugging ? undefined : ["console", "debugger"],
|
|
||||||
inject: ["src/ui/inject.js"],
|
|
||||||
target: "es2022",
|
|
||||||
format: "esm",
|
|
||||||
plugins: [postcss()],
|
|
||||||
loader: {
|
|
||||||
".svg": "dataurl",
|
|
||||||
".js": "jsx"
|
|
||||||
},
|
|
||||||
define: {
|
|
||||||
__isDev: "0",
|
|
||||||
"process.env.NODE_ENV": '"production"'
|
|
||||||
},
|
|
||||||
chunkNames: "chunks/[name]-[hash]",
|
|
||||||
logLevel: "error"
|
|
||||||
});
|
|
||||||
|
|
||||||
// Write manifest
|
|
||||||
{
|
|
||||||
const manifest: Record<string, object> = {};
|
|
||||||
const toAsset = (output: string) => {
|
|
||||||
const name = output.split("/").pop()!;
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
path: output,
|
|
||||||
mime: guessMimeType(name)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const info = Object.entries(result.metafile.outputs)
|
|
||||||
.filter(([, meta]) => {
|
|
||||||
return meta.entryPoint && meta.entryPoint === "src/ui/main.tsx";
|
|
||||||
})
|
|
||||||
.map(([output, meta]) => ({ output, meta }));
|
|
||||||
|
|
||||||
for (const { output, meta } of info) {
|
|
||||||
manifest[meta.entryPoint as string] = toAsset(output);
|
|
||||||
if (meta.cssBundle) {
|
|
||||||
manifest["src/ui/main.css"] = toAsset(meta.cssBundle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const manifest_file = "dist/static/manifest.json";
|
|
||||||
await Bun.write(manifest_file, JSON.stringify(manifest, null, 2));
|
|
||||||
console.log(`Manifest written to ${manifest_file}`, manifest);
|
|
||||||
|
|
||||||
// copy assets to static
|
|
||||||
await $`cp -r src/ui/assets/* dist/static/assets`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Building backend and general API
|
* Building backend and general API
|
||||||
*/
|
*/
|
||||||
@@ -201,33 +131,22 @@ function baseConfig(adapter: string): tsup.Options {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await tsup.build(baseConfig("remix"));
|
||||||
|
await tsup.build(baseConfig("bun"));
|
||||||
|
await tsup.build(baseConfig("astro"));
|
||||||
|
await tsup.build(baseConfig("cloudflare"));
|
||||||
|
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...baseConfig("vite"),
|
...baseConfig("vite"),
|
||||||
platform: "node"
|
platform: "node"
|
||||||
});
|
});
|
||||||
|
|
||||||
await tsup.build({
|
|
||||||
...baseConfig("cloudflare")
|
|
||||||
});
|
|
||||||
|
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...baseConfig("nextjs"),
|
...baseConfig("nextjs"),
|
||||||
platform: "node"
|
platform: "node"
|
||||||
});
|
});
|
||||||
|
|
||||||
await tsup.build({
|
|
||||||
...baseConfig("remix")
|
|
||||||
});
|
|
||||||
|
|
||||||
await tsup.build({
|
|
||||||
...baseConfig("bun")
|
|
||||||
});
|
|
||||||
|
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...baseConfig("node"),
|
...baseConfig("node"),
|
||||||
platform: "node"
|
platform: "node"
|
||||||
});
|
});
|
||||||
|
|
||||||
await tsup.build({
|
|
||||||
...baseConfig("astro")
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -3,20 +3,19 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"version": "0.5.0-rc15",
|
"version": "0.5.0-rc16",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:all": "NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"test": "ALL_TESTS=1 bun test --bail",
|
"test": "ALL_TESTS=1 bun test --bail",
|
||||||
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
||||||
|
"build:all": "rm -rf dist && bun run build:static && NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
||||||
|
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify",
|
||||||
|
"build:static": "vite build",
|
||||||
"watch": "bun run build.ts --types --watch",
|
"watch": "bun run build.ts --types --watch",
|
||||||
"types": "bun tsc --noEmit",
|
"types": "bun tsc --noEmit",
|
||||||
"clean:types": "find ./dist -name '*.d.ts' -delete && rm -f ./dist/tsconfig.tsbuildinfo",
|
"clean:types": "find ./dist -name '*.d.ts' -delete && rm -f ./dist/tsconfig.tsbuildinfo",
|
||||||
"build:types": "tsc --emitDeclarationOnly && tsc-alias",
|
"build:types": "tsc --emitDeclarationOnly && tsc-alias",
|
||||||
"build:css": "bun tailwindcss -i src/ui/main.css -o ./dist/static/styles.css",
|
|
||||||
"watch:css": "bun tailwindcss --watch -i src/ui/main.css -o ./dist/styles.css",
|
|
||||||
"updater": "bun x npm-check-updates -ui",
|
"updater": "bun x npm-check-updates -ui",
|
||||||
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify",
|
|
||||||
"cli": "LOCAL=1 bun src/cli/index.ts",
|
"cli": "LOCAL=1 bun src/cli/index.ts",
|
||||||
"prepublishOnly": "bun run test && bun run build:all"
|
"prepublishOnly": "bun run test && bun run build:all"
|
||||||
},
|
},
|
||||||
@@ -170,7 +169,7 @@
|
|||||||
"require": "./dist/adapter/astro/index.cjs"
|
"require": "./dist/adapter/astro/index.cjs"
|
||||||
},
|
},
|
||||||
"./dist/styles.css": "./dist/ui/main.css",
|
"./dist/styles.css": "./dist/ui/main.css",
|
||||||
"./dist/manifest.json": "./dist/static/manifest.json"
|
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@@ -74,9 +74,6 @@ export class App {
|
|||||||
this.trigger_first_boot = true;
|
this.trigger_first_boot = true;
|
||||||
},
|
},
|
||||||
onServerInit: async (server) => {
|
onServerInit: async (server) => {
|
||||||
server.get("/favicon.ico", (c) =>
|
|
||||||
c.redirect(config.server.assets_path + "/favicon.ico")
|
|
||||||
);
|
|
||||||
server.use(async (c, next) => {
|
server.use(async (c, next) => {
|
||||||
c.set("app", this);
|
c.set("app", this);
|
||||||
await next();
|
await next();
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export interface DB {}
|
|||||||
export const config = {
|
export const config = {
|
||||||
server: {
|
server: {
|
||||||
default_port: 1337,
|
default_port: 1337,
|
||||||
assets_path: "/assets/"
|
// resetted to root for now, bc bundling with vite
|
||||||
|
assets_path: "/"
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
default_primary_field: "id"
|
default_primary_field: "id"
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export class AdminController extends Controller {
|
|||||||
const { auth: authMiddleware, permission } = this.middlewares;
|
const { auth: authMiddleware, permission } = this.middlewares;
|
||||||
const hono = this.create().use(
|
const hono = this.create().use(
|
||||||
authMiddleware({
|
authMiddleware({
|
||||||
skip: [/favicon\.ico$/]
|
//skip: [/favicon\.ico$/]
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -102,6 +102,7 @@ export class AdminController extends Controller {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @todo: only load known paths
|
||||||
hono.get(
|
hono.get(
|
||||||
"/*",
|
"/*",
|
||||||
permission(SystemPermissions.accessAdmin, {
|
permission(SystemPermissions.accessAdmin, {
|
||||||
@@ -160,8 +161,9 @@ export class AdminController extends Controller {
|
|||||||
const manifest = await import("bknd/dist/manifest.json", {
|
const manifest = await import("bknd/dist/manifest.json", {
|
||||||
assert: { type: "json" }
|
assert: { type: "json" }
|
||||||
}).then((m) => m.default);
|
}).then((m) => m.default);
|
||||||
assets.js = manifest["src/ui/main.tsx"].name;
|
// @todo: load all marked as entry (incl. css)
|
||||||
assets.css = manifest["src/ui/main.css"].name;
|
assets.js = manifest["src/ui/main.tsx"].file;
|
||||||
|
assets.css = manifest["src/ui/main.tsx"].css[0] as any;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error loading manifest", e);
|
console.error("Error loading manifest", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,350 +0,0 @@
|
|||||||
/**
|
|
||||||
* @todo: currently just hard importing this library due to building and react issues with static assets
|
|
||||||
* man I hate bundling for react.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
type ComponentPropsWithoutRef,
|
|
||||||
type ForwardedRef,
|
|
||||||
type ReactNode,
|
|
||||||
type RefObject,
|
|
||||||
forwardRef,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useRef,
|
|
||||||
useState
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
export type JSONSchemaTypeName =
|
|
||||||
| "string"
|
|
||||||
| "number"
|
|
||||||
| "integer"
|
|
||||||
| "boolean"
|
|
||||||
| "object"
|
|
||||||
| "array"
|
|
||||||
| "null"
|
|
||||||
| string;
|
|
||||||
|
|
||||||
export type JSONSchemaDefinition = JSONSchema | boolean;
|
|
||||||
|
|
||||||
export interface JSONSchema {
|
|
||||||
$id?: string;
|
|
||||||
$ref?: string;
|
|
||||||
$schema?: string;
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
default?: any;
|
|
||||||
|
|
||||||
// Data types
|
|
||||||
type?: JSONSchemaTypeName | JSONSchemaTypeName[];
|
|
||||||
enum?: any[];
|
|
||||||
const?: any;
|
|
||||||
|
|
||||||
// Numbers
|
|
||||||
multipleOf?: number;
|
|
||||||
maximum?: number;
|
|
||||||
exclusiveMaximum?: number;
|
|
||||||
minimum?: number;
|
|
||||||
exclusiveMinimum?: number;
|
|
||||||
|
|
||||||
// Strings
|
|
||||||
maxLength?: number;
|
|
||||||
minLength?: number;
|
|
||||||
pattern?: string;
|
|
||||||
format?: string;
|
|
||||||
|
|
||||||
// Arrays
|
|
||||||
items?: JSONSchemaDefinition | JSONSchemaDefinition[];
|
|
||||||
additionalItems?: JSONSchemaDefinition;
|
|
||||||
uniqueItems?: boolean;
|
|
||||||
maxItems?: number;
|
|
||||||
minItems?: number;
|
|
||||||
|
|
||||||
// Objects
|
|
||||||
properties?: { [key: string]: JSONSchemaDefinition };
|
|
||||||
patternProperties?: { [key: string]: JSONSchemaDefinition };
|
|
||||||
additionalProperties?: JSONSchemaDefinition;
|
|
||||||
required?: string[];
|
|
||||||
maxProperties?: number;
|
|
||||||
minProperties?: number;
|
|
||||||
dependencies?: { [key: string]: JSONSchemaDefinition | string[] };
|
|
||||||
|
|
||||||
// Combining schemas
|
|
||||||
allOf?: JSONSchemaDefinition[];
|
|
||||||
anyOf?: JSONSchemaDefinition[];
|
|
||||||
oneOf?: JSONSchemaDefinition[];
|
|
||||||
not?: JSONSchemaDefinition;
|
|
||||||
if?: JSONSchemaDefinition;
|
|
||||||
then?: JSONSchemaDefinition;
|
|
||||||
else?: JSONSchemaDefinition;
|
|
||||||
|
|
||||||
// Definitions
|
|
||||||
definitions?: { [key: string]: JSONSchemaDefinition };
|
|
||||||
$comment?: string;
|
|
||||||
[key: string | symbol]: any; // catch-all for custom extensions
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formDataToNestedObject(
|
|
||||||
formData: FormData,
|
|
||||||
formElement: HTMLFormElement
|
|
||||||
): Record<string, any> {
|
|
||||||
const result: Record<string, any> = {};
|
|
||||||
|
|
||||||
formData.forEach((value, key) => {
|
|
||||||
const inputElement = formElement.querySelector(`[name="${key}"]`) as
|
|
||||||
| HTMLInputElement
|
|
||||||
| HTMLTextAreaElement
|
|
||||||
| HTMLSelectElement
|
|
||||||
| null;
|
|
||||||
|
|
||||||
if (!inputElement) {
|
|
||||||
return; // Skip if the input element is not found
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip fields with empty values
|
|
||||||
if (value === "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = key
|
|
||||||
.replace(/\[([^\]]*)\]/g, ".$1") // Convert [key] to .key
|
|
||||||
.split(".") // Split by dots
|
|
||||||
.filter(Boolean); // Remove empty parts
|
|
||||||
|
|
||||||
let current = result;
|
|
||||||
|
|
||||||
keys.forEach((k, i) => {
|
|
||||||
if (i === keys.length - 1) {
|
|
||||||
let parsedValue: any = value;
|
|
||||||
|
|
||||||
if (inputElement.type === "number") {
|
|
||||||
parsedValue = !Number.isNaN(Number(value)) ? Number(value) : value;
|
|
||||||
} else if (inputElement.type === "checkbox") {
|
|
||||||
parsedValue = "checked" in inputElement && inputElement.checked;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle array or single value
|
|
||||||
if (current[k] !== undefined) {
|
|
||||||
if (!Array.isArray(current[k])) {
|
|
||||||
current[k] = [current[k]];
|
|
||||||
}
|
|
||||||
current[k].push(parsedValue);
|
|
||||||
} else {
|
|
||||||
current[k] = parsedValue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Ensure the key exists as an object
|
|
||||||
if (current[k] === undefined || typeof current[k] !== "object") {
|
|
||||||
current[k] = {};
|
|
||||||
}
|
|
||||||
current = current[k];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cache = new Map<string, JSONSchema>();
|
|
||||||
|
|
||||||
export type ChangeSet = { name: string; value: any };
|
|
||||||
|
|
||||||
export type Validator<Err = unknown, FormData = any> = {
|
|
||||||
validate: (schema: JSONSchema | any, data: FormData) => Promise<Err[]> | Err[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FormRenderProps<Err> = {
|
|
||||||
errors: Err[];
|
|
||||||
schema: JSONSchema;
|
|
||||||
submitting: boolean;
|
|
||||||
dirty: boolean;
|
|
||||||
submit: () => Promise<void>;
|
|
||||||
reset: () => void;
|
|
||||||
resetDirty: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FormRef<FormData, Err> = {
|
|
||||||
submit: () => Promise<void>;
|
|
||||||
validate: () => Promise<{ data: FormData; errors: Err[] }>;
|
|
||||||
reset: () => void;
|
|
||||||
resetDirty: () => void;
|
|
||||||
formRef: RefObject<HTMLFormElement | null>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FormProps<FormData, ValFn, Err> = Omit<
|
|
||||||
ComponentPropsWithoutRef<"form">,
|
|
||||||
"onSubmit" | "onChange" | "children"
|
|
||||||
> & {
|
|
||||||
schema: `http${string}` | `/${string}` | JSONSchema;
|
|
||||||
validator: Validator<Err, FormData>;
|
|
||||||
validationMode?: "submit" | "change";
|
|
||||||
children: (props: FormRenderProps<Err>) => ReactNode;
|
|
||||||
onChange?: (formData: FormData, changed: ChangeSet) => void | Promise<void>;
|
|
||||||
onSubmit?: (formData: FormData) => void | Promise<void>;
|
|
||||||
onSubmitInvalid?: (errors: Err[], formData: FormData) => void | Promise<void>;
|
|
||||||
resetOnSubmit?: boolean;
|
|
||||||
revalidateOnError?: boolean;
|
|
||||||
hiddenSubmit?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FormComponent = <FormData, ValFn, Err>(
|
|
||||||
{
|
|
||||||
schema: initialSchema,
|
|
||||||
validator,
|
|
||||||
validationMode = "submit",
|
|
||||||
children,
|
|
||||||
onChange,
|
|
||||||
onSubmit,
|
|
||||||
onSubmitInvalid,
|
|
||||||
resetOnSubmit,
|
|
||||||
revalidateOnError = true,
|
|
||||||
hiddenSubmit,
|
|
||||||
...formProps
|
|
||||||
}: FormProps<FormData, ValFn, Err>,
|
|
||||||
ref: ForwardedRef<FormRef<FormData, Err>>
|
|
||||||
) => {
|
|
||||||
const is_schema = typeof initialSchema !== "string";
|
|
||||||
const [schema, setSchema] = useState<JSONSchema | undefined>(
|
|
||||||
is_schema ? initialSchema : undefined
|
|
||||||
);
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
|
||||||
const [errors, setErrors] = useState<any[]>([]);
|
|
||||||
const [dirty, setDirty] = useState(false);
|
|
||||||
const formRef = useRef<HTMLFormElement | null>(null);
|
|
||||||
|
|
||||||
function resetDirty() {
|
|
||||||
setDirty(false);
|
|
||||||
setDirty(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
submit: submit,
|
|
||||||
validate: validate,
|
|
||||||
reset: reset,
|
|
||||||
resetDirty,
|
|
||||||
formRef
|
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
if (!is_schema) {
|
|
||||||
if (cache.has(initialSchema)) {
|
|
||||||
setSchema(cache.get(initialSchema));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(initialSchema);
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
const s = (await res.json()) as JSONSchema;
|
|
||||||
setSchema(s);
|
|
||||||
cache.set(initialSchema, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [initialSchema]);
|
|
||||||
|
|
||||||
async function handleChangeEvent(e: React.FormEvent<HTMLFormElement>) {
|
|
||||||
const form = formRef.current;
|
|
||||||
if (!form) return;
|
|
||||||
setDirty(true);
|
|
||||||
const target = e.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null;
|
|
||||||
|
|
||||||
if (!target || !form.contains(target)) {
|
|
||||||
return; // Ignore events from outside the form
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = target.name;
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const data = formDataToNestedObject(formData, form) as FormData;
|
|
||||||
const value = formData.get(name);
|
|
||||||
|
|
||||||
await onChange?.(data, { name, value });
|
|
||||||
|
|
||||||
if ((revalidateOnError && errors.length > 0) || validationMode === "change") {
|
|
||||||
await validate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validate() {
|
|
||||||
const form = formRef.current;
|
|
||||||
if (!form || !schema) return { data: {} as FormData, errors: [] };
|
|
||||||
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const data = formDataToNestedObject(formData, form) as FormData;
|
|
||||||
|
|
||||||
const errors = await validator.validate(schema, data);
|
|
||||||
setErrors(errors);
|
|
||||||
return { data, errors };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
||||||
e.preventDefault();
|
|
||||||
await submit();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submit() {
|
|
||||||
const form = formRef.current;
|
|
||||||
if (!form || !schema) {
|
|
||||||
console.log("invalid", { form, schema });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, errors } = await validate();
|
|
||||||
if (errors.length > 0) {
|
|
||||||
await onSubmitInvalid?.(errors, data);
|
|
||||||
} else {
|
|
||||||
setSubmitting(true);
|
|
||||||
try {
|
|
||||||
if (onSubmit) {
|
|
||||||
await onSubmit?.(data);
|
|
||||||
if (resetOnSubmit) {
|
|
||||||
reset();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
form.submit();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
console.warn("You should wrap your submit handler in a try/catch block");
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false);
|
|
||||||
setDirty(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
formRef.current?.reset();
|
|
||||||
setErrors([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form {...formProps} onSubmit={handleSubmit} ref={formRef} onChange={handleChangeEvent}>
|
|
||||||
{children({
|
|
||||||
schema: schema as any,
|
|
||||||
submit,
|
|
||||||
dirty,
|
|
||||||
reset,
|
|
||||||
resetDirty,
|
|
||||||
submitting,
|
|
||||||
errors
|
|
||||||
})}
|
|
||||||
|
|
||||||
{hiddenSubmit && (
|
|
||||||
<input type="submit" style={{ visibility: "hidden" }} disabled={errors.length > 0} />
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Form = forwardRef(FormComponent) as <
|
|
||||||
FormData = any,
|
|
||||||
ValidatorActual = Validator,
|
|
||||||
Err = ValidatorActual extends Validator<infer E, FormData> ? Awaited<E> : never
|
|
||||||
>(
|
|
||||||
props: FormProps<FormData, ValidatorActual, Err> & {
|
|
||||||
ref?: ForwardedRef<HTMLFormElement>;
|
|
||||||
}
|
|
||||||
) => ReturnType<typeof FormComponent>;
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import type { PrimaryFieldType } from "core";
|
import type { PrimaryFieldType } from "core";
|
||||||
import { encodeSearch } from "core/utils";
|
import { encodeSearch } from "core/utils";
|
||||||
import { atom, useSetAtom } from "jotai";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { useBaseUrl } from "../client";
|
|
||||||
import { useBknd } from "../client/BkndProvider";
|
import { useBknd } from "../client/BkndProvider";
|
||||||
|
|
||||||
export const routes = {
|
export const routes = {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { ValueError } from "@sinclair/typebox/value";
|
import type { ValueError } from "@sinclair/typebox/value";
|
||||||
import type { AppAuthOAuthStrategy, AppAuthSchema } from "auth/auth-schema";
|
import type { AppAuthOAuthStrategy, AppAuthSchema } from "auth/auth-schema";
|
||||||
import { type TSchema, Type, Value } from "core/utils";
|
import { type TSchema, Type, Value } from "core/utils";
|
||||||
|
import { Form, type Validator } from "json-schema-form-react";
|
||||||
import { transform } from "lodash-es";
|
import { transform } from "lodash-es";
|
||||||
import type { ComponentPropsWithoutRef } from "react";
|
import type { ComponentPropsWithoutRef } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import { Group, Input, Label } from "ui/components/form/Formy";
|
import { Group, Input, Label } from "ui/components/form/Formy";
|
||||||
import { Form, type Validator } from "ui/lib/json-schema-form";
|
|
||||||
import { SocialLink } from "ui/modules/auth/SocialLink";
|
import { SocialLink } from "ui/modules/auth/SocialLink";
|
||||||
|
|
||||||
export type LoginFormProps = Omit<ComponentPropsWithoutRef<"form">, "onSubmit" | "action"> & {
|
export type LoginFormProps = Omit<ComponentPropsWithoutRef<"form">, "onSubmit" | "action"> & {
|
||||||
|
|||||||
@@ -25,5 +25,12 @@ export default defineConfig({
|
|||||||
...devServerConfig,
|
...devServerConfig,
|
||||||
entry: "./vite.dev.ts"
|
entry: "./vite.dev.ts"
|
||||||
})
|
})
|
||||||
]
|
],
|
||||||
|
build: {
|
||||||
|
manifest: true,
|
||||||
|
outDir: "./dist/static",
|
||||||
|
rollupOptions: {
|
||||||
|
input: "./src/ui/main.tsx"
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
125
tmp/lazy_codemirror.patch
Normal file
125
tmp/lazy_codemirror.patch
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
Subject: [PATCH] lazy codemirror
|
||||||
|
---
|
||||||
|
Index: app/src/ui/components/code/LiquidJsEditor.tsx
|
||||||
|
IDEA additional info:
|
||||||
|
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||||
|
<+>UTF-8
|
||||||
|
===================================================================
|
||||||
|
diff --git a/app/src/ui/components/code/LiquidJsEditor.tsx b/app/src/ui/components/code/LiquidJsEditor.tsx
|
||||||
|
--- a/app/src/ui/components/code/LiquidJsEditor.tsx (revision b1a32f370565aded3a34b79ffd254c3c45d1085c)
|
||||||
|
+++ b/app/src/ui/components/code/LiquidJsEditor.tsx (date 1736687726081)
|
||||||
|
@@ -1,7 +1,7 @@
|
||||||
|
-import { liquid } from "@codemirror/lang-liquid";
|
||||||
|
-import type { ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||||
|
import { Suspense, lazy } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
+
|
||||||
|
+import type { CodeEditorProps } from "./CodeEditor";
|
||||||
|
const CodeEditor = lazy(() => import("./CodeEditor"));
|
||||||
|
|
||||||
|
const filters = [
|
||||||
|
@@ -106,7 +106,7 @@
|
||||||
|
{ label: "when" }
|
||||||
|
];
|
||||||
|
|
||||||
|
-export function LiquidJsEditor({ editable, ...props }: ReactCodeMirrorProps) {
|
||||||
|
+export function LiquidJsEditor({ editable, ...props }: CodeEditorProps) {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<CodeEditor
|
||||||
|
@@ -115,7 +115,9 @@
|
||||||
|
!editable && "opacity-70"
|
||||||
|
)}
|
||||||
|
editable={editable}
|
||||||
|
- extensions={[liquid({ filters, tags })]}
|
||||||
|
+ _extensions={{
|
||||||
|
+ liquid: { filters, tags }
|
||||||
|
+ }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
Index: app/src/ui/components/code/CodeEditor.tsx
|
||||||
|
IDEA additional info:
|
||||||
|
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||||
|
<+>UTF-8
|
||||||
|
===================================================================
|
||||||
|
diff --git a/app/src/ui/components/code/CodeEditor.tsx b/app/src/ui/components/code/CodeEditor.tsx
|
||||||
|
--- a/app/src/ui/components/code/CodeEditor.tsx (revision b1a32f370565aded3a34b79ffd254c3c45d1085c)
|
||||||
|
+++ b/app/src/ui/components/code/CodeEditor.tsx (date 1736687634668)
|
||||||
|
@@ -1,8 +1,22 @@
|
||||||
|
import { default as CodeMirror, type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||||
|
-
|
||||||
|
import { useBknd } from "ui/client/bknd";
|
||||||
|
|
||||||
|
-export default function CodeEditor({ editable, basicSetup, ...props }: ReactCodeMirrorProps) {
|
||||||
|
+import { json } from "@codemirror/lang-json";
|
||||||
|
+import { type LiquidCompletionConfig, liquid } from "@codemirror/lang-liquid";
|
||||||
|
+
|
||||||
|
+export type CodeEditorProps = ReactCodeMirrorProps & {
|
||||||
|
+ _extensions?: Partial<{
|
||||||
|
+ json: boolean;
|
||||||
|
+ liquid: LiquidCompletionConfig;
|
||||||
|
+ }>;
|
||||||
|
+};
|
||||||
|
+
|
||||||
|
+export default function CodeEditor({
|
||||||
|
+ editable,
|
||||||
|
+ basicSetup,
|
||||||
|
+ _extensions = {},
|
||||||
|
+ ...props
|
||||||
|
+}: CodeEditorProps) {
|
||||||
|
const b = useBknd();
|
||||||
|
const theme = b.app.getAdminConfig().color_scheme;
|
||||||
|
const _basicSetup: Partial<ReactCodeMirrorProps["basicSetup"]> = !editable
|
||||||
|
@@ -13,11 +27,21 @@
|
||||||
|
}
|
||||||
|
: basicSetup;
|
||||||
|
|
||||||
|
+ const extensions = Object.entries(_extensions ?? {}).map(([ext, config]: any) => {
|
||||||
|
+ switch (ext) {
|
||||||
|
+ case "json":
|
||||||
|
+ return json();
|
||||||
|
+ case "liquid":
|
||||||
|
+ return liquid(config);
|
||||||
|
+ }
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
return (
|
||||||
|
<CodeMirror
|
||||||
|
theme={theme === "dark" ? "dark" : "light"}
|
||||||
|
editable={editable}
|
||||||
|
basicSetup={_basicSetup}
|
||||||
|
+ extensions={extensions}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
Index: app/src/ui/components/code/JsonEditor.tsx
|
||||||
|
IDEA additional info:
|
||||||
|
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||||
|
<+>UTF-8
|
||||||
|
===================================================================
|
||||||
|
diff --git a/app/src/ui/components/code/JsonEditor.tsx b/app/src/ui/components/code/JsonEditor.tsx
|
||||||
|
--- a/app/src/ui/components/code/JsonEditor.tsx (revision b1a32f370565aded3a34b79ffd254c3c45d1085c)
|
||||||
|
+++ b/app/src/ui/components/code/JsonEditor.tsx (date 1736687681965)
|
||||||
|
@@ -1,10 +1,9 @@
|
||||||
|
-import { json } from "@codemirror/lang-json";
|
||||||
|
-import type { ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||||
|
import { Suspense, lazy } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
+import type { CodeEditorProps } from "./CodeEditor";
|
||||||
|
const CodeEditor = lazy(() => import("./CodeEditor"));
|
||||||
|
|
||||||
|
-export function JsonEditor({ editable, className, ...props }: ReactCodeMirrorProps) {
|
||||||
|
+export function JsonEditor({ editable, className, ...props }: CodeEditorProps) {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<CodeEditor
|
||||||
|
@@ -14,7 +13,7 @@
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
editable={editable}
|
||||||
|
- extensions={[json()]}
|
||||||
|
+ _extensions={{ json: true }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
Reference in New Issue
Block a user