cli #6

Merged
SethTrowbridge merged 11 commits from cli into master 2023-09-24 09:33:29 -04:00
5 changed files with 295 additions and 322 deletions
Showing only changes of commit b602ffcbcc - Show all commits

246
checker.tsx Normal file
View File

@ -0,0 +1,246 @@
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(text)
{
try
{
json = JSONC(text);
}
catch(e)
{
// malformed config
json = undefined;
}
}
let imports:ConfigCheck = {};
if(json && json.imports)
{
imports.json = json;
imports.text = JSON.stringify(json);
imports.path = path;
}
else if(json && !json.imports && json.importMap)
{
try
{
imports.path = json.importMap;
resp = await fetch(Root + "/" + 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 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)
{
if(confirm("🚨🚧 No Deno configuration found. Create a new one?"))
{
await Install("deno.jsonc");
Check();
return;
}
else
{
throw("⛔ Configuration is required.");
}
}
else if(!imports.json || !imports.json?.imports)
{
const resp = confirm(`🚨🔧 Configuration found, but has no import map. Fix it now?`);
if(resp)
{
const text = config.text||"";
const startBracket = text.indexOf("{");
config.text = `{
"imports": {}${startBracket < 0 ? "\n}\n" : ",\n"}` + text.substring(startBracket+1);
await Deno.writeTextFile(Deno.cwd()+"/"+config.path, config.text);
Check();
return;
}
else
{
throw("⛔ Import maps are required.");
}
}
if(config.json && imports.text && imports.json?.imports)
{
const importMap = imports.json.imports as Record<string, string>;
let changes = ``;
const match = imports.text.search(/(?<=(['"`])imports\1[^{}]*{)/);
const part1 = imports.text.substring(0, match);
const part2 = imports.text.substring(match);
const bake =async()=> await Deno.writeTextFile(Deno.cwd()+"/"+config.path, part1 + changes + part2);
if(!importMap["react"])
{
const resp = confirm(`🚨🔧 Import map has no specifier for React ("react"). Fix it now? (Will use Preact compat)`);
if(resp)
{
changes += `"react": "https://esm.sh/preact@10.16.0/compat",\n`;
if(!importMap["react/"])
{
changes += `"react/": "https://esm.sh/preact@10.16.0/compat/",\n`;
}
await bake();
}
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)
{
changes += `">able/": "${RootHost}",\n`;
await bake();
}
else
{
throw(`⛔ The Able import (">able/") is required.`);
}
}
if(!importMap[">able/app.tsx"])
{
const resp = confirm(`🚨🔧 Import map has no specifier for your starter app (">able/app.tsx"). Fix it now?`);
if(resp)
{
changes += `">able/app.tsx": "./app.tsx",\n`;
await bake();
await Install("app.tsx");
}
else
{
throw(`⛔ The "starter app" import (">able/app.tsx") is required.`);
}
}
if(!importMap[">able/api.tsx"])
{
const resp = confirm(`🚨🔧 OPTIONAL: Import map has no specifier for your backend app (">able/api.tsx"). Fix it now?`);
if(resp)
{
changes += `">able/api.tsx": "./api.tsx",\n`;
await bake();
await Install("api.tsx");
}
}
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!`);
}
Check();

241
cli.tsx
View File

@ -1,8 +1,7 @@
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 { 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("./");
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>=>
@ -37,86 +36,6 @@ 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[])
{
const command = new Deno.Command(
@ -144,162 +63,6 @@ export async function SubProcess(args:string[])
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)
{

View File

@ -1,42 +1,6 @@
{
"imports":
{
"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/", // (required) Specifier 'able'. (See note below about "isomorphic proxies")
">able/app.tsx": "./app.tsx",
">able/api.tsx": "./api.tsx"
},
"imports": {},
"tasks":
{
"local": "deno run -A --reload=http://localhost:4507 --no-lock ./run-local.tsx --port=1234",
"serve": "deno run -A --reload=http://localhost:4507 --no-lock ./run-serve.tsx --port=1234",
"cloud": "deno run -A --reload=http://localhost:4507 --no-lock ./run-deploy.tsx",
"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.
//imports?
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";
...
*/
}

8
deno.lock Normal file
View File

@ -0,0 +1,8 @@
{
"version": "2",
"remote": {
"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"
}
}

View File

@ -1,70 +1,62 @@
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 } from "./checker.tsx";
import CustomServe from ">able/api.tsx";
export const Root = new URL(`file://${Deno.cwd().replaceAll("\\", "/")}`).toString();
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, path}] = 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])=>
{
if(value.startsWith("./"))
if(imports["react"])
{
json.imports[key] = value.substring(1);
console.log(`"react" specifier not defined in import map`);
}
if(key.startsWith(">"))
else if(!imports["react/"])
{
imports["react/"] = imports["react"]+"/";
}
ImportMapProxies = {};
Object.entries(imports).forEach(([key, value])=>
{
if(value.startsWith("./"))
{
ImportMapProxies[encodeURI(key)] = value.substring(1);
json.imports[key] = value.substring(1);
imports[key] = value.substring(1);
}
else
if(key.startsWith(">"))
{
ImportMapProxies["/"+encodeURI(key)] = value;
json.imports[key] = "/"+key;
if(value.startsWith("./"))
{
ImportMapProxies[encodeURI(key)] = value.substring(1);
imports[key] = value.substring(1);
}
else
{
ImportMapProxies["/"+encodeURI(key)] = value;
imports[key] = "/"+key;
}
}
}
});
});
ImportMap.imports = Configuration.Remap(imports, Configuration);
}
else
{
}
ImportMap.imports = Configuration.Remap(json.imports, Configuration);
};
export type CustomHTTPHandler = (inReq:Request, inURL:URL, inExt:string|false, inMap:{imports:Record<string, string>}, inConfig:Configuration)=>void|false|Response|Promise<Response|void|false>;