From 6855e6f7e995dc1e9db9f01f64e36d3ca22b2772 Mon Sep 17 00:00:00 2001 From: dswbx Date: Tue, 14 Jan 2025 07:51:14 +0100 Subject: [PATCH] using vite for static bundling to solve external package's react resolution --- app/build.ts | 95 +----- app/package.json | 11 +- app/src/App.ts | 3 - app/src/core/config.ts | 3 +- app/src/modules/server/AdminController.tsx | 8 +- app/src/ui/lib/json-schema-form/index.tsx | 350 --------------------- app/src/ui/lib/routes.ts | 3 - app/src/ui/modules/auth/AuthForm.tsx | 2 +- app/vite.config.ts | 9 +- tmp/lazy_codemirror.patch | 125 ++++++++ 10 files changed, 153 insertions(+), 456 deletions(-) delete mode 100644 app/src/ui/lib/json-schema-form/index.tsx create mode 100644 tmp/lazy_codemirror.patch diff --git a/app/build.ts b/app/build.ts index 0f682c6..b41814d 100644 --- a/app/build.ts +++ b/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 = {}; - 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") -}); diff --git a/app/package.json b/app/package.json index bd3e221..5a83b58 100644 --- a/app/package.json +++ b/app/package.json @@ -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" diff --git a/app/src/App.ts b/app/src/App.ts index bead262..2df3e84 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -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(); diff --git a/app/src/core/config.ts b/app/src/core/config.ts index d4e56ba..a99d549 100644 --- a/app/src/core/config.ts +++ b/app/src/core/config.ts @@ -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" diff --git a/app/src/modules/server/AdminController.tsx b/app/src/modules/server/AdminController.tsx index dd918c4..8b8ae9a 100644 --- a/app/src/modules/server/AdminController.tsx +++ b/app/src/modules/server/AdminController.tsx @@ -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); } diff --git a/app/src/ui/lib/json-schema-form/index.tsx b/app/src/ui/lib/json-schema-form/index.tsx deleted file mode 100644 index 56bbbb6..0000000 --- a/app/src/ui/lib/json-schema-form/index.tsx +++ /dev/null @@ -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 { - const result: Record = {}; - - 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(); - -export type ChangeSet = { name: string; value: any }; - -export type Validator = { - validate: (schema: JSONSchema | any, data: FormData) => Promise | Err[]; -}; - -export type FormRenderProps = { - errors: Err[]; - schema: JSONSchema; - submitting: boolean; - dirty: boolean; - submit: () => Promise; - reset: () => void; - resetDirty: () => void; -}; - -export type FormRef = { - submit: () => Promise; - validate: () => Promise<{ data: FormData; errors: Err[] }>; - reset: () => void; - resetDirty: () => void; - formRef: RefObject; -}; - -export type FormProps = Omit< - ComponentPropsWithoutRef<"form">, - "onSubmit" | "onChange" | "children" -> & { - schema: `http${string}` | `/${string}` | JSONSchema; - validator: Validator; - validationMode?: "submit" | "change"; - children: (props: FormRenderProps) => ReactNode; - onChange?: (formData: FormData, changed: ChangeSet) => void | Promise; - onSubmit?: (formData: FormData) => void | Promise; - onSubmitInvalid?: (errors: Err[], formData: FormData) => void | Promise; - resetOnSubmit?: boolean; - revalidateOnError?: boolean; - hiddenSubmit?: boolean; -}; - -const FormComponent = ( - { - schema: initialSchema, - validator, - validationMode = "submit", - children, - onChange, - onSubmit, - onSubmitInvalid, - resetOnSubmit, - revalidateOnError = true, - hiddenSubmit, - ...formProps - }: FormProps, - ref: ForwardedRef> -) => { - const is_schema = typeof initialSchema !== "string"; - const [schema, setSchema] = useState( - is_schema ? initialSchema : undefined - ); - const [submitting, setSubmitting] = useState(false); - const [errors, setErrors] = useState([]); - const [dirty, setDirty] = useState(false); - const formRef = useRef(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) { - 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) { - 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 ( -
- {children({ - schema: schema as any, - submit, - dirty, - reset, - resetDirty, - submitting, - errors - })} - - {hiddenSubmit && ( - 0} /> - )} -
- ); -}; - -export const Form = forwardRef(FormComponent) as < - FormData = any, - ValidatorActual = Validator, - Err = ValidatorActual extends Validator ? Awaited : never ->( - props: FormProps & { - ref?: ForwardedRef; - } -) => ReturnType; diff --git a/app/src/ui/lib/routes.ts b/app/src/ui/lib/routes.ts index 5ca8fe2..44818fe 100644 --- a/app/src/ui/lib/routes.ts +++ b/app/src/ui/lib/routes.ts @@ -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 = { diff --git a/app/src/ui/modules/auth/AuthForm.tsx b/app/src/ui/modules/auth/AuthForm.tsx index a530c17..fa864ef 100644 --- a/app/src/ui/modules/auth/AuthForm.tsx +++ b/app/src/ui/modules/auth/AuthForm.tsx @@ -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, "onSubmit" | "action"> & { diff --git a/app/vite.config.ts b/app/vite.config.ts index 530c0c1..5da4aeb 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -25,5 +25,12 @@ export default defineConfig({ ...devServerConfig, entry: "./vite.dev.ts" }) - ] + ], + build: { + manifest: true, + outDir: "./dist/static", + rollupOptions: { + input: "./src/ui/main.tsx" + } + } }); diff --git a/tmp/lazy_codemirror.patch b/tmp/lazy_codemirror.patch new file mode 100644 index 0000000..425cac5 --- /dev/null +++ b/tmp/lazy_codemirror.patch @@ -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 ( + + + +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 = !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 ( + + ); +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 ( + + +