Compare commits

...

46 Commits

Author SHA1 Message Date
11f896f12b Merge branch 'master' of https://gitea.hptrow.me/SethTrowbridge/able-baker 2024-05-06 06:27:50 -04:00
130ccd949b simplify jsx config 2024-05-06 06:24:50 -04:00
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
d9801f3e95 adjust import proxies
something was very wrong
and could still be...
2024-05-05 16:03:00 -04:00
81b5a3d880 Merge pull request 'pin new versions' (#13) from fix-dependencies into master
Reviewed-on: #13
2024-05-04 11:35:14 -04:00
c4ee536c7a pin new versions 2024-05-04 11:34:12 -04:00
4964dd7823 Merge pull request 'version deploy ctl' (#12) from fix-deploy into master
Reviewed-on: #12
2024-05-04 08:18:24 -04:00
d941abde0f version deploy ctl
update to new exclusion glob patterns
2024-05-04 08:12:06 -04:00
67b2eef7ae Merge pull request 'fix config merging' (#11) from shadow-option into master
Reviewed-on: #11
2023-10-31 09:46:29 -04:00
1a1d7c4fd8 fix config merging 2023-10-31 09:45:26 -04:00
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
9f3d26d779 make shadow dom optional 2023-10-31 09:14:22 -04:00
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
1061b6efff add deep equality for hrm signals 2023-10-23 11:00:00 -04:00
dd006daae7 Merge pull request 'dreaded-fixes' (#8) from dreaded-fixes into master
Reviewed-on: #8
2023-10-17 20:10:12 -04:00
fbe3959c07 Merge branch 'dreaded-fixes' of https://gitea.hptrow.me/SethTrowbridge/able-baker into dreaded-fixes 2023-10-16 21:20:47 -04:00
ca2b8ec596 Merge pull request 'dreaded-fixes' (#7) from dreaded-fixes into master
Reviewed-on: #7
2023-10-16 13:30:24 -04:00
4f90a33a93 deploy exclusion 2023-10-16 13:29:10 -04:00
7824b47634 hmr signal hook! 2023-10-16 12:33:07 -04:00
b0f87d20c5 signal hmr! 2023-10-16 10:27:22 -04:00
9433c15bd9 fix es export { } syntax 2023-10-15 08:52:35 -04:00
9e7fe002b0 signal started 2023-10-14 23:47:20 -04:00
2b83a2abe6 separate HMR listeners 2023-10-14 12:09:06 -04:00
69f902c927 fix lib comp opts 2023-10-14 12:06:11 -04:00
c44c1df257 misc items 2023-10-12 22:47:45 -04:00
736d0c6a9a Merge pull request 'cli' (#6) from cli into master
Reviewed-on: #6
2023-09-24 09:33:29 -04:00
4ef3cc47a5 more cli fixes 2023-09-24 09:19:50 -04:00
2757998675 config checking 2023-08-28 22:16:56 -04:00
acda59303d wording 2023-08-13 22:58:15 -04:00
783970f2c0 improved checking and options 2023-08-13 11:00:48 -04:00
b602ffcbcc checker started 2023-08-12 20:30:44 -04:00
67b5f03a7a hunt config 2023-08-10 22:37:53 -04:00
29768761cb use deno confirm 2023-08-02 06:07:34 -04:00
c558ecb6d2 import map and settings started 2023-08-01 22:42:31 -04:00
2d2ba6fa2b install function 2023-08-01 17:23:19 -04:00
35f8e64a26 installer started 2023-08-01 15:30:31 -04:00
2398899662 add config hunter 2023-08-01 10:19:39 -04:00
327efd3940 Merge pull request 'start-file' (#5) from start-file into master
Reviewed-on: #5
2023-07-30 14:29:34 -04:00
bd7af1241e allow for production deployment 2023-07-30 14:21:35 -04:00
79c5a0aff0 fix flag names 2023-07-30 14:01:14 -04:00
1f6ab7cf47 only run once 2023-07-30 13:29:15 -04:00
78230a5672 allow for code or config setup 2023-07-30 13:04:26 -04:00
6134b7be34 switch to code configuration 2023-07-30 09:16:17 -04:00
a9ce1c6f4c Merge pull request 'fix config arg' (#4) from api into master
Reviewed-on: #4
2023-07-29 21:23:11 -04:00
e5a0a526a8 Merge branch 'master' into api 2023-07-29 21:23:04 -04:00
56c4ec7242 fix config arg 2023-07-29 21:21:23 -04:00
27 changed files with 751 additions and 478 deletions

2
.gitingore Normal file
View File

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

8
.vscode/launch.json vendored
View File

@ -5,13 +5,17 @@
"version": "0.2.0",
"configurations": [
{
"request": "launch",
"name": "Debug Serve Mode",
"request": "launch",
"type": "node",
"cwd": "${workspaceFolder}/example",
"runtimeExecutable": "deno",
"runtimeArgs": ["task", "debug"],
"attachSimplePort": 9229
},
{
"name":"Attach",
"request": "attach",
"type": "node"
}
]
}

View File

@ -1,4 +0,0 @@
{
"deno.enable": true,
"deno.unstable": true
}

View File

1
app
View File

@ -1 +0,0 @@
export default ()=>{};

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,19 +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/": "http://localhost:4507/"
},
"tasks":
{
"local": "deno run -A --reload=http://localhost --no-lock ./run-local.tsx --port=1234",
"serve": "deno run -A --reload=http://localhost --no-lock ./run-serve.tsx --port=1234",
"debug": "deno run -A --reload=http://localhost --inspect-wait --no-lock ./run-serve.tsx --port=1234",
"deploy":"deno run -A --no-lock --reload=http://localhost http://localhost:4507/run-deploy.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 +0,0 @@
DENO_DEPLOY_TOKEN=1

View File

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

View File

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

View File

@ -1,17 +0,0 @@
{
"compilerOptions": { "lib": ["deno.window", "dom"] },
"imports":
{
"react": "https://esm.sh/preact@10.15.1/compat",
">able/": "http://localhost:4507/",
">able/app": "./app.tsx",
">able/api": "./api.tsx"
},
"tasks":
{
"local": "deno run --config=deno.json -A --no-lock --reload=http://localhost:4507 http://localhost:4507/run-local.tsx",
"serve": "deno run --config=deno.json -A --no-lock --reload=http://localhost:4507 http://localhost:4507/run-serve.tsx",
"deploy":"deno run -A --no-lock --reload=http://localhost:4507 http://localhost:4507/run-deploy.tsx",
"debug": "deno run --config=deno.json -A --inspect-wait --no-lock --reload=http://localhost:4507 http://localhost:4507/run-serve.tsx"
}
}

View File

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

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

View File

@ -65,7 +65,7 @@ const findNextExport =(inFile:string, inIndex=0, inLocal:Array<string>, inForeig
}
else
{
const members = inFile.substring(nextCharInd+1, endBracketInd);
const members = inFile.substring(nextCharInd+1, endBracketInd).replace(/\s/g, '');
members.split(",").forEach(part=>
{
const renamed = part.split(" as ");

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

View File

@ -1,5 +1,5 @@
import React from "react";
import * as TW from "https://esm.sh/v126/@twind/core@1.1.3/es2022/core.mjs";
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";
@ -10,22 +10,25 @@ 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(merge, TW.cssom(ShadowCSS)), ShadowDiv);
TW.observe(TW.twind(inConfig, TW.cssom(ShadowCSS)), ShadowDiv);
return ShadowDiv;
};
export default async(inSelector:string, inModulePath:string, inMemberApp="default", inMemberCSS="CSS"):Promise<(()=>void)|false>=>
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)
{
@ -34,7 +37,17 @@ export default async(inSelector:string, inModulePath:string, inMemberApp="defaul
}
const module = await import(inModulePath);
dom = Shadow(dom as HTMLElement, module[inMemberCSS])
const merge = inMemberCSS ? {...Configure, ...module[inMemberCSS]} : Configure;
if(inShadow)
{
dom = Shadow(dom as HTMLElement, merge);
}
else
{
TW.install(merge);
}
const app = React.createElement(()=> React.createElement(module[inMemberApp], null), null);
if(React.render)
@ -49,5 +62,4 @@ export default async(inSelector:string, inModulePath:string, inMemberApp="defaul
root.render(app);
return root.unmount;
}
};

View File

@ -30,6 +30,7 @@ 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>=>
@ -45,8 +46,7 @@ const prompt =async(question: string):Promise<string>=>
try
{
console.log("Runing deploy!");
const serveScript = import.meta.resolve("./run-serve.tsx");
console.log("Runing deploy!", Deno.mainModule);
let arg = parse(Deno.args);
let env = await Env.load();
@ -54,6 +54,17 @@ try
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`,
{
@ -64,9 +75,10 @@ try
"https://deno.land/x/deploy/deployctl.ts",
"deploy",
`--project=${useProject}`,
`--config=deno.json`,
`--import-map=./deno.json`,
`--token=${useToken}`,
serveScript
...scanProd,
Deno.mainModule
],
stdin: "piped",
stdout: "piped"

View File

@ -1,4 +1,5 @@
import {Configure, Transpile, Extension, Root} from "./run-serve.tsx";
import {Configure, Transpile, Extension} from "./run-serve.tsx";
import { Root } from "./checker.tsx";
import * as Collect from "./hmr-static.tsx";
const SocketsLive:Set<WebSocket> = new Set();
@ -16,6 +17,10 @@ Configure({
{
syntax: "typescript",
tsx: true,
},
transform:
{
react: { runtime: "automatic" }
}
}
},
@ -23,9 +28,13 @@ Configure({
{
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 Serve(inReq, inURL, inExt, inMap, inConfig)
async Extra(inReq, inURL, inExt, inMap, inConfig)
{
if(!inURL.pathname.startsWith(encodeURI("/>")))
{

View File

@ -1,91 +1,48 @@
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 Api from ">able/api";
export const Root = new URL(`file://${Deno.cwd().replaceAll("\\", "/")}`).toString();
Deno.args.forEach(arg=>
{
if(arg.startsWith("--"))
{
const kvp = arg.substring(2).split("=");
Deno.env.set(kvp[0], kvp[1] || "true");
}
});
import { HuntConfig, Root } from "./checker.tsx";
import CustomServe from ">able/api.tsx";
type DenoConfig = {imports:Record<string, string>};
const ImportMap:DenoConfig = {imports:{}};
let ImportMapOriginal = {};
let ImportMapProxies:Record<string, string> = {};
const ImportMapReload =async()=>
{
let json:DenoConfig;
const path = Root+"/deno.json";
try
{
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;
}
const [, {json}] = await HuntConfig();
const imports = (json as DenoConfig).imports;
if(!json.imports["react"])
if(imports)
{
console.log(`"react" specifier not defined in import map`);
}
else if(!json.imports["react/"])
{
json.imports["react/"] = json.imports["react"]+"/";
}
if(!json.imports["able:app"])
{
console.log(`"able:app" specifier not defined in import map.`);
}
ImportMapProxies = {};
Object.entries(json.imports).forEach(([key, value])=>
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("./"))
{
json.imports[key] = value.substring(1);
imports[key] = value.substring(1);
}
if(key.startsWith(">"))
if(key.startsWith(">") && key.endsWith("/"))
{
if(value.startsWith("./"))
{
ImportMapProxies[encodeURI(key)] = value.substring(1);
json.imports[key] = value.substring(1);
}
else
{
ImportMapProxies["/"+encodeURI(key)] = value;
json.imports[key] = "/"+key;
}
imports[key] = "/"+key;
}
});
ImportMap.imports = Configuration.Remap(json.imports, Configuration);
ImportMap.imports = Configuration.Remap(imports, 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 = {Allow:string, Reset:string, SWCOp:SWCW.Options, Serve:CustomHTTPHandler, Shell:CustomHTTPHandler, Remap:CustomRemapper};
type ConfigurationArgs = {Allow?:string, Reset?:string, SWCOp?:SWCW.Options, Serve?:CustomHTTPHandler, Shell?:CustomHTTPHandler, Remap?:CustomRemapper};
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 Serve(inReq, inURL, inExt, inMap, inConfig){},
async Extra(inReq, inURL, inExt, inMap, inConfig){},
Serve: CustomServe,
Remap: (inImports, inConfig)=>
{
return inImports;
@ -101,10 +58,10 @@ let Configuration:Configuration =
</head>
<body>
<div id="app"></div>
<script type="importmap">${JSON.stringify(inMap)}</script>
<script type="importmap">${JSON.stringify(inMap, null, " ")}</script>
<script type="module">
import Mount from ">able/run-browser.tsx";
Mount("#app", ">able/app");
Mount("#app", "${inConfig.Start}");
</script>
</body>
</html>`, {status:200, headers:{"content-type":"text/html"}});
@ -149,85 +106,6 @@ export const Transpile =
ImportMapReload();
return size;
},
/**
* DONT USE
* 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);
@ -254,19 +132,6 @@ 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("/");
@ -280,25 +145,31 @@ export const Configure =(config:ConfigurationArgs)=>
ImportMapReload();
}
await ImportMapReload();
try
let running = false;
export default async()=>
{
if(running){return};
running = true;
try
{
await ImportMapReload();
await SWCW.default();
}
catch(e)
{
}
catch(e)
{
console.log("swc init error:", e);
}
}
const server = Deno.serve({port:parseInt(Deno.env.get("port")||"8000")}, async(req: Request)=>
{
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("/__"))
if(url.pathname.includes("__/") || url.pathname.lastIndexOf("__.") > -1)
{
return new Response(`{"error":"unmatched route", "path":"${url.pathname}"}`, {status:404, headers});
}
@ -306,31 +177,18 @@ const server = Deno.serve({port:parseInt(Deno.env.get("port")||"8000")}, async(r
// proxy imports
if(url.pathname.startsWith(encodeURI("/>")))
{
let bestMatch="";
for(let key in ImportMapProxies)
{
if(url.pathname.startsWith(key) && key.length > bestMatch.length)
{
bestMatch = key;
}
}
if(bestMatch.length)
{
const match = ImportMapProxies[bestMatch];
const path = url.pathname.substring(bestMatch.length);
proxy = path ? match + path : Root + match;
}
/** pathname with no leading slash */
const clippedPath = decodeURI(url.pathname.substring(1));
proxy = import.meta.resolve(clippedPath);
}
// allow for custom handler
const custom = await Configuration.Serve(req, url, ext, ImportMap, Configuration);
// allow for custom handlers
const custom = await Configuration.Extra(req, url, ext, ImportMap, Configuration);
if(custom)
{
return custom;
}
const api = Api(req);
const api = await Configuration.Serve(req, url, ext, ImportMap, Configuration);
if(api)
{
return api;
@ -339,7 +197,6 @@ const server = Deno.serve({port:parseInt(Deno.env.get("port")||"8000")}, async(r
// transpileable files
if(Transpile.Check(ext))
{
console.log("transpiling:", proxy);
const code = await Transpile.Fetch(proxy, url.pathname);
if(code)
{
@ -380,5 +237,5 @@ const server = Deno.serve({port:parseInt(Deno.env.get("port")||"8000")}, async(r
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();
}

View File

@ -1,56 +0,0 @@
# 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:
-