diff --git a/checker.tsx b/checker.tsx new file mode 100644 index 0000000..3d7cdb4 --- /dev/null +++ b/checker.tsx @@ -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>}; +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; + 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; + 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(); \ No newline at end of file diff --git a/cli.tsx b/cli.tsx index 667df90..25159c1 100644 --- a/cli.tsx +++ b/cli.tsx @@ -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, inEnv:Record):Promise=> @@ -37,86 +36,6 @@ const collect =async(inKey:string, inArg:Record, inEnv:Record>}; -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"; - 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; - 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; - 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) { diff --git a/deno.jsonc b/deno.jsonc index 8b9e02f..6c923cd 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -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"; - ... - -*/ } \ No newline at end of file diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..2be9e7a --- /dev/null +++ b/deno.lock @@ -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" + } +} diff --git a/run-serve.tsx b/run-serve.tsx index 90bd739..84d9b38 100644 --- a/run-serve.tsx +++ b/run-serve.tsx @@ -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}; const ImportMap:DenoConfig = {imports:{}}; -let ImportMapOriginal = {}; let ImportMapProxies:Record = {}; + 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}, inConfig:Configuration)=>void|false|Response|Promise;