diff --git a/deep/file.txt b/deep/file.txt deleted file mode 100644 index e69de29..0000000 diff --git a/deno.json b/deno.json index df9bcff..2e7b733 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,14 @@ { + "compilerOptions": {"lib": [ + "deno.window", "DOM" + ]}, + "imports": { + "react-original": "https://esm.sh/react@18.2.0" + }, "tasks": { - "install": "deno install -f -A --unstable --no-lock -n tsx server.tsx" + "install": "deno install -f -A --unstable --no-lock -n eno server.tsx", + "run": "deno run -A --unstable --no-lock server.tsx", + "host": "deno run -A --unstable https://deno.land/std@0.181.0/http/file_server.ts" } } \ No newline at end of file diff --git a/deno.lock b/deno.lock deleted file mode 100644 index f896ca5..0000000 --- a/deno.lock +++ /dev/null @@ -1,10 +0,0 @@ -{ - "version": "2", - "remote": { - "https://deno.land/std@0.151.0/async/debounce.ts": "564273ef242bcfcda19a439132f940db8694173abffc159ea34f07d18fc42620", - "https://deno.land/x/denoflate@1.2.1/mod.ts": "f5628e44b80b3d80ed525afa2ba0f12408e3849db817d47a883b801f9ce69dd6", - "https://deno.land/x/denoflate@1.2.1/pkg/denoflate.js": "b9f9ad9457d3f12f28b1fb35c555f57443427f74decb403113d67364e4f2caf4", - "https://deno.land/x/denoflate@1.2.1/pkg/denoflate_bg.wasm.js": "d581956245407a2115a3d7e8d85a9641c032940a8e810acbd59ca86afd34d44d", - "https://deno.land/x/esbuild@v0.14.45/mod.js": "ab18347f6a57e56f776a9997a5b726f7f6c8d1f008f08e41e7273be8c31e12f4" - } -} diff --git a/lib/hmr.tsx b/lib/hmr.tsx new file mode 100644 index 0000000..59e1d1d --- /dev/null +++ b/lib/hmr.tsx @@ -0,0 +1,74 @@ + +let reloads = 0; +const listeners = new Map() as Mapvoid>>; +new WebSocket("ws://"+document.location.host).addEventListener('message', (event) => +{ + let handlers = listeners.get(event.data)??[]; + reloads++; + Promise.all( + handlers.map(handler=> + { + return import(event.data+"?reload="+reloads) + .then(updatedModule=>handler(updatedModule)); + }) + ).then(()=>HMR.update()); +}); + +export const FileListen =(inPath:string, inHandler:()=>void)=> +{ + const members = listeners.get(inPath)??[]; + members.push(inHandler); + listeners.set(inPath, members); +}; + +const HMR = { + reloads:0, + registered: new Map() as Mapvoid>, + states: new Map(), + statesOld: new Map(), + wireframe: false, + onChange(key:string, value:()=>void):void + { + this.registered.set(key, value); + }, + update() + { + this.reloads++; + this.registered.forEach(handler=>handler()); + this.registered.clear(); + this.statesOld = this.states; + this.states = new Map(); + this.echoState(); + }, + 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); + } +}; + +export {HMR}; + +export const MapAt =(inMap, inIndex)=> +{ + let index = 0; + for(const kvp of inMap) + { + if(index == inIndex) + { + return kvp; + } + index++; + } + return false; +}; \ No newline at end of file diff --git a/lib/react.tsx b/lib/react.tsx new file mode 100644 index 0000000..71780c7 --- /dev/null +++ b/lib/react.tsx @@ -0,0 +1,76 @@ +import * as ReactParts from "react-original"; +import { HMR, MapAt } from "./hmr.tsx"; + +const H = ReactParts.createElement; + +const ProxyElement = (props)=> +{ + const id = ReactParts.useId(); + const [stateGet, stateSet] = ReactParts.useState(0); + ReactParts.useEffect(()=>HMR.onChange(id, ()=>stateSet(stateGet+1))); + + 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-original"; +export { ProxyCreate as createElement, ProxyState as useState }; +export default {...ReactParts.default, createElement:ProxyCreate, useState:ProxyState}; \ No newline at end of file diff --git a/runner.tsx b/runner.tsx deleted file mode 100644 index f6b7cfa..0000000 --- a/runner.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import * as FS from "https://deno.land/std@0.144.0/fs/mod.ts"; - -console.log(Deno.args, import.meta.url); -/* -await Deno.mkdir("deep"); -await Deno.create(`deep/file.txt`, "sup"); -*/ \ No newline at end of file diff --git a/server.tsx b/server.tsx index 2664a9a..e4444d5 100644 --- a/server.tsx +++ b/server.tsx @@ -1,118 +1,132 @@ import * as ESBuild from 'https://deno.land/x/esbuild@v0.14.45/mod.js'; +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"; -console.log(`Serving files from "${Deno.cwd()}"`); - -const MIME:Record = { - ".aac": "audio/aac", - ".abw": "application/x-abiword", - ".arc": "application/x-freearc", - ".avif": "image/avif", - ".avi": "video/x-msvideo", - ".azw": "application/vnd.amazon.ebook", - ".bin": "application/octet-stream", - ".bmp": "image/bmp", - ".bz": "application/x-bzip", - ".bz2": "application/x-bzip2", - ".cda": "application/x-cdf", - ".csh": "application/x-csh", - ".css": "text/css", - ".csv": "text/csv", - ".doc": "application/msword", - ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - ".eot": "application/vnd.ms-fontobject", - ".epub": "application/epub+zip", - ".gz": "application/gzip", - ".gif": "image/gif", - ".htm": "text/html", - ".html": "text/html", - ".ico": "image/x-icon", - ".ics": "text/calendar", - ".jar": "application/java-archive", - ".jpeg": "image/jpeg", - ".jpg": "image/jpeg", - ".js": "application/javascript", - ".jsx": "application/javascript", - ".json": "application/json", - ".jsonld": "application/ld+json", - ".mid": "audio/midi", - ".midi": "audio/midi", - ".mjs": "text/javascript", - ".mp3": "audio/mpeg", - ".mp4": "video/mp4", - ".mpeg": "video/mpeg", - ".mpkg": "application/vnd.apple.installer+xml", - ".odp": "application/vnd.oasis.opendocument.presentation", - ".ods": "application/vnd.oasis.opendocument.spreadsheet", - ".odt": "application/vnd.oasis.opendocument.text", - ".oga": "audio/ogg", - ".ogv": "video/ogg", - ".ogx": "application/ogg", - ".opus": "audio/opus", - ".otf": "font/otf", - ".png": "image/png", - ".pdf": "application/pdf", - ".php": "application/x-httpd-php", - ".ppt": "application/vnd.ms-powerpoint", - ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", - ".rar": "application/vnd.rar", - ".rtf": "application/rtf", - ".sh": "application/x-sh", - ".svg": "image/svg+xml", - ".swf": "application/x-shockwave-flash", - ".tar": "application/x-tar", - ".tif": "image/tiff", - ".tiff": "image/tiff", - ".ts": "application/javascript", - ".tsx": "application/javascript", - ".ttf": "font/ttf", - ".txt": "text/plain", - ".vsd": "application/vnd.visio", - ".wav": "audio/wav", - ".weba": "audio/webm", - ".webm": "video/webm", - ".webp": "image/webp", - ".woff": "font/woff", - ".woff2": "font/woff2", - ".xhtml": "application/xhtml+xml", - ".xls": "application/vnd.ms-excel", - ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ".xml": "application/xml", - ".xul": "application/vnd.mozilla.xul+xml", - ".zip": "application/zip", - ".3gp": "video/3gpp", - ".3g2": "video/3gpp2", - ".7z": "application/x-7z-compressed" -}; -const Type =(inPath:string):string|undefined=> -{ - const dot:number = inPath.lastIndexOf("."); - const ext = dot > -1 ? inPath.substring(dot) : ".html"; - return MIME[ext]; -}; const Transpiled = new Map(); -const Transpile =async(inPath:string, inKey:string):Promise=> +const Transpileable =(inFilePath:string):boolean=> { - const body = await Deno.readTextFile(inPath); - const transpile = await ESBuild.transform(body, { loader: "tsx", sourcemap: "inline" }); + let dotIndex = inFilePath.length-4; + if(inFilePath[dotIndex] !== ".") + { + if(inFilePath[++dotIndex] !== ".") + { + return false; + } + } + + if(inFilePath[dotIndex+2] == "s") + { + const first = inFilePath[dotIndex+1]; + return (first == "t" || first == "j"); + } + + return false; +}; +const Transpile =async(inCode:string, inKey:string):Promise=> +{ + const transpile = await ESBuild.transform(inCode, { loader: "tsx", sourcemap: "inline", 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) + { + const cached = Transpiled.get(inKey); + if(cached) + { + return cached; + } + } + const path = import.meta.resolve(`.${inPath}`); + let body = await fetch(path); + let text = await body.text(); + return Transpile(text, inKey); +}; -const confDeno = await Deno.readTextFile(Deno.cwd()+"/deno.json"); -const pathImport = JSON.parse(confDeno).importMap; -const confImports = await Deno.readTextFile(pathImport); +const LibPath = "lib"; +type ImportMap = {imports?:Record, importMap?:string}; +let ImportString = ``; +let ImportObject:ImportMap = {}; +try +{ + const confDeno = await Deno.readTextFile(Deno.cwd()+"/deno.json"); + const confDenoParsed:ImportMap = JSON.parse(confDeno); + if(confDenoParsed.importMap) + { + try + { + const confImports = await Deno.readTextFile(confDenoParsed.importMap); + try + { + ImportObject = JSON.parse(confImports); + ImportString = confImports; + } + catch(e) + { + console.log(`"importMap" "${confDenoParsed.importMap}" contains invalid JSON`); + } + } + catch(e) + { + console.log(`"importMap" "${confDenoParsed.importMap}" cannot be found`); + } + } + else if(confDenoParsed.imports) + { + 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"] = `./${LibPath}/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 = ` + +
Loading
- - @@ -136,42 +153,90 @@ const Index = ` Deno.serve({ port: 3000 }, async(_req:Request) => { const url:URL = new URL(_req.url); - const type = Type(url.pathname); const fsPath = Deno.cwd()+url.pathname; console.log(`Request for "${url.pathname}"...`); + if(_req.headers.get("upgrade") == "websocket") + { + try + { + const { response, socket } = Deno.upgradeWebSocket(_req); + socket.onopen = () => + { + Sockets.add(socket); + console.log("Overwatch: Socket created"); + }; + socket.onclose = () => + { + Sockets.delete(socket); + console.log("Overwatch: Socket deleted"); + }; + socket.onmessage = (e) => {}; + socket.onerror = (e) => console.log("Overwatch: Socket errored:", e); + return response; + } + catch(e) + { + // + } + } + try { - let body:BodyInit; - if(type == "application/javascript") + // serve index by default + let type = `text/html`; + let body:BodyInit = Index; + + const isLib = url.pathname.startsWith(`/${LibPath}/`) + + // serve .tsx .jsx .ts .js + if(Transpileable(url.pathname)) { - body = Transpiled.get(url.pathname); - if(!body) + type = `application/javascript`; + if(isLib) { - body = await Transpile(fsPath, url.pathname); - console.log(` ...added to cache`); + body = await TranspileURL(url.pathname, url.pathname, true); + } + else 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 {FileListen} from "/${LibPath}/hmr.tsx"; +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") } +}; +FileListen("${url.pathname}", reloadHandler);`; + } else { - console.log(` ...retrieved from cache`); + body = await TranspileFS(fsPath, url.pathname, true); } } - else + // serve static media + else if( url.pathname.endsWith("/") === false) { - if(url.pathname == "/") + type = MIME.typeByExtension(url.pathname.substring(url.pathname.lastIndexOf("."))) || "text/html"; + if(isLib) { - body = Index; + const _fetch = await fetch(import.meta.resolve(`.${url.pathname}`)); + body = await _fetch.text(); } else { - let file = fsPath; - if(url.pathname.endsWith("/")) - { - console.log(` ...implied index.html`); - file += "index.html"; - } - body = await Deno.readFile(file); + body = await Deno.readFile(fsPath); } } @@ -179,35 +244,48 @@ Deno.serve({ port: 3000 }, async(_req:Request) => } catch(error) { - console.log(` ...404`); + console.log(error); return new Response(error, {status:404}); } }); -const filesChanged:Map = new Map(); + +const Sockets:Set = new Set(); +const SocketsBroadcast =(inData:string)=>{ for (const socket of Sockets){ socket.send(inData); } } + +const FilesChanged:Map = new Map(); const ProcessFiles =debounce(async()=> { - console.log("Files changed...") - for await (const [file, action] of filesChanged) + console.log("Processing Files...", FilesChanged); + for await (const [path, action] of FilesChanged) { - const pathname = file.substring(Deno.cwd().length).replaceAll("\\", "/"); - const type = Type(pathname); - console.log(pathname, type, action); - if(type == "application/javascript") + const key = path.substring(Deno.cwd().length).replaceAll("\\", "/"); + console.log(key, action); + + if(action != "remove") { - if(action !== "remove") - { - await Transpile(file, pathname); - console.log(` ...cached "${pathname}"`); - } + await TranspileFS(path, key, false); + console.log(` ...cached "${key}"`); + SocketsBroadcast(key); + } + else + { + Transpiled.delete(key) } } - filesChanged.clear(); + FilesChanged.clear(); }, 1000); - -const watcher = Deno.watchFs(Deno.cwd()); -for await (const event of watcher) +for await (const event of Deno.watchFs(Deno.cwd())) { - event.paths.forEach( path => filesChanged.set(path, event.kind) ); - ProcessFiles(); + event.paths.forEach( path => + { + if(Transpileable(path)) + { + FilesChanged.set(path, event.kind); + } + }); + if(FilesChanged.size) + { + ProcessFiles(); + } } \ No newline at end of file