diff --git a/example/app.tsx b/example/app.tsx index 405429d..ea4529d 100644 --- a/example/app.tsx +++ b/example/app.tsx @@ -1,18 +1,26 @@ -import TWPreTail from "https://esm.sh/@twind/preset-tailwind@1.1.4"; -import TWPreAuto from "https://esm.sh/@twind/preset-autoprefix@1.0.7"; +import TWPreTail from "https://esm.sh/v115/@twind/preset-tailwind@1.1.4/es2022/preset-tailwind.mjs"; +import TWPreAuto from "https://esm.sh/v115/@twind/preset-autoprefix@1.0.7/es2022/preset-autoprefix.mjs"; import React from "react"; import Component from "./deep/component.tsx"; import * as Iso from "@eno/iso"; export default ()=> { - console.log(Iso.Meta); - const [countGet, countSet] = React.useState(1); return

Title!!

subtitle

+ + + + About us! + sorry no page + + + lol/idk +

404!

+
; }; diff --git a/example/deep/component.tsx b/example/deep/component.tsx index 637cedd..124ecfc 100644 --- a/example/deep/component.tsx +++ b/example/deep/component.tsx @@ -4,8 +4,14 @@ import * as Iso from "@eno/iso"; export default ()=> { const [countGet, countSet] = React.useState(1); + + const [routeGet, routeSet] = Iso.Router.Consumer(); + return
+ Route is: {routeGet.Path.toString()} + Component!!! + a link
; }; \ No newline at end of file diff --git a/example/deno.json b/example/deno.json index bb0e33d..ed849e6 100644 --- a/example/deno.json +++ b/example/deno.json @@ -8,6 +8,7 @@ "@eno/iso": "http://localhost:4507/lib/iso.tsx" }, "tasks": { + "host": "deno run -A --unstable https://deno.land/std@0.181.0/http/file_server.ts", "dev": "deno run -A --unstable --reload=http://localhost:4507/ --no-lock --config=deno.json 'http://localhost:4507/server.tsx?reload=1'" } } \ No newline at end of file diff --git a/lib/iso.tsx b/lib/iso.tsx index 9243188..2b1789a 100644 --- a/lib/iso.tsx +++ b/lib/iso.tsx @@ -1,6 +1,5 @@ import React from "react"; - type Meta = {title:string, description:string, keywords:string, image:string, canonical:string } type MetaKeys = keyof Meta; export const Meta:Meta = { @@ -22,13 +21,141 @@ export const Metas =(props:{concatListed?:boolean; dropUnlisted?:boolean}&MetasI const propValue = props[metaKey]||""; propValue ? additive(metaKey, propValue) : subtractive(metaKey); }) - - console.log(`rendering metas`, Meta) - if(window.innerWidth) { document.title = Meta.title; } return null; -} \ No newline at end of file +} + + +export type Children = string | number | React.JSX.Element | React.JSX.Element[]; + +type RoutePath = Array; +type RouteParams = Record; +type RouteState = {URL:URL, Path:RoutePath, Params:RouteParams, Anchor:string}; +type RouteContext = [Route:RouteState, Update:(inPath?:RoutePath, inParams?:RouteParams, inAnchor?:string)=>void]; +type RouteProps = {children:Children, url?:URL }; +export const Router = { + Parse(url:URL):RouteState + { + const Path = url.pathname.substring(1, url.pathname.endsWith("/") ? url.pathname.length-1 : url.pathname.length).split("/"); + const Params:RouteParams = {}; + new URLSearchParams(url.search).forEach((k, v)=> Params[k] = v); + const Anchor = url.hash.substring(1); + return {URL:url, Path, Params, Anchor} as RouteState; + }, + Context:React.createContext([{URL:new URL("https://original.route/"), Path:[], Params:{}, Anchor:""}, ()=>{}] as RouteContext), + Provider(props:RouteProps) + { + const [routeGet, routeSet] = React.useState(Router.Parse(props.url || new URL(document.location.href))); + const [dirtyGet, dirtySet] = React.useState(true); + + const routeUpdate:RouteContext[1] =(inPath, inParams, inAnchor)=> + { + const clone = new URL(routeGet.URL); + inPath && (clone.pathname = inPath.join("/")); + inParams && (clone.search = new URLSearchParams(inParams as Record).toString()); + routeSet({ + URL:clone, + Path: inPath || routeGet.Path, + Params: inParams || routeGet.Params, + Anchor: inAnchor || routeGet.Anchor + }); + }; + + // when the state changes, update the page url + React.useEffect(()=> dirtyGet ? dirtySet(false) : history.pushState({...routeGet, URL:undefined}, "", routeGet.URL), [routeGet.URL.href]); + + React.useEffect(()=>{ + history.replaceState({...routeGet, URL:undefined}, "", routeGet.URL); + window.addEventListener("popstate", ({state})=> + { + dirtySet(true); + routeUpdate(state.Path, state.Params, state.Anchor); + }); + document.addEventListener("click", e=> + { + const t = e.target as HTMLAnchorElement; + if(t.href) + { + const u = new URL(t.href); + if(u.origin == document.location.origin) + { + e.preventDefault(); + const parts = Router.Parse(u); + routeUpdate(parts.Path, parts.Params, parts.Anchor); + } + } + }) + }, []); + + return {props.children}; + }, + Consumer() + { + return React.useContext(Router.Context); + } +}; + +type SwitchContext = {depth:number, keys:Record}; +export const SwitchContext = React.createContext({depth:0, keys:{}} as SwitchContext); +export const Switch =({children}:{children:Children})=> +{ + let fallback = null; + if(Array.isArray(children)) + { + const contextSelection = React.useContext(SwitchContext); + const [contextRoute] = Router.Consumer(); + const routeSegment = contextRoute.Path.slice(contextSelection.depth); + const checkChild =(inChild:{props:{value?:string}})=> + { + if(inChild?.props?.value) + { + const parts = inChild.props.value.split("/"); + if(parts.length > routeSegment.length) + { + return false; + } + + const output:SwitchContext = {depth:contextSelection.depth+parts.length, keys:{}}; + for(let i=0; i{childCaseChildren} + } + if(childCase?.props?.default && !fallback) + { + console.log(routeSegment); + fallback = childCaseChildren; + } + } + } + + return fallback; +}; +export const Case =({children, value}:{children:Children, value?:string, default?:true})=>null; +export const useRouteVars =()=> React.useContext(SwitchContext).keys; \ No newline at end of file diff --git a/server.tsx b/server.tsx index f07399e..300d9c6 100644 --- a/server.tsx +++ b/server.tsx @@ -3,9 +3,16 @@ import * as MIME from "https://deno.land/std@0.180.0/media_types/mod.ts"; import { debounce } from "https://deno.land/std@0.151.0/async/debounce.ts"; import SSR from "https://esm.sh/v113/preact-render-to-string@6.0.2"; import * as Twind from "https://esm.sh/@twind/core@1.1.3"; -import React, {JSX, createElement as h} from "react"; +import React from "react"; import * as Iso from "@eno/iso"; +let hosted = import.meta.resolve("./"); +const Path = { + Hosted:hosted.substring(0, hosted.length-1), + Active:`file://${Deno.cwd().replaceAll("\\", "/")}` +}; +console.log(Path); + const Transpiled = new Map(); const Transpileable =(inFilePath:string):boolean=> { @@ -28,24 +35,11 @@ const Transpileable =(inFilePath:string):boolean=> }; const Transpile =async(inCode:string, inKey:string):Promise=> { - const transpile = await ESBuild.transform(inCode, { loader: "tsx", sourcemap: "inline", minify:true }); + const transpile = await ESBuild.transform(inCode, { loader: "tsx", minify:true}); Transpiled.set(inKey, transpile.code); return transpile.code; }; type Transpiler = (inPath:string, inKey:string, inCheck?:boolean)=>Promise; -const TranspileFS:Transpiler =async(inPath, inKey, inCheck)=> -{ - if(inCheck) - { - const cached = Transpiled.get(inKey); - if(cached) - { - return cached; - } - } - const body = await Deno.readTextFile(inPath); - return Transpile(body, inKey); -}; const TranspileURL:Transpiler =async(inPath, inKey, inCheck)=> { if(inCheck) @@ -56,8 +50,7 @@ const TranspileURL:Transpiler =async(inPath, inKey, inCheck)=> return cached; } } - const path = import.meta.resolve(`.${inPath}`); - let body = await fetch(path); + let body = await fetch(inPath); let text = await body.text(); return Transpile(text, inKey); }; @@ -68,8 +61,8 @@ type ImportMap = {imports?:Record, importMap?:string}; let ImportObject:ImportMap = {}; try { - const confDeno = await Deno.readTextFile(Deno.cwd()+"/deno.json"); - const confDenoParsed:ImportMap = JSON.parse(confDeno); + const confDeno = await fetch(`${Path.Active}/deno.json`); + const confDenoParsed:ImportMap = await confDeno.json(); if(confDenoParsed.importMap) { try @@ -147,14 +140,13 @@ catch(e) Deno.serve({ port: Deno.args[0]||3000 }, async(_req:Request) => { const url:URL = new URL(_req.url); - const fsPath = Deno.cwd()+url.pathname; const pathParts = url.pathname.substring(1, url.pathname.endsWith("/") ? url.pathname.length-1 : url.pathname.length).split("/"); - const pathLast = pathParts[pathParts.length-1]; - const pathExt:string|undefined = pathLast.split(".")[1]; + const pathLast = pathParts.at(-1); + const pathExt:string|undefined = pathLast?.split(".")[1]; console.log(pathParts, pathLast, pathExt); - console.log(`Request for "${url.pathname}"...`); + console.log(`Request for "${url.pathname}"...`); if(_req.headers.get("upgrade") == "websocket") { @@ -189,17 +181,16 @@ Deno.serve({ port: Deno.args[0]||3000 }, async(_req:Request) => const isLib = url.pathname.startsWith(`/${LibPath}/`); - if(Transpileable(pathLast)) + if(Transpileable(url.pathname)) { type = `application/javascript`; if(isLib) { - body = await TranspileURL(url.pathname, url.pathname, true); + body = await TranspileURL(Path.Hosted+url.pathname, url.pathname, true); } else if(!url.searchParams.get("reload")) { - const path = `file://${Deno.cwd().replaceAll("\\", "/")+url.pathname}`; - const imp = await import(path); + const imp = await import(Path.Active+url.pathname); const members = []; for( const key in imp ) { members.push(key); } body = @@ -218,26 +209,19 @@ FileListen("${url.pathname}", reloadHandler);`; } else { - body = await TranspileFS(fsPath, url.pathname, true); + body = await TranspileURL(Path.Active+url.pathname, url.pathname, true); } } // serve static media else if( pathExt ) { type = MIME.typeByExtension(pathExt) || "text/html"; - if(isLib) - { - const _fetch = await fetch(import.meta.resolve(`.${url.pathname}`)); - body = await _fetch.text(); - } - else - { - body = await Deno.readFile(fsPath); - } + const _fetch = await fetch((isLib ? Path.Hosted : Path.Active)+url.pathname); + body = await _fetch.text(); } else { - const results = Twind.extract(SSR(), TwindInst); + const results = Twind.extract(SSR(), TwindInst); type = `text/html`; body = ` @@ -248,16 +232,16 @@ FileListen("${url.pathname}", reloadHandler);`; -
${results.html}
`; @@ -289,7 +273,7 @@ const ProcessFiles =debounce(async()=> if(action != "remove") { - await TranspileFS(path, key, false); + await TranspileURL(Path.Active+key, key, false); console.log(` ...cached "${key}"`); SocketsBroadcast(key); }