Compare commits

..

No commits in common. "master" and "0.0.1" have entirely different histories.

26 changed files with 317 additions and 1660 deletions

View File

@ -1,2 +0,0 @@
deno.lock
.env

21
.vscode/launch.json vendored
View File

@ -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
View File

@ -0,0 +1,5 @@
{
"deno.enable": true,
"deno.unstable": true,
"deno.config": "./deno.json"
}

View File

@ -1,4 +0,0 @@
export default (req:Request):Response|false=>
{
return false;
}

View File

@ -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>;

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import * as TW from "@twind/core"; import * as TW from "https://esm.sh/v126/@twind/core@1.1.3/es2022/core.mjs";
import TWPreTail from "https://esm.sh/v126/@twind/preset-tailwind@1.1.3/es2022/preset-tailwind.mjs"; 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 TWPreAuto from "https://esm.sh/v126/@twind/preset-autoprefix@1.0.7/es2022/preset-autoprefix.mjs";
@ -10,25 +10,22 @@ const Configure =
hash: false hash: false
} as TW.TwindUserConfig; } 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 ShadowDOM = inElement.attachShadow({ mode: "open" });
const ShadowDiv = document.createElement("div"); const ShadowDiv = document.createElement("div");
const ShadowCSS = document.createElement("style"); const ShadowCSS = document.createElement("style");
ShadowDOM.append(ShadowCSS); ShadowDOM.append(ShadowCSS);
ShadowDOM.append(ShadowDiv); ShadowDOM.append(ShadowDiv);
TW.observe(TW.twind(inConfig, TW.cssom(ShadowCSS)), ShadowDiv); TW.observe(TW.twind(merge, TW.cssom(ShadowCSS)), ShadowDiv);
return 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); let dom = document.querySelector(inSelector);
if(!dom) if(!dom)
{ {
@ -37,17 +34,7 @@ export default async(inSelector:string, inModulePath:string, inMemberApp="defaul
} }
const module = await import(inModulePath); const module = await import(inModulePath);
dom = Shadow(dom as HTMLElement, module[inMemberCSS])
const merge = inMemberCSS ? {...Configure, ...module[inMemberCSS]} : Configure;
if(inShadow)
{
dom = Shadow(dom as HTMLElement, merge);
}
else
{
TW.install(merge);
}
const app = React.createElement(()=> React.createElement(module[inMemberApp], null), null); const app = React.createElement(()=> React.createElement(module[inMemberApp], null), null);
if(React.render) if(React.render)
@ -62,4 +49,5 @@ export default async(inSelector:string, inModulePath:string, inMemberApp="defaul
root.render(app); root.render(app);
return root.unmount; return root.unmount;
} }
}; };

15
boot-server.tsx Normal file
View File

@ -0,0 +1,15 @@
import "./run-serve.tsx";
Deno.args.forEach(arg=>
{
if(arg.startsWith("--"))
{
const kvp = arg.substring(2).split("=");
Deno.env.set(kvp[0], kvp[1] || "true");
}
});
if(Deno.env.get("dev"))
{
await import("./run-local.tsx");
}

View File

@ -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
View File

@ -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
View 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",
"@able/": "./"
},
"tasks":
{
"local": "deno run -A --no-lock ./local.tsx",
"serve": "deno run -A --no-lock ./serve.tsx"
}
}

View File

@ -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"]
}
}

View File

@ -1,32 +0,0 @@
{
"version": "3",
"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"
}
}

53
example/app.tsx Normal file
View File

@ -0,0 +1,53 @@
import "@able/boot-server.tsx";
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 text-red-500">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
View File

@ -0,0 +1,13 @@
{
"compilerOptions": { "lib": ["deno.window", "dom"] },
"imports":
{
"react":"https://esm.sh/preact@10.15.1/compat",
"@able/":"http://localhost:4507/"
},
"tasks":
{
"local": "deno run -A --no-lock --reload=http://localhost:4507 app.tsx --dev",
"serve": "deno run -A --no-lock --reload=http://localhost:4507 app.tsx"
}
}

View File

@ -1,5 +1,4 @@
import { HMR } from "./hmr-react.tsx"; import { type StateCapture } from "./hmr-react.tsx";
import { GroupSignal, GroupSignalHook } from "./hmr-signal.tsx";
const FileListeners = new Map() as Map<string, Array<(module:unknown)=>void>>; const FileListeners = new Map() as Map<string, Array<(module:unknown)=>void>>;
export const FileListen =(inPath:string, inHandler:()=>void)=> export const FileListen =(inPath:string, inHandler:()=>void)=>
@ -14,18 +13,58 @@ Socket.addEventListener('message', async(event:{data:string})=>
{ {
// When a file changes, dynamically re-import it to get the updated members // When a file changes, dynamically re-import it to get the updated members
// send the updated members to any listeners for that file // send the updated members to any listeners for that file
const reImport = await import(event.data+"?reload="+Math.random());
GroupSignal.reset();
const reImport = await import(document.location.origin+event.data+"?reload="+Math.random());
FileListeners.get(event.data)?.forEach(reExport=>reExport(reImport)); FileListeners.get(event.data)?.forEach(reExport=>reExport(reImport));
GroupSignal.swap();
GroupSignalHook.reset();
HMR.update(); HMR.update();
GroupSignalHook.reset();
}); });
Socket.addEventListener("error", ()=>{clearInterval(SocketTimer); console.log("HMR socket lost")}) Socket.addEventListener("error", ()=>{clearInterval(SocketTimer); console.log("HMR socket lost")})
const SocketTimer = setInterval(()=>{Socket.send("ping")}, 5000); 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};

View File

@ -1,51 +1,5 @@
import * as ReactParts from "react-original"; import * as ReactParts from "react-original";
import { HMR } from "./hmr-listen.tsx";
/*
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();
}
};
export type StateType = boolean|number|string|Record<string, string> export type StateType = boolean|number|string|Record<string, string>
export type StateCapture = {state:StateType, set:ReactParts.StateUpdater<StateType>, reload:number}; export type StateCapture = {state:StateType, set:ReactParts.StateUpdater<StateType>, reload:number};

View File

@ -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};

View File

@ -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);
*/

View File

@ -1,4 +0,0 @@
export default (req:Request):Response|false=>
{
return false;
}

View File

@ -1,3 +0,0 @@
export default ()=><div>
<h1>App!</h1>
</div>;

View File

@ -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;
}
};

View File

@ -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;
}

View File

@ -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);
}

View File

@ -1,6 +1,4 @@
import {Configure, Transpile, Extension} from "./run-serve.tsx"; 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 SocketsLive:Set<WebSocket> = new Set();
const SocketsSend =(inData:string)=>{ for (const socket of SocketsLive){ socket.send(inData); } } const SocketsSend =(inData:string)=>{ for (const socket of SocketsLive){ socket.send(inData); } }
@ -17,42 +15,31 @@ Configure({
{ {
syntax: "typescript", syntax: "typescript",
tsx: true, tsx: true,
},
transform:
{
react: { runtime: "automatic" }
} }
} }
}, },
Remap: (inImports, inConfig)=> Remap: (inImports, inConfig)=>
{ {
inImports["react-original"] = inImports["react"]; inImports["react-original"] = inImports["react"];
inImports["react"] = `/>able/hmr-react.tsx`; inImports["react"] = `${inConfig.Spoof}/hmr-react.tsx`;
inImports["signals-original"] = inImports["@preact/signals"];
inImports["@preact/signals"] = `/>able/hmr-signal.tsx`;
return inImports; return inImports;
}, },
async Extra(inReq, inURL, inExt, inMap, inConfig) async Serve(inReq, inURL, inExt, inMap, inConfig)
{ {
if(!inURL.pathname.startsWith(encodeURI("/>"))) if(Transpile.Check(inExt) && !inURL.searchParams.get("reload") && !inURL.pathname.startsWith(inConfig.Spoof+"/"))
{
if(Transpile.Check(inExt) && !inURL.searchParams.get("reload"))
{ {
const imp = await import(inConfig.Proxy+inURL.pathname);
const members = [];
for( const key in imp ) { members.push(key); }
// 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 =` const code =`
import {FileListen} from ">able/hmr-listen.tsx"; import {FileListen} from "${inConfig.Spoof}/hmr-listen.tsx";
import * as Import from "${inURL.pathname}?reload=0"; import * as Import from "${inURL.pathname}?reload=0";
${ local.map(m=>`let proxy_${m} = Import.${m}; export { proxy_${m} as ${m} };`).join("\n") } ${ members.map(m=>`let proxy_${m} = Import.${m}; export { proxy_${m} as ${m} };`).join("\n") }
FileListen("${inURL.pathname}", (updatedModule)=> FileListen("${inURL.pathname}", (updatedModule)=>
{ {
${ local.map(m=>`proxy_${m} = updatedModule.${m};`).join("\n") } ${ members.map(m=>`proxy_${m} = updatedModule.${m};`).join("\n") }
}); });`
${ foreign.join(";\n") }
`
return new Response(code, {headers:{"content-type":"application/javascript"}}); return new Response(code, {headers:{"content-type":"application/javascript"}});
} }
@ -72,8 +59,6 @@ Configure({
catch(e){ /**/ } catch(e){ /**/ }
} }
} }
}
}); });
const Watcher =async()=> const Watcher =async()=>
@ -95,7 +80,7 @@ const Watcher =async()=>
const key = path.substring(Deno.cwd().length).replaceAll("\\", "/"); const key = path.substring(Deno.cwd().length).replaceAll("\\", "/");
if(action != "remove") if(action != "remove")
{ {
const tsx = await Transpile.Fetch(Root+key, key, true); const tsx = await Transpile.Fetch(`file://${Deno.cwd().replaceAll("\\", "/")}`+key, key, true);
tsx && SocketsSend(key); tsx && SocketsSend(key);
} }
else else

View File

@ -1,54 +1,75 @@
import * as MIME from "https://deno.land/std@0.180.0/media_types/mod.ts"; 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"; 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>}; type DenoConfig = {imports:Record<string, string>};
const ImportMap:DenoConfig = {imports:{}}; const ImportMap:DenoConfig = {imports:{}};
const ImportMapReload =async()=> const ImportMapReload =async()=>
{ {
const [, {json}] = await HuntConfig(); let json:DenoConfig;
const imports = (json as DenoConfig).imports; const path = Configuration.Proxy+"/deno.json";
try
if(imports)
{ {
Object.entries(imports).forEach(([key, value])=> const resp = await fetch(path);
json = await resp.json();
if(!json?.imports)
{ throw new Error("imports not specified in deno.json") }
}
catch(e)
{ {
// re-write deno project-relative paths (e.g. "./app.tsx") to root-relative for the browser ("/app.tsx"); console.log(`error reading deno config "${path}" message:"${e}"`);
if(value.startsWith("./")) return;
{
imports[key] = value.substring(1);
} }
if(key.startsWith(">") && key.endsWith("/")) Object.entries(json.imports).forEach(([key, value])=>
{ {
imports[key] = "/"+key; if(value.startsWith("./"))
{
json.imports[key] = value.substring(1);
} }
}); });
ImportMap.imports = Configuration.Remap(imports, Configuration); const mapKey = (Configuration.Spoof.startsWith("/") ? Configuration.Spoof.substring(1) : Configuration.Spoof)+"/";
if(!json.imports[mapKey])
{
console.log(`"${mapKey}" specifier not defined in import map`);
}
json.imports[mapKey] = Configuration.Spoof+"/";
if(!json.imports["react"])
{
console.log(`"react" specifier not defined in import map`);
} }
ImportMap.imports = Configuration.Remap(json.imports, Configuration);
console.log(ImportMap.imports);
}; };
export type CustomHTTPHandler = (inReq:Request, inURL:URL, inExt:string|false, inMap:{imports:Record<string, string>}, inConfig:Configuration)=>void|false|Response|Promise<Response|void|false>; 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>; 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}; type Configuration = {Proxy:string, Spoof:string, Allow:string, Reset:string, SWCOp:SWCW.Options, Serve:CustomHTTPHandler, Shell:CustomHTTPHandler, Remap:CustomRemapper};
export type ConfigurationArgs = Partial<Configuration>; type ConfigurationArgs = {Proxy?:string, Spoof?:string, Allow?:string, Reset?:string, SWCOp?:SWCW.Options, Serve?:CustomHTTPHandler, Shell?:CustomHTTPHandler, Remap?:CustomRemapper};
let Configuration:Configuration = let Configuration:Configuration =
{ {
Start: ">able/app.tsx", Proxy: new URL(`file://${Deno.cwd().replaceAll("\\", "/")}`).toString(),
Allow: "*", Allow: "*",
Reset: "/clear-cache", Reset: "/clear-cache",
async Extra(inReq, inURL, inExt, inMap, inConfig){}, Spoof: "/@able",
Serve: CustomServe, Serve(inReq, inURL, inExt, inMap, inConfig){},
Remap: (inImports, inConfig)=> Remap: (inImports, inConfig)=>
{ {
const reactURL = inImports["react"];
const setting = Configuration.SWCOp?.jsc?.transform?.react;
if(setting && reactURL)
{
setting.importSource = reactURL;
}
return inImports; return inImports;
}, },
Shell(inReq, inURL, inExt, inMap, inConfig) Shell(inReq, inURL, inExt, inMap, inConfig)
{ {
const parts = Deno.mainModule.split(inConfig.Proxy);
return new Response( return new Response(
`<!doctype html> `<!doctype html>
<html> <html>
@ -58,10 +79,10 @@ let Configuration:Configuration =
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="importmap">${JSON.stringify(inMap, null, " ")}</script> <script type="importmap">${JSON.stringify(inMap)}</script>
<script type="module"> <script type="module">
import Mount from ">able/run-browser.tsx"; import Mount from "${inConfig.Spoof}/boot-browser.tsx";
Mount("#app", "${inConfig.Start}"); Mount("#app", "${parts[1]??"/app.tsx"}");
</script> </script>
</body> </body>
</html>`, {status:200, headers:{"content-type":"text/html"}}); </html>`, {status:200, headers:{"content-type":"text/html"}});
@ -106,7 +127,7 @@ export const Transpile =
ImportMapReload(); ImportMapReload();
return size; return size;
}, },
async Fetch(inPath:string, inKey:string, inCheckCache=true) Fetch: async function(inPath:string, inKey:string, inCheckCache=true)
{ {
const check = this.Cache.get(inPath); const check = this.Cache.get(inPath);
if(check && inCheckCache) if(check && inCheckCache)
@ -145,59 +166,52 @@ export const Configure =(config:ConfigurationArgs)=>
ImportMapReload(); ImportMapReload();
} }
let running = false;
export default async()=>
{
if(running){return};
running = true;
try
{
await ImportMapReload(); await ImportMapReload();
await SWCW.default(); await SWCW.default();
} HTTP.serve(async(req: Request)=>
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 url:URL = new URL(req.url);
const ext = Extension(url.pathname); const ext = Extension(url.pathname);
const headers = {"content-type":"application/json", "Access-Control-Allow-Origin": Configuration.Allow, "charset":"UTF-8"}; 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) // cache-reset route
if(url.pathname === Configuration.Reset)
{ {
return new Response(`{"error":"unmatched route", "path":"${url.pathname}"}`, {status:404, headers}); return new Response(`{"cleared":${Transpile.Clear()}}`, {headers});
} }
// proxy imports // allow for custom handler
if(url.pathname.startsWith(encodeURI("/>"))) const custom = await Configuration.Serve(req, url, ext, ImportMap, Configuration);
{
/** 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) if(custom)
{ {
return custom; return custom;
} }
const api = await Configuration.Serve(req, url, ext, ImportMap, Configuration);
if(api)
{
return api;
}
// transpileable files // transpileable files
if(Transpile.Check(ext)) if(Transpile.Check(ext))
{ {
const code = await Transpile.Fetch(proxy, url.pathname); let code;
let path;
if(url.pathname.startsWith(Configuration.Spoof+"/"))
{
const clipRoot = import.meta.url.substring(0, import.meta.url.lastIndexOf("/"));
const clipPath = url.pathname.substring(url.pathname.indexOf("/", 1));
if(clipPath.startsWith("/boot-"))
{
path = clipRoot+"/boot-browser.tsx";
}
else
{
path = clipRoot + clipPath;
}
code = await Transpile.Fetch(path, url.pathname, true);
}
else
{
path = Configuration.Proxy + url.pathname;
code = await Transpile.Fetch(path, url.pathname);
}
if(code) if(code)
{ {
return new Response(code, {headers:{...headers, "content-type":"application/javascript"}} ); return new Response(code, {headers:{...headers, "content-type":"application/javascript"}} );
@ -214,20 +228,15 @@ export default async()=>
} }
} }
// cache-reset route
if(url.pathname === Configuration.Reset)
{
return new Response(`{"cleared":${Transpile.Clear()}}`, {headers});
}
// all other static files // all other static files
if(ext) if(ext)
{ {
try try
{ {
const type = MIME.typeByExtension(ext); const type = MIME.typeByExtension(ext);
const file = await fetch(proxy); const file = await fetch(Configuration.Proxy + url.pathname);
return new Response(file.body, {headers:{...headers, "content-type":type||""}}); const text = await file.text();
return new Response(text, {headers:{...headers, "content-type":type||""}});
} }
catch(e) catch(e)
{ {
@ -238,4 +247,3 @@ export default async()=>
return new Response(`{"error":"unmatched route", "path":"${url.pathname}"}`, {status:404, headers}); return new Response(`{"error":"unmatched route", "path":"${url.pathname}"}`, {status:404, headers});
}); });
}

34
run.tsx
View File

@ -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();
}