From 052dd13bb9958b5f96965a20665f1a52e5d9fbfd Mon Sep 17 00:00:00 2001 From: Seth Trowbridge Date: Tue, 27 Jun 2023 21:54:39 -0400 Subject: [PATCH 1/3] import conversion --- example/dyn-test.tsx | 12 +++ iso-menu.tsx | 224 +++++++++++++++++++++++++++++++++++++++++++ run-serve.tsx | 33 ++++++- 3 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 example/dyn-test.tsx create mode 100644 iso-menu.tsx diff --git a/example/dyn-test.tsx b/example/dyn-test.tsx new file mode 100644 index 0000000..6bb8bf1 --- /dev/null +++ b/example/dyn-test.tsx @@ -0,0 +1,12 @@ + + +import('https://esm.sh/react').then((module) => { + console.log(module); +}); + + +function unimport(n:number) +{ + return n; +} +unimport(123) \ No newline at end of file diff --git a/iso-menu.tsx b/iso-menu.tsx new file mode 100644 index 0000000..bf2ebe3 --- /dev/null +++ b/iso-menu.tsx @@ -0,0 +1,224 @@ +import React from "react"; + +type StateArgs = {done?:boolean, open?:boolean}; +type StateObj = {done:boolean, open:boolean}; +type StateBinding = [state:StateObj, update:(args:StateArgs)=>void]; + +const CTX = React.createContext([{done:true, open:false}, (args)=>{}] as StateBinding); + +export const Group =(props:{children:React.JSX.Element|React.JSX.Element[]})=> +{ + const [stateGet, stateSet] = React.useState({done:true, open:false} as StateObj); + return stateSet({...stateGet, ...args})]}>{props.children}; +}; + +export const Menu =(props:{children:React.JSX.Element|React.JSX.Element[]})=> +{ + const [stateGet, stateSet] = React.useContext(CTX); + const refElement:React.MutableRefObject = React.useRef( null ); + const refControl:React.MutableRefObject = React.useRef( null ); + const refInitial:React.MutableRefObject = React.useRef(true); + + type MenuClassStates = {Keep:string, Open:string, Shut:string, Move:string, Exit:string}; + const base = `relative transition-all border(8 black) overflow-hidden`; + const Classes:MenuClassStates = + { + Shut: `${base} h-0 top-0 w-1/2 duration-300`, + Open: `${base} h-auto top-8 w-full duration-700`, + lol: `${base} h-auto top-36 bg-yellow-500 w-2/3 duration-700`, + }; + const Window = window as {TwindInst?:(c:string)=>string}; + if(Window.TwindInst) + { + for(let stateName in Classes) + { + Classes[stateName as keyof MenuClassStates] = Window.TwindInst(Classes[stateName as keyof MenuClassStates]); + } + } + + React.useEffect(()=> + { + refControl.current = refElement.current && Collapser(refElement.current, stateGet.open ? "Open" : "Shut", Classes); + } + , []); + React.useEffect(()=> + { + (!refInitial.current && refControl.current) && refControl.current(stateGet.open ? "Open" : "Shut", ()=>stateSet({done:true})); + refInitial.current = false; + } + , [stateGet.open]); + + useAway(refElement, (e)=>stateSet({open:false, done:false}) ); + + return
} class={Classes.Shut}> + { (!stateGet.open && stateGet.done) ? null : props.children} +
; +}; + +export const Button =()=> +{ + const [stateGet, stateSet] = React.useContext(CTX); + return <> +

{JSON.stringify(stateGet)}

+ + + ; +}; + +type Handler = (e:MouseEvent)=>void +const Refs:Map> = new Map(); +function isHighest(inElement:HTMLElement, inSelection:HTMLElement[]) +{ + let currentNode = inElement; + while (currentNode != document.body) + { + currentNode = currentNode.parentNode as HTMLElement; + if(currentNode.hasAttribute("data-use-away") && inSelection.includes(currentNode)) + { + return false; + } + } + return true; +} +window.innerWidth && document.addEventListener("click", e=> +{ + const path = e.composedPath(); + const away:HTMLElement[] = []; + + Refs.forEach( (handlerRef, element)=> + { + if(!path.includes(element) && handlerRef.current) + { + away.push(element); + } + }); + + away.forEach((element)=> + { + if(isHighest(element, away)) + { + const handler = Refs.get(element); + handler?.current && handler.current(e); + } + }); + +} +, true); +const useAway =(inRef:React.Ref, handleAway:Handler)=> +{ + const refHandler:React.MutableRefObject = React.useRef(handleAway); + refHandler.current = handleAway; + + React.useEffect(()=> + { + if(inRef.current) + { + inRef.current.setAttribute("data-use-away", "0"); + Refs.set(inRef.current, refHandler); + } + return ()=> inRef.current && Refs.delete(inRef.current); + } + , []); +}; + +type StyleSize = [classes:string, width:number, height:number]; +type StylePack = Record; +type StyleCalc = Record; +const StyleCalc =(inElement:HTMLElement, inClasses:StylePack)=> +{ + const initialStyle = inElement.getAttribute("style")||""; + const initialClass = inElement.getAttribute("class")||""; + const output = {} as StyleCalc; + + inElement.setAttribute("style", `transition: none;`); + Object.entries(inClasses).forEach(([key, value])=> + { + inElement.setAttribute("class", value); + output[key] = [value, inElement.offsetWidth, inElement.offsetHeight]; + }); + inElement.setAttribute("class", initialClass); + inElement.offsetHeight; // this has be be exactly here + inElement.setAttribute("style", initialStyle); + + return output; +}; + +type DoneCallback =(inState:string)=>void; +export type CollapseControls =(inOpen?:string, inDone?:DoneCallback)=>void; +export function Collapser(inElement:HTMLElement, initialState:string, library:Record) +{ + let userDone:DoneCallback = (openState) => {}; + let userMode = initialState; + let frameRequest = 0; + let inTransition = false; + let measurements:StyleCalc; + const transitions:Set = new Set(); + + const run = (inEvent:TransitionEvent)=> (inEvent.target == inElement) && transitions.add(inEvent.propertyName); + const end = (inEvent:TransitionEvent)=> + { + if (inEvent.target == inElement) + { + transitions.delete(inEvent.propertyName); + if(transitions.size === 0) + { + measurements = StyleCalc(inElement, library); + const [, w, h] = measurements[userMode]; + if(inElement.offsetHeight != h || inElement.offsetWidth != w) + { + anim(userMode, userDone); + } + else + { + inElement.setAttribute("style", ""); + inTransition = false; + userDone(userMode); + } + } + } + }; + const anim = function(inState:string, inDone) + { + cancelAnimationFrame(frameRequest); + + if(arguments.length) + { + if(!library[inState]){ return; } + + userDone = inDone|| ((m)=>{}) as DoneCallback; + userMode = inState; + + if(!inTransition) + { + measurements = StyleCalc(inElement, library); + } + + if(measurements) + { + const [classes, width, height] = measurements[inState] as StyleSize; + const oldWidth = inElement.offsetWidth; + const oldHeight = inElement.offsetHeight; + inElement.style.width = oldWidth + "px"; + inElement.style.height = oldHeight + "px"; + inTransition = true; + + frameRequest = requestAnimationFrame(()=> + { + inElement.style.height = height + "px"; + inElement.style.width = width + "px"; + inElement.className = classes; + }); + } + } + else + { + inElement.removeEventListener("transitionrun", run); + inElement.removeEventListener("transitionend", end); + } + } as CollapseControls; + + inElement.addEventListener("transitionend", end); + inElement.addEventListener("transitionrun", run); + + return anim; +} diff --git a/run-serve.tsx b/run-serve.tsx index f67320a..b0dc224 100644 --- a/run-serve.tsx +++ b/run-serve.tsx @@ -55,7 +55,16 @@ let Configuration:Configuration = Allow: "*", Reset: "/clear-cache", Spoof: "/@able", - Serve(inReq, inURL, inExt, inMap, inConfig){}, + async Serve(inReq, inURL, inExt, inMap, inConfig) + { + if(inReq.headers.get("user-agent")?.startsWith("Deno")) + { + const file = await fetch(inConfig.Proxy + inURL.pathname); + const text = await file.text(); + + return new Response(Transpile.Patch(text), {headers:{"content-type":"application/javascript"}} ); + } + }, Remap: (inImports, inConfig)=> { const reactURL = inImports["react"]; @@ -89,6 +98,9 @@ let Configuration:Configuration = }, SWCOp: { + env:{ + dynamicImport:false + }, sourceMaps: false, minify: true, jsc: @@ -127,6 +139,25 @@ export const Transpile = ImportMapReload(); return size; }, + Patch(inText) + { + const regex = /(? Date: Sat, 1 Jul 2023 10:43:29 -0400 Subject: [PATCH 2/3] apply import maps --- example/dyn-test.tsx | 5 +++- run-serve.tsx | 65 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/example/dyn-test.tsx b/example/dyn-test.tsx index 6bb8bf1..63ca671 100644 --- a/example/dyn-test.tsx +++ b/example/dyn-test.tsx @@ -1,6 +1,9 @@ +import * as Util from "@able/"; +import * as React from "react"; +import {createElement} from "react/client"; -import('https://esm.sh/react').then((module) => { +import('react').then((module) => { console.log(module); }); diff --git a/run-serve.tsx b/run-serve.tsx index b0dc224..bb93c77 100644 --- a/run-serve.tsx +++ b/run-serve.tsx @@ -57,12 +57,17 @@ let Configuration:Configuration = Spoof: "/@able", async Serve(inReq, inURL, inExt, inMap, inConfig) { - if(inReq.headers.get("user-agent")?.startsWith("Deno")) + if( inReq.headers.get("user-agent")?.startsWith("Deno") || inURL.searchParams.get("deno") ) { + console.log("patching...") const file = await fetch(inConfig.Proxy + inURL.pathname); const text = await file.text(); - return new Response(Transpile.Patch(text), {headers:{"content-type":"application/javascript"}} ); + return new Response(Transpile.Patch(text, inMap), {headers:{"content-type":"application/javascript"}} ); + } + else + { + console.log("not a deno-able file") } }, Remap: (inImports, inConfig)=> @@ -139,20 +144,60 @@ export const Transpile = ImportMapReload(); return size; }, - Patch(inText) + /** + * Converts dynamic module imports in to static, also can resolve paths with an import map + */ + Patch(inText:string, inMap?:DenoConfig) { - const regex = /(? + { + const match = inMap.imports[inPath]; + if(match) + { + return match; + } + else if(inPath.includes("/")) + { + let bestKey = ""; + let bestLength = 0; + Object.keys(inMap.imports).forEach((key, i, arr)=> + { + if(key.endsWith("/") && inPath.startsWith(key) && key.length > bestLength) + { + bestKey = key; + bestLength = bestLength; + } + }); + if(bestKey) + { + return inMap.imports[bestKey]+inPath.substring(bestKey.length); + } + } + return inPath; + } + : (inPath:string)=>inPath; + let match, regex; let convertedBody = inText; + + // remap static imports + regex = /from\s+(['"`])(.*?)\1/g; + while ((match = regex.exec(inText))) + { + const importStatement = match[0]; + const importPath = match[2]; + convertedBody = convertedBody.replace(importStatement, `from "${remap(importPath)}"`); + } + + // convert dynamic imports into static (to work around deno deploy) + const staticImports = []; + regex = /(? Date: Sat, 1 Jul 2023 10:53:10 -0400 Subject: [PATCH 3/3] patch caching --- run-serve.tsx | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/run-serve.tsx b/run-serve.tsx index bb93c77..2ca9f30 100644 --- a/run-serve.tsx +++ b/run-serve.tsx @@ -59,15 +59,9 @@ let Configuration:Configuration = { if( inReq.headers.get("user-agent")?.startsWith("Deno") || inURL.searchParams.get("deno") ) { - console.log("patching...") const file = await fetch(inConfig.Proxy + inURL.pathname); const text = await file.text(); - - return new Response(Transpile.Patch(text, inMap), {headers:{"content-type":"application/javascript"}} ); - } - else - { - console.log("not a deno-able file") + return new Response(Transpile.Patch(text, "deno-"+inURL.pathname, inMap), {headers:{"content-type":"application/javascript"}} ); } }, Remap: (inImports, inConfig)=> @@ -147,8 +141,18 @@ export const Transpile = /** * Converts dynamic module imports in to static, also can resolve paths with an import map */ - Patch(inText:string, inMap?:DenoConfig) + Patch(inText:string, inKey:string|false = false, inMap?:DenoConfig) { + + if(inKey) + { + const check = this.Cache.get(inKey); + if(check) + { + return check; + } + } + const remap = inMap ? (inPath:string)=> { const match = inMap.imports[inPath]; @@ -200,8 +204,10 @@ export const Transpile = staticImports.push(`import ${moduleName} from ${importPath};`); convertedBody = convertedBody.replace(importStatement, `Promise.resolve(${moduleName})`); } - - return staticImports.join("\n") + convertedBody; + convertedBody = staticImports.join("\n") + convertedBody; + + inKey && this.Cache.set(inKey, convertedBody); + return convertedBody; }, Fetch: async function(inPath:string, inKey:string, inCheckCache=true) {