mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +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 * as esbuild from "esbuild";
|
||||
import postcss from "esbuild-postcss";
|
||||
import * as tsup from "tsup";
|
||||
import { guessMimeType } from "./src/media/storage/mime-types";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const watch = args.includes("--watch");
|
||||
@@ -11,12 +8,9 @@ const types = args.includes("--types");
|
||||
const sourcemap = args.includes("--sourcemap");
|
||||
const clean = args.includes("--clean");
|
||||
|
||||
// keep console logs if not minified
|
||||
const debugging = minify;
|
||||
|
||||
if (clean) {
|
||||
console.log("Cleaning dist");
|
||||
await $`rm -rf dist`;
|
||||
console.log("Cleaning dist (w/o static)");
|
||||
await $`find dist -mindepth 1 ! -path "dist/static/*" ! -path "dist/static" -exec rm -rf {} +`;
|
||||
}
|
||||
|
||||
let types_running = false;
|
||||
@@ -52,70 +46,6 @@ if (types && !watch) {
|
||||
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
|
||||
*/
|
||||
@@ -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({
|
||||
...baseConfig("vite"),
|
||||
platform: "node"
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("cloudflare")
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("nextjs"),
|
||||
platform: "node"
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("remix")
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("bun")
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("node"),
|
||||
platform: "node"
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("astro")
|
||||
});
|
||||
|
||||
@@ -3,20 +3,19 @@
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"bin": "./dist/cli/index.js",
|
||||
"version": "0.5.0-rc15",
|
||||
"version": "0.5.0-rc16",
|
||||
"scripts": {
|
||||
"build:all": "NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
||||
"dev": "vite",
|
||||
"test": "ALL_TESTS=1 bun test --bail",
|
||||
"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",
|
||||
"types": "bun tsc --noEmit",
|
||||
"clean:types": "find ./dist -name '*.d.ts' -delete && rm -f ./dist/tsconfig.tsbuildinfo",
|
||||
"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",
|
||||
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify",
|
||||
"cli": "LOCAL=1 bun src/cli/index.ts",
|
||||
"prepublishOnly": "bun run test && bun run build:all"
|
||||
},
|
||||
@@ -170,7 +169,7 @@
|
||||
"require": "./dist/adapter/astro/index.cjs"
|
||||
},
|
||||
"./dist/styles.css": "./dist/ui/main.css",
|
||||
"./dist/manifest.json": "./dist/static/manifest.json"
|
||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -74,9 +74,6 @@ export class App {
|
||||
this.trigger_first_boot = true;
|
||||
},
|
||||
onServerInit: async (server) => {
|
||||
server.get("/favicon.ico", (c) =>
|
||||
c.redirect(config.server.assets_path + "/favicon.ico")
|
||||
);
|
||||
server.use(async (c, next) => {
|
||||
c.set("app", this);
|
||||
await next();
|
||||
|
||||
@@ -11,7 +11,8 @@ export interface DB {}
|
||||
export const config = {
|
||||
server: {
|
||||
default_port: 1337,
|
||||
assets_path: "/assets/"
|
||||
// resetted to root for now, bc bundling with vite
|
||||
assets_path: "/"
|
||||
},
|
||||
data: {
|
||||
default_primary_field: "id"
|
||||
|
||||
@@ -50,7 +50,7 @@ export class AdminController extends Controller {
|
||||
const { auth: authMiddleware, permission } = this.middlewares;
|
||||
const hono = this.create().use(
|
||||
authMiddleware({
|
||||
skip: [/favicon\.ico$/]
|
||||
//skip: [/favicon\.ico$/]
|
||||
})
|
||||
);
|
||||
|
||||
@@ -102,6 +102,7 @@ export class AdminController extends Controller {
|
||||
});
|
||||
}
|
||||
|
||||
// @todo: only load known paths
|
||||
hono.get(
|
||||
"/*",
|
||||
permission(SystemPermissions.accessAdmin, {
|
||||
@@ -160,8 +161,9 @@ export class AdminController extends Controller {
|
||||
const manifest = await import("bknd/dist/manifest.json", {
|
||||
assert: { type: "json" }
|
||||
}).then((m) => m.default);
|
||||
assets.js = manifest["src/ui/main.tsx"].name;
|
||||
assets.css = manifest["src/ui/main.css"].name;
|
||||
// @todo: load all marked as entry (incl. css)
|
||||
assets.js = manifest["src/ui/main.tsx"].file;
|
||||
assets.css = manifest["src/ui/main.tsx"].css[0] as any;
|
||||
} catch (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 { encodeSearch } from "core/utils";
|
||||
import { atom, useSetAtom } from "jotai";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { useBaseUrl } from "../client";
|
||||
import { useBknd } from "../client/BkndProvider";
|
||||
|
||||
export const routes = {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { ValueError } from "@sinclair/typebox/value";
|
||||
import type { AppAuthOAuthStrategy, AppAuthSchema } from "auth/auth-schema";
|
||||
import { type TSchema, Type, Value } from "core/utils";
|
||||
import { Form, type Validator } from "json-schema-form-react";
|
||||
import { transform } from "lodash-es";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
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";
|
||||
|
||||
export type LoginFormProps = Omit<ComponentPropsWithoutRef<"form">, "onSubmit" | "action"> & {
|
||||
|
||||
@@ -25,5 +25,12 @@ export default defineConfig({
|
||||
...devServerConfig,
|
||||
entry: "./vite.dev.ts"
|
||||
})
|
||||
]
|
||||
],
|
||||
build: {
|
||||
manifest: true,
|
||||
outDir: "./dist/static",
|
||||
rollupOptions: {
|
||||
input: "./src/ui/main.tsx"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user