Compare commits
	
		
			No commits in common. "master" and "hmr-cleanup" have entirely different histories.
		
	
	
		
			master
			...
			hmr-cleanu
		
	
		
| @ -1,2 +0,0 @@ | ||||
| deno.lock | ||||
| .env | ||||
							
								
								
									
										21
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							| @ -1,21 +0,0 @@ | ||||
| { | ||||
|     // Use IntelliSense to learn about possible attributes. | ||||
|     // Hover to view descriptions of existing attributes. | ||||
|     // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | ||||
|     "version": "0.2.0", | ||||
|     "configurations": [ | ||||
|         { | ||||
|             "name": "Debug Serve Mode", | ||||
|             "request": "launch", | ||||
|             "type": "node", | ||||
|             "runtimeExecutable": "deno", | ||||
|             "runtimeArgs": ["task", "debug"], | ||||
|             "attachSimplePort": 9229 | ||||
|         }, | ||||
|         { | ||||
|             "name":"Attach", | ||||
|             "request": "attach", | ||||
|             "type": "node" | ||||
|         } | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										5
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| { | ||||
|     "deno.enable": true, | ||||
|     "deno.unstable": true, | ||||
|     "deno.config": "./deno.json" | ||||
| } | ||||
							
								
								
									
										74
									
								
								_lib_/hmr.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								_lib_/hmr.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,74 @@ | ||||
| import { type StateCapture } from "./react.tsx"; | ||||
| 
 | ||||
| 
 | ||||
| const FileListeners = new Map() as Map<string, Array<(module:unknown)=>void>>; | ||||
| export const FileListen =(inPath:string, inHandler:()=>void)=> | ||||
| { | ||||
|     const members = FileListeners.get(inPath)??[]; | ||||
|     members.push(inHandler); | ||||
|     FileListeners.set(inPath, members); | ||||
| }; | ||||
| 
 | ||||
| const Socket:WebSocket = new WebSocket("ws://"+document.location.host); | ||||
| Socket.addEventListener('message', async(event:{data:string})=> | ||||
| { | ||||
|     // When a file changes, dynamically re-import it to get the updated members
 | ||||
|     // send the updated members to any listeners for that file
 | ||||
|     const reImport = await import(event.data+"?reload="+Math.random()); | ||||
|     FileListeners.get(event.data)?.forEach(reExport=>reExport(reImport)); | ||||
|     HMR.update(); | ||||
| }); | ||||
| Socket.addEventListener("error", ()=>{clearInterval(SocketTimer); console.log("HMR socket lost")}) | ||||
| const SocketTimer = setInterval(()=>{Socket.send("ping")}, 5000); | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| 
 | ||||
| Each custom component is secretly modified to have an extra state and id. | ||||
| When there is an HMR update, this state is changed, forcing it to re-render. | ||||
| 
 | ||||
| Each *user-created* React.useState is secretly modified and accompanied by an ID. | ||||
| Every time its state is set, the HMR.statesNew map for this ID is set to contain the new state and updater. | ||||
| When a component is removed, any of it's states in HMR.statesNew are also removed.  | ||||
| (HMR.statesNew is the "running total" of all states currently at play). | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| When a state is interacted with: | ||||
| - statesNew for this id is set | ||||
| - the internal state is also set in the traditional way | ||||
| 
 | ||||
| When there is an HMR update: | ||||
| - All custom components are re-rendered... | ||||
|   for each useState(value) call that then happens in the re-render: | ||||
|   - if there is a "statesOld" value for this state, use that and ignore the passed value, otherwise use the passed value | ||||
|   - if this state has not been interacted with since the last reload (statesNew is empty at this id), set statesNew<id> with whatever is in statesOld<id> | ||||
| - statesNew is moved into *statesOld* | ||||
| - statesNew is cleared. | ||||
| 
 | ||||
| 
 | ||||
| */ | ||||
| 
 | ||||
| 
 | ||||
| const HMR = | ||||
| { | ||||
|     reloads:1, | ||||
|     RegisteredComponents: new Map() as Map<string, ()=>void>, | ||||
|     statesNew: new Map() as Map<string, StateCapture>, | ||||
|     statesOld: new Map() as Map<string, StateCapture>, | ||||
|     wireframe: false, | ||||
|     RegisterComponent(reactID:string, value:()=>void):void | ||||
|     { | ||||
|         this.RegisteredComponents.set(reactID, value); | ||||
|     }, | ||||
|     update() | ||||
|     { | ||||
|         this.reloads++; | ||||
|         this.RegisteredComponents.forEach(handler=>handler()); | ||||
|         this.RegisteredComponents.clear(); | ||||
|         this.statesOld = this.statesNew; | ||||
|         this.statesNew = new Map(); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export {HMR}; | ||||
| @ -1,7 +1,7 @@ | ||||
| import React from "react"; | ||||
| import * as TW from   "@twind/core"; | ||||
| import TWPreTail from "https://esm.sh/v126/@twind/preset-tailwind@1.1.3/es2022/preset-tailwind.mjs"; | ||||
| import TWPreAuto from "https://esm.sh/v126/@twind/preset-autoprefix@1.0.7/es2022/preset-autoprefix.mjs"; | ||||
| import * as TW from   "https://esm.sh/@twind/core@1.0.1"; | ||||
| import TWPreTail from "https://esm.sh/@twind/preset-tailwind@1.0.1"; | ||||
| import TWPreAuto from "https://esm.sh/@twind/preset-autoprefix@1.0.1"; | ||||
| 
 | ||||
| const Configure = | ||||
| { | ||||
| @ -10,25 +10,22 @@ const Configure = | ||||
|   hash: false | ||||
| } as TW.TwindUserConfig; | ||||
| 
 | ||||
| export const Shadow =(inElement:HTMLElement, inConfig:TW.TwindUserConfig)=> | ||||
| export const Shadow =(inElement:HTMLElement, inConfig?:TW.TwindUserConfig)=> | ||||
| { | ||||
|     const merge = inConfig ? {...Configure, ...inConfig} : Configure; | ||||
| 
 | ||||
|     const ShadowDOM = inElement.attachShadow({ mode: "open" }); | ||||
|     const ShadowDiv = document.createElement("div"); | ||||
|     const ShadowCSS = document.createElement("style"); | ||||
| 
 | ||||
|     ShadowDOM.append(ShadowCSS); | ||||
|     ShadowDOM.append(ShadowDiv); | ||||
|     TW.observe(TW.twind(inConfig, TW.cssom(ShadowCSS)), ShadowDiv); | ||||
|     TW.observe(TW.twind(merge, TW.cssom(ShadowCSS)), ShadowDiv); | ||||
|     return ShadowDiv; | ||||
| }; | ||||
| 
 | ||||
| export default async(inSelector:string, inModulePath:string, inMemberApp="default", inMemberCSS="CSS", inShadow=false):Promise<(()=>void)|false>=> | ||||
| export default async(inSelector:string, inModulePath:string, inMemberApp="default", inMemberCSS="CSS"):Promise<(()=>void)|false>=> | ||||
| { | ||||
|   if(!inModulePath) | ||||
|   { | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   let dom = document.querySelector(inSelector); | ||||
|   if(!dom) | ||||
|   { | ||||
| @ -37,17 +34,7 @@ export default async(inSelector:string, inModulePath:string, inMemberApp="defaul | ||||
|   } | ||||
| 
 | ||||
|   const module = await import(inModulePath); | ||||
| 
 | ||||
|   const merge = inMemberCSS ? {...Configure, ...module[inMemberCSS]} : Configure; | ||||
| 
 | ||||
|   if(inShadow) | ||||
|   { | ||||
|     dom = Shadow(dom as HTMLElement, merge); | ||||
|   } | ||||
|   else | ||||
|   { | ||||
|     TW.install(merge); | ||||
|   } | ||||
|   dom = Shadow(dom as HTMLElement, module[inMemberCSS]) | ||||
| 
 | ||||
|   const app = React.createElement(()=> React.createElement(module[inMemberApp], null), null); | ||||
|   if(React.render) | ||||
| @ -62,4 +49,5 @@ export default async(inSelector:string, inModulePath:string, inMemberApp="defaul | ||||
|     root.render(app); | ||||
|     return root.unmount;         | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| }; | ||||
| @ -1,51 +1,5 @@ | ||||
| import * as ReactParts from "react-original"; | ||||
| 
 | ||||
| /* | ||||
| 
 | ||||
| Each custom component is secretly modified to have an extra state and id. | ||||
| When there is an HMR update, this state is changed, forcing it to re-render. | ||||
| 
 | ||||
| Each *user-created* React.useState is secretly modified and accompanied by an ID. | ||||
| Every time its state is set, the HMR.statesNew map for this ID is set to contain the new state and updater. | ||||
| When a component is removed, any of it's states in HMR.statesNew are also removed.  | ||||
| (HMR.statesNew is the "running total" of all states currently at play). | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| When a state is interacted with: | ||||
| - statesNew for this id is set | ||||
| - the internal state is also set in the traditional way | ||||
| 
 | ||||
| When there is an HMR update: | ||||
| - All custom components are re-rendered... | ||||
|   for each useState(value) call that then happens in the re-render: | ||||
|   - if there is a "statesOld" value for this state, use that and ignore the passed value, otherwise use the passed value | ||||
|   - if this state has not been interacted with since the last reload (statesNew is empty at this id), set statesNew<id> with whatever is in statesOld<id> | ||||
| - statesNew is moved into *statesOld* | ||||
| - statesNew is cleared. | ||||
| 
 | ||||
| */ | ||||
| export const HMR = | ||||
| { | ||||
|     reloads:1, | ||||
|     RegisteredComponents: new Map() as Map<string, ()=>void>, | ||||
|     statesNew: new Map() as Map<string, StateCapture>, | ||||
|     statesOld: new Map() as Map<string, StateCapture>, | ||||
|     wireframe: false, | ||||
|     RegisterComponent(reactID:string, value:()=>void):void | ||||
|     { | ||||
|         this.RegisteredComponents.set(reactID, value); | ||||
|     }, | ||||
|     update() | ||||
|     { | ||||
|         this.reloads++; | ||||
|         this.RegisteredComponents.forEach(handler=>handler()); | ||||
|         this.RegisteredComponents.clear(); | ||||
|         this.statesOld = this.statesNew; | ||||
|         this.statesNew = new Map(); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| import { HMR } from "./hmr.tsx"; | ||||
| 
 | ||||
| export type StateType = boolean|number|string|Record<string, string> | ||||
| export type StateCapture = {state:StateType, set:ReactParts.StateUpdater<StateType>, reload:number}; | ||||
| @ -114,6 +68,7 @@ const ProxyState =(argNew:StateType)=> | ||||
|                 // this is a switch/ui change, not a HMR reload change
 | ||||
|                 const oldState = MapIndex(HMR.statesOld, HMR.statesNew.size-1); | ||||
|                 oldState && HMR.statesOld.set(oldState[0], {...oldState[1], state:argNew}); | ||||
|                 console.log("check: ui-invoked") | ||||
|             } | ||||
| 
 | ||||
|             HMR.statesNew.delete(id); | ||||
| @ -157,6 +112,7 @@ const ProxyReducer =(inReducer:(inState:Storelike, inAction:string)=>Storelike, | ||||
|         const capture = inReducer(inInterceptState, inInterceptAction); | ||||
|         const stateUser = {state:capture, set:()=>{}, reload:HMR.reloads}; | ||||
|         HMR.statesNew.set(id, stateUser); | ||||
|         console.log("interepted reducer", stateUser); | ||||
|         return capture; | ||||
|     }; | ||||
| 
 | ||||
							
								
								
									
										7
									
								
								app.tsx
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								app.tsx
									
									
									
									
									
								
							| @ -1,7 +0,0 @@ | ||||
| import * as ISO from ">able/iso-elements.tsx"; | ||||
| 
 | ||||
| console.log(ISO) | ||||
| 
 | ||||
| export default ()=><div> | ||||
|     <h1 class="p-4 bg-red-500 text-white">App</h1> | ||||
| </div>; | ||||
							
								
								
									
										233
									
								
								checker.tsx
									
									
									
									
									
								
							
							
						
						
									
										233
									
								
								checker.tsx
									
									
									
									
									
								
							| @ -1,233 +0,0 @@ | ||||
| import { parse as JSONC } from "https://deno.land/x/jsonct@v0.1.0/mod.ts"; | ||||
| 
 | ||||
| type ConfigCheck = {path?:string, text?:string, json?:Record<string, string|Record<string, string|string[]>>}; | ||||
| type ConfigCheckPair = [config:ConfigCheck, imports:ConfigCheck]; | ||||
| 
 | ||||
| export const RootHost = import.meta.resolve("./"); | ||||
| export const Root = new URL(`file://${Deno.cwd().replaceAll("\\", "/")}`).toString(); | ||||
| export async function HuntConfig() | ||||
| { | ||||
|     console.log("hunting in", Root); | ||||
|     let path:string, resp:Response, text="", json; | ||||
|     try | ||||
|     { | ||||
|         path = "deno.json" | ||||
|         resp = await fetch(Root + "/" + path); | ||||
|         text = await resp.text(); | ||||
|     } | ||||
|     catch(e) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             path = "deno.jsonc"; | ||||
|             resp = await fetch(Root + "/" + path); | ||||
|             text = await resp.text(); | ||||
|         } | ||||
|         catch(e) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 path = ".vscode/settings.json"; | ||||
|                 resp = await fetch(Root + "/" + path); | ||||
|                 json = await resp.json(); | ||||
| 
 | ||||
|                 path = json["deno.config"]; | ||||
|                 json = undefined; | ||||
|                 if(path) | ||||
|                 { | ||||
|                     resp = await fetch(Root + "/" + path); | ||||
|                     text = await resp.text(); | ||||
|                 } | ||||
|             } | ||||
|             catch(e) | ||||
|             { | ||||
|                 path = ""; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if(path) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             json = JSONC(text); | ||||
|         } | ||||
|         catch(e) | ||||
|         { | ||||
|             json = undefined; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     let imports:ConfigCheck = {}; | ||||
|     if(json && json.imports) | ||||
|     { | ||||
|         // config.imports
 | ||||
|         imports.json = json; | ||||
|         imports.text = JSON.stringify(json); | ||||
|         imports.path = path; | ||||
|     } | ||||
|     else if(json && !json.imports && json.importMap) | ||||
|     { | ||||
|         // config.importMap
 | ||||
|         try | ||||
|         { | ||||
|             imports.path = json.importMap; | ||||
|             resp = await fetch(Root + "/" + imports.path); | ||||
|             imports.text = await resp.text(); | ||||
|             try | ||||
|             { | ||||
|                 imports.json = JSONC(imports.text); | ||||
|             } | ||||
|             catch(e) | ||||
|             { | ||||
|                 imports.json = undefined; | ||||
|             } | ||||
|         } | ||||
|         catch(e) | ||||
|         { | ||||
|             // malformed import map
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return [{path, text, json}, imports] as ConfigCheckPair | ||||
| } | ||||
| 
 | ||||
| export async function Install(file:string, overrideName?:string, handler?:(content:string)=>string) | ||||
| { | ||||
|     const pathFile = RootHost + "install__/" + file; | ||||
| 
 | ||||
|     try{ | ||||
|         const check = await Deno.readTextFile(Deno.cwd()+"/"+file); | ||||
|         const replace = confirm(`⚠️🚧 The file "${file}" already exists. Replace it?`); | ||||
|         if(replace) | ||||
|         { | ||||
|             throw("") | ||||
|         } | ||||
|         console.log(`Using pre-existing "${file}" for now.`); | ||||
|     } | ||||
|     catch(e) | ||||
|     { | ||||
|         const resp = await fetch(pathFile); | ||||
|         const text = await resp.text(); | ||||
|         const name = overrideName || file; | ||||
|         await Deno.writeTextFile(Deno.cwd()+"/"+name, handler ? handler(text) : text);    | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export async function Check() | ||||
| { | ||||
|     let [config, imports] = await HuntConfig(); | ||||
|     try | ||||
|     { | ||||
|          | ||||
|         //console.log(config, imports);
 | ||||
|         if(!config.path) | ||||
|         { | ||||
|             console.log(`🛠️ No Deno configuration found. Creating "deno.json" now.`); | ||||
|             await Deno.writeTextFile(Deno.cwd()+"/deno.json", `{"imports":{}}`);    | ||||
|             Check(); | ||||
|             return; | ||||
|         } | ||||
|         else if(!config.json) | ||||
|         { | ||||
|             if(confirm(`🚧 Deno configuration is malformed. Replace "${config.path}" with a new one?.`)) | ||||
|             { | ||||
|                 await Deno.writeTextFile(Deno.cwd()+"/"+config.path, `{"imports":{}}`);    | ||||
|                 Check(); | ||||
|                 return; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 throw("⛔ Invalid configuration."); | ||||
|             } | ||||
|         } | ||||
|         else if(!imports.json) | ||||
|         {         | ||||
|             if(imports.path != config.path) | ||||
|             { | ||||
|                 if(confirm(`🚧 External import map "${imports.path}" is missing or malformed. Replace it with defaults?.`)) | ||||
|                 { | ||||
|                     await Deno.writeTextFile(Deno.cwd()+"/"+imports.path, `{"imports":{}}`);    | ||||
|                     Check(); | ||||
|                     return; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     throw("⛔ Invalid configuration."); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         else if(!imports.json?.imports) | ||||
|         { | ||||
|             imports.json.imports = {}; | ||||
|         } | ||||
| 
 | ||||
|         if(config.json && imports.json?.imports) | ||||
|         { | ||||
|             const importMap = imports.json.imports as Record<string, string>; | ||||
|             const bake =async(obj:ConfigCheck)=> await Deno.writeTextFile(Deno.cwd()+"/"+obj.path, JSON.stringify(obj.json, null, "\t"));  | ||||
| 
 | ||||
|             importMap["react"] = `https://esm.sh/preact@10.20.2/compat`; | ||||
|             importMap["react/"] = `https://esm.sh/preact@10.20.2/compat/`; | ||||
|             importMap["@preact/signals"] = `https://esm.sh/@preact/signals@1.2.3?deps=preact@10.20.2`; | ||||
|             importMap["@twind/core"] = `https://esm.sh/v126/@twind/core@1.1.3/es2022/core.mjs`; | ||||
|             importMap[">able/"] = `${RootHost}`; | ||||
|             if(!importMap[">able/app.tsx"]) | ||||
|             { | ||||
|                 importMap[">able/app.tsx"] = `./app.tsx`; | ||||
|                 await Install("app.tsx"); | ||||
|             } | ||||
|             if(!importMap[">able/api.tsx"]) | ||||
|             { | ||||
|                 if(confirm(`🤔 OPTIONAL: Add backend ">able/api.tsx"?`)) | ||||
|                 { | ||||
|                     importMap[">able/api.tsx"] = "./api.tsx"; | ||||
|                     await Install("api.tsx"); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             const tasks:Record<string, string> = { | ||||
|                 "check": `deno run -A --no-lock ${RootHost}cli.tsx check`, | ||||
|                 "local": `deno run -A --no-lock ${RootHost}cli.tsx local`, | ||||
|                 "debug": `deno run -A --no-lock ${RootHost}cli.tsx debug`, | ||||
|                 "serve": `deno run -A --no-lock ${RootHost}cli.tsx serve`, | ||||
|                 "cloud": `deno run -A --no-lock ${RootHost}cli.tsx cloud`         | ||||
|             }; | ||||
|             const confTasks = (config.json.tasks || {}) as Record<string, string>; | ||||
|             config.json.tasks = {...confTasks, ...tasks}; | ||||
| 
 | ||||
|             const optionsRequired =  | ||||
|             { | ||||
|                 "lib": ["deno.window", "dom", "dom.iterable", "dom.asynciterable"], | ||||
|                 "jsx": "react-jsx" | ||||
|             } | ||||
|             const optionsCurrent = config.json.compilerOptions as Record<string, string|string[]> || {}; | ||||
|             //const compLib:string[] = compOpts.lib as string[] || [];
 | ||||
| 
 | ||||
|             if(!optionsCurrent.lib) | ||||
|             { | ||||
|                 optionsCurrent.lib = []; | ||||
|             } | ||||
|             optionsRequired.lib.forEach(s=> | ||||
|             { | ||||
|                 if(!optionsCurrent.lib.includes(s)) | ||||
|                 { | ||||
|                     (optionsCurrent.lib as string[]).push(s); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             optionsCurrent.jsx = optionsRequired.jsx; | ||||
|             config.json.compilerOptions = optionsCurrent; | ||||
| 
 | ||||
|             await bake(imports); | ||||
|             await bake(config); | ||||
|         } | ||||
|     } | ||||
|     catch(e) | ||||
|     { | ||||
|         console.log(e, "\n (Able Exiting...)"); | ||||
|         Deno.exit(); | ||||
|     } | ||||
|     console.log(`🚗 Good to go!`); | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										140
									
								
								cli.tsx
									
									
									
									
									
								
							
							
						
						
									
										140
									
								
								cli.tsx
									
									
									
									
									
								
							| @ -1,140 +0,0 @@ | ||||
| import * as Env from "https://deno.land/std@0.194.0/dotenv/mod.ts"; | ||||
| import * as Arg from "https://deno.land/std@0.194.0/flags/mod.ts"; | ||||
| import { RootHost, HuntConfig, Install, Check } from "./checker.tsx"; | ||||
| 
 | ||||
| let arg = await Arg.parse(Deno.args); | ||||
| let env = await Env.load(); | ||||
| const collect =async(inKey:string, inArg:Record<string, string>, inEnv:Record<string, string>):Promise<string|undefined>=> | ||||
| { | ||||
|     const scanArg = inArg[inKey]; | ||||
|     const scanEnvFile = inEnv[inKey]; | ||||
|     const scanEnvDeno = Deno.env.get(inKey); | ||||
| 
 | ||||
|     if(scanArg) | ||||
|     { | ||||
|         console.log(`Using "${inKey}" from passed argument.`); | ||||
|         return scanArg; | ||||
|     } | ||||
|     if(scanEnvFile) | ||||
|     { | ||||
|         console.log(`Using "${inKey}" from .env file.`); | ||||
|         return scanEnvFile; | ||||
|     } | ||||
|     if(scanEnvDeno) | ||||
|     { | ||||
|         console.log(`Using "${inKey}" from environment variable.`); | ||||
|         return scanEnvDeno; | ||||
|     } | ||||
| 
 | ||||
|     const scanUser = prompt(`No "${inKey}" found. Enter one here:`); | ||||
|     if(!scanUser || scanUser?.length < 3) | ||||
|     { | ||||
|         console.log("Exiting..."); | ||||
|         Deno.exit(); | ||||
|     } | ||||
|     return scanUser; | ||||
| }; | ||||
| 
 | ||||
| export async function SubProcess(args:string[]) | ||||
| { | ||||
|     const command = new Deno.Command( | ||||
|         `deno`, | ||||
|         { | ||||
|             args, | ||||
|             stdin: "piped", | ||||
|             stdout: "piped" | ||||
|         } | ||||
|     ); | ||||
| 
 | ||||
|     const child = command.spawn(); | ||||
| 
 | ||||
|     // open a file and pipe the subprocess output to it.
 | ||||
|     const writableStream = new WritableStream({ | ||||
|         write(chunk: Uint8Array): Promise<void> { | ||||
|             Deno.stdout.write(chunk); | ||||
|             return Promise.resolve(); | ||||
|         }, | ||||
|     }); | ||||
|     child.stdout.pipeTo(writableStream); | ||||
| 
 | ||||
|     // manually close stdin
 | ||||
|     child.stdin.close(); | ||||
|     const status = await child.status;     | ||||
|      | ||||
|     return status; | ||||
| } | ||||
| 
 | ||||
| if(arg._.length) | ||||
| { | ||||
| 
 | ||||
|     const [config, imports] = await HuntConfig(); | ||||
| 
 | ||||
|     console.log("able subprocesses running with ", config.path); | ||||
| 
 | ||||
|     switch(arg._[0]) | ||||
|     { | ||||
|         case "check" : | ||||
|         case "setup" : | ||||
|         { | ||||
|             await Check(); | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         case "local" : | ||||
|         { | ||||
|             await SubProcess(["run", `-A`, `--no-lock`, `--config=${config.path}`, RootHost+"run.tsx", "--dev", ...Deno.args]); | ||||
|             break; | ||||
|         } | ||||
|         case "debug" : | ||||
|         { | ||||
|             await SubProcess(["run", `-A`, `--no-lock`, `--config=${config.path}`, `--inspect-brk`, RootHost+"run.tsx", "--dev", ...Deno.args]); | ||||
|             break; | ||||
|         } | ||||
|         case "serve" : | ||||
|         { | ||||
|             const args = ["run", `-A`, `--no-lock`, `--config=${config.path}`, RootHost+"run.tsx", ...Deno.args]; | ||||
|             console.log("args are", args); | ||||
|             await SubProcess(args); | ||||
|             break; | ||||
|         } | ||||
|         case "cloud" : | ||||
|         { | ||||
|             const useToken = await collect("DENO_DEPLOY_TOKEN", arg, env); | ||||
|             const useProject = await collect("DENO_DEPLOY_PROJECT", arg, env); | ||||
|              | ||||
|             let scanProd:string[]|string|null = prompt(`Do you want to deploy to *production*?`); | ||||
|             if(scanProd) | ||||
|             { | ||||
|                 scanProd = prompt(`Are you sure? This will update the live project at "${useProject}"`); | ||||
|                 scanProd = scanProd ? ["--prod"] : []; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 scanProd = []; | ||||
|             } | ||||
| 
 | ||||
|             const command = [ | ||||
|                 "run", | ||||
|                 "-A", | ||||
|                 "--no-lock", | ||||
|                 `--config=${config.path}`, | ||||
|                 "https://deno.land/x/deploy@1.12.0/deployctl.ts", | ||||
|                 "deploy", | ||||
|                 `--project=${useProject}`, | ||||
|                 `--token=${useToken}`, | ||||
|                 `--import-map=${imports.path}`, | ||||
|                 `--exclude=.*`, | ||||
|                 ...scanProd, | ||||
|                 RootHost+"run.tsx"]; | ||||
| 
 | ||||
|             await SubProcess(command); | ||||
| 
 | ||||
|             break; | ||||
|         } | ||||
|         case "upgrade" : | ||||
|         { | ||||
|             await SubProcess(["install", `-A`, `-r`, `-f`, `--no-lock`, `--config=${config.path}`, RootHost+"cli.tsx", ...Deno.args]); | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										17
									
								
								deno.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								deno.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| { | ||||
|     "compilerOptions": { "lib": ["deno.window", "dom"], | ||||
|         "jsx": "react-jsx", | ||||
|         "jsxImportSource": "https://esm.sh/preact@10.15.1/compat" | ||||
|     }, | ||||
|     "imports": | ||||
|     { | ||||
|         "react":"https://esm.sh/preact@10.15.1/compat", | ||||
|         "react-original":"https://esm.sh/preact@10.15.1/compat", | ||||
|         "@app": "./app.tsx" | ||||
|     }, | ||||
|     "tasks": | ||||
|     { | ||||
|         "local": "deno run -A --no-lock ./local.tsx", | ||||
|         "serve": "deno run -A --no-lock ./serve.tsx" | ||||
|     } | ||||
| } | ||||
							
								
								
									
										28
									
								
								deno.jsonc
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								deno.jsonc
									
									
									
									
									
								
							| @ -1,28 +0,0 @@ | ||||
| { | ||||
| 	"imports": { | ||||
| 
 | ||||
| 		"react": "https://esm.sh/preact@10.20.2/compat", | ||||
| 		"react-original": "https://esm.sh/preact@10.20.2/compat", | ||||
| 		"react-original/": "https://esm.sh/preact@10.20.2/compat/", | ||||
| 		"react/": "https://esm.sh/preact@10.20.2/compat/", | ||||
| 		"@preact/signals": "https://esm.sh/@preact/signals@1.2.3?deps=preact@10.20.2", | ||||
| 		"signals-original": "https://esm.sh/@preact/signals@1.2.3?deps=preact@10.20.2", | ||||
| 		"@twind/core": "https://esm.sh/v126/@twind/core@1.1.3/es2022/core.mjs", | ||||
| 
 | ||||
| 		">other/": "https://esm.sh/", | ||||
| 		">able/": "./", | ||||
| 		">able/app.tsx": "./app.tsx" | ||||
| 	}, | ||||
| 	"tasks": { | ||||
| 		"check": "deno run -A --no-lock ./cli.tsx check", | ||||
| 		"local": "deno run -A --no-lock ./cli.tsx local", | ||||
| 		"debug": "deno run -A --no-lock ./cli.tsx debug", | ||||
| 		"serve": "deno run -A --no-lock ./cli.tsx serve", | ||||
| 		"cloud": "deno run -A --no-lock ./cli.tsx cloud", | ||||
| 		"install": "deno install -A -r -f -n able ./cli.tsx" | ||||
| 	}, | ||||
| 	"compilerOptions": { | ||||
| 		"jsx": "react-jsx", | ||||
| 		"lib": ["deno.window", "dom", "dom.iterable", "dom.asynciterable"] | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										47
									
								
								deno.lock
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								deno.lock
									
									
									
									
									
								
							| @ -1,32 +1,23 @@ | ||||
| { | ||||
|   "version": "3", | ||||
|   "version": "2", | ||||
|   "remote": { | ||||
|     "https://deno.land/std@0.180.0/media_types/_db.ts": "7606d83e31f23ce1a7968cbaee852810c2cf477903a095696cdc62eaab7ce570", | ||||
|     "https://deno.land/std@0.180.0/media_types/_util.ts": "916efbd30b6148a716f110e67a4db29d6949bf4048997b754415dd7e42c52378", | ||||
|     "https://deno.land/std@0.180.0/media_types/content_type.ts": "c682589a0aeb016bfed355cc1ed6fbb3ead2ea48fc0000ac5de6a5730613ad1c", | ||||
|     "https://deno.land/std@0.180.0/media_types/extension.ts": "7a4ef2813d7182f724a941f38161525996e4a67abc3cf6a0f9bc2168d73a0f0e", | ||||
|     "https://deno.land/std@0.180.0/media_types/extensions_by_type.ts": "4358023feac696e6e9d49c0f1e76a859f03ca254df57812f31f8536890c3a443", | ||||
|     "https://deno.land/std@0.180.0/media_types/format_media_type.ts": "1e35e16562e5c417401ffc388a9f8f421f97f0ee06259cbe990c51bae4e6c7a8", | ||||
|     "https://deno.land/std@0.180.0/media_types/get_charset.ts": "8be15a1fd31a545736b91ace56d0e4c66ea0d7b3fdc5c90760e8202e7b4b1fad", | ||||
|     "https://deno.land/std@0.180.0/media_types/mod.ts": "d3f0b99f85053bc0b98ecc24eaa3546dfa09b856dc0bbaf60d8956d2cdd710c8", | ||||
|     "https://deno.land/std@0.180.0/media_types/parse_media_type.ts": "bed260d868ea271445ae41d748e7afed9b5a7f407d2777ead08cecf73e9278de", | ||||
|     "https://deno.land/std@0.180.0/media_types/type_by_extension.ts": "6076a7fc63181d70f92ec582fdea2c927eb2cfc7f9c9bee9d6add2aca86f2355", | ||||
|     "https://deno.land/std@0.180.0/media_types/vendor/mime-db.v1.52.0.ts": "6925bbcae81ca37241e3f55908d0505724358cda3384eaea707773b2c7e99586", | ||||
|     "https://deno.land/std@0.194.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", | ||||
|     "https://deno.land/std@0.194.0/collections/filter_values.ts": "5b9feaf17b9a6e5ffccdd36cf6f38fa4ffa94cff2602d381c2ad0c2a97929652", | ||||
|     "https://deno.land/std@0.194.0/collections/without_all.ts": "a89f5da0b5830defed4f59666e188df411d8fece35a5f6ca69be6ca71a95c185", | ||||
|     "https://deno.land/std@0.194.0/dotenv/mod.ts": "39e5d19e077e55d7e01ea600eb1c6d1e18a8dfdfc65d68826257a576788da3a4", | ||||
|     "https://deno.land/std@0.194.0/flags/mod.ts": "17f444ddbee43c5487568de0c6a076c7729cfe90d96d2ffcd2b8f8adadafb6e8", | ||||
|     "https://deno.land/x/jsonct@v0.1.0/mod.ts": "dba7e7f3529be6369f5c718e3a18b69f15ffa176006d2a7565073ce6c5bd9f3f", | ||||
|     "https://deno.land/x/jsonct@v0.1.0/src/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", | ||||
|     "https://deno.land/x/jsonct@v0.1.0/src/parse.ts": "a3a016822446b0584b40bae9098df480db5590a9915c9e3c623ba2801cf1b8df", | ||||
|     "https://esm.sh/@swc/wasm-web@1.3.62": "b43fb5cde95beb7736182fa62250235dfa6b71717b9d38aa4e6077f05ec90e5e", | ||||
|     "https://esm.sh/preact@10.20.2/compat/jsx-runtime": "e3942a5ffd734d5eaf0790ada3ed4ad81c0c0c2ff56a8e4740247de259f7fb65", | ||||
|     "https://esm.sh/stable/preact@10.20.2/denonext/compat.js": "2e0564fd10e09b587503f9ecd4407ac8726c79beae80026ac89034a47b270c68", | ||||
|     "https://esm.sh/stable/preact@10.20.2/denonext/compat/jsx-runtime.js": "fbbbceb98af95d1c73181f9e5043fad6cdae30ef9e5fcf90d44ffd6fa6055c02", | ||||
|     "https://esm.sh/stable/preact@10.20.2/denonext/hooks.js": "91d64a217b2f2c9f724042d0ed1b87bf3edf721261e86358aa6fd55501ee915f", | ||||
|     "https://esm.sh/stable/preact@10.20.2/denonext/jsx-runtime.js": "2a5b981955e92e3ff86906ac0e5955ec0e6e5ca71032f3f063912cb85ae9a7f1", | ||||
|     "https://esm.sh/stable/preact@10.20.2/denonext/preact.mjs": "f418bc70c24b785703afb9d4dea8cdc1e315e43c8df620a0c52fd27ad9bd70eb", | ||||
|     "https://esm.sh/v135/@swc/wasm-web@1.3.62/denonext/wasm-web.mjs": "57046d46c9ef1398a294ba7447034f5966e48866a05c309cccec4bb4d6e7c61b" | ||||
|     "https://esm.sh/preact@10.15.1": "2b79349676a4942fbcf835c4efa909791c2f0aeca195225bf22bac9866e94b4e", | ||||
|     "https://esm.sh/preact@10.15.1/compat": "07273e22b1c335b8acc9f33c5e78165319c59bd8e2d0f3e5a2b4e028329424d9", | ||||
|     "https://esm.sh/react@18.2.0": "742d8246041966ba1137ec8c60888c35882a9d2478bce63583875f86c1e3687c", | ||||
|     "https://esm.sh/stable/preact@10.15.1/denonext/compat.js": "bad6b5b4d4fdfa5975b7a8d30410bd6877247f058e4952799fab39f66a94b8cf", | ||||
|     "https://esm.sh/stable/preact@10.15.1/denonext/hooks.js": "5c989ad368cf4f2cb3a5d7d1801843d9348c599fe3e7731d04728f7b845d724e", | ||||
|     "https://esm.sh/stable/preact@10.15.1/denonext/preact.mjs": "30710ac1d5ff3711ae0c04eddbeb706f34f82d97489f61aaf09897bc75d2a628", | ||||
|     "https://esm.sh/stable/react@18.2.0/deno/react.mjs": "a5a73ee24acca4744ee22c51d9357f31968d1f684ce253bde222b4e26d09f49f", | ||||
|     "https://esm.sh/v118/@types/prop-types@15.7.5/index.d.ts": "6a386ff939f180ae8ef064699d8b7b6e62bc2731a62d7fbf5e02589383838dea", | ||||
|     "https://esm.sh/v118/@types/react@18.2.0/global.d.ts": "549df62b64a71004aee17685b445a8289013daf96246ce4d9b087d13d7a27a61", | ||||
|     "https://esm.sh/v118/@types/react@18.2.0/index.d.ts": "b091747b1f503f434d3cac4217a13858baba87b421a7054ffdfd797da7737678", | ||||
|     "https://esm.sh/v118/@types/scheduler@0.16.3/tracing.d.ts": "f5a8b384f182b3851cec3596ccc96cb7464f8d3469f48c74bf2befb782a19de5", | ||||
|     "https://esm.sh/v118/csstype@3.1.2/index.d.ts": "4c68749a564a6facdf675416d75789ee5a557afda8960e0803cf6711fa569288", | ||||
|     "https://esm.sh/v118/preact@10.15.1/compat/src/index.d.ts": "9ec63be9612a10ff72fd4183179cde7d551ce43b3a0c2f549d8f788910d8263d", | ||||
|     "https://esm.sh/v118/preact@10.15.1/compat/src/suspense-list.d.ts": "b8e274324392157ce476ef3a48ae215c9f7003b08525d140645f19eab20d1948", | ||||
|     "https://esm.sh/v118/preact@10.15.1/compat/src/suspense.d.ts": "81f5266e0977a94347505d11b8103024211f2b4f3b2eb2aa276a10d8fd169e65", | ||||
|     "https://esm.sh/v118/preact@10.15.1/hooks/src/index.d.ts": "933eab6436614f8cd8e9b7c9b8bd54c6f3f14c3f065ba42c8c87a42d93de6595", | ||||
|     "https://esm.sh/v118/preact@10.15.1/src/index.d.ts": "fa83186a4b6caca36d52ca2d49b481c3ca5460988d4a8388d44dadc28987fb27", | ||||
|     "https://esm.sh/v118/preact@10.15.1/src/jsx.d.ts": "a6e4b7e4af3b959f8cfd41a0f475c547807ebcec8524d9605ab5c6de79f302fa" | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										52
									
								
								example/app.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								example/app.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | ||||
| import React from "react"; | ||||
| 
 | ||||
| const CTXString = React.createContext("lol"); | ||||
| 
 | ||||
| type StateBinding<T> = [get:T, set:React.StateUpdater<T>]; | ||||
| const CTXState = React.createContext(null) as React.Context<StateBinding<number>|null>; | ||||
| const Outer =(props:{children:React.JSX.Element})=> | ||||
| { | ||||
|     const binding = React.useState(11); | ||||
|     return <CTXState.Provider value={binding}> | ||||
|         {props.children} | ||||
|     </CTXState.Provider> | ||||
| }; | ||||
| const Inner =()=> | ||||
| { | ||||
|     const [stateGet, stateSet] = React.useContext(CTXState) || ["default", ()=>{}]; | ||||
|     return <button onClick={e=>stateSet((old)=>old+1)}>count: {stateGet} :)</button> | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| type Store = {name:string, age:number} | ||||
| const reducer =(inState:Store, inAction:number)=> | ||||
| { | ||||
|     return {...inState, age:inState.age+inAction}; | ||||
| } | ||||
| 
 | ||||
| const builder =(inState:Store):Store=> | ||||
| { | ||||
|     inState.age = 100; | ||||
|     return inState; | ||||
| } | ||||
| 
 | ||||
| export default ()=> | ||||
| { | ||||
|      | ||||
|     const [Store, Dispatch] = React.useReducer(reducer, {name:"seth", age:24} as Store, builder) | ||||
|     return <CTXString.Provider value="intradestink"> | ||||
|         <div class="my-4 font-sans"> | ||||
|             <h1 class="font-black text-xl">Title!!!!</h1> | ||||
|             <h2>subtitle!</h2> | ||||
|             <p> | ||||
|                 <button onClick={e=>Dispatch(1)}>{Store.name}|{Store.age}?</button> | ||||
|             </p> | ||||
|         </div> | ||||
|         <Outer> | ||||
|             <Inner/> | ||||
|         </Outer> | ||||
|         <Outer> | ||||
|             <Inner/> | ||||
|         </Outer> | ||||
|     </CTXString.Provider> | ||||
| } | ||||
							
								
								
									
										13
									
								
								example/deno.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								example/deno.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| { | ||||
|     "compilerOptions": { "lib": ["deno.window", "dom"] }, | ||||
|     "imports": | ||||
|     { | ||||
|         "react":"https://esm.sh/preact@10.15.1/compat", | ||||
|         "@app": "./app.tsx" | ||||
|     }, | ||||
|     "tasks": | ||||
|     { | ||||
|         "local": "deno run -A --no-lock ../local.tsx", | ||||
|         "serve": "deno run -A --no-lock ../serve.tsx" | ||||
|     } | ||||
| } | ||||
| @ -1,31 +0,0 @@ | ||||
| import { HMR } from "./hmr-react.tsx"; | ||||
| import { GroupSignal, GroupSignalHook } from "./hmr-signal.tsx"; | ||||
| 
 | ||||
| const FileListeners = new Map() as Map<string, Array<(module:unknown)=>void>>; | ||||
| export const FileListen =(inPath:string, inHandler:()=>void)=> | ||||
| { | ||||
|     const members = FileListeners.get(inPath)??[]; | ||||
|     members.push(inHandler); | ||||
|     FileListeners.set(inPath, members); | ||||
| }; | ||||
| 
 | ||||
| const Socket:WebSocket = new WebSocket("ws://"+document.location.host); | ||||
| Socket.addEventListener('message', async(event:{data:string})=> | ||||
| { | ||||
|     // When a file changes, dynamically re-import it to get the updated members
 | ||||
|     // send the updated members to any listeners for that file
 | ||||
| 
 | ||||
|     GroupSignal.reset(); | ||||
| 
 | ||||
|     const reImport = await import(document.location.origin+event.data+"?reload="+Math.random()); | ||||
|     FileListeners.get(event.data)?.forEach(reExport=>reExport(reImport)); | ||||
| 
 | ||||
|     GroupSignal.swap(); | ||||
| 
 | ||||
|     GroupSignalHook.reset(); | ||||
|     HMR.update(); | ||||
|     GroupSignalHook.reset(); | ||||
| 
 | ||||
| }); | ||||
| Socket.addEventListener("error", ()=>{clearInterval(SocketTimer); console.log("HMR socket lost")}) | ||||
| const SocketTimer = setInterval(()=>{Socket.send("ping")}, 5000); | ||||
| @ -1,47 +0,0 @@ | ||||
| import * as SignalsParts from "signals-original"; | ||||
| import DeepEqual from "https://esm.sh/deep-eql@4.1.3"; | ||||
| 
 | ||||
| type Entry<T> = [signal:SignalsParts.Signal<T>, initArg:T]; | ||||
| 
 | ||||
| function ProxyGroup<T>(inFunc:(initArg:T)=>SignalsParts.Signal<T>) | ||||
| { | ||||
|     let recordEntry:Entry<T>[] = []; | ||||
|     let recordEntryNew:Entry<T>[] = []; | ||||
|     let recordIndex = 0; | ||||
|     const reset =()=> recordIndex = 0; | ||||
|     const swap =()=> | ||||
|     { | ||||
|         recordEntry = recordEntryNew; | ||||
|         recordEntryNew = [] as Entry<T>[]; | ||||
|     }; | ||||
|     const proxy =(arg:T)=> | ||||
|     { | ||||
|         const lookupOld = recordEntry[recordIndex]; | ||||
|         if(lookupOld && DeepEqual(lookupOld[1], arg)) | ||||
|         { | ||||
|             recordEntryNew[recordIndex] = lookupOld; | ||||
|             recordIndex++; | ||||
|             return lookupOld[0]; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             const sig = inFunc(arg); | ||||
|             recordEntryNew[recordIndex] = [sig, arg]; | ||||
|             recordEntry[recordIndex] = [sig, arg]; | ||||
|             recordIndex++; | ||||
|             return sig; | ||||
|         } | ||||
|     }; | ||||
|     return {reset, swap, proxy}; | ||||
| } | ||||
| 
 | ||||
| export const GroupSignal = ProxyGroup(SignalsParts.signal); | ||||
| export const GroupSignalHook = ProxyGroup(SignalsParts.useSignal); | ||||
| 
 | ||||
| 
 | ||||
| const proxySignal = GroupSignal.proxy; | ||||
| const proxySignalHook = GroupSignalHook.proxy; | ||||
| 
 | ||||
| export * from "signals-original"; | ||||
| export { proxySignal as signal, proxySignalHook as useSignal }; | ||||
| export default {...SignalsParts, signal:proxySignal, useSignal:proxySignalHook}; | ||||
							
								
								
									
										139
									
								
								hmr-static.tsx
									
									
									
									
									
								
							
							
						
						
									
										139
									
								
								hmr-static.tsx
									
									
									
									
									
								
							| @ -1,139 +0,0 @@ | ||||
| 
 | ||||
| type GlyphCheck = (inGlyph:string)=>boolean | ||||
| const isAlphaLike:GlyphCheck =(inGlyph:string)=> | ||||
| { | ||||
|     const inCode = inGlyph.charCodeAt(0); | ||||
| 
 | ||||
|     if(inCode >= 97 && inCode <= 122) | ||||
|     { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     if(inCode >= 65 && inCode <= 90) | ||||
|     { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     return `$_.`.includes(inGlyph); | ||||
| } | ||||
| const isWhiteSpace:GlyphCheck =(inGlyph:string)=> `\n\r\t `.includes(inGlyph); | ||||
| const isQuote:GlyphCheck =(inGlyph:string)=>`"'\``.includes(inGlyph) | ||||
| const isNot =(inCheck:GlyphCheck)=> (inGlyph:string)=>!inCheck(inGlyph); | ||||
| const contiguous =(inText:string, inStart:number, inTest:GlyphCheck):number=> | ||||
| { | ||||
|     let ok = true; | ||||
|     let index = inStart; | ||||
|     let count = 0; | ||||
|     while(ok && count < inText.length) | ||||
|     { | ||||
|         count++; | ||||
|         ok = inTest(inText.charAt(index++)); | ||||
|     } | ||||
|     return index-1; | ||||
| } | ||||
| 
 | ||||
| const findNextExport =(inFile:string, inIndex=0, inLocal:Array<string>, inForeign:Array<string>)=> | ||||
| { | ||||
|     const pos = inFile.indexOf("export", inIndex); | ||||
|     if(pos !== -1) | ||||
|     { | ||||
|         if(!isAlphaLike(inFile.charAt(pos-1)) || !isAlphaLike(inFile.charAt(pos+6))) | ||||
|         { | ||||
|              | ||||
|             const nextCharInd = contiguous(inFile, pos+6, isWhiteSpace); | ||||
|             const nextChar = inFile[nextCharInd]; | ||||
| 
 | ||||
|             //console.log(inFile.substring(pos, nextCharInd+1), `>>${nextChar}<<`)
 | ||||
| 
 | ||||
|             if(nextChar === "*") | ||||
|             { | ||||
|                 const  firstQuoteInd = contiguous(inFile,   nextCharInd+1, isNot(isQuote) ); | ||||
|                 const secondQuoteInd = contiguous(inFile, firstQuoteInd+1, isNot(isQuote) ); | ||||
|                 //console.log("ASTERISK:", inFile.substring(pos, secondQuoteInd+1));
 | ||||
|                 inForeign.push(inFile.substring(nextCharInd, secondQuoteInd+1)); | ||||
|             } | ||||
|             else if(nextChar == "{") | ||||
|             { | ||||
|                 const endBracketInd = contiguous(inFile, nextCharInd, (inGlyph:string)=>inGlyph!=="}"); | ||||
|                 const nextLetterInd = contiguous(inFile, endBracketInd+1, isWhiteSpace); | ||||
|                 if(inFile.substring(nextLetterInd, nextLetterInd+4) == "from") | ||||
|                 { | ||||
|                     const  firstQuoteInd = contiguous(inFile, nextLetterInd+4, isNot(isQuote) ); | ||||
|                     const secondQuoteInd = contiguous(inFile, firstQuoteInd+1, isNot(isQuote) ); | ||||
|                     //console.log(`BRACKET foreign: >>${inFile.substring(nextCharInd, secondQuoteInd+1)}<<`);
 | ||||
|                     inForeign.push(inFile.substring(nextCharInd, secondQuoteInd+1)); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     const members = inFile.substring(nextCharInd+1, endBracketInd).replace(/\s/g, ''); | ||||
|                     members.split(",").forEach(part=> | ||||
|                     { | ||||
|                         const renamed = part.split(" as "); | ||||
|                         inLocal.push(renamed[1] || renamed[0]); | ||||
|                     }); | ||||
|                 } | ||||
|                  | ||||
|             } | ||||
|             else if(isAlphaLike(nextChar)) | ||||
|             { | ||||
|                 const keywordEndInd = contiguous(inFile, nextCharInd, isAlphaLike); | ||||
|                 const keyword = inFile.substring(nextCharInd, keywordEndInd); | ||||
|                 if(keyword === "default") | ||||
|                 { | ||||
|                     inLocal.push(keyword); | ||||
|                     //console.log(`MEMBER: >>${keyword})}<<`);
 | ||||
|                 } | ||||
|                 else if(["const", "let", "var", "function", "class"].includes(keyword)) | ||||
|                 { | ||||
|                     const varStartInd = contiguous(inFile, keywordEndInd+1, isWhiteSpace); | ||||
|                     const varEndInd = contiguous(inFile, varStartInd+1, isAlphaLike); | ||||
|                     //console.log(`MEMBER: >>${inFile.substring(varStartInd, varEndInd)}<<`);
 | ||||
|                     inLocal.push(inFile.substring(varStartInd, varEndInd)) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return pos + 7; | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         return false; | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export const Exports =(inFile:string)=> | ||||
| { | ||||
|     let match = 0 as number|false; | ||||
|     let count = 0; | ||||
|     const local = [] as string[]; | ||||
|     const foreign = [] as string[]; | ||||
|     while(match !== false && count <200) | ||||
|     { | ||||
|         count++; | ||||
|         match = findNextExport(inFile, match, local, foreign); | ||||
|     } | ||||
|     return[local, foreign] as [local:string[], foreign:string[]]; | ||||
| }; | ||||
| 
 | ||||
| export const FileExports =async(inURL:string|URL)=> | ||||
| { | ||||
|     const resp = await fetch(inURL); | ||||
|     const text = await resp.text(); | ||||
|     return Exports(text); | ||||
| } | ||||
| 
 | ||||
| //console.log(await FileExports(import.meta.resolve("./hmr-listen.tsx")));
 | ||||
| 
 | ||||
| /* | ||||
| const [local, global] = Exports(` | ||||
| // export in comment
 | ||||
| export * from "react"; | ||||
| const fakeexport =()=>{}; | ||||
| export{ thing1 as remapped, thing2} | ||||
| export{ thing1 as remapped, thing2} from 'React'; | ||||
| export  | ||||
| export const func=()=>{}; | ||||
| `);
 | ||||
| 
 | ||||
| console.log(local, global); | ||||
| */ | ||||
| @ -1,4 +0,0 @@ | ||||
| export default (req:Request):Response|false=> | ||||
| { | ||||
|     return false; | ||||
| } | ||||
| @ -1,3 +0,0 @@ | ||||
| export default ()=><div> | ||||
|     <h1>App!</h1> | ||||
| </div>; | ||||
							
								
								
									
										396
									
								
								iso-elements.tsx
									
									
									
									
									
								
							
							
						
						
									
										396
									
								
								iso-elements.tsx
									
									
									
									
									
								
							| @ -1,396 +0,0 @@ | ||||
| import React from "react"; | ||||
| 
 | ||||
| type MetasInputs = { [Property in MetaKeys]?: string }; | ||||
| type MetasModeArgs = {concatListed?:string; dropUnlisted?:boolean}; | ||||
| type MetasStackItem = MetasModeArgs&MetasInputs&{id:string, depth:number} | ||||
| type Meta = {title:string, description:string, keywords:string, image:string, canonical:string } | ||||
| type MetaKeys = keyof Meta; | ||||
| 
 | ||||
| export const Meta = | ||||
| { | ||||
|     Stack:[] as MetasStackItem[], | ||||
|     Meta: { | ||||
|         title:"", | ||||
|         description:"", | ||||
|         keywords:"", | ||||
|         image:"", | ||||
|         canonical:"" | ||||
|     } as Meta, | ||||
|     ComputeFinal(inStack:MetasStackItem[], inStart=0) | ||||
|     { | ||||
|         const seed = { | ||||
|             title:"", | ||||
|             description:"", | ||||
|             keywords:"", | ||||
|             image:"", | ||||
|             canonical:"" | ||||
|         }; | ||||
|         if(inStack.length>0) | ||||
|         { | ||||
|             let final = {...seed, ...inStack[0]}; | ||||
|             for(let i=inStart+1; i<inStack.length; i++) | ||||
|             { | ||||
|                 const curr = inStack[i]; | ||||
|                 Object.keys(seed).forEach(key=> | ||||
|                 { | ||||
|                     const lookup = key as MetaKeys | ||||
|                     const valPrev = final[lookup]; | ||||
|                     const valCurr = curr[lookup]; | ||||
| 
 | ||||
|                     if(valPrev && !valCurr) | ||||
|                     { | ||||
|                         final[lookup] = curr.dropUnlisted ? "" : valPrev; | ||||
|                     } | ||||
|                     else if(valPrev && valCurr) | ||||
|                     { | ||||
|                         final[lookup] = curr.concatListed ? valPrev + curr.concatListed + valCurr : valCurr | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         final[lookup] = valCurr||""; | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|             return final; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             return seed; | ||||
|         } | ||||
|     }, | ||||
|     Context: React.createContext([[], ()=>{}] as [Get:MetasStackItem[], Set:React.StateUpdater<MetasStackItem[]>]), | ||||
|     Provider({children}:{children:Children}) | ||||
|     { | ||||
|         const binding = React.useState([] as MetasStackItem[]); | ||||
| 
 | ||||
|         type MetaDOM = {description:NodeListOf<Element>, title:NodeListOf<Element>, image:NodeListOf<Element>, url:NodeListOf<Element>}; | ||||
| 
 | ||||
|         const refElements = React.useRef(null as null | MetaDOM); | ||||
| 
 | ||||
|         React.useEffect(()=> | ||||
|         { | ||||
|             refElements.current = { | ||||
|                 description:document.querySelectorAll(`head > meta[name$='description']`), | ||||
|                 title:document.querySelectorAll(`head > meta[name$='title']`), | ||||
|                 image:document.querySelectorAll(`head > meta[name$='image']`), | ||||
|                 url:document.querySelectorAll(`head > link[rel='canonical']`) | ||||
|             }; | ||||
|         }, []); | ||||
| 
 | ||||
|         React.useEffect(()=> | ||||
|         { | ||||
|             if(refElements.current) | ||||
|             { | ||||
|                 const final = Meta.ComputeFinal(binding[0]); | ||||
| 
 | ||||
|                 refElements.current.url.forEach(e=>e.setAttribute("content", final.canonical||"")); | ||||
|                 document.title = final.title; | ||||
|                 refElements.current.title.forEach(e=>e.setAttribute("content", final.title||"")); | ||||
|                 refElements.current.image.forEach(e=>e.setAttribute("content", final.image||"")); | ||||
|                 refElements.current.description.forEach(e=>e.setAttribute("content", final.description||"")); | ||||
|             } | ||||
|         }); | ||||
|         return <Meta.Context.Provider value={binding}>{children}</Meta.Context.Provider>; | ||||
|     }, | ||||
|     Metas({concatListed=undefined, dropUnlisted=false, ...props}:MetasModeArgs&MetasInputs):null | ||||
|     { | ||||
|         const id = React.useId(); | ||||
|         const [, metasSet] = React.useContext(Meta.Context); | ||||
|         const {depth} = React.useContext(SwitchContext); | ||||
| 
 | ||||
|         React.useEffect(()=>{ | ||||
|             metasSet((m)=>{ | ||||
|                 console.log(`adding meta`, props, depth); | ||||
|                 const clone = [...m]; | ||||
|                 let i; | ||||
|                 for(i=clone.length-1; i>-1; i--) | ||||
|                 { | ||||
|                     if(clone[i].depth <= depth) | ||||
|                     { | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|                 clone.splice(i+1, 0, {id, depth, concatListed, dropUnlisted, ...props}); | ||||
|                 return clone; | ||||
|             }); | ||||
|             return ()=> | ||||
|             { | ||||
|                 metasSet((m)=>{ | ||||
|                     const clone = [...m]; | ||||
|                     const ind = clone.findIndex(i=>i.id === id);       | ||||
|                     if(ind > -1) | ||||
|                     { | ||||
|                         console.log(`removing meta`, props, depth); | ||||
|                         clone.splice(ind, 1); | ||||
|                     }  | ||||
|                     return clone; | ||||
|                 }); | ||||
| 
 | ||||
|             }; | ||||
|         }, []); | ||||
| 
 | ||||
|         React.useEffect(()=>{ | ||||
|             metasSet((m)=>{ | ||||
|                 const clone = [...m]; | ||||
|                 const ind = clone.findIndex(i=>i.id === id);       | ||||
|                 if(ind > -1) | ||||
|                 { | ||||
|                     console.log(`updating meta`, props, depth); | ||||
|                     clone[ind] = {...clone[ind], ...props}; | ||||
|                 }  | ||||
|                 return clone; | ||||
|             }); | ||||
|         }, Object.keys(props).map( (key) => props[key as MetaKeys] )); | ||||
| 
 | ||||
|         if(!window.innerWidth && props.title) | ||||
|         { | ||||
|             Meta.Stack.push({id, depth, concatListed, dropUnlisted, ...props}); | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| export type Children = string | number | React.JSX.Element | React.JSX.Element[]; | ||||
| 
 | ||||
| type RoutePath = Array<string>; | ||||
| type RouteParams = Record<string, string|number|boolean>; | ||||
| 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<string, string>).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 path = e.composedPath() as HTMLAnchorElement[]; | ||||
|                 for(let i=0; i<path.length; i++) | ||||
|                 { | ||||
|                     if(path[i].href) | ||||
|                     { | ||||
|                         const u = new URL(path[i].href); | ||||
|                         if(u.origin == document.location.origin) | ||||
|                         { | ||||
|                             e.preventDefault(); | ||||
|                             const parts = Router.Parse(u); | ||||
|                             routeUpdate(parts.Path, parts.Params, parts.Anchor); | ||||
|                         } | ||||
|                         return; | ||||
|                     } | ||||
|                 } | ||||
|             }) | ||||
|         }, []); | ||||
| 
 | ||||
|         return <Router.Context.Provider value={[routeGet, routeUpdate]}>{props.children}</Router.Context.Provider>; | ||||
|     }, | ||||
|     Consumer() | ||||
|     { | ||||
|         return React.useContext(Router.Context); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| type SwitchContext = {depth:number, keys:Record<string, string>}; | ||||
| 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<parts.length; i++) | ||||
|                 { | ||||
|                     const partRoute = routeSegment[i]; | ||||
|                     const partCase = parts[i]; | ||||
|                     if(partCase[0] == ":") | ||||
|                     { | ||||
|                         output.keys[partCase.substring(1)] = partRoute; | ||||
|                     } | ||||
|                     else if(partCase != "*" && partCase != partRoute) | ||||
|                     { | ||||
|                         return false; | ||||
|                     } | ||||
|                 } | ||||
|                 return output; | ||||
|             } | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         for(let i=0; i<children.length; i++) | ||||
|         { | ||||
|             const childCase =  children[i]; | ||||
|             const childCaseChildren = childCase.props?.__args?.slice(2) || childCase.props.children; | ||||
|             const newContextValue = checkChild(childCase); | ||||
|             if(newContextValue) | ||||
|             { | ||||
|                 return <SwitchContext.Provider value={newContextValue}>{childCaseChildren}</SwitchContext.Provider> | ||||
|             } | ||||
|             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; | ||||
| 
 | ||||
| export type FetchCachOptions = {CacheFor:number, CacheOnServer:boolean, DelaySSR:boolean, Seed:boolean}; | ||||
| export type FetchRecord = {URL:string, Promise?:Promise<FetchRecord>, CachedAt:number, Error?:string, JSON?:object} & FetchCachOptions; | ||||
| type FetchGuide = [Record:FetchRecord, Init:boolean, Listen:boolean]; | ||||
| export type FetchHookState = [Data:undefined|object, Updating:boolean]; | ||||
| export const Fetch = { | ||||
|     Cache:new Map() as Map<string, FetchRecord>, | ||||
|     ServerBlocking:false as false|Promise<FetchRecord>[], | ||||
|     ServerTouched:false as false|Set<FetchRecord>, | ||||
|     ServerRemove:false as false|Set<FetchRecord>, | ||||
|     Seed(seed:FetchRecord[]) | ||||
|     { | ||||
|         seed.forEach(r=>{ | ||||
|             //r.Promise = Promise.resolve(r);
 | ||||
|             Fetch.Cache.set(r.URL, r) | ||||
|         }); | ||||
|     }, | ||||
|     DefaultOptions:{CacheFor:60, CacheOnServer:true, DelaySSR:true, Seed:true} as FetchCachOptions, | ||||
|     Request(URL:string, Init?:RequestInit|null, CacheFor:number = 60, CacheOnServer:boolean = true, DelaySSR:boolean = true, Seed:boolean = true):FetchGuide | ||||
|     { | ||||
|         let check = Fetch.Cache.get(URL); | ||||
|          | ||||
|         const load =(inCheck:FetchRecord)=> | ||||
|         { | ||||
|             Fetch.Cache.set(URL, inCheck); | ||||
|             inCheck.CachedAt = 0; | ||||
|             inCheck.Promise = fetch(URL, Init?Init:undefined).then(resp=>resp.json()).then((json)=>{ | ||||
|                 inCheck.JSON = json; | ||||
|                 inCheck.CachedAt = new Date().getTime(); | ||||
|                 //console.log(`...cached!`);
 | ||||
|                 return inCheck; | ||||
|             }); | ||||
|             return inCheck; | ||||
|         }; | ||||
| 
 | ||||
|         if(!check) | ||||
|         { | ||||
|             // not in the cache
 | ||||
|             // - listen
 | ||||
| 
 | ||||
|             //console.log(`making new cache record...`);
 | ||||
|             return [load({URL, CacheFor, CachedAt:0, CacheOnServer, DelaySSR, Seed}), false, true]; | ||||
|         } | ||||
|         else if(check.CachedAt == 0) | ||||
|         { | ||||
|             // loading started but not finished
 | ||||
|             // - listen
 | ||||
|             // - possibly init if there is something in JSON
 | ||||
| 
 | ||||
|             //console.log(`currently being cached...`);
 | ||||
|             return [check, check.JSON ? true : false, true]; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             //console.log(`found in cache...`);
 | ||||
|             let secondsAge = (new Date().getTime() - check.CachedAt)/1000; | ||||
|             if(secondsAge > check.CacheFor) | ||||
|             { | ||||
|                 // cached but expired
 | ||||
|                 // - listen
 | ||||
|                 // - init
 | ||||
|                 //console.log(`...outdated...`);
 | ||||
|                 return [load(check), true, true]; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // cached and ready
 | ||||
|                 // - init
 | ||||
|                 //console.log(`...retrieved!`);
 | ||||
|                 return [check, true, false]; | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     Use(URL:string, Init?:RequestInit|null, Options?:FetchCachOptions) | ||||
|     { | ||||
|         const config = {...Fetch.DefaultOptions, ...Options}; | ||||
|         const [receipt, init, listen] = Fetch.Request(URL, Init, config.CacheFor, config.CacheOnServer, config.DelaySSR, config.Seed); | ||||
|         const initialState:FetchHookState = init ? [receipt.JSON, listen] : [undefined, true]; | ||||
|         const [cacheGet, cacheSet] = React.useState(initialState); | ||||
| 
 | ||||
|         if(Fetch.ServerBlocking && Fetch.ServerTouched && config.DelaySSR) // if server-side rendering
 | ||||
|         { | ||||
|             if(listen) // if the request is pending
 | ||||
|             { | ||||
|                 receipt.Promise && Fetch.ServerBlocking.push(receipt.Promise); // add promise to blocking list
 | ||||
|                 return [undefined, listen] as FetchHookState; // no need to return any actual data while waiting server-side
 | ||||
|             } | ||||
|             else // if request is ready
 | ||||
|             { | ||||
|                 receipt.Seed && Fetch.ServerTouched.add(receipt); // add record to client seed list (if specified in receipt.seed)
 | ||||
|                 return [receipt.JSON, false] as FetchHookState; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         React.useEffect(()=> | ||||
|         { | ||||
|             if(listen) | ||||
|             { | ||||
|                 //const receipt = Fetch.Request(URL, Init, CacheFor, CacheOnServer, DelaySSR);
 | ||||
|                 receipt.Promise?.then(()=>cacheSet([receipt.JSON, receipt.CachedAt === 0])); | ||||
|             } | ||||
|         } | ||||
|         , []); | ||||
| 
 | ||||
|         return cacheGet; | ||||
|     } | ||||
| }; | ||||
							
								
								
									
										224
									
								
								iso-menu.tsx
									
									
									
									
									
								
							
							
						
						
									
										224
									
								
								iso-menu.tsx
									
									
									
									
									
								
							| @ -1,224 +0,0 @@ | ||||
| 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 <CTX.Provider value={[stateGet, (args:StateArgs)=> stateSet({...stateGet, ...args})]}>{props.children}</CTX.Provider>; | ||||
| }; | ||||
| 
 | ||||
| export const Menu =(props:{children:React.JSX.Element|React.JSX.Element[]})=> | ||||
| { | ||||
|     const [stateGet, stateSet] = React.useContext(CTX); | ||||
|     const refElement:React.MutableRefObject<HTMLElement|null> = React.useRef( null ); | ||||
|     const refControl:React.MutableRefObject<CollapseControls|null> = React.useRef( null ); | ||||
|     const refInitial:React.MutableRefObject<boolean> = 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 <div ref={refElement as React.Ref<HTMLDivElement>} class={Classes.Shut}> | ||||
|       { (!stateGet.open && stateGet.done) ? null : props.children} | ||||
|     </div>; | ||||
| }; | ||||
| 
 | ||||
| export const Button =()=> | ||||
| { | ||||
|     const [stateGet, stateSet] = React.useContext(CTX); | ||||
|     return <> | ||||
|         <p>{JSON.stringify(stateGet)}</p> | ||||
|         <button class="px-10 py-2 bg-red-500 text-white" onClick={e=>stateSet({open:true,  done:false})}>Open</button> | ||||
|         <button class="px-10 py-2 bg-red-500 text-white" onClick={e=>stateSet({open:false, done:false})}>Close</button> | ||||
|     </>; | ||||
| }; | ||||
| 
 | ||||
| type Handler = (e:MouseEvent)=>void | ||||
| const Refs:Map<HTMLElement, React.Ref<Handler>> = 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<HTMLElement>, handleAway:Handler)=> | ||||
| { | ||||
|   const refHandler:React.MutableRefObject<Handler> = 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<string, string>; | ||||
| type StyleCalc = Record<string, StyleSize>; | ||||
| 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<string, string>) | ||||
| { | ||||
|     let userDone:DoneCallback = (openState) => {}; | ||||
|     let userMode = initialState; | ||||
|     let frameRequest = 0; | ||||
|     let inTransition = false; | ||||
|     let measurements:StyleCalc; | ||||
|     const transitions:Set<string> = 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; | ||||
| }  | ||||
							
								
								
									
										107
									
								
								local.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								local.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,107 @@ | ||||
| import {Configure, Transpile, Extension} from "./serve.tsx"; | ||||
| 
 | ||||
| const SocketsLive:Set<WebSocket> = new Set(); | ||||
| const SocketsSend =(inData:string)=>{ console.log(inData); for (const socket of SocketsLive){ socket.send(inData); } } | ||||
| const Directory = `file://${Deno.cwd().replaceAll("\\", "/")}`; | ||||
| 
 | ||||
| Configure({ | ||||
|     Proxy:Directory, | ||||
|     SWCOp: | ||||
|     { | ||||
|         sourceMaps: "inline", | ||||
|         minify: false, | ||||
|         jsc: | ||||
|         { | ||||
|             target:"es2022", | ||||
|             parser: | ||||
|             { | ||||
|                 syntax: "typescript", | ||||
|                 tsx: true, | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     Remap: (inImports)=> | ||||
|     { | ||||
|         console.log("running remapper"); | ||||
|         Object.entries(inImports).forEach(([key, value])=> | ||||
|         { | ||||
|             if(value.startsWith("./")) | ||||
|             { | ||||
|                 inImports[key] = value.substring(1); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         inImports["react-original"] = inImports["react"]; | ||||
|         inImports["react"] = "/_lib_/react.tsx"; | ||||
|         console.log(inImports); | ||||
|         return inImports; | ||||
|     }, | ||||
|     async Serve(inReq, inURL, inExt, inMap) | ||||
|     { | ||||
|         if(Transpile.Check(inExt) && !inURL.searchParams.get("reload") && !inURL.pathname.startsWith("/_lib_/")) | ||||
|         { | ||||
|             const imp = await import(Directory+inURL.pathname); | ||||
|             const members = []; | ||||
|             for( const key in imp ) { members.push(key); } | ||||
| 
 | ||||
|             const code =` | ||||
| import {FileListen} from "/_lib_/hmr.tsx"; | ||||
| import * as Import from "${inURL.pathname}?reload=0"; | ||||
| ${ members.map(m=>`let proxy_${m} = Import.${m}; export { proxy_${m} as ${m} };`).join("\n") } | ||||
| FileListen("${inURL.pathname}", (updatedModule)=> | ||||
| { | ||||
|     ${ members.map(m=>`proxy_${m} = updatedModule.${m};`).join("\n") } | ||||
| });`            
 | ||||
| 
 | ||||
|             return new Response(code, {headers:{"content-type":"application/javascript"}}); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         if(inReq.headers.get("upgrade") == "websocket") | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|               const { response, socket } = Deno.upgradeWebSocket(inReq); | ||||
|               socket.onopen = () => SocketsLive.add(socket); | ||||
|               socket.onclose = () => SocketsLive.delete(socket); | ||||
|               socket.onmessage = (e) => {}; | ||||
|               socket.onerror = (e) => console.log("Socket errored:", e); | ||||
|               return response; | ||||
|             } | ||||
|             catch(e){ /**/ } | ||||
|         } | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| let blocking = false; | ||||
| const filesChanged:Map<string, string> = new Map(); | ||||
| for await (const event of Deno.watchFs(Deno.cwd())) | ||||
| { | ||||
|     event.paths.forEach( path => filesChanged.set(path, event.kind) ); | ||||
|     if(!blocking) | ||||
|     { | ||||
|         blocking = true; | ||||
|         setTimeout(async()=> | ||||
|         { | ||||
|             for await (const [path, action] of filesChanged) | ||||
|             { | ||||
|                 if(Transpile.Check(Extension(path))) | ||||
|                 { | ||||
|                     const key = path.substring(Deno.cwd().length).replaceAll("\\", "/"); | ||||
|                     if(action != "remove") | ||||
|                     {    | ||||
|                         const tsx = await Transpile.Fetch(Directory+key, key, true); | ||||
|                         tsx && SocketsSend(key); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         Transpile.Cache.delete(key); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             filesChanged.clear(); | ||||
|             blocking = false; | ||||
|         } | ||||
|         , 1000); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										106
									
								
								run-deploy.tsx
									
									
									
									
									
								
							
							
						
						
									
										106
									
								
								run-deploy.tsx
									
									
									
									
									
								
							| @ -1,106 +0,0 @@ | ||||
| import * as Env from "https://deno.land/std@0.194.0/dotenv/mod.ts"; | ||||
| import { parse } from "https://deno.land/std@0.194.0/flags/mod.ts"; | ||||
| 
 | ||||
| 
 | ||||
| const collect =async(inKey:string, inArg:Record<string, string>, inEnv:Record<string, string>):Promise<string|undefined>=> | ||||
| { | ||||
|     const scanArg = inArg[inKey]; | ||||
|     const scanEnvFile = inEnv[inKey]; | ||||
|     const scanEnvDeno = Deno.env.get(inKey); | ||||
| 
 | ||||
|     if(scanArg) | ||||
|     { | ||||
|         console.log(`Using "${inKey}" from passed argument.`); | ||||
|         return scanArg; | ||||
|     } | ||||
|     if(scanEnvFile) | ||||
|     { | ||||
|         console.log(`Using "${inKey}" from .env file.`); | ||||
|         return scanEnvFile; | ||||
|     } | ||||
|     if(scanEnvDeno) | ||||
|     { | ||||
|         console.log(`Using "${inKey}" from environment variable.`); | ||||
|         return scanEnvDeno; | ||||
|     } | ||||
| 
 | ||||
|     const scanUser = await prompt(`No "${inKey}" found. Enter one here:`); | ||||
|     if(!scanUser || scanUser?.length < 3) | ||||
|     { | ||||
|         console.log("Exiting..."); | ||||
|         Deno.exit(); | ||||
|     } | ||||
|     return scanUser; | ||||
| }; | ||||
| 
 | ||||
| const prompt =async(question: string):Promise<string>=> | ||||
| { | ||||
|     const buf = new Uint8Array(1024); | ||||
|     await Deno.stdout.write(new TextEncoder().encode(question)); | ||||
|     const bytes = await Deno.stdin.read(buf); | ||||
|     if (bytes) { | ||||
|         return new TextDecoder().decode(buf.subarray(0, bytes)).trim(); | ||||
|     } | ||||
|     throw new Error("Unexpected end of input"); | ||||
| }; | ||||
| 
 | ||||
| try | ||||
| { | ||||
|     console.log("Runing deploy!", Deno.mainModule); | ||||
| 
 | ||||
|     let arg = parse(Deno.args); | ||||
|     let env = await Env.load(); | ||||
| 
 | ||||
|     let useToken = await collect("DENO_DEPLOY_TOKEN", arg, env); | ||||
|     let useProject = await collect("DENO_DEPLOY_PROJECT", arg, env); | ||||
|      | ||||
|     let scanProd:string|string[] = await prompt(`Do you want to deploy to *production*? [y/n]`); | ||||
|     if(scanProd == "y") | ||||
|     { | ||||
|         scanProd = await prompt(`This will update the live project at ${useProject} are you sure you want to continue? [y/n]`); | ||||
|         scanProd = scanProd=="y" ? ["--prod"] : []; | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         scanProd = []; | ||||
|     } | ||||
| 
 | ||||
|     const command = new Deno.Command( | ||||
|         `deno`, | ||||
|         { | ||||
|             args:[ | ||||
|                 "run", | ||||
|                 "-A", | ||||
|                 "--no-lock", | ||||
|                 "https://deno.land/x/deploy/deployctl.ts", | ||||
|                 "deploy", | ||||
|                 `--project=${useProject}`, | ||||
|                 `--import-map=./deno.json`, | ||||
|                 `--token=${useToken}`, | ||||
|                 ...scanProd, | ||||
|                 Deno.mainModule | ||||
|             ], | ||||
|             stdin: "piped", | ||||
|             stdout: "piped" | ||||
|         } | ||||
|     ); | ||||
| 
 | ||||
|     const child = command.spawn(); | ||||
|      | ||||
|     // open a file and pipe the subprocess output to it.
 | ||||
|     const writableStream = new WritableStream({ | ||||
|         write(chunk: Uint8Array): Promise<void> { | ||||
|             Deno.stdout.write(chunk); | ||||
|             return Promise.resolve(); | ||||
|         }, | ||||
|     }); | ||||
|     child.stdout.pipeTo(writableStream); | ||||
| 
 | ||||
|     // manually close stdin
 | ||||
|     child.stdin.close(); | ||||
|     const status = await child.status; | ||||
| } | ||||
| catch(e) | ||||
| { | ||||
|     console.error(e); | ||||
| } | ||||
							
								
								
									
										115
									
								
								run-local.tsx
									
									
									
									
									
								
							
							
						
						
									
										115
									
								
								run-local.tsx
									
									
									
									
									
								
							| @ -1,115 +0,0 @@ | ||||
| import {Configure, Transpile, Extension} from "./run-serve.tsx"; | ||||
| import { Root } from "./checker.tsx"; | ||||
| import * as Collect from "./hmr-static.tsx"; | ||||
| 
 | ||||
| const SocketsLive:Set<WebSocket> = new Set(); | ||||
| const SocketsSend =(inData:string)=>{ for (const socket of SocketsLive){ socket.send(inData); } } | ||||
| 
 | ||||
| Configure({ | ||||
|     SWCOp: | ||||
|     { | ||||
|         sourceMaps: "inline", | ||||
|         minify: false, | ||||
|         jsc: | ||||
|         { | ||||
|             target:"es2022", | ||||
|             parser: | ||||
|             { | ||||
|                 syntax: "typescript", | ||||
|                 tsx: true, | ||||
|             }, | ||||
|             transform: | ||||
|             { | ||||
|                 react: { runtime: "automatic" } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     Remap: (inImports, inConfig)=> | ||||
|     { | ||||
|         inImports["react-original"] = inImports["react"]; | ||||
|         inImports["react"] = `/>able/hmr-react.tsx`; | ||||
| 
 | ||||
|         inImports["signals-original"] = inImports["@preact/signals"]; | ||||
|         inImports["@preact/signals"] = `/>able/hmr-signal.tsx`; | ||||
| 
 | ||||
|         return inImports; | ||||
|     }, | ||||
|     async Extra(inReq, inURL, inExt, inMap, inConfig) | ||||
|     { | ||||
|         if(!inURL.pathname.startsWith(encodeURI("/>"))) | ||||
|         { | ||||
|             if(Transpile.Check(inExt) && !inURL.searchParams.get("reload")) | ||||
|             { | ||||
| 
 | ||||
|                 // we dont need to add ?reload= because this fetch is by way the file system not the hosted url
 | ||||
|                 const [local, foreign] = await Collect.FileExports(Root+inURL.pathname); | ||||
|                 const code =` | ||||
|     import {FileListen} from ">able/hmr-listen.tsx"; | ||||
|     import * as Import from "${inURL.pathname}?reload=0"; | ||||
|     ${ local.map(m=>`let proxy_${m} = Import.${m}; export { proxy_${m} as ${m} };`).join("\n") } | ||||
|     FileListen("${inURL.pathname}", (updatedModule)=> | ||||
|     { | ||||
|         ${ local.map(m=>`proxy_${m} = updatedModule.${m};`).join("\n") } | ||||
|     }); | ||||
|     ${ foreign.join(";\n") } | ||||
|     `    
 | ||||
| 
 | ||||
|                 return new Response(code, {headers:{"content-type":"application/javascript"}}); | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             if(inReq.headers.get("upgrade") == "websocket") | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                 const { response, socket } = Deno.upgradeWebSocket(inReq); | ||||
|                 socket.onopen = () => SocketsLive.add(socket); | ||||
|                 socket.onclose = () => SocketsLive.delete(socket); | ||||
|                 socket.onmessage = (e) => {}; | ||||
|                 socket.onerror = (e) => console.log("Socket errored:", e); | ||||
|                 return response; | ||||
|                 } | ||||
|                 catch(e){ /**/ } | ||||
|             }             | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| const Watcher =async()=> | ||||
| { | ||||
|     let blocking = false; | ||||
|     const filesChanged:Map<string, string> = new Map(); | ||||
|     for await (const event of Deno.watchFs(Deno.cwd())) | ||||
|     { | ||||
|         event.paths.forEach( path => filesChanged.set(path, event.kind) ); | ||||
|         if(!blocking) | ||||
|         { | ||||
|             blocking = true; | ||||
|             setTimeout(async()=> | ||||
|             { | ||||
|                 for await (const [path, action] of filesChanged) | ||||
|                 { | ||||
|                     if(Transpile.Check(Extension(path))) | ||||
|                     { | ||||
|                         const key = path.substring(Deno.cwd().length).replaceAll("\\", "/"); | ||||
|                         if(action != "remove") | ||||
|                         {    | ||||
|                             const tsx = await Transpile.Fetch(Root+key, key, true); | ||||
|                             tsx && SocketsSend(key); | ||||
|                         } | ||||
|                         else | ||||
|                         { | ||||
|                             Transpile.Cache.delete(key); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 filesChanged.clear(); | ||||
|                 blocking = false; | ||||
|             } | ||||
|             , 1000); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| Watcher(); | ||||
							
								
								
									
										241
									
								
								run-serve.tsx
									
									
									
									
									
								
							
							
						
						
									
										241
									
								
								run-serve.tsx
									
									
									
									
									
								
							| @ -1,241 +0,0 @@ | ||||
| import * as MIME from "https://deno.land/std@0.180.0/media_types/mod.ts"; | ||||
| import * as SWCW from "https://esm.sh/@swc/wasm-web@1.3.62"; | ||||
| import { HuntConfig, Root } from "./checker.tsx"; | ||||
| import CustomServe from ">able/api.tsx";  | ||||
| 
 | ||||
| type DenoConfig = {imports:Record<string, string>}; | ||||
| const ImportMap:DenoConfig = {imports:{}}; | ||||
| 
 | ||||
| const ImportMapReload =async()=> | ||||
| { | ||||
|     const [, {json}] = await HuntConfig(); | ||||
|     const imports = (json as DenoConfig).imports; | ||||
| 
 | ||||
|     if(imports) | ||||
|     { | ||||
|         Object.entries(imports).forEach(([key, value])=> | ||||
|         { | ||||
|             // re-write deno project-relative paths (e.g. "./app.tsx") to root-relative for the browser ("/app.tsx"); 
 | ||||
|             if(value.startsWith("./")) | ||||
|             { | ||||
|                 imports[key] = value.substring(1); | ||||
|             } | ||||
| 
 | ||||
|             if(key.startsWith(">") && key.endsWith("/")) | ||||
|             { | ||||
|                 imports[key] = "/"+key; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         ImportMap.imports = Configuration.Remap(imports, Configuration); | ||||
|     } | ||||
| 
 | ||||
| }; | ||||
| 
 | ||||
| export type CustomHTTPHandler = (inReq:Request, inURL:URL, inExt:string|false, inMap:{imports:Record<string, string>}, inConfig:Configuration)=>void|false|Response|Promise<Response|void|false>; | ||||
| export type CustomRemapper = (inImports:Record<string, string>, inConfig:Configuration)=>Record<string, string>; | ||||
| export type Configuration     = {Start:string, Allow:string, Reset:string, SWCOp:SWCW.Options, Serve:CustomHTTPHandler, Extra:CustomHTTPHandler, Shell:CustomHTTPHandler, Remap:CustomRemapper}; | ||||
| export type ConfigurationArgs = Partial<Configuration>; | ||||
| let Configuration:Configuration = | ||||
| { | ||||
|     Start: ">able/app.tsx", | ||||
|     Allow: "*", | ||||
|     Reset: "/clear-cache", | ||||
|     async Extra(inReq, inURL, inExt, inMap, inConfig){}, | ||||
|     Serve: CustomServe, | ||||
|     Remap: (inImports, inConfig)=> | ||||
|     { | ||||
|         return inImports; | ||||
|     }, | ||||
|     Shell(inReq, inURL, inExt, inMap, inConfig) | ||||
|     { | ||||
|         return new Response( | ||||
|             `<!doctype html>
 | ||||
|             <html> | ||||
|                 <head> | ||||
|                     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> | ||||
|                     <meta charset="utf-8"/> | ||||
|                 </head> | ||||
|                 <body> | ||||
|                     <div id="app"></div> | ||||
|                     <script type="importmap">${JSON.stringify(inMap, null, " ")}</script> | ||||
|                     <script type="module"> | ||||
|                         import Mount from ">able/run-browser.tsx"; | ||||
|                         Mount("#app", "${inConfig.Start}"); | ||||
|                     </script> | ||||
|                 </body> | ||||
|             </html>`, {status:200, headers:{"content-type":"text/html"}});
 | ||||
|     }, | ||||
|     SWCOp: | ||||
|     { | ||||
|         sourceMaps: false, | ||||
|         minify: true, | ||||
|         jsc: | ||||
|         { | ||||
|             target:"es2022", | ||||
|             minify: | ||||
|             { | ||||
|                 compress: { unused: true }, | ||||
|                 mangle: true | ||||
|             }, | ||||
|             parser: | ||||
|             { | ||||
|                 syntax: "typescript", | ||||
|                 tsx: true, | ||||
|             }, | ||||
|             transform: | ||||
|             { | ||||
|                 react: { runtime: "automatic" } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export const Transpile = | ||||
| { | ||||
|     Cache: new Map() as Map<string, string>, | ||||
|     Files: ["tsx", "jsx", "ts", "js", "mjs"], | ||||
|     Check(inExtension:string|false) | ||||
|     { | ||||
|         return inExtension ? this.Files.includes(inExtension) : false; | ||||
|     }, | ||||
|     Clear() | ||||
|     { | ||||
|         const size = this.Cache.size; | ||||
|         this.Cache.clear(); | ||||
|         ImportMapReload(); | ||||
|         return size; | ||||
|     }, | ||||
|     async Fetch(inPath:string, inKey:string, inCheckCache=true) | ||||
|     { | ||||
|         const check = this.Cache.get(inPath); | ||||
|         if(check && inCheckCache) | ||||
|         { | ||||
|             return check; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 const resp = await fetch(inPath); | ||||
|                 const text = await resp.text(); | ||||
|                 const {code} = await SWCW.transform(text, { ...Configuration.SWCOp, filename:inKey}); | ||||
|                 this.Cache.set(inKey, code); | ||||
|                 return code; | ||||
|             } | ||||
|             catch(e) | ||||
|             { | ||||
|                 console.log(`Transpile.Fetch error. Key:"${inKey}" Path:"${inPath}" Error:"${e}"`); | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export const Extension =(inPath:string)=> | ||||
| { | ||||
|     const posSlash = inPath.lastIndexOf("/"); | ||||
|     const posDot = inPath.lastIndexOf("."); | ||||
|     return posDot > posSlash ? inPath.substring(posDot+1).toLowerCase() : false; | ||||
| }; | ||||
| 
 | ||||
| export const Configure =(config:ConfigurationArgs)=> | ||||
| { | ||||
|     Configuration = {...Configuration, ...config}; | ||||
|     ImportMapReload(); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| let running = false; | ||||
| export default async()=> | ||||
| { | ||||
|     if(running){return}; | ||||
|     running = true; | ||||
|      | ||||
|     try | ||||
|     { | ||||
|         await ImportMapReload(); | ||||
|         await SWCW.default(); | ||||
|     } | ||||
|     catch(e) | ||||
|     { | ||||
|         console.log("swc init error:", e); | ||||
|     } | ||||
|      | ||||
|     const server = Deno.serve({port:parseInt(Deno.env.get("port")||"8000")}, async(req: Request)=> | ||||
|     { | ||||
|         const url:URL = new URL(req.url); | ||||
|         const ext = Extension(url.pathname); | ||||
|         const headers = {"content-type":"application/json", "Access-Control-Allow-Origin": Configuration.Allow, "charset":"UTF-8"}; | ||||
|         let proxy = Root + url.pathname; | ||||
|      | ||||
|         if(url.pathname.includes("__/") || url.pathname.lastIndexOf("__.") > -1) | ||||
|         { | ||||
|             return new Response(`{"error":"unmatched route", "path":"${url.pathname}"}`, {status:404, headers}); | ||||
|         } | ||||
|      | ||||
|         // proxy imports
 | ||||
|         if(url.pathname.startsWith(encodeURI("/>"))) | ||||
|         { | ||||
|             /** pathname with no leading slash */ | ||||
|             const clippedPath = decodeURI(url.pathname.substring(1)); | ||||
|             proxy = import.meta.resolve(clippedPath); | ||||
|         } | ||||
|      | ||||
|         // allow for custom handlers
 | ||||
|         const custom = await Configuration.Extra(req, url, ext, ImportMap, Configuration); | ||||
|         if(custom) | ||||
|         { | ||||
|             return custom; | ||||
|         } | ||||
|         const api = await Configuration.Serve(req, url, ext, ImportMap, Configuration); | ||||
|         if(api) | ||||
|         { | ||||
|             return api; | ||||
|         } | ||||
|      | ||||
|         // transpileable files
 | ||||
|         if(Transpile.Check(ext)) | ||||
|         { | ||||
|             const code = await Transpile.Fetch(proxy, url.pathname);     | ||||
|             if(code) | ||||
|             { | ||||
|                 return new Response(code, {headers:{...headers, "content-type":"application/javascript"}} );      | ||||
|             }  | ||||
|         } | ||||
|      | ||||
|         // custom page html
 | ||||
|         if(!ext) | ||||
|         { | ||||
|             const shell = await Configuration.Shell(req, url, ext, ImportMap, Configuration); | ||||
|             if(shell) | ||||
|             { | ||||
|                 return shell; | ||||
|             } | ||||
|         } | ||||
|      | ||||
|         // cache-reset route
 | ||||
|         if(url.pathname === Configuration.Reset) | ||||
|         { | ||||
|             return new Response(`{"cleared":${Transpile.Clear()}}`, {headers}); | ||||
|         } | ||||
|      | ||||
|         // all other static files
 | ||||
|         if(ext) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 const type = MIME.typeByExtension(ext); | ||||
|                 const file = await fetch(proxy); | ||||
|                 return new Response(file.body, {headers:{...headers, "content-type":type||""}}); | ||||
|             } | ||||
|             catch(e) | ||||
|             { | ||||
|                 return new Response(`{"error":"${e}", "path":"${url.pathname}"}`, {status:404, headers}); | ||||
|             } | ||||
|         } | ||||
|      | ||||
|         return new Response(`{"error":"unmatched route", "path":"${url.pathname}"}`, {status:404, headers}); | ||||
|      | ||||
|     }); | ||||
| } | ||||
							
								
								
									
										34
									
								
								run.tsx
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								run.tsx
									
									
									
									
									
								
							| @ -1,34 +0,0 @@ | ||||
| import * as Serve from "./run-serve.tsx"; | ||||
| 
 | ||||
| Deno.args.forEach(arg=> | ||||
| { | ||||
|     if(arg.startsWith("--")) | ||||
|     { | ||||
|         const kvp = arg.substring(2).split("="); | ||||
|         Deno.env.set(kvp[0], kvp[1] || "true"); | ||||
|     } | ||||
| }); | ||||
| const isDeploy = Deno.env.get("dep"); | ||||
| const isDevelop = Deno.env.get("dev"); | ||||
| 
 | ||||
| export default function(config:Serve.ConfigurationArgs) | ||||
| { | ||||
|     if(!isDeploy) | ||||
|     { | ||||
|         Serve.Configure(config) | ||||
|         Serve.default(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| if(isDeploy) | ||||
| { | ||||
|     import("./run-deploy.tsx"); | ||||
| } | ||||
| else | ||||
| { | ||||
|     if(isDevelop) | ||||
|     { | ||||
|         await import("./run-local.tsx"); | ||||
|     } | ||||
|     Serve.default(); | ||||
| } | ||||
							
								
								
									
										218
									
								
								serve.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								serve.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,218 @@ | ||||
| import * as MIME from "https://deno.land/std@0.180.0/media_types/mod.ts"; | ||||
| import * as HTTP from "https://deno.land/std@0.177.0/http/server.ts"; | ||||
| import * as SWCW from "https://esm.sh/@swc/wasm-web@1.3.62"; | ||||
| 
 | ||||
| type DenoConfig = {imports:Record<string, string>}; | ||||
| const ImportMap:DenoConfig = {imports:{}}; | ||||
| const ImportMapReload =async()=> | ||||
| { | ||||
|     let json:DenoConfig; | ||||
|     const path = Configuration.Proxy+"/deno.json"; | ||||
|     try | ||||
|     { | ||||
|         const resp = await fetch(path); | ||||
|         json = await resp.json(); | ||||
|         if(!json?.imports) | ||||
|         { throw new Error("imports not specified in deno.json") } | ||||
|     } | ||||
|     catch(e) | ||||
|     { | ||||
|         console.log(`error reading deno config "${path}" message:"${e}"`); | ||||
|         return; | ||||
|     } | ||||
|     ImportMap.imports = Configuration.Remap(json.imports); | ||||
| }; | ||||
| 
 | ||||
| type CustomHTTPHandler = (inReq:Request, inURL:URL, inExt:string|false, inMap:{imports:Record<string, string>})=>void|false|Response|Promise<Response|void|false>; | ||||
| type CustomRemapper = (inImports:Record<string, string>)=>Record<string, string>; | ||||
| type Configuration = {Proxy:string, Allow:string, Reset:string, SWCOp:SWCW.Options, Serve:CustomHTTPHandler, Shell:CustomHTTPHandler, Remap:CustomRemapper}; | ||||
| type ConfigurationArgs = {Proxy?:string, Allow?:string, Reset?:string, SWCOp?:SWCW.Options, Serve?:CustomHTTPHandler, Shell?:CustomHTTPHandler, Remap?:CustomRemapper}; | ||||
| let Configuration:Configuration = | ||||
| { | ||||
|     Proxy: `file://${Deno.cwd().replaceAll("\\", "/")}`, | ||||
|     Allow: "*", | ||||
|     Reset: "/clear-cache", | ||||
|     Serve(inReq, inURL, inExt, inMap){}, | ||||
|     Remap: (inImports)=> | ||||
|     { | ||||
|         Object.entries(inImports).forEach(([key, value])=> | ||||
|         { | ||||
|             if(value.startsWith("./")) | ||||
|             { | ||||
|                 inImports[key] = value.substring(1); | ||||
|             } | ||||
|         }); | ||||
|         const reactURL = inImports["react"] ?? console.log("React is not defined in imports"); | ||||
|         const setting = Configuration.SWCOp?.jsc?.transform?.react; | ||||
|         if(setting) | ||||
|         { | ||||
|             setting.importSource = reactURL; | ||||
|         } | ||||
|         console.log(inImports); | ||||
|         return inImports; | ||||
|     }, | ||||
|     Shell(inReq, inURL, inExt, inMap) | ||||
|     { | ||||
|         return new Response( | ||||
|             `<!doctype html>
 | ||||
|             <html> | ||||
|                 <head> | ||||
|                 </head> | ||||
|                 <body> | ||||
|                     <div id="app"></div> | ||||
|                     <script type="importmap">${JSON.stringify(inMap)}</script> | ||||
|                     <script type="module"> | ||||
|                        import Mount from "/_lib_/mount.tsx"; | ||||
|                        Mount("#app", "@app"); | ||||
|                     </script> | ||||
|                 </body> | ||||
|             </html>`, {status:200, headers:{"content-type":"text/html"}});
 | ||||
|     }, | ||||
|     SWCOp: | ||||
|     { | ||||
|         sourceMaps: false, | ||||
|         minify: true, | ||||
|         jsc: | ||||
|         { | ||||
|             target:"es2022", | ||||
|             minify: | ||||
|             { | ||||
|                 compress: { unused: true }, | ||||
|                 mangle: true | ||||
|             }, | ||||
|             parser: | ||||
|             { | ||||
|                 syntax: "typescript", | ||||
|                 tsx: true, | ||||
|             }, | ||||
|             transform: | ||||
|             { | ||||
|                 react: { runtime: "automatic" } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export const Transpile = | ||||
| { | ||||
|     Cache: new Map() as Map<string, string>, | ||||
|     Files: ["tsx", "jsx", "ts", "js", "mjs"], | ||||
|     Check(inExtension:string|false) | ||||
|     { | ||||
|         return inExtension ? this.Files.includes(inExtension) : false; | ||||
|     }, | ||||
|     Clear() | ||||
|     { | ||||
|         const size = this.Cache.size; | ||||
|         this.Cache.clear(); | ||||
|         ImportMapReload(); | ||||
|         return size; | ||||
|     }, | ||||
|     Fetch: async function(inPath:string, inKey:string, inCheckCache=true) | ||||
|     { | ||||
|         const check = this.Cache.get(inPath); | ||||
|         if(check && inCheckCache) | ||||
|         { | ||||
|             return check; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 const resp = await fetch(inPath); | ||||
|                 const text = await resp.text(); | ||||
|                 const {code} = await SWCW.transform(text, { ...Configuration.SWCOp, filename:inKey}); | ||||
|                 this.Cache.set(inKey, code); | ||||
|                 return code; | ||||
|             } | ||||
|             catch(e) | ||||
|             { | ||||
|                 console.log(`Transpile.Fetch error. Key:"${inKey}" Path:"${inPath}" Error:"${e}"`); | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export const Extension =(inPath:string)=> | ||||
| { | ||||
|     const posSlash = inPath.lastIndexOf("/"); | ||||
|     const posDot = inPath.lastIndexOf("."); | ||||
|     return posDot > posSlash ? inPath.substring(posDot+1).toLowerCase() : false; | ||||
| }; | ||||
| 
 | ||||
| export const Configure =(config:ConfigurationArgs)=> | ||||
| { | ||||
|     Configuration = {...Configuration, ...config}; | ||||
|     ImportMapReload(); | ||||
| } | ||||
| 
 | ||||
| await ImportMapReload(); | ||||
| await SWCW.default(); | ||||
| HTTP.serve(async(req: Request)=> | ||||
| { | ||||
|     const url:URL = new URL(req.url); | ||||
|     const ext = Extension(url.pathname); | ||||
|     const headers = {"content-type":"application/json", "Access-Control-Allow-Origin": Configuration.Allow, "charset":"UTF-8"}; | ||||
| 
 | ||||
|     // cache-reset route
 | ||||
|     if(url.pathname === Configuration.Reset) | ||||
|     { | ||||
|         return new Response(`{"cleared":${Transpile.Clear()}}`, {headers}); | ||||
|     } | ||||
| 
 | ||||
|     // allow for custom handler
 | ||||
|     const custom = await Configuration.Serve(req, url, ext, ImportMap); | ||||
|     if(custom) | ||||
|     { | ||||
|         return custom; | ||||
|     } | ||||
| 
 | ||||
|     // transpileable files
 | ||||
|     if(Transpile.Check(ext)) | ||||
|     { | ||||
|         if(url.pathname.startsWith("/_lib_/")) | ||||
|         { | ||||
|             const path = import.meta.url+"/.."+url.pathname; | ||||
|             const code = await Transpile.Fetch(path, url.pathname, true); | ||||
|             if(code) | ||||
|             { | ||||
|                 return new Response(code, {headers:{"content-type":"application/javascript"}}); | ||||
|             } | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             const lookup = await Transpile.Fetch(Configuration.Proxy + url.pathname, url.pathname); | ||||
|             return new Response(lookup, {status:lookup?200:404, headers:{...headers, "content-type":"application/javascript"}} );             | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // custom page html
 | ||||
|     if(!ext) | ||||
|     { | ||||
|         const shell = await Configuration.Shell(req, url, ext, ImportMap); | ||||
|         if(shell) | ||||
|         { | ||||
|             return shell; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // all other static files
 | ||||
|     if(ext) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             const type = MIME.typeByExtension(ext); | ||||
|             const file = await fetch(Configuration.Proxy + url.pathname); | ||||
|             const text = await file.text(); | ||||
|             return new Response(text, {headers:{...headers, "content-type":type||""}}); | ||||
|         } | ||||
|         catch(e) | ||||
|         { | ||||
|             return new Response(`{"error":"${e}", "path":"${url.pathname}"}`, {status:404, headers}); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return new Response(`{"error":"unmatched route", "path":"${url.pathname}"}`, {status:404, headers}); | ||||
| 
 | ||||
| }); | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user