Compare commits

..

No commits in common. "783970f2c008f427c42b5b95f676d9305c55f505" and "67b5f03a7a8536dae074ab5df465bfd4595b28a9" have entirely different histories.

7 changed files with 328 additions and 344 deletions

View File

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

View File

@ -1,283 +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()
{
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()
{
try
{
let [config, imports] = await HuntConfig();
//console.log(config, imports);
if(!config.path)
{
console.log(`🛠️ No Deno configuration found. Creating "deno.jsonc" now.`);
await Deno.writeTextFile(Deno.cwd()+"/deno.jsonc", `{"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"));
if(!importMap["react"])
{
console.log(`🛠️ Adding React import specifier ("react")`);
importMap["react"] = `https://esm.sh/preact@10.16.0/compat`;
importMap["react/"] = `https://esm.sh/preact@10.16.0/compat/`;
await bake(imports);
console.log(`🚦 NOTE: Deno will need to cache "react" for intellisense to work properly.`)
}
if(!importMap[">able/"])
{
console.log(`🛠️ Adding Able import specifier (">able/").`);
importMap[">able/"] = `${RootHost}`;
await bake(imports);
}
if(!importMap[">able/app.tsx"])
{
const resp = confirm(`🤔 OPTIONAL: Import map has no specifier for your starter app (">able/app.tsx"). Create one?`);
if(resp)
{
importMap[">able/app.tsx"] = `./app.tsx`;
await bake(imports);
await Install("app.tsx");
}
}
else
{
try
{
const app = await import(importMap[">able/app.tsx"]);
// @ts-ignore
const result = app.default().$$typeof;
}
catch(e)
{
console.log(e);
if(confirm(`🚧 Your starter app ("${importMap[">able/app.tsx"]}") does not export a default function that returns VDOM nodes. Replace it?`))
{
await Install("app.tsx", importMap[">able/app.tsx"]);
}
else
{
throw("⛔ Starter app has incorrect export types.");
}
}
}
if(!importMap[">able/api.tsx"])
{
const resp = confirm(`🤔 OPTIONAL: Import map has no specifier for your starter backend app (">able/api.tsx"). Create one?`);
if(resp)
{
importMap[">able/api.tsx"] = "./api.tsx";
await bake(imports);
await Install("api.tsx");
}
}
else
{
try
{
const api = await import(importMap[">able/api.tsx"]);
const result = api.default(new Request(new URL("https://fake-deno-testing-domain.com/")));
}
catch(e)
{
if(confirm(`🚧 Your starter backend app ("${importMap[">able/api.tsx"]}") does not export a default function that accepts a Request. Replace it?`))
{
await Install("api.tsx", importMap[">able/api.tsx"]);
}
else
{
throw("⛔ Starter backend app has incorrect export types.");
}
}
}
const options =
{
"lib": ["deno.window", "dom", "dom.asynciterable"],
"jsx": "react-jsx",
"jsxImportSource": "react"
}
const compOpts = config.json.compilerOptions as Record<string, string|string[]> || {};
const compJSX = compOpts.jsx == options.jsx;
const compJSXImportSource = compOpts.jsxImportSource == options.jsxImportSource;
const compLib:string[] = compOpts.lib as string[] || [];
let compLibHasAll = true;
options.lib.forEach(item=> !compLib.includes(item) && (compLibHasAll = false))
if(!compOpts || !compJSX || !compJSXImportSource || !compLibHasAll)
{
console.log(`🛠️ Adding values to "compilerOptions" configuration.`);
compOpts.jsx = options.jsx;
compOpts.jsxImportSource = options.jsxImportSource;
compOpts.lib = [...compLib, ...options.lib];
config.json.compilerOptions = compOpts;
await bake(config);
}
}
}
catch(e)
{
console.log(e, "\n (Able Exiting...)");
Deno.exit();
}
console.log(`🚗 Good to go!`);
}
Check();

241
cli.tsx
View File

@ -1,7 +1,8 @@
import * as Env from "https://deno.land/std@0.194.0/dotenv/mod.ts"; 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 * as Arg from "https://deno.land/std@0.194.0/flags/mod.ts";
import { RootHost, HuntConfig, Install, Check } from "./checker.tsx"; import { parse as JSONC } from "https://deno.land/x/jsonct@v0.1.0/mod.ts";
const RootFile = new URL(`file://${Deno.cwd().replaceAll("\\", "/")}`).toString();
const RootHost = import.meta.resolve("./");
let arg = await Arg.parse(Deno.args); let arg = await Arg.parse(Deno.args);
let env = await Env.load(); let env = await Env.load();
const collect =async(inKey:string, inArg:Record<string, string>, inEnv:Record<string, string>):Promise<string|undefined>=> const collect =async(inKey:string, inArg:Record<string, string>, inEnv:Record<string, string>):Promise<string|undefined>=>
@ -36,6 +37,86 @@ const collect =async(inKey:string, inArg:Record<string, string>, inEnv:Record<st
}; };
type ConfigCheck = {path?:string, text?:string, json?:Record<string, string|Record<string, string|string[]>>};
type ConfigCheckPair = [config:ConfigCheck, imports:ConfigCheck];
export async function HuntConfig()
{
let path:string, resp:Response, text="", json;
try
{
path = "deno.json"
resp = await fetch(RootFile + "/" + path);
text = await resp.text();
}
catch(e)
{
try
{
path = "deno.jsonc";
resp = await fetch(RootFile + "/" + path);
text = await resp.text();
}
catch(e)
{
try
{
path = RootFile+"/.vscode/settings.json"
resp = await fetch(path);
json = await resp.json();
path = json["deno.config"];
json = undefined;
if(path)
{
path = RootFile + "/" + path
resp = await fetch(path);
text = await resp.text();
}
}
catch(e)
{
path = "";
}
}
}
if(text)
{
try
{
json = JSONC(text);
}
catch(e)
{
// malformed config
}
}
let imports:ConfigCheck = {};
if(json && json.imports)
{
imports.json = json;
imports.text = JSON.stringify(json.imports);
imports.path = path;
}
else if(json && !json.imports && json.importMap)
{
try
{
imports.path = RootFile + "/" + json.importMap;
resp = await fetch(imports.path);
imports.text = await resp.text();
imports.json = JSONC(text);
}
catch(e)
{
// malformed import map
}
}
return [{path, text, json}, imports] as ConfigCheckPair
}
export async function SubProcess(args:string[]) export async function SubProcess(args:string[])
{ {
const command = new Deno.Command( const command = new Deno.Command(
@ -63,6 +144,162 @@ export async function SubProcess(args:string[])
const status = await child.status; const status = await child.status;
} }
export async function Install(file:string, handler:(content:string)=>string = (s)=>s)
{
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();
await Deno.writeTextFile(Deno.cwd()+"/"+file, handler(text));
}
}
export async function Check()
{
console.info(`👷 Checking your project`)
try
{
let [config, imports] = await HuntConfig();
if(!config.path)
{
//const resp1 = await Prompt(" ! No Deno configuration found. Create one? [y/n]");
const resp1 = confirm("🚨🚧 No Deno configuration found. Create one?");
if(resp1)
{
const resp2 = confirm("⚠️🚧 Do you also want to add starter files?");
let replaceApp = "./path/to/app.tsx";
let replaceApi = "./path/to/api.tsx";
let replaceCommentApp = "// (required) module with default export ()=>React.JSX.Element";
let replaceCommentApi = "// (optional) module with default export (req:Request, url:URL)=>Promise<Response|false>";
if(resp2)
{
replaceApp = "./app.tsx";
replaceApi = "./api.tsx";
replaceCommentApp = "";
replaceCommentApi = "";
await Install("app.tsx");
await Install("api.tsx");
}
else
{
// config initialized with no app or api
}
await Install("deno.jsonc", (s)=>s
.replace("{{server}}", RootHost)
.replace("{{app}}", replaceApp)
.replace("{{api}}", replaceApi)
.replace("{{commentApp}}", replaceCommentApp)
.replace("{{commentApi}}", replaceCommentApi)
);
[config, imports] = await HuntConfig();
}
else
{
throw("⛔ Config is required.");
}
}
/*
const inputString = `Some text 'imports' more text { some content }`;
const regex = /(?<=(['"`])imports\1[^{}]*{)/;
const match = inputString.search(regex);
if (match !== -1) {
console.log("Index of '{':", match);
} else {
console.log("'{': Not found.");
}
*/
if(!config.json)
{
throw("⛔ Config is malformed.");
}
else if(!imports.json?.imports)
{
const resp = confirm(`🚨🔧 Configuration has no import map. Fix it now?`);
if(resp)
{
}
}
if(config.json && imports.json?.imports)
{
const importMap = imports.json.imports as Record<string, string>;
let changes = ``;
if(!importMap["react"])
{
const resp = confirm(`🚨🔧 Import map has no specifier for React ("react"). Fix it now? (Will use Preact compat)`);
if(resp)
{
importMap["react"] = "https://esm.sh/preact@10.16.0/compat";
changes += `"react": "https://esm.sh/preact@10.16.0/compat",\n`;
}
else
{
throw(`⛔ A React import ("react") is required.`);
}
}
if(!importMap[">able/"])
{
const resp = confirm(`🚨🔧 Import map has no specifier for Able (">able/"). Fix it now?`);
if(resp)
{
importMap[">able/"] = RootHost;
changes += `">able": "${RootHost}",\n`;
}
else
{
throw(`⛔ The Able import (">able/") is required.`);
}
}
const compOpts = imports.json.compilerOptions as Record<string, string>;
if(compOpts)
{
const compJSX = compOpts["jsx"];
const compJSXImportSource = compOpts["jsxImportSource"]
if(compJSX || compJSXImportSource)
{
if(!importMap["react/"])
{
//const resp = await Prompt(` ! Import map has no specifier for React ("react"). Add it now? [y/n]`);
}
}
}
}
}
catch(e)
{
console.log(e, "\n (Able Exiting...)");
Deno.exit();
}
console.log(`🚗 Good to go!`);
}
if(arg._.length) if(arg._.length)
{ {

View File

@ -1,16 +1,42 @@
{ {
"imports": { "imports":
"react": "https://esm.sh/preact@10.16.0/compat", {
"react/": "https://esm.sh/preact@10.16.0/compat/", "react/":"https://esm.sh/preact@10.15.1/compat/", // (conditional) This allows the use of JSX without explicitly importing React into a module. If you choose to remove this (and make importing react required), also remove "jsx" and "jsxImportSource" from "compilerOptions" (below)
">able/": "file:///C:/Web%20Projects/able-baker/" ">able/": "file:///C:/Web%20Projects/able-baker/", // (required) Specifier 'able'. (See note below about "isomorphic proxies")
">able/app.tsx": "./app.tsx",
">able/api.tsx": "./api.tsx"
}, },
"compilerOptions": {
"jsx": "react-jsx", "tasks":
"jsxImportSource": "react", {
"lib": [ "local": "deno run -A --reload=http://localhost:4507 --no-lock ./run-local.tsx --port=1234",
"deno.window", "serve": "deno run -A --reload=http://localhost:4507 --no-lock ./run-serve.tsx --port=1234",
"dom", "cloud": "deno run -A --reload=http://localhost:4507 --no-lock ./run-deploy.tsx",
"dom.asynciterable" "debug": "deno run -A --no-lock --inspect-wait ./cli.tsx work --port=1234"
] },
"compilerOptions":
{
"lib": ["deno.window", "dom"], // makes the Deno Language Server OK with browser-specific code
"jsx": "react-jsx", // see "react/" import above
"jsxImportSource": "react" // ^
} }
/*
Imports prefixed with ">" are "isomorphic proxies."
In addition to functioning normally as bare module specifiers for Deno, **these imports are added as routes when the server starts**.
Assuming the specifier points to remotely a hosted directory containing typescript files, requests to your running Able server on these proxy routes are actually fetched from the remote, then transpiled (and cached), then send back as a response.
For example, after the Able server starts, if it sees a web request to '/>able/iso-elements.tsx' it would actually return a browser-friendly transpiled copy of what was on the remote.
Conversely, if the Deno Language Server were to see: `import * as Iso from ">able/iso-elements.tsx";` in one of your modules,
that will be resolved normally with the import map and Deno will just receive the tsx file as-is from the remote, typings and all, so intellisense will work in your IDE.
While ">able/" is a required "import proxy" to pull in Able source code, you are free to use this convention to also add your own proxies as you see fit.
E.g. adding this record to imports:
">your-import/": "https://raw.githubusercontent.com/your-name/your-lib/master/"
will give both Deno and browsers running your Able project everything they need
import CoolComponent from ">your-import/cc.tsx";
...
*/
} }

View File

@ -8,7 +8,7 @@
"react":"https://esm.sh/preact@10.15.1/compat", "react":"https://esm.sh/preact@10.15.1/compat",
"react/":"https://esm.sh/preact@10.15.1/compat/", "react/":"https://esm.sh/preact@10.15.1/compat/",
"react-original":"https://esm.sh/preact@10.15.1/compat", "react-original":"https://esm.sh/preact@10.15.1/compat",
">able/": "http://localhost:4507/"
}, },
"tasks": "tasks":
{ {

View File

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

View File

@ -1,62 +1,70 @@
import * as MIME from "https://deno.land/std@0.180.0/media_types/mod.ts"; 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 * as SWCW from "https://esm.sh/@swc/wasm-web@1.3.62";
import { HuntConfig } from "./checker.tsx";
import CustomServe from ">able/api.tsx"; import CustomServe from ">able/api.tsx";
export const Root = new URL(`file://${Deno.cwd().replaceAll("\\", "/")}`).toString(); export const Root = new URL(`file://${Deno.cwd().replaceAll("\\", "/")}`).toString();
type DenoConfig = {imports:Record<string, string>}; type DenoConfig = {imports:Record<string, string>};
const ImportMap:DenoConfig = {imports:{}}; const ImportMap:DenoConfig = {imports:{}};
let ImportMapOriginal = {};
let ImportMapProxies:Record<string, string> = {}; let ImportMapProxies:Record<string, string> = {};
const ImportMapReload =async()=> const ImportMapReload =async()=>
{ {
const [, {json, path}] = await HuntConfig(); let json:DenoConfig;
const imports = (json as DenoConfig).imports; const path = Root+"/deno.json";
try
if(imports)
{ {
if(imports["react"]) 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;
}
if(!json.imports["react"])
{ {
console.log(`"react" specifier not defined in import map`); console.log(`"react" specifier not defined in import map`);
} }
else if(!imports["react/"]) else if(!json.imports["react/"])
{ {
imports["react/"] = imports["react"]+"/"; json.imports["react/"] = json.imports["react"]+"/";
} }
if(!json.imports["able:app"])
{
console.log(`"able:app" specifier not defined in import map.`);
}
ImportMapProxies = {}; ImportMapProxies = {};
Object.entries(imports).forEach(([key, value])=> Object.entries(json.imports).forEach(([key, value])=>
{ {
if(value.startsWith("./")) if(value.startsWith("./"))
{ {
imports[key] = value.substring(1); json.imports[key] = value.substring(1);
} }
if(key.startsWith(">")) if(key.startsWith(">"))
{ {
if(value.startsWith("./")) if(value.startsWith("./"))
{ {
ImportMapProxies[encodeURI(key)] = value.substring(1); ImportMapProxies[encodeURI(key)] = value.substring(1);
imports[key] = value.substring(1); json.imports[key] = value.substring(1);
} }
else else
{ {
ImportMapProxies["/"+encodeURI(key)] = value; ImportMapProxies["/"+encodeURI(key)] = value;
imports[key] = "/"+key; json.imports[key] = "/"+key;
} }
} }
}); });
ImportMap.imports = Configuration.Remap(imports, Configuration); ImportMap.imports = Configuration.Remap(json.imports, Configuration);
}
else
{
}
}; };
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 CustomHTTPHandler = (inReq:Request, inURL:URL, inExt:string|false, inMap:{imports:Record<string, string>}, inConfig:Configuration)=>void|false|Response|Promise<Response|void|false>;