public commit

This commit is contained in:
dswbx
2024-11-16 12:01:47 +01:00
commit 90f80c4280
582 changed files with 49291 additions and 0 deletions

33
packages/cli/package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "bknd-cli",
"type": "module",
"bin": "./dist/cli/index.js",
"version": "0.0.7",
"scripts": {
"build": "rm -rf dist && mkdir -p dist/{static,cli} && cp -r ../../app/dist/static/ dist/static && cp -r ../../app/dist/cli/ dist/cli",
"cli": "node dist/cli/index.js"
},
"files": [
"dist",
"README.md"
],
"dependencies": {
"@libsql/client": "^0.14.0",
"hono": "^4.6.7"
},
"tsup": {
"entry": [
"src/index.ts"
],
"minify": true,
"outDir": "dist",
"format": [
"esm"
],
"platform": "neutral",
"splitting": false,
"loader": {
".md": "copy"
}
}
}

175
packages/plasmic/.gitignore vendored Normal file
View File

@@ -0,0 +1,175 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

View File

@@ -0,0 +1,15 @@
# bknd-plasmic
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.1.4. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

View File

@@ -0,0 +1,243 @@
"use client";
import type { CodeComponentMeta } from "@plasmicapp/host";
//import { PlasmicCanvasContext } from "@plasmicapp/loader-react";
import { useContext, useEffect, useRef, useState } from "react";
type PlasmicImageProps = {
asset: {
aspectRatio?: any;
dataUri: string;
name: string;
type: string;
uid: number;
uuid: string;
width: number;
height: number;
};
uid: number;
};
type ImageProps = {
className?: string;
src?: string | PlasmicImageProps;
alt?: string;
width?: number | string;
height?: number | string;
ratio?: number;
backgroundColor?: string;
forceLoad?: boolean;
transformations?: string;
transitionSpeed?: number;
loadTreshold?: number;
};
function numeric(value: number | string): number {
return typeof value === "number" ? value : Number.parseFloat(value);
}
function getDimensionDefaults(
width: number | string | undefined,
height: number | string | undefined,
ratio: number | undefined,
) {
let _width = width;
let _height = height;
let _ratio = ratio;
if (_width && ratio) {
_height = (1 / ratio) * numeric(_width);
} else if (_height && ratio) {
_width = ratio * numeric(_height);
}
if (_width && _height && !_ratio) {
_ratio = numeric(_width) / numeric(_height);
}
return { width: _width, height: _height, ratio: _ratio };
}
function getPlaceholderStyle(
width: number | string | undefined,
height: number | string | undefined,
ratio: number | undefined,
) {
let paddingBottom = 0;
if (width && height) {
paddingBottom = (1 / (numeric(width) / numeric(height))) * 100;
//paddingBottom = `${numeric(width)}px / ${numeric(height)}px * 100%}`;
//
} else if (ratio) {
paddingBottom = (1 / ratio) * 100;
}
return {
paddingBottom: paddingBottom + "%",
};
}
export const Image: React.FC<ImageProps> = ({
className,
src,
alt = "",
width,
height,
ratio,
backgroundColor = "rgba(225, 225, 225, 0.2)",
forceLoad = false,
transformations = "",
transitionSpeed = 200,
loadTreshold = 0.1,
...rest
}) => {
const inEditor = false; // !!useContext(PlasmicCanvasContext);
const [loaded, setLoaded] = useState(false);
const [isInView, setIsInView] = useState(inEditor ?? forceLoad);
const [transitioned, setTransitioned] = useState(forceLoad);
const imgRef = useRef<any>(null);
if (src) {
if (typeof src === "object") {
src = src.asset.dataUri;
}
if (/cloudinary/.test(src)) {
if (transformations) {
src = src.replace("/upload", "/upload/" + transformations);
}
}
}
//console.log("after:src", src);
useEffect(() => {
if (forceLoad) {
setIsInView(true);
return;
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.disconnect();
}
});
},
{ threshold: loadTreshold },
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => {
observer.disconnect();
};
}, [forceLoad]);
const onLoad = () => {
setTimeout(() => {
setLoaded(true);
}, 0);
setTimeout(() => {
setTransitioned(true);
}, transitionSpeed);
};
const {
width: _width,
height: _height,
ratio: _ratio,
} = getDimensionDefaults(width, height, ratio);
const imgStyle: any = {
objectFit: "cover",
transition: `opacity ${transitionSpeed}ms linear`,
position: "relative",
maxWidth: "100%",
maxHeight: "100%",
width: _width || "100%",
height: "auto",
//height: _height || "auto",
//height: !transitioned ? _height || "auto" : "auto",
opacity: forceLoad || loaded ? 1 : 0,
};
const placeholderStyle: any = {
position: "absolute",
maxWidth: "100%",
maxHeight: "100%",
backgroundColor,
width: _width || "100%",
height: 0,
//height: transitioned ? "auto" : 0,
...getPlaceholderStyle(_width, _height, _ratio),
};
const wrapperStyle: any = {
position: "relative",
width: _width,
...getPlaceholderStyle(_width, _height, _ratio),
height: 0,
margin: 0,
lineHeight: 0,
//height: _height,
maxWidth: "100%",
maxHeight: "100%",
};
if (loaded) {
wrapperStyle.height = "auto";
wrapperStyle.paddingBottom = 0;
}
if (!src) return <div className={className} style={wrapperStyle} ref={imgRef} />;
return (
<div className={className} style={wrapperStyle} ref={imgRef}>
<div style={placeholderStyle} />
{isInView && (
<img
src={src}
alt={alt}
onLoad={onLoad}
style={imgStyle}
width={_width}
height={_height}
{...rest}
/>
)}
</div>
);
};
export const ImageMeta: CodeComponentMeta<React.ComponentType<ImageProps>> = {
name: "ImageLazy",
importPath: import.meta.dir,
props: {
src: {
type: "imageUrl",
displayName: "Image",
},
alt: "string",
width: "number",
height: "number",
ratio: "number",
forceLoad: "boolean",
transformations: "string",
//backgroundColor: "color",
transitionSpeed: {
type: "number",
helpText: "How fast image should fade in. Default is 200 (ms).",
},
loadTreshold: {
type: "number",
displayName: "Treshold",
//defaultValue: 0.1,
helpText:
"Number between 0 and 1. Default is 0.1. Determines how much of the image must be in viewport before it gets loaded",
},
},
};

View File

@@ -0,0 +1,111 @@
import type { CodeComponentMeta } from "@plasmicapp/host";
import { useEffect, useRef, useState } from "react";
interface LazyRenderProps {
className?: string;
forceLoad?: boolean;
forceFallback?: boolean;
threshold?: number;
fallback?: React.ReactNode;
delay?: number;
children?: React.ReactNode;
onBecomesVisible?: () => void;
}
const DefaultFallback = () => <div style={{ height: 50 }}>asdf</div>;
export const LazyRender: React.FC<LazyRenderProps> = ({
className = "",
children,
forceLoad = false,
forceFallback = false,
threshold = 0.1,
delay = 0,
fallback = <DefaultFallback />,
onBecomesVisible,
}) => {
const [isVisible, setIsVisible] = useState(forceLoad);
const ref = useRef<HTMLDivElement>(null);
/* console.log("props", {
delay,
threshold,
fallback,
isVisible,
forceLoad,
forceFallback,
children,
}); */
useEffect(() => {
if (forceLoad || forceFallback) {
setIsVisible(true);
return;
}
const observerOptions: IntersectionObserverInit = {
threshold: threshold < 1 ? threshold : 0.1,
};
const observerCallback: IntersectionObserverCallback = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !isVisible) {
setTimeout(() => {
setIsVisible(true);
onBecomesVisible?.();
if (ref.current) observer.unobserve(ref.current);
}, delay);
}
});
};
const observer = new IntersectionObserver(observerCallback, observerOptions);
if (ref.current) observer.observe(ref.current);
return () => {
if (ref.current) observer.unobserve(ref.current);
};
}, [forceLoad, threshold, forceFallback, delay]);
return (
<div className={className} ref={ref}>
{isVisible && !forceFallback ? children : fallback}
</div>
);
};
export const LazyRenderMeta: CodeComponentMeta<React.ComponentType<LazyRenderProps>> = {
name: "LazyRender",
importPath: import.meta.dir,
props: {
forceLoad: {
type: "boolean",
defaultValue: false,
},
forceFallback: {
type: "boolean",
defaultValue: false,
},
threshold: {
type: "number",
defaultValue: 0.1,
},
fallback: {
type: "slot",
//allowedComponents: ["*"],
},
delay: {
type: "number",
defaultValue: 0,
},
onBecomesVisible: {
type: "code",
lang: "javascript",
},
children: {
type: "slot",
//allowedComponents: ["*"],
},
},
};

View File

@@ -0,0 +1,23 @@
import type { CodeComponentMeta } from "@plasmicapp/host";
import { Link } from "wouter";
export function WouterLink({ href, className, children, ...props }) {
return (
<Link href={href ?? "#"} className={className} {...props}>
{children}
</Link>
);
}
export const WouterLinkMeta: CodeComponentMeta<any> = {
name: "WouterLink",
importPath: import.meta.dir,
props: {
href: {
type: "href",
},
children: {
type: "slot",
},
},
};

View File

@@ -0,0 +1,385 @@
import { type CodeComponentMeta, DataProvider, usePlasmicCanvasContext } from "@plasmicapp/host";
import type { RepoQuery } from "bknd/data";
import { useEntities, useEntity } from "bknd/ui";
import { encodeSearch } from "bknd/utils";
import { useContext, useEffect, useState } from "react";
import { usePlasmicBkndContext } from "../../contexts/BkndContext";
type BkndEntitiesProps = {
children?: React.ReactNode;
loading?: React.ReactNode;
error?: React.ReactNode;
empty?: React.ReactNode;
setControlContextData?: (ctxData: {
entities: string[];
fields: string[];
references: string[];
}) => void;
className?: string;
limit?: number;
offset?: number;
withRefs?: string[];
joinRefs?: string[];
dataName?: string;
entityId?: number;
entity?: string;
sortBy: string;
sortDir: "asc" | "desc";
where?: string;
mode?: "fetch" | "react-query";
noLayout?: boolean;
preview?: boolean;
previewSlot?: "loading" | "error" | "empty";
};
const LoadingComponent = ({ loading }: { loading?: React.ReactNode }) => {
return loading ? <>{loading}</> : <>Loading...</>;
};
const ErrorComponent = ({ error }: { error?: React.ReactNode }) => {
return error ? <>{error}</> : <>Error</>;
};
const EmptyComponent = ({ empty }: { empty?: React.ReactNode }) => {
return empty ? <>{empty}</> : <>No data</>;
};
export function BkndData({
children,
loading,
error,
empty,
entity,
setControlContextData,
dataName,
limit,
offset,
entityId,
where,
withRefs,
joinRefs,
sortBy = "id",
sortDir = "asc",
mode = "fetch",
noLayout,
preview,
previewSlot,
...props
}: BkndEntitiesProps) {
const inEditor = !!usePlasmicCanvasContext();
const plasmicContext = usePlasmicBkndContext();
if (inEditor && preview) {
let Component: React.ReactNode;
switch (previewSlot) {
case "loading":
Component = <LoadingComponent loading={loading} />;
break;
case "error":
Component = <ErrorComponent error={error} />;
break;
case "empty":
Component = <EmptyComponent empty={empty} />;
break;
}
if (Component) {
return noLayout ? Component : <div className={props.className}>{Component}</div>;
}
}
let _where: any = undefined;
if (where) {
if (typeof where === "string") {
try {
_where = JSON.parse(where);
} catch (e) {}
} else {
_where = where;
}
}
const query = {
limit: entityId ? undefined : limit,
offset: entityId ? undefined : offset,
where: _where,
sort: { by: sortBy, dir: sortDir },
with: withRefs,
join: joinRefs
};
console.log("---context", plasmicContext);
if (plasmicContext.appConfig?.data?.entities) {
const { entities, relations } = plasmicContext.appConfig.data;
console.log("entities", entities);
//setControlContextData?.({ entities, fields: ["id"] });
let fields: string[] = ["id"];
let references: string[] = [];
if (entity && entity in entities) {
fields = Object.keys(entities[entity].fields!);
if (relations) {
const rels = Object.values(relations).filter(
// biome-ignore lint/suspicious/noDoubleEquals: <explanation>
(r: any) => r.source == entity
);
// @ts-ignore
references = rels?.map((r) => r.config?.mappedBy ?? r.target);
//console.log("relations", relations, references);
}
}
setControlContextData?.({ entities: Object.keys(entities), fields, references });
}
if (!entity) {
return <div>Select an entity</div>;
}
const modeProps: ModeProps = {
loading,
error,
empty,
dataName: dataName ?? entity ?? "data",
entityId,
entity,
query,
children
};
const Component =
mode === "react-query" ? <ModeReactQuery {...modeProps} /> : <ModeFetch {...modeProps} />;
return noLayout ? Component : <div className={props.className}>{Component}</div>;
}
type ModeProps = {
entity: string;
dataName: string;
children?: React.ReactNode;
loading?: React.ReactNode;
error?: React.ReactNode;
empty?: React.ReactNode;
entityId?: number;
query?: Partial<RepoQuery>;
};
const ModeFetch = ({
children,
loading,
error,
empty,
dataName,
entityId,
entity,
query
}: ModeProps) => {
const [data, setData] = useState<any[]>([]);
const [isLoading, setLoading] = useState(true);
const [hasError, setError] = useState<string>();
const plasmicContext = usePlasmicBkndContext();
const basepath = "/api/data";
const path = entityId ? `${basepath}/${entity}/${entityId}` : `${basepath}/${entity}`;
console.log("query", path, query);
const url = `${plasmicContext.baseUrl}${path}?${encodeSearch(query)}`;
useEffect(() => {
(async () => {
try {
const res = await fetch(url);
const result = (await res.json()) as any;
//console.log("result", result);
setData(result.data);
setLoading(false);
setError(undefined);
} catch (e) {
console.error(e);
setError(String(e));
setLoading(false);
}
})();
}, [url]);
console.log("--data", { name: dataName ?? entity ?? "data", data, isLoading, hasError });
if (isLoading) {
return <LoadingComponent loading={loading} />;
}
if (hasError) {
return <ErrorComponent error={error} />;
}
if (data.length === 0) {
return <EmptyComponent empty={empty} />;
}
console.log("--here1");
return (
<DataProvider name={dataName ?? entity ?? "data"} data={data}>
{children}
</DataProvider>
);
};
const ModeReactQuery = (props: ModeProps) => {
return props.entityId ? (
<ModeReactQuerySingle {...props} />
) : (
<ModeReactQueryMultiple {...props} />
);
};
const ModeReactQuerySingle = ({
children,
loading,
error,
dataName,
entityId,
empty,
entity
}: ModeProps) => {
const container = useEntity(entity, entityId);
const { isLoading, isError } = container.status.fetch;
if (isLoading) {
return <LoadingComponent loading={loading} />;
}
if (isError) {
return <ErrorComponent error={error} />;
}
if (!container.data) {
return <EmptyComponent empty={empty} />;
}
return (
<DataProvider name={dataName ?? entity ?? "data"} data={container.data}>
{children}
</DataProvider>
);
};
const ModeReactQueryMultiple = ({
children,
loading,
error,
empty,
dataName,
entity,
query
}: ModeProps) => {
const container = useEntities(entity, query);
const { isLoading, isError } = container.status.fetch;
if (isLoading) {
return <LoadingComponent loading={loading} />;
}
if (isError) {
return <ErrorComponent error={error} />;
}
if (!container.data || container.data.length === 0) {
return <EmptyComponent empty={empty} />;
}
return (
<DataProvider name={dataName ?? entity ?? "data"} data={container.data}>
{children}
</DataProvider>
);
};
export const BkndDataMeta: CodeComponentMeta<React.ComponentType<BkndEntitiesProps>> = {
name: "BKND Data",
section: "BKND",
importPath: import.meta.dir,
providesData: true,
props: {
entity: {
type: "choice",
options: (props, ctx) => ctx.entities
},
dataName: {
type: "string"
},
entityId: {
type: "number"
},
limit: {
type: "number",
defaultValue: 10,
// @ts-ignore
hidden: (props) => !!props.entityId,
min: 0
},
offset: {
type: "number",
defaultValue: 0,
// @ts-ignore
hidden: (props) => !!props.entityId,
min: 0
},
withRefs: {
displayName: "With",
type: "choice",
multiSelect: true,
options: (props, ctx) => ctx.references
},
joinRefs: {
displayName: "Join",
type: "choice",
multiSelect: true,
options: (props, ctx) => ctx.references
},
where: {
type: "code",
lang: "json"
},
sortBy: {
type: "choice",
options: (props, ctx) => ctx.fields
},
sortDir: {
type: "choice",
options: ["asc", "desc"],
defaultValue: "asc"
},
children: {
type: "slot"
},
loading: {
type: "slot"
},
error: {
type: "slot"
},
empty: {
type: "slot"
},
mode: {
type: "choice",
options: ["fetch", "react-query"],
defaultValue: "fetch",
advanced: true
},
noLayout: {
type: "boolean",
defaultValue: true,
advanced: true
},
preview: {
type: "boolean",
defaultValue: false,
advanced: true
},
previewSlot: {
type: "choice",
options: ["loading", "error", "empty"],
hidden: (props: any) => props.preview !== true,
advanced: true
}
}
};

View File

@@ -0,0 +1,4 @@
export { BkndData, BkndDataMeta } from "./data/BkndData";
export { WouterLink, WouterLinkMeta } from "./WouterLink";
export { Image, ImageMeta } from "./Image";
export { LazyRender, LazyRenderMeta } from "./LazyRender";

View File

@@ -0,0 +1,143 @@
import { DataProvider, GlobalActionsProvider, usePlasmicCanvasContext } from "@plasmicapp/host";
import type { AppConfig } from "bknd";
import { ClientProvider, useAuth, useBaseUrl } from "bknd/ui";
import { createContext, useContext, useEffect, useMemo, useState } from "react";
// Users will be able to set these props in Studio.
interface BkndGlobalContextProps {
// You might use this to override the auth URL to a test or local URL.
baseUrl?: string;
appConfig?: AppConfig;
auth: any; // @todo: add typings
}
type BkndContextProps = {
baseUrl?: string;
initialAuth?: any;
};
const BkndContextContext = createContext<BkndGlobalContextProps>({} as any);
function getBaseUrlFromWindow() {
if (typeof window === "undefined") {
return "";
}
const protocol = window.location.protocol;
const host = window.location.host;
return `${protocol}//${host}`;
}
// @todo: it's an issue that we need auth, so we cannot make baseurl adjustable (maybe add an option to useAuth with a specific base url?)
export const BkndContext = ({
children,
baseUrl,
initialAuth
}: React.PropsWithChildren<BkndContextProps>) => {
const auth = useAuth();
const baseurl = useBaseUrl();
const [data, setData] = useState<BkndGlobalContextProps>({
baseUrl: baseurl,
/*baseUrl: (baseUrl && baseUrl.length > 0 ? baseUrl : getBaseUrlFromWindow()).replace(
/\/+$/,
""
),*/
auth: auth ?? initialAuth,
appConfig: undefined
});
const inEditor = !!usePlasmicCanvasContext();
console.log("context:user", data);
useEffect(() => {
setData((prev) => ({ ...prev, auth: auth }));
}, [auth.user]);
useEffect(() => {
(async () => {
if (inEditor) {
const res = await fetch(`${baseurl}/api/system/config`);
const result = (await res.json()) as BkndGlobalContextProps["appConfig"];
console.log("appconfig", result);
setData((prev) => ({ ...prev, appConfig: result }));
}
})();
}, [inEditor]);
const actions = useMemo(
() => ({
login: async (data: any) => {
console.log("login", data);
const result = await auth.login(data);
console.log("login:result", result);
if (result.res.ok && "user" in result.data) {
//result.data.
return result.data;
} else {
console.log("login failed", result);
}
return false;
},
register: async (data: any) => {
console.log("register", data);
const result = await auth.register(data);
console.log("register:result", result);
if (result.res.ok && "user" in result.data) {
//result.data.
return result.data;
}
return false;
},
logout: async () => {
await auth.logout();
console.log("logged out");
return true;
},
setToken: auth.setToken
}),
[baseUrl]
);
console.log("plasmic.bknd.context", data);
return (
<GlobalActionsProvider contextName="BkndContext" actions={actions}>
<BkndContextContext.Provider value={data}>
<DataProvider name="bknd" data={data}>
{/*<ClientProvider baseUrl={data.baseUrl}>{children}</ClientProvider>*/}
{children}
</DataProvider>
</BkndContextContext.Provider>
</GlobalActionsProvider>
);
};
export function usePlasmicBkndContext() {
const context = useContext(BkndContextContext);
return context;
}
export const BkndContextMeta = {
name: "BkndContext",
props: { baseUrl: { type: "string" }, initialAuth: { type: "object" } },
providesData: true,
globalActions: {
login: {
parameters: [{ name: "data", type: "object" }]
},
register: {
parameters: [{ name: "data", type: "object" }]
},
logout: {
parameters: []
},
setToken: {
parameters: [{ name: "token", type: "string" }]
},
sayHi: {
parameters: [{ name: "message", type: "string" }]
}
}
};

View File

@@ -0,0 +1 @@
export { BkndContext, BkndContextMeta } from "./BkndContext";

View File

@@ -0,0 +1,4 @@
export { loader as loadBkndComponents, CatchAllPage, createWouterPlasmicApp } from "./loader";
export * from "./components";
export * from "./contexts";

109
packages/plasmic/loader.tsx Normal file
View File

@@ -0,0 +1,109 @@
import { PlasmicCanvasHost, type registerComponent } from "@plasmicapp/host";
import {
type ComponentRenderData,
PlasmicComponent,
type PlasmicComponentLoader,
PlasmicRootProvider
} from "@plasmicapp/loader-react";
import { forwardRef, useEffect, useState } from "react";
import { Link, Route, Router, Switch } from "wouter";
import {
BkndData,
BkndDataMeta,
Image,
ImageMeta,
LazyRender,
LazyRenderMeta,
WouterLink,
WouterLinkMeta
} from "./components";
import { BkndContext, BkndContextMeta } from "./contexts";
export function loader(PLASMIC: PlasmicComponentLoader) {
PLASMIC.registerComponent(BkndData, BkndDataMeta);
PLASMIC.registerComponent(WouterLink, WouterLinkMeta);
PLASMIC.registerComponent(Image, ImageMeta);
PLASMIC.registerComponent(LazyRender, LazyRenderMeta);
PLASMIC.registerGlobalContext(BkndContext, BkndContextMeta as any);
}
const CustomLink = forwardRef<any, any>((props, ref) => {
//console.log("rendering custom link", props);
//return null;
if ("data-replace" in props) {
return <a ref={ref} {...props} />;
}
//return <a ref={ref} {...props} />;
// @ts-ignore it's because of the link
return <Link ref={ref} {...props} />;
});
const Wrapper = ({ children }) => {
return (
<div
style={{
width: "100wh",
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
>
<div style={{ opacity: 0.5, textTransform: "uppercase" }}>{children}</div>
</div>
);
};
export function CatchAllPage({
PLASMIC,
prefix = ""
}: { PLASMIC: PlasmicComponentLoader; prefix?: string }) {
const [loading, setLoading] = useState(true);
const [pageData, setPageData] = useState<ComponentRenderData | null>(null);
//const params = useParams();
const pathname = location.pathname.replace(prefix, "");
const path = pathname.length === 0 ? "/" : pathname;
//console.log("path", path, params);
useEffect(() => {
async function load() {
const pageData = await PLASMIC.maybeFetchComponentData(path);
//console.log("pageData", pageData);
setPageData(pageData);
setLoading(false);
}
load();
}, []);
if (loading) {
return <Wrapper>Loading ...</Wrapper>;
}
if (!pageData) {
return <Wrapper>Not found</Wrapper>;
}
const pageMeta = pageData.entryCompMetas[0];
// The page will already be cached from the `load` call above.
return (
<PlasmicRootProvider loader={PLASMIC} pageParams={pageMeta.params} Link={CustomLink}>
<PlasmicComponent component={path} />
</PlasmicRootProvider>
);
}
export function createWouterPlasmicApp(PLASMIC: PlasmicComponentLoader, prefix = "") {
return function App() {
return (
<Router base={prefix}>
<Switch>
<Route path="/host" component={PlasmicCanvasHost as any} />
<Route
path="/*"
component={() => <CatchAllPage PLASMIC={PLASMIC} prefix={prefix} />}
/>
</Switch>
</Router>
);
};
}

View File

@@ -0,0 +1,48 @@
{
"name": "@bknd/plasmic",
"type": "module",
"sideEffects": false,
"scripts": {
"build": "rm -rf dist && bun tsup && bun run build:types",
"watch": "bun tsup --watch --onSuccess 'bun run build:types'",
"build:only": "rm -rf dist && bun tsup",
"types": "bun tsc -p tsconfig.json --noEmit --skipLibCheck",
"build:types": "bun tsc --emitDeclarationOnly",
"updater": "bun x npm-check-updates -ui"
},
"dependencies": {
"wouter": "^3.3.5"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.0.0"
},
"peerDependencies": {
"@plasmicapp/host": ">=1.0.0",
"bknd": "workspace:*",
"react": ">=18",
"react-dom": ">=18"
},
"tsup": {
"entry": ["index.ts"],
"minify": true,
"clean": true,
"external": ["react", "react-dom", "@plasmicapp/host", "@plasmicapp/loader-react", "@plasmicapp/loader-core"],
"format": ["esm"],
"platform": "browser",
"shims": true,
"bundle": true,
"metafile": true,
"splitting": false,
"sourceMap": true,
"outDir": "dist"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.js"
}
},
"files": ["dist"]
}

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"composite": true,
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"jsx": "react-jsx",
"allowJs": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": false,
"verbatimModuleSyntax": true,
"strict": true,
"outDir": "dist",
"declaration": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": false,
"noPropertyAccessFromIndexSignature": false
},
"include": ["index.ts", "loader.tsx", "components", "contexts"],
"exclude": ["@bknd/app", "@bknd/core", "dist", "node_modules", "build.ts"]
}