Compare commits

...

8 Commits

Author SHA1 Message Date
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
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
9 changed files with 149 additions and 151 deletions

View File

@ -167,9 +167,10 @@ export async function Check()
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.17.1/compat`;
importMap["react/"] = `https://esm.sh/preact@10.17.1/compat/`;
importMap["react"] = `https://esm.sh/preact@10.18.1/compat`;
importMap["react/"] = `https://esm.sh/preact@10.18.1/compat/`;
importMap["@preact/signals"] = `https://esm.sh/@preact/signals@1.2.1`;
importMap["@twind/core"] = `https://esm.sh/@twind/core@1.1.3`;
importMap[">able/"] = `${RootHost}`;
if(!importMap[">able/app.tsx"])
{
@ -195,18 +196,30 @@ export async function Check()
const confTasks = (config.json.tasks || {}) as Record<string, string>;
config.json.tasks = {...confTasks, ...tasks};
const options =
const optionsRequired =
{
"lib": ["deno.window", "dom", "dom.asynciterable"],
"lib": ["deno.window", "dom", "dom.iterable", "dom.asynciterable"],
"jsx": "react-jsx",
"jsxImportSource": "react"
}
const compOpts = config.json.compilerOptions as Record<string, string|string[]> || {};
const compLib:string[] = compOpts.lib as string[] || [];
compOpts.jsx = options.jsx;
compOpts.jsxImportSource = options.jsxImportSource;
compOpts.lib = [...compLib, ...options.lib];
config.json.compilerOptions = compOpts;
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;
optionsCurrent.jsxImportSource = optionsRequired.jsxImportSource;
config.json.compilerOptions = optionsCurrent;
await bake(imports);
await bake(config);

14
cli.tsx
View File

@ -99,8 +99,8 @@ if(arg._.length)
}
case "cloud" :
{
let useToken = await collect("DENO_DEPLOY_TOKEN", arg, env);
let useProject = await collect("DENO_DEPLOY_PROJECT", arg, env);
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)
@ -113,7 +113,7 @@ if(arg._.length)
scanProd = [];
}
await SubProcess([
const command = [
"run",
"-A",
"--no-lock",
@ -123,9 +123,13 @@ if(arg._.length)
`--project=${useProject}`,
`--token=${useToken}`,
`--import-map=${imports.path}`,
RootHost+"run.tsx",
`--exclude=.*,.*/,`,
...scanProd,
...Deno.args]);
RootHost+"run.tsx"];
await SubProcess(command);
break;
}
case "upgrade" :
{

View File

@ -1,10 +1,13 @@
{
"imports": {
"react": "https://esm.sh/preact@10.17.1/compat",
"react-original": "https://esm.sh/preact@10.17.1/compat",
"react/": "https://esm.sh/preact@10.17.1/compat/",
"@preact/signals": "https://esm.sh/@preact/signals@1.2.1",
"signals-original": "https://esm.sh/@preact/signals@1.2.1",
"@twind/core": "https://esm.sh/@twind/core@1.1.3",
">able/": "http://localhost:4507/",
">able/app.tsx": "./app.tsx",
"@preact/signals": "https://esm.sh/@preact/signals@1.2.1"
">able/app.tsx": "./app.tsx"
},
"tasks": {
"check": "deno run -A --no-lock http://localhost:4507/cli.tsx check",
@ -12,7 +15,7 @@
"debug": "deno run -A --no-lock http://localhost:4507/cli.tsx debug",
"serve": "deno run -A --no-lock http://localhost:4507/cli.tsx serve",
"cloud": "deno run -A --no-lock http://localhost:4507/cli.tsx cloud",
"install": "deno install -A -r -f http://localhost:4507/cli.tsx"
"install": "deno install -A -r -f -n able http://localhost:4507/cli.tsx"
},
"compilerOptions": {
"jsx": "react-jsx",

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};
const SocketTimer = setInterval(()=>{Socket.send("ping")}, 5000);

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

46
hmr-signal.tsx Normal file
View File

@ -0,0 +1,46 @@
import * as SignalsParts from "signals-original";
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 && lookupOld[1] === arg)
{
recordEntryNew[recordIndex] = lookupOld;
recordIndex++;
return lookupOld[0];
}
else
{
const sig = inFunc(arg);
recordEntryNew[recordIndex] = [sig, arg];
recordEntry[recordIndex] = [sig, arg];
recordIndex++;
return sig;
}
};
return {reset, swap, proxy};
}
export const GroupSignal = ProxyGroup(SignalsParts.signal);
export const GroupSignalHook = ProxyGroup(SignalsParts.useSignal);
const proxySignal = GroupSignal.proxy;
const proxySignalHook = GroupSignalHook.proxy;
export * from "signals-original";
export { proxySignal as signal, proxySignalHook as useSignal };
export default {...SignalsParts, signal:proxySignal, useSignal:proxySignalHook};

View File

@ -1,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";

View File

@ -27,6 +27,10 @@ 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 Extra(inReq, inURL, inExt, inMap, inConfig)

View File

@ -74,7 +74,7 @@ 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", "${inConfig.Start}");
@ -122,85 +122,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);