Compare commits

...

77 Commits

Author SHA1 Message Date
Seth Trowbridge 11f896f12b Merge branch 'master' of https://gitea.hptrow.me/SethTrowbridge/able-baker 2024-05-06 06:27:50 -04:00
Seth Trowbridge 130ccd949b simplify jsx config 2024-05-06 06:24:50 -04:00
SethTrowbridge 160324751e Merge pull request 'adjust import proxies' (#14) from fix-proxied-imports into master
Reviewed-on: #14
2024-05-05 16:05:01 -04:00
Seth Trowbridge d9801f3e95 adjust import proxies
something was very wrong
and could still be...
2024-05-05 16:03:00 -04:00
SethTrowbridge 81b5a3d880 Merge pull request 'pin new versions' (#13) from fix-dependencies into master
Reviewed-on: #13
2024-05-04 11:35:14 -04:00
Seth Trowbridge c4ee536c7a pin new versions 2024-05-04 11:34:12 -04:00
SethTrowbridge 4964dd7823 Merge pull request 'version deploy ctl' (#12) from fix-deploy into master
Reviewed-on: #12
2024-05-04 08:18:24 -04:00
Seth Trowbridge d941abde0f version deploy ctl
update to new exclusion glob patterns
2024-05-04 08:12:06 -04:00
SethTrowbridge 67b2eef7ae Merge pull request 'fix config merging' (#11) from shadow-option into master
Reviewed-on: #11
2023-10-31 09:46:29 -04:00
Seth Trowbridge 1a1d7c4fd8 fix config merging 2023-10-31 09:45:26 -04:00
SethTrowbridge e2ee67a888 Merge pull request 'make shadow dom optional' (#10) from shadow-option into master
Reviewed-on: #10
2023-10-31 09:14:58 -04:00
Seth Trowbridge 9f3d26d779 make shadow dom optional 2023-10-31 09:14:22 -04:00
SethTrowbridge 559d4d0ecd Merge pull request 'add deep equality for hrm signals' (#9) from object-compare into master
Reviewed-on: #9
2023-10-23 11:01:34 -04:00
Seth Trowbridge 1061b6efff add deep equality for hrm signals 2023-10-23 11:00:00 -04:00
SethTrowbridge dd006daae7 Merge pull request 'dreaded-fixes' (#8) from dreaded-fixes into master
Reviewed-on: #8
2023-10-17 20:10:12 -04:00
Seth Trowbridge fbe3959c07 Merge branch 'dreaded-fixes' of https://gitea.hptrow.me/SethTrowbridge/able-baker into dreaded-fixes 2023-10-16 21:20:47 -04:00
SethTrowbridge ca2b8ec596 Merge pull request 'dreaded-fixes' (#7) from dreaded-fixes into master
Reviewed-on: #7
2023-10-16 13:30:24 -04:00
Seth Trowbridge 4f90a33a93 deploy exclusion 2023-10-16 13:29:10 -04:00
Seth Trowbridge 7824b47634 hmr signal hook! 2023-10-16 12:33:07 -04:00
Seth Trowbridge b0f87d20c5 signal hmr! 2023-10-16 10:27:22 -04:00
Seth Trowbridge 9433c15bd9 fix es export { } syntax 2023-10-15 08:52:35 -04:00
Seth Trowbridge 9e7fe002b0 signal started 2023-10-14 23:47:20 -04:00
Seth Trowbridge 2b83a2abe6 separate HMR listeners 2023-10-14 12:09:06 -04:00
Seth Trowbridge 69f902c927 fix lib comp opts 2023-10-14 12:06:11 -04:00
Seth Trowbridge c44c1df257 misc items 2023-10-12 22:47:45 -04:00
SethTrowbridge 736d0c6a9a Merge pull request 'cli' (#6) from cli into master
Reviewed-on: #6
2023-09-24 09:33:29 -04:00
Seth Trowbridge 4ef3cc47a5 more cli fixes 2023-09-24 09:19:50 -04:00
Seth Trowbridge 2757998675 config checking 2023-08-28 22:16:56 -04:00
Seth Trowbridge acda59303d wording 2023-08-13 22:58:15 -04:00
Seth Trowbridge 783970f2c0 improved checking and options 2023-08-13 11:00:48 -04:00
Seth Trowbridge b602ffcbcc checker started 2023-08-12 20:30:44 -04:00
Seth Trowbridge 67b5f03a7a hunt config 2023-08-10 22:37:53 -04:00
Seth Trowbridge 29768761cb use deno confirm 2023-08-02 06:07:34 -04:00
Seth Trowbridge c558ecb6d2 import map and settings started 2023-08-01 22:42:31 -04:00
Seth Trowbridge 2d2ba6fa2b install function 2023-08-01 17:23:19 -04:00
Seth Trowbridge 35f8e64a26 installer started 2023-08-01 15:30:31 -04:00
Seth Trowbridge 2398899662 add config hunter 2023-08-01 10:19:39 -04:00
SethTrowbridge 327efd3940 Merge pull request 'start-file' (#5) from start-file into master
Reviewed-on: #5
2023-07-30 14:29:34 -04:00
Seth Trowbridge bd7af1241e allow for production deployment 2023-07-30 14:21:35 -04:00
Seth Trowbridge 79c5a0aff0 fix flag names 2023-07-30 14:01:14 -04:00
Seth Trowbridge 1f6ab7cf47 only run once 2023-07-30 13:29:15 -04:00
Seth Trowbridge 78230a5672 allow for code or config setup 2023-07-30 13:04:26 -04:00
Seth Trowbridge 6134b7be34 switch to code configuration 2023-07-30 09:16:17 -04:00
SethTrowbridge a9ce1c6f4c Merge pull request 'fix config arg' (#4) from api into master
Reviewed-on: #4
2023-07-29 21:23:11 -04:00
SethTrowbridge e5a0a526a8 Merge branch 'master' into api 2023-07-29 21:23:04 -04:00
Seth Trowbridge 56c4ec7242 fix config arg 2023-07-29 21:21:23 -04:00
SethTrowbridge 506fce9d7d Merge pull request 'api' (#3) from api into master
Reviewed-on: #3
2023-07-29 20:30:02 -04:00
Seth Trowbridge 19097e1e68 tweak deploy script 2023-07-29 20:25:41 -04:00
Seth Trowbridge 06981cc067 fix path (no ext) 2023-07-29 18:49:44 -04:00
Seth Trowbridge 6cee7ac065 custom api handler started 2023-07-29 18:21:59 -04:00
Seth Trowbridge b118ba02f9 setup debug 2023-07-29 17:13:49 -04:00
Seth Trowbridge 254cd45a2c fix hmr, modify example 2023-07-29 16:45:41 -04:00
Seth Trowbridge 2b59bd33aa add proxy imports!
remove file.deno.tsx falback
2023-07-29 13:57:44 -04:00
Seth Trowbridge d2938ce6fe obscure double-undescore routes 2023-07-22 10:08:53 -04:00
Seth Trowbridge 65d6d827e3 misc cleanup 2023-07-17 22:41:22 -04:00
Seth Trowbridge c3e5319d47 automatically add "react/" 2023-07-17 09:26:41 -04:00
Seth Trowbridge 3fed5dfdba fix iso-elements 2023-07-17 05:16:16 -04:00
Seth Trowbridge b038cecb08 exchange boot for "entry" 2023-07-16 22:23:10 -04:00
Seth Trowbridge 58d624ff54 start deploy script 2023-07-16 21:37:39 -04:00
Seth Trowbridge b8f4ee6618 introduce ESM export parser 2023-07-16 21:29:50 -04:00
Seth Trowbridge 0d856e1b4a custom export parser 2023-07-15 23:37:16 -04:00
Seth Trowbridge caeade853d fix static file body 2023-07-11 07:57:24 -04:00
Seth Trowbridge 0f5b990b5f add iso elements 2023-07-08 22:16:32 -04:00
Seth Trowbridge d29414691c remove spoof convention 2023-07-08 15:25:09 -04:00
Seth Trowbridge 0ea5c3e562 outpost relay mode started 2023-07-08 12:21:58 -04:00
Seth Trowbridge 66b148d7f5 starting to remove spoof path 2023-07-03 22:27:35 -04:00
Seth Trowbridge e5176e0eee Merge branch 'convert-dynamic' into ssr-work 2023-07-02 09:09:27 -04:00
Seth Trowbridge 3a3fdcf5e2 patch caching 2023-07-01 10:53:10 -04:00
Seth Trowbridge d6a9a269a7 apply import maps 2023-07-01 10:43:29 -04:00
SethTrowbridge b78699844d Merge pull request 'naming' (#2) from naming into master
Reviewed-on: #2
2023-06-30 10:23:59 -04:00
Seth Trowbridge 052dd13bb9 import conversion 2023-06-27 21:54:39 -04:00
Seth Trowbridge d7a5350256 configurable lib folder 2023-06-21 22:35:59 -04:00
Seth Trowbridge f521ed23eb cleanup console logs 2023-06-21 21:51:38 -04:00
Seth Trowbridge cc80a2725d pass config though handlers 2023-06-21 21:26:54 -04:00
Seth Trowbridge a9139c6cd3 flatten 2023-06-21 17:32:10 -04:00
Seth Trowbridge 55b5bd28ab prefixing 2023-06-21 17:21:36 -04:00
SethTrowbridge 6a5d97677a Merge pull request 'boot-function' (#1) from boot-function into master
Reviewed-on: #1
2023-06-21 07:53:13 -04:00
30 changed files with 1919 additions and 614 deletions

2
.gitingore Normal file
View File

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

21
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,21 @@
{
// 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"
}
]
}

View File

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

View File

@ -1,15 +0,0 @@
import "../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("../local.tsx");
}

View File

@ -1,74 +0,0 @@
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};

View File

@ -1,89 +0,0 @@
import React from "react";
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 =
{
theme: {},
presets: [TWPreTail(), TWPreAuto()],
hash: false
} as 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(merge, TW.cssom(ShadowCSS)), ShadowDiv);
return ShadowDiv;
};
let booted = false;
export const Boot =async(inSettings:{App:()=>React.JSX.Element, CSS?:TW.TwindUserConfig, DOM?:string})=>
{
if(booted){return;}
booted = true;
const settings = {CSS:{...Configure, ...inSettings.CSS||{} }, DOM:inSettings.DOM||"#app", App:inSettings.App};
console.log("Clinet boot called")
let dom = document.querySelector(settings.DOM);
if(!dom)
{
console.log(`element "${settings.DOM}" not found.`);
return false;
}
dom = Shadow(dom as HTMLElement, settings.CSS)
const app = React.createElement(()=> React.createElement(settings.App, null), null);
if(React.render)
{
React.render(app, dom);
return ()=>dom && React.unmountComponentAtNode(dom);
}
else
{
const reactDom = await import(`https://esm.sh/react-dom@${React.version}/client`);
const root = reactDom.createRoot(dom);
root.render(app);
return root.unmount;
}
};
export default async(inSelector:string, inModulePath:string, inMemberApp="default", inMemberCSS="CSS"):Promise<(()=>void)|false>=>
{
let dom = document.querySelector(inSelector);
if(!dom)
{
console.log(`element "${inSelector}" not found.`);
return false;
}
const module = await import(inModulePath);
dom = Shadow(dom as HTMLElement, module[inMemberCSS])
const app = React.createElement(()=> React.createElement(module[inMemberApp], null), null);
if(React.render)
{
React.render(app, dom);
return ()=>dom && React.unmountComponentAtNode(dom);
}
else
{
const reactDom = await import(`https://esm.sh/react-dom@${React.version}/client`);
const root = reactDom.createRoot(dom);
root.render(app);
return root.unmount;
}
};

4
api.tsx Normal file
View File

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

7
app.tsx Normal file
View File

@ -0,0 +1,7 @@
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 Normal file
View File

@ -0,0 +1,233 @@
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 Normal file
View File

@ -0,0 +1,140 @@
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;
}
}
}

View File

@ -1,17 +0,0 @@
{
"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/": "./_lib_/"
},
"tasks":
{
"local": "deno run -A --no-lock ./local.tsx",
"serve": "deno run -A --no-lock ./serve.tsx"
}
}

28
deno.jsonc Normal file
View File

@ -0,0 +1,28 @@
{
"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"]
}
}

32
deno.lock Normal file
View File

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

View File

@ -1,53 +0,0 @@
import "@able/boot.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>;
}

View File

@ -1,13 +0,0 @@
{
"compilerOptions": { "lib": ["deno.window", "dom"] },
"imports":
{
"react":"https://esm.sh/preact@10.15.1/compat",
"@able/":"http://localhost:4507/_lib_/"
},
"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"
}
}

31
hmr-listen.tsx Normal file
View File

@ -0,0 +1,31 @@
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);

View File

@ -1,5 +1,51 @@
import * as ReactParts from "react-original";
import { HMR } from "./hmr.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 StateCapture = {state:StateType, set:ReactParts.StateUpdater<StateType>, reload:number};
@ -68,7 +114,6 @@ 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);
@ -112,7 +157,6 @@ 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;
};

47
hmr-signal.tsx Normal file
View File

@ -0,0 +1,47 @@
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 Normal file
View File

@ -0,0 +1,139 @@
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);
*/

4
install__/api.tsx Normal file
View File

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

3
install__/app.tsx Normal file
View File

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

396
iso-elements.tsx Normal file
View File

@ -0,0 +1,396 @@
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 Normal file
View File

@ -0,0 +1,224 @@
import React from "react";
type StateArgs = {done?:boolean, open?:boolean};
type StateObj = {done:boolean, open:boolean};
type StateBinding = [state:StateObj, update:(args:StateArgs)=>void];
const CTX = React.createContext([{done:true, open:false}, (args)=>{}] as StateBinding);
export const Group =(props:{children:React.JSX.Element|React.JSX.Element[]})=>
{
const [stateGet, stateSet] = React.useState({done:true, open:false} as StateObj);
return <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;
}

100
local.tsx
View File

@ -1,100 +0,0 @@
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); } }
Configure({
SWCOp:
{
sourceMaps: "inline",
minify: false,
jsc:
{
target:"es2022",
parser:
{
syntax: "typescript",
tsx: true,
}
}
},
Remap: (inImports)=>
{
inImports["react-original"] = inImports["react"];
inImports["react"] = "/_lib_/react.tsx";
return inImports;
},
async Serve(inReq, inURL, inExt, inMap, inProxy)
{
if(Transpile.Check(inExt) && !inURL.searchParams.get("reload") && !inURL.pathname.startsWith("/_lib_/"))
{
const imp = await import(inProxy+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){ /**/ }
}
}
});
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(`file://${Deno.cwd().replaceAll("\\", "/")}`+key, key, true);
tsx && SocketsSend(key);
}
else
{
Transpile.Cache.delete(key);
}
}
}
filesChanged.clear();
blocking = false;
}
, 1000);
}
}
}
Watcher().then(()=>console.log("done watching"));

65
run-browser.tsx Normal file
View File

@ -0,0 +1,65 @@
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";
const Configure =
{
theme: {},
presets: [TWPreTail(), TWPreAuto()],
hash: false
} as TW.TwindUserConfig;
export const Shadow =(inElement:HTMLElement, inConfig:TW.TwindUserConfig)=>
{
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);
return ShadowDiv;
};
export default async(inSelector:string, inModulePath:string, inMemberApp="default", inMemberCSS="CSS", inShadow=false):Promise<(()=>void)|false>=>
{
if(!inModulePath)
{
return false;
}
let dom = document.querySelector(inSelector);
if(!dom)
{
console.log(`element "${inSelector}" not found.`);
return false;
}
const module = await import(inModulePath);
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);
if(React.render)
{
React.render(app, dom);
return ()=>dom && React.unmountComponentAtNode(dom);
}
else
{
const reactDom = await import(`https://esm.sh/react-dom@${React.version}/client`);
const root = reactDom.createRoot(dom);
root.render(app);
return root.unmount;
}
};

106
run-deploy.tsx Normal file
View File

@ -0,0 +1,106 @@
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 Normal file
View File

@ -0,0 +1,115 @@
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 Normal file
View File

@ -0,0 +1,241 @@
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 Normal file
View File

@ -0,0 +1,34 @@
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();
}

245
serve.tsx
View File

@ -1,245 +0,0 @@
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;
}
Object.entries(json.imports).forEach(([key, value])=>
{
if(value.startsWith("./"))
{
json.imports[key] = value.substring(1);
}
});
if(!json.imports["@able/"])
{
console.log(`"@able/" specifier not defined in import map`);
}
json.imports["@able/"] = "/_lib_/";
if(!json.imports["react"])
{
console.log(`"react" specifier not defined in import map`);
}
ImportMap.imports = Configuration.Remap(json.imports);
console.log(ImportMap.imports);
};
type CustomHTTPHandler = (inReq:Request, inURL:URL, inExt:string|false, inMap:{imports:Record<string, string>}, inProxy: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: new URL(`file://${Deno.cwd().replaceAll("\\", "/")}`).toString(),
Allow: "*",
Reset: "/clear-cache",
Serve(inReq, inURL, inExt, inMap, inProxy){},
Remap: (inImports)=>
{
const reactURL = inImports["react"];
const setting = Configuration.SWCOp?.jsc?.transform?.react;
if(setting && reactURL)
{
setting.importSource = reactURL;
}
return inImports;
},
Shell(inReq, inURL, inExt, inMap, inProxy)
{
console.log("Start app:", Deno.mainModule, "start dir", inProxy);
console.log("Split:", Deno.mainModule.split(inProxy) );
const parts = Deno.mainModule.split(inProxy);
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", "${parts[1]??"/app.tsx"}");
</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, Configuration.Proxy);
if(custom)
{
return custom;
}
// transpileable files
if(Transpile.Check(ext))
{
let code;
let path;
if(url.pathname.startsWith("/_lib_/"))
{
if(url.pathname.endsWith("boot.tsx"))
{
path = import.meta.url+"/../_lib_/mount.tsx";
}
else
{
path = import.meta.url+"/.."+url.pathname;
}
code = await Transpile.Fetch(path, url.pathname, true);
}
else
{
path = Configuration.Proxy + url.pathname;
code = await Transpile.Fetch(path, 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.Proxy);
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});
});