using vite for static bundling to solve external package's react resolution

This commit is contained in:
dswbx
2025-01-14 07:51:14 +01:00
parent b1a32f3705
commit 6855e6f7e9
10 changed files with 153 additions and 456 deletions

View File

@@ -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")
});

View File

@@ -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"

View File

@@ -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();

View File

@@ -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"

View File

@@ -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);
}

View File

@@ -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>;

View File

@@ -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 = {

View File

@@ -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"> & {

View File

@@ -25,5 +25,12 @@ export default defineConfig({
...devServerConfig,
entry: "./vite.dev.ts"
})
]
],
build: {
manifest: true,
outDir: "./dist/static",
rollupOptions: {
input: "./src/ui/main.tsx"
}
}
});

125
tmp/lazy_codemirror.patch Normal file
View 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>