Compare commits

..

No commits in common. "master" and "ssr-work" have entirely different histories.

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

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 ./run-local.tsx --port=1234",
"serve": "deno run -A --no-lock ./run-serve.tsx --port=1234"
}
}

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

52
example/app.tsx Normal file
View File

@ -0,0 +1,52 @@
import React from "react";
const CTXString = React.createContext("lol");
type StateBinding<T> = [get:T, set:React.StateUpdater<T>];
const CTXState = React.createContext(null) as React.Context<StateBinding<number>|null>;
const Outer =(props:{children:React.JSX.Element})=>
{
const binding = React.useState(11);
return <CTXState.Provider value={binding}>
{props.children}
</CTXState.Provider>
};
const Inner =()=>
{
const [stateGet, stateSet] = React.useContext(CTXState) || ["default", ()=>{}];
return <button onClick={e=>stateSet((old)=>old+1)}>count: {stateGet} :)</button>
};
type Store = {name:string, age:number}
const reducer =(inState:Store, inAction:number)=>
{
return {...inState, age:inState.age+inAction};
}
const builder =(inState:Store):Store=>
{
inState.age = 100;
return inState;
}
export default ()=>
{
const [Store, Dispatch] = React.useReducer(reducer, {name:"seth", age:24} as Store, builder)
return <CTXString.Provider value="intradestink">
<div class="my-4 font-sans">
<h1 class="font-black text-xl text-red-500">Title</h1>
<h2 class="font-black text-blue-500">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",
"entry":"./app.tsx"
},
"tasks":
{
"local": "deno run -A --no-lock --reload=http://localhost:1234 http://localhost:1234/run-local.tsx",
"serve": "deno run -A --no-lock --reload=http://localhost:1234 http://localhost:1234/run-serve.tsx"
}
}

15
example/dyn-test.tsx Normal file
View File

@ -0,0 +1,15 @@
import * as Util from "@able/";
import * as React from "react";
import {createElement} from "react/client";
import('react').then((module) => {
console.log(module);
});
function unimport(n:number)
{
return n;
}
unimport(123)

View File

@ -1,5 +1,4 @@
import { HMR } from "./hmr-react.tsx";
import { GroupSignal, GroupSignalHook } from "./hmr-signal.tsx";
import { type StateCapture } from "./hmr-react.tsx";
const FileListeners = new Map() as Map<string, Array<(module:unknown)=>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
// 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);
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";
/*
Each custom component is secretly modified to have an extra state and id.
When there is an HMR update, this state is changed, forcing it to re-render.
Each *user-created* React.useState is secretly modified and accompanied by an ID.
Every time its state is set, the HMR.statesNew map for this ID is set to contain the new state and updater.
When a component is removed, any of it's states in HMR.statesNew are also removed.
(HMR.statesNew is the "running total" of all states currently at play).
---
When a state is interacted with:
- statesNew for this id is set
- the internal state is also set in the traditional way
When there is an HMR update:
- All custom components are re-rendered...
for each useState(value) call that then happens in the re-render:
- if there is a "statesOld" value for this state, use that and ignore the passed value, otherwise use the passed value
- if this state has not been interacted with since the last reload (statesNew is empty at this id), set statesNew<id> with whatever is in statesOld<id>
- statesNew is moved into *statesOld*
- statesNew is cleared.
*/
export const HMR =
{
reloads:1,
RegisteredComponents: new Map() as Map<string, ()=>void>,
statesNew: new Map() as Map<string, StateCapture>,
statesOld: new Map() as Map<string, StateCapture>,
wireframe: false,
RegisterComponent(reactID:string, value:()=>void):void
{
this.RegisteredComponents.set(reactID, value);
},
update()
{
this.reloads++;
this.RegisteredComponents.forEach(handler=>handler());
this.RegisteredComponents.clear();
this.statesOld = this.statesNew;
this.statesNew = new Map();
}
};
import { HMR } from "./hmr-listen.tsx";
export type StateType = boolean|number|string|Record<string, string>
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

@ -65,7 +65,7 @@ const findNextExport =(inFile:string, inIndex=0, inLocal:Array<string>, inForeig
}
else
{
const members = inFile.substring(nextCharInd+1, endBracketInd).replace(/\s/g, '');
const members = inFile.substring(nextCharInd+1, endBracketInd);
members.split(",").forEach(part=>
{
const renamed = part.split(" as ");
@ -117,6 +117,7 @@ export const Exports =(inFile:string)=>
export const FileExports =async(inURL:string|URL)=>
{
console.log("scanning", inURL, "for exports")
const resp = await fetch(inURL);
const text = await resp.text();
return Exports(text);

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,4 +1,4 @@
import * as Env from "https://deno.land/std@0.194.0/dotenv/mod.ts";
import { load } from "https://deno.land/std@0.194.0/dotenv/mod.ts";
import { parse } from "https://deno.land/std@0.194.0/flags/mod.ts";
@ -30,7 +30,6 @@ const collect =async(inKey:string, inArg:Record<string, string>, inEnv:Record<st
console.log("Exiting...");
Deno.exit();
}
return scanUser;
};
const prompt =async(question: string):Promise<string>=>
@ -46,24 +45,20 @@ const prompt =async(question: string):Promise<string>=>
try
{
console.log("Runing deploy!", Deno.mainModule);
console.log("Runing deploy!");
const serveScript = import.meta.resolve("./run-serve.tsx");
console.log("Serve script:", serveScript);
let arg = parse(Deno.args);
let env = await Env.load();
let env = await 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 = [];
}
let useEntry = await collect("DENO_DEPLOY_ENTRY", arg, env);
Deno.env.set("DENO_DEPLOY_TOKEN", useToken || "");
Deno.env.set("DENO_DEPLOY_ENTRY", useEntry || "");
const command = new Deno.Command(
`deno`,
@ -75,10 +70,7 @@ try
"https://deno.land/x/deploy/deployctl.ts",
"deploy",
`--project=${useProject}`,
`--import-map=./deno.json`,
`--token=${useToken}`,
...scanProd,
Deno.mainModule
serveScript
],
stdin: "piped",
stdout: "piped"

View File

@ -1,5 +1,4 @@
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();
@ -17,62 +16,64 @@ Configure({
{
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`;
inImports["react"] = import.meta.resolve(`./hmr-react.tsx`);
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"))
{
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") }
`
// const imp = await import(inConfig.Proxy+inURL.pathname);
// const members = [];
// for( const key in imp ) { members.push(key); }
//
// const code =`
//import {FileListen} from "${import.meta.resolve(`./hmr-listen.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"}});
}
// 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(inConfig.Proxy+inURL.pathname);
console.log(local, foreign);
const code =`
import {FileListen} from "${import.meta.resolve(`./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") }
`
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){ /**/ }
}
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){ /**/ }
}
}
});
@ -95,7 +96,7 @@ const Watcher =async()=>
const key = path.substring(Deno.cwd().length).replaceAll("\\", "/");
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);
}
else

View File

@ -1,54 +1,87 @@
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";
Deno.args.forEach(arg=>
{
if(arg.startsWith("--"))
{
const kvp = arg.substring(2).split("=");
Deno.env.set(kvp[0], kvp[1] || "true");
}
});
const urls = {
Cwd: new URL(`file://${Deno.cwd().replaceAll("\\", "/")}`).toString(),
App: Deno.mainModule,
Mod: import.meta.url,
Look: import.meta.resolve("./boot.tsx")
};
console.log(JSON.stringify(urls, null, " "));
type DenoConfig = {imports:Record<string, string>};
const ImportMap:DenoConfig = {imports:{}};
let ImportMapOriginal = {};
const ImportMapReload =async()=>
{
const [, {json}] = await HuntConfig();
const imports = (json as DenoConfig).imports;
if(imports)
let json:DenoConfig;
const path = Configuration.Proxy+"/deno.json";
try
{
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);
const resp = await fetch(path);
json = await resp.json();
if(!json?.imports)
{ throw new Error("imports not specified in deno.json") }
ImportMapOriginal = 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["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>;
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>;
type CustomHTTPHandler = (inReq:Request, inURL:URL, inExt:string|false, inMap:{imports:Record<string, string>}, inConfig:Configuration)=>void|false|Response|Promise<Response|void|false>;
type CustomRemapper = (inImports:Record<string, string>, inConfig:Configuration)=>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 =
{
Start: ">able/app.tsx",
Proxy: new URL(`file://${Deno.cwd().replaceAll("\\", "/")}`).toString(),
Allow: "*",
Reset: "/clear-cache",
async Extra(inReq, inURL, inExt, inMap, inConfig){},
Serve: CustomServe,
async Serve(inReq, inURL, inExt, inMap, inConfig){},
Remap: (inImports, inConfig)=>
{
const reactURL = inImports["react"];
const setting = Configuration.SWCOp?.jsc?.transform?.react;
if(setting && reactURL)
{
setting.importSource = reactURL;
}
return inImports;
},
Shell(inReq, inURL, inExt, inMap, inConfig)
{
const parts = Deno.mainModule.split(inConfig.Proxy);
return new Response(
`<!doctype html>
<html>
@ -58,10 +91,10 @@ let Configuration:Configuration =
</head>
<body>
<div id="app"></div>
<script type="importmap">${JSON.stringify(inMap, null, " ")}</script>
<script type="importmap">${JSON.stringify(inMap)}</script>
<script type="module">
import Mount from ">able/run-browser.tsx";
Mount("#app", "${inConfig.Start}");
import Mount from "${import.meta.resolve("./boot.tsx")}";
Mount("#app", "entry");
</script>
</body>
</html>`, {status:200, headers:{"content-type":"text/html"}});
@ -106,6 +139,84 @@ export const Transpile =
ImportMapReload();
return size;
},
/**
* Converts dynamic module imports in to static, also can resolve paths with an import map
*/
async Patch(inPath:string, inKey:string, inMap?:DenoConfig)
{
const check = this.Cache.get(inKey);
if(check)
{
return check;
}
let file, text;
try
{
file = await fetch(inPath);
text = await file.text();
}
catch(e)
{
return false;
}
const remap = inMap ? (inPath:string)=>
{
const match = inMap.imports[inPath];
if(match)
{
return match;
}
else if(inPath.includes("/"))
{
let bestKey = "";
let bestLength = 0;
Object.keys(inMap.imports).forEach((key, i, arr)=>
{
if(key.endsWith("/") && inPath.startsWith(key) && key.length > bestLength)
{
bestKey = key;
bestLength = key.length;
}
});
if(bestKey)
{
return inMap.imports[bestKey]+inPath.substring(bestKey.length);
}
}
return inPath;
}
: (inPath:string)=>inPath;
let match, regex;
let convertedBody = text;
// remap static imports
regex = /from\s+(['"`])(.*?)\1/g;
while ((match = regex.exec(text)))
{
const importStatement = match[0];
const importPath = match[2];
convertedBody = convertedBody.replace(importStatement, `from "${remap(importPath)}"`);
}
// convert dynamic imports into static (to work around deno deploy)
const staticImports = [];
regex = /(?<![\w.])import\(([^)]+)(?!import\b)\)/g;
while ((match = regex.exec(text)))
{
const importStatement = match[0];
const importPath = remap(match[1].substring(1, match[1].length-1));
const moduleName = `_dyn_${staticImports.length}` as string;
staticImports.push(`import ${moduleName} from ${importPath};`);
convertedBody = convertedBody.replace(importStatement, `Promise.resolve(${moduleName})`);
}
convertedBody = staticImports.join("\n") + convertedBody;
inKey && this.Cache.set(inKey, convertedBody);
return convertedBody;
},
async Fetch(inPath:string, inKey:string, inCheckCache=true)
{
const check = this.Cache.get(inPath);
@ -132,6 +243,19 @@ export const Transpile =
}
};
export const ExtensionPrefix =(string:string, suffix:string)=>
{
const lastDotIndex = string.lastIndexOf(".");
if (lastDotIndex === -1) {
return string;
}
const prefix = string.substring(0, lastDotIndex);
const suffixString = string.substring(lastDotIndex + 1);
return prefix + "." + suffix + "." + suffixString;
};
export const Extension =(inPath:string)=>
{
const posSlash = inPath.lastIndexOf("/");
@ -145,97 +269,94 @@ export const Configure =(config:ConfigurationArgs)=>
ImportMapReload();
}
let running = false;
export default async()=>
await ImportMapReload();
await SWCW.default();
const server = Deno.serve({port:parseInt(Deno.env.get("port")||"8000")}, async(req: Request)=>
{
if(running){return};
running = true;
try
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)
{
await ImportMapReload();
await SWCW.default();
return new Response(`{"cleared":${Transpile.Clear()}}`, {headers});
}
catch(e)
// allow for custom handler
const custom = await Configuration.Serve(req, url, ext, ImportMap, Configuration);
if(custom)
{
console.log("swc init error:", e);
return custom;
}
const server = Deno.serve({port:parseInt(Deno.env.get("port")||"8000")}, async(req: Request)=>
// transpileable files
if(Transpile.Check(ext))
{
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)
let code;
let path;
let file;
if( req.headers.get("user-agent")?.startsWith("Deno") || url.searchParams.has("deno") )
{
try
{
const type = MIME.typeByExtension(ext);
const file = await fetch(proxy);
return new Response(file.body, {headers:{...headers, "content-type":type||""}});
path = Configuration.Proxy + ExtensionPrefix(url.pathname, "deno");
console.log("looking for", path);
file = await fetch(path) as Response;
if(file.ok)
{
code = await file.text();
}
else
{
throw new Error("404")
}
}
catch(e)
{
return new Response(`{"error":"${e}", "path":"${url.pathname}"}`, {status:404, headers});
path = Configuration.Proxy + url.pathname;
console.log("falling back to", path);
file = await fetch(path);
code = await file.text();
}
}
return new Response(`{"error":"unmatched route", "path":"${url.pathname}"}`, {status:404, headers});
});
}
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);
if(shell)
{
return shell;
}
}
// all other static files
if(ext)
{
try
{
const type = MIME.typeByExtension(ext);
const file = await fetch(Configuration.Proxy + url.pathname);
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
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();
}

56
things.md Normal file
View File

@ -0,0 +1,56 @@
# Outpost
## Deno Deploy
```
accept: */*
accept-encoding: gzip, br
accept-language: *
cdn-loop: deno;s=deno;d=ah40t9m8n54g
host: bad-goat-66.deno.dev
user-agent: Deno/1.34.1
```
## Deno
```
accept: */*
accept-encoding: gzip, br
accept-language: *
host: bad-goat-66.deno.dev
user-agent: Deno/1.34.3
```
## Edge
```
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
host: bad-goat-66.deno.dev
referer: https://dash.deno.com/
sec-ch-ua: "Microsoft Edge";v="117", "Not;A=Brand";v="8", "Chromium";v="117"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
sec-fetch-dest: iframe
sec-fetch-mode: navigate
sec-fetch-site: cross-site
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.0.0
```
## Firefox
```
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.5
host: bad-goat-66.deno.dev
sec-fetch-dest: document
sec-fetch-mode: navigate
sec-fetch-site: cross-site
te: trailers
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0
```
When a requet comes in:
- if its for a transpile-able document:
- if its a request from deno:
-