Compare commits

..

1 Commits

Author SHA1 Message Date
915fdf2d54 add url remapper file 2023-07-24 17:41:13 -04:00
26 changed files with 552 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/": "http://localhost:1234/"
},
"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"
}
}

56
example/app.tsx Normal file
View File

@ -0,0 +1,56 @@
import { Router, Switch, Case } from "able/iso-elements.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">
<Router.Provider>
<div class="my-4 font-sans">
<h1 class="font-black text-xl text-red-500">Title</h1>
<h2 class="font-black text-blue-500 p-4">subtitle</h2>
<p>
<button onClick={e=>Dispatch(1)}>{Store.name}|{Store.age}?</button>
</p>
</div>
<Outer>
<Inner/>
</Outer>
<Outer>
<Inner/>
</Outer>
</Router.Provider>
</CTXString.Provider>;
}

14
example/deno.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": { "lib": ["deno.window", "dom"] },
"imports":
{
"react": "https://esm.sh/preact@10.15.1/compat",
"able/": "http://localhost:1234/",
"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);
/*
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 useEntry = await collect("DENO_DEPLOY_ENTRY", arg, env);
Deno.env.set("DENO_DEPLOY_TOKEN", useToken || "");
Deno.env.set("DENO_DEPLOY_ENTRY", useEntry || "");
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`,
@ -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,42 +16,46 @@ 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)
{
if(!inURL.pathname.startsWith(encodeURI("/>")))
async Serve(inReq, inURL, inExt, inMap, inConfig)
{
if(Transpile.Check(inExt) && !inURL.searchParams.get("reload"))
{
// const imp = await import(inConfig.Proxy+inURL.pathname);
// const members = [];
// for( const key in imp ) { members.push(key); }
//
// 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") }
//});`
// 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 [local, foreign] = await Collect.FileExports(inConfig.Proxy+inURL.pathname);
console.log(local, foreign);
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)=>
{
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") }
`
});
${ foreign.join(";\n") }
`
return new Response(code, {headers:{"content-type":"application/javascript"}});
}
@ -72,8 +75,6 @@ Configure({
catch(e){ /**/ }
}
}
}
});
const Watcher =async()=>
@ -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,85 @@
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])=>
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)
{
// 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);
console.log(`error reading deno config "${path}" message:"${e}"`);
return;
}
if(key.startsWith(">") && key.endsWith("/"))
Object.entries(json.imports).forEach(([key, value])=>
{
imports[key] = "/"+key;
if(value.startsWith("./"))
{
json.imports[key] = value.substring(1);
}
});
ImportMap.imports = Configuration.Remap(imports, Configuration);
if(!json.imports["react"])
{
console.log(`"react" specifier not defined in import map`);
}
else if(!json.imports["react/"])
{
json.imports["react/"] = json.imports["react"]+"/";
}
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)=>
{
return inImports;
},
Shell(inReq, inURL, inExt, inMap, inConfig)
{
const parts = Deno.mainModule.split(inConfig.Proxy);
return new Response(
`<!doctype html>
<html>
@ -58,10 +89,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 +137,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 +241,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,59 +267,63 @@ 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
{
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)
// cache-reset route
if(url.pathname === Configuration.Reset)
{
return new Response(`{"error":"unmatched route", "path":"${url.pathname}"}`, {status:404, headers});
return new Response(`{"cleared":${Transpile.Clear()}}`, {headers});
}
// proxy imports
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);
// allow for custom handler
const custom = await Configuration.Serve(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);
let code;
let path;
let file;
if( req.headers.get("user-agent")?.startsWith("Deno") || url.searchParams.has("deno") )
{
try
{
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)
{
path = Configuration.Proxy + url.pathname;
console.log("falling back to", path);
file = await fetch(path);
code = await file.text();
}
}
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"}} );
@ -214,19 +340,13 @@ export default async()=>
}
}
// 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);
const file = await fetch(Configuration.Proxy + url.pathname);
return new Response(file.body, {headers:{...headers, "content-type":type||""}});
}
catch(e)
@ -237,5 +357,4 @@ export default async()=>
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:
-

41
url-remapper.tsx Normal file
View File

@ -0,0 +1,41 @@
function remapUrl({ input, mapFrom, mapTo }: { input: string, mapFrom: string, mapTo: string}): string | undefined
{
const partsInput = input.split('/');
const partsMapFrom = mapFrom.split('/');
const partsMapTo = mapTo.split('/');
if(partsInput.length == partsMapFrom.length)
{
for(let i=0; i<partsInput.length; i++)
{
const partInput = partsInput[i];
const partMapFrom = partsMapFrom[i];
if(partMapFrom.startsWith(":"))
{
const indexMapTo = partsMapTo.indexOf(partMapFrom);
if(indexMapTo > -1)
{
const partMapTo = partsMapTo[indexMapTo];
if(partMapTo === partMapFrom)
{
partsMapTo[indexMapTo] = partInput;
continue;
}
}
return;
}
}
return partsMapTo.join("/");
}
}
const remap: string | undefined = remapUrl({
input: "/base/theproj/app01",
mapFrom: "/base/:project/:id",
mapTo: "https://vcs.com/remote/raw/:project/show/:id"
});
console.log(remap); // Output: "https://vcs.com/remote/raw/theproj/show/app01"