From 08ecdfa6f054cd1d837d327e7fef8acf5897d509 Mon Sep 17 00:00:00 2001 From: Seth Trowbridge Date: Wed, 22 Mar 2023 17:45:25 -0400 Subject: [PATCH] serving adjacent files --- hmr/react.tsx | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++ server.tsx | 105 ++++++++++++++++++++++++++------------- 2 files changed, 203 insertions(+), 34 deletions(-) create mode 100644 hmr/react.tsx diff --git a/hmr/react.tsx b/hmr/react.tsx new file mode 100644 index 0000000..3647b1f --- /dev/null +++ b/hmr/react.tsx @@ -0,0 +1,132 @@ +import * as ReactParts from "react-original"; + +const H = ReactParts.createElement; + +const HMR = { + registered: new Map(), + states: new Map(), + statesOld: new Map(), + reloads: 0, + wireframe: false +}; +HMR.onChange =(key, value)=> +{ + HMR.registered.set(key, value); +}; +HMR.update =()=> +{ + HMR.reloads++; + const keys = []; + for(const [key, value] of HMR.registered){ keys.push(key); } + HMR.registered.clear(); + HMR.statesOld = HMR.states; + HMR.states = new Map(); + keys.forEach(k=>k()); + HMR.echoState(); +}; +HMR.echoState =()=> +{ + let output = []; + for(const[key, val] of HMR.statesOld) + { + output[key] = val.state+"--"+val.reload; + } + console.log(output); + output = []; + for(const[key, val] of HMR.states) + { + output[key] = val.state+"--"+val.reload; + } + console.log(output); +}; +HMR.wipe =()=> +{ + HMR.statesOld = new Map(); +}; + +const MapAt =(inMap, inIndex)=> +{ + let index = 0; + for(const kvp of inMap) + { + if(index == inIndex) + { + return kvp; + } + index++; + } + return false; +} + +window.HMR = HMR; + +const ProxyElement = (props)=> +{ + const [stateGet, stateSet] = ReactParts.useState(0); + ReactParts.useEffect(()=>HMR.onChange( ()=>stateSet(stateGet+1), "ProxyElement" )); + + const child = H(...props.__args); + + if(HMR.wireframe) + { + return H("div", {style:{padding:"10px", border:"2px solid red"}}, + H("p", null, stateGet), + child + ); + } + else + { + return child; + } +}; + +const ProxyCreate =(...args)=> +{ + return typeof args[0] != "string" ? H(ProxyElement, {__args:args, ...args[1]}) : H(...args); +}; + +const ProxyState =(arg)=> +{ + const id = ReactParts.useId(); + const trueArg = arg; + + // does statesOld have an entry for this state? use that instead of the passed arg + const check = MapAt(HMR.statesOld, HMR.states.size); + if(check) + { + arg = check[1].state; + console.info(`BOOTING with ${arg}`); + } + + const lastKnowReloads = HMR.reloads; + const [stateGet, stateSet] = ReactParts.useState(arg); + ReactParts.useEffect(()=>{ + return ()=>{ + if(HMR.reloads == lastKnowReloads) + { + // this is a switch/ui change, not a HMR reload change + const oldState = MapAt(HMR.statesOld, HMR.states.size-1); + HMR.statesOld.set(oldState[0], {...oldState[1], state:trueArg}); + } + HMR.states.delete(id); + } + }, []); + + if(!HMR.states.has(id)) + { + HMR.states.set(id, {state:arg, set:stateSet, reload:HMR.reloads}); + } + + function proxySetter (arg) + { + //console.log("state spy update", id, arg); + HMR.states.set(id, {state:arg, set:stateSet, reload:HMR.reloads}); + return stateSet(arg); + } + return [stateGet, proxySetter]; + +}; + +export * from "react-alias"; +export { ProxyCreate as createElement, ProxyState as useState }; +export default {...ReactParts.default, createElement:ProxyCreate, useState:ProxyState}; \ No newline at end of file diff --git a/server.tsx b/server.tsx index 85405c7..cdb2d4d 100644 --- a/server.tsx +++ b/server.tsx @@ -34,13 +34,13 @@ export const Transpile =async(inPath:string, inKey:string):Promise=> }; +type ImportMap = {imports?:Record, importMap?:string}; let ImportString = ``; -let ImportObject = {}; +let ImportObject:ImportMap = {}; try { - const confDeno = await Deno.readTextFile(Deno.cwd()+"/deno.json"); - const confDenoParsed:{importMap?:string, imports?:string} = JSON.parse(confDeno); + const confDenoParsed:ImportMap = JSON.parse(confDeno); if(confDenoParsed.importMap) { try @@ -53,12 +53,12 @@ try } catch(e) { - console.log(`importMap "${confDenoParsed.importMap}" contains invalid JSON`); + console.log(`"importMap" "${confDenoParsed.importMap}" contains invalid JSON`); } } catch(e) { - console.log(`importMap "${confDenoParsed.importMap}" cannot be found`); + console.log(`"importMap" "${confDenoParsed.importMap}" cannot be found`); } } else if(confDenoParsed.imports) @@ -66,11 +66,33 @@ try ImportObject = {imports:confDenoParsed.imports}; ImportString = JSON.stringify(ImportObject); } + + if(ImportObject.imports) + { + const importReact = ImportObject.imports?.["react"]; + if(importReact) + { + ImportObject.imports["react-original"] = importReact; + ImportObject.imports["react"] = "/hmr/react.tsx"; + ImportString = JSON.stringify(ImportObject); + } + else + { + console.log(`"imports" configuration does not alias "react"`); + } + } + else + { + console.log(`No "imports" found in configuration`); + } + } catch(e) { console.log(`deno.json not found`); } + + const Index = ` @@ -130,6 +152,9 @@ window.HMR = (path, handler)=> `; +const path = import.meta.resolve(`./hmr/react.sx`); +console.log("meta path", path); + Deno.serve({ port: 3000 }, async(_req:Request) => { const url:URL = new URL(_req.url); @@ -168,38 +193,50 @@ Deno.serve({ port: 3000 }, async(_req:Request) => let type = `text/html`; let body:BodyInit = Index; - // serve .tsx .jsx .ts .js - if(Transpileable(url.pathname)) + if(url.pathname.startsWith("/hmr/")) { - type = `application/javascript`; - if(!url.searchParams.get("reload")) - { - const path = `file://${Deno.cwd().replaceAll("\\", "/")+url.pathname}`; - console.log(path); - - const imp = await import(path); - const members = []; - for( const key in imp ) { members.push(key); } - body = -`import * as Import from "${url.pathname}?reload=0"; -${ members.map(m=>`let proxy_${m} = Import.${m}; export { proxy_${m} as ${m} };`).join(" ") } -const reloadHandler = (updatedModule)=> -{ - ${ members.map(m=>`proxy_${m} = updatedModule.${m};`).join("\n") } -}; -window.HMR("${url.pathname}", reloadHandler);`; - - } - else - { - body = Transpiled.get(url.pathname) || await Transpile(fsPath, url.pathname); - } + const path = import.meta.resolve(`.${url.pathname}`); + console.log("import path", path); + const resp = await fetch(path); + body = await resp.text(); + //body = Transpiled.get(url.pathname) || await Transpile("."+url.pathname, url.pathname); } - // serve static media - else if( url.pathname.endsWith("/") === false) + else { - body = await Deno.readFile(fsPath); - type = MIME.typeByExtension(url.pathname.substring(url.pathname.lastIndexOf("."))) || "text/html"; + // serve .tsx .jsx .ts .js + if(Transpileable(url.pathname)) + { + type = `application/javascript`; + if(!url.searchParams.get("reload")) + { + const path = `file://${Deno.cwd().replaceAll("\\", "/")+url.pathname}`; + console.log(path); + + const imp = await import(path); + const members = []; + for( const key in imp ) { members.push(key); } + body = + `import * as Import from "${url.pathname}?reload=0"; + ${ members.map(m=>`let proxy_${m} = Import.${m}; export { proxy_${m} as ${m} };`).join(" ") } + const reloadHandler = (updatedModule)=> + { + ${ members.map(m=>`proxy_${m} = updatedModule.${m};`).join("\n") } + }; + window.HMR("${url.pathname}", reloadHandler);`; + + } + else + { + body = Transpiled.get(url.pathname) || await Transpile(fsPath, url.pathname); + } + } + // serve static media + else if( url.pathname.endsWith("/") === false) + { + body = await Deno.readFile(fsPath); + type = MIME.typeByExtension(url.pathname.substring(url.pathname.lastIndexOf("."))) || "text/html"; + } + } return new Response(body, {headers:{"content-type":type as string}});