From b5aece91e10a3ddeeba0c41bfff0198c2b4e1df9 Mon Sep 17 00:00:00 2001 From: Seth Trowbridge Date: Fri, 14 Jun 2024 17:07:50 -0400 Subject: [PATCH] lots of changes --- .vscode/launch.json | 13 ++++ deno.json | 3 +- deno.lock | 32 -------- introspect.ts | 20 ++--- jsapi.tsx | 10 ++- mod.ts | 26 +++---- serve.tsx | 178 ++++++++++++++++++++++++-------------------- 7 files changed, 141 insertions(+), 141 deletions(-) create mode 100644 .vscode/launch.json delete mode 100644 deno.lock diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..eb9ace7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "request":"attach", + "name":"Attach", + "type": "node" + } + ] +} \ No newline at end of file diff --git a/deno.json b/deno.json index 2f4acd2..820ca77 100644 --- a/deno.json +++ b/deno.json @@ -3,6 +3,7 @@ "version": "0.1.0", "exports": "./mod.ts", "tasks": { - "serve": "deno run -A --no-lock serve.tsx --path=./test" + "serve": "deno run -A --no-lock serve.tsx --path=./test --html=index.html", + "jsapi": "deno run -A --no-lock jsapi.tsx" } } \ No newline at end of file diff --git a/deno.lock b/deno.lock deleted file mode 100644 index 2ea5425..0000000 --- a/deno.lock +++ /dev/null @@ -1,32 +0,0 @@ -{ - "version": "3", - "packages": { - "specifiers": { - "jsr:@std/cli": "jsr:@std/cli@0.224.6" - }, - "jsr": { - "@std/cli@0.224.6": { - "integrity": "98d7e17a70c1c24980baa2b9a511b29e0a460e7a3b8e3d4e220979d77d5f348e" - } - } - }, - "remote": { - "https://deno.land/x/esbuild@v0.19.2/wasm.js": "5ffeb3d973e57351eb4d2d03ffafc8ce5672e946d0f0a786c4aed2ca29cec004", - "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/esbuild-plugin-importmaps@1.0.0": "ab9e79660ff4d57d2ed7ef5e8516fbe0e79305267f22e1e7270d0a17ae0c2029", - "https://esm.sh/mime@4.0.3": "9e2ae6a32fa99974824ef71da6e10a921702bc82dd321802ad14b84ae68b515d", - "https://esm.sh/preact@10.22.0/compat/jsx-runtime": "a2f6ddc2ce374813df1c13826a9ad010e90b5a70a989f1069a367ef60dd52eb0", - "https://esm.sh/stable/preact@10.22.0/denonext/compat.js": "7c0b206984707cfef58efae492ea8d5212b8f620dd8c83294a5c832fb422c766", - "https://esm.sh/stable/preact@10.22.0/denonext/compat/jsx-runtime.js": "fecfa3df69d580507801575175087de9a2a9fc23bb4900004a1f4cbd5b362634", - "https://esm.sh/stable/preact@10.22.0/denonext/hooks.js": "09230113132c216bbc3847aaad11289771e088be1b0eb9e49cbc724faaeac205", - "https://esm.sh/stable/preact@10.22.0/denonext/jsx-runtime.js": "de60943799b1cbe6066c4f83f4ca71ef37011d7f5be7bef58ed980e8ff3f996a", - "https://esm.sh/stable/preact@10.22.0/denonext/preact.mjs": "20c9563e051dd66e053d3afb450f61b48f2fa0d0ce4f69f8f0a2f23c1ef090da", - "https://esm.sh/v135/@jspm/import-map@1.0.8/denonext/import-map.mjs": "fc291e729df6bef849df47df8893b64749785ca65fd5fe1d0e7969db5d3b63ea", - "https://esm.sh/v135/esbuild-plugin-importmaps@1.0.0/denonext/esbuild-plugin-importmaps.mjs": "08b603d074dd2861345f7d224c255c46d7f7213a283026552c492f465fe595ce", - "https://esm.sh/v135/mime@4.0.3/denonext/mime.mjs": "b0fd9fc3e38041cc74ea371133ae2d30e1d610f6c89f038c609a63cce1160d05", - "https://esm.sh/v135/mime@4.0.3/denonext/types/other.js": "45cb4b3bf4f070e2d92a2c60e67570fb73b6aac6672a86da50448a85ee15be1d", - "https://esm.sh/v135/mime@4.0.3/denonext/types/standard.js": "46a2e7cf4eec9dfed5e9928d6597aa01898030654c18e8be71facd4d904e4d1c" - } -} diff --git a/introspect.ts b/introspect.ts index 7b66836..f57f56a 100644 --- a/introspect.ts +++ b/introspect.ts @@ -1,10 +1,4 @@ import { parse as JSONC } from "https://deno.land/x/jsonct@v0.1.0/mod.ts"; -import { parseArgs } from "jsr:@std/cli/parse-args"; - -if(Deno.mainModule == import.meta.url) -{ - serve(parseArgs(Deno.args) as ServeArgs); -} type JsonLike = { [key: string]: string | string[] | JsonLike; }; @@ -13,11 +7,13 @@ export const Root = new URL(`file://${Deno.cwd().replaceAll("\\", "/")}`).toStri export default async function HuntConfig(directory=Root) { let path:string, json:JsonLike|undefined; - + console.log("searchig in directory", directory) const loadJSON =async(inPath:string)=>{ try{ - const resp = await fetch(directory + inPath); - if(inPath.endsWith(".jsonc")) + const path = new URL(inPath, directory); + console.log("looking at", path.href); + const resp = await fetch(path); + if(inPath.endsWith("./.jsonc")) { const text = await resp.text(); json = JSONC(text) as JsonLike; @@ -36,20 +32,20 @@ export default async function HuntConfig(directory=Root) try// look for `deno.json` { - json = await loadJSON("deno.json"); + json = await loadJSON("./deno.json"); } catch(_e) { try // look for `deno.jsonc` { - json = await loadJSON("deno.jsonc"); + json = await loadJSON("./deno.jsonc"); } catch(_e) { try // look in the vscode plugin settings { - json = await loadJSON(".vscode/settings.json") + json = await loadJSON("./.vscode/settings.json") path = json ? json["deno.config"] as string : ""; json = undefined; if(path) diff --git a/jsapi.tsx b/jsapi.tsx index 8512104..4001c3c 100644 --- a/jsapi.tsx +++ b/jsapi.tsx @@ -1,3 +1,9 @@ -import Bundle from "./mod.ts"; +import Bundle, { type ImportMap} from "./mod.ts"; +import Config from "./test/deno.json" with {type:"json"}; -Bundle({entryPoints:["./test/app.tsx"]}, {}) +const path = import.meta.resolve("./test/"); +const {outputFiles} = await Bundle(path, {entryPoints:["./app.tsx"]}, Config); +for(const item of outputFiles) +{ + console.log(item.text); +} diff --git a/mod.ts b/mod.ts index b8e51d9..a957ea1 100644 --- a/mod.ts +++ b/mod.ts @@ -1,14 +1,14 @@ import * as ESBuild from "https://deno.land/x/esbuild@v0.19.2/wasm.js"; import * as Mapper from "https://esm.sh/esbuild-plugin-importmaps@1.0.0"; // https://github.com/andstellar/esbuild-plugin-importmaps -import Introspect, {Root} from "./introspect.ts"; +import Introspect from "./introspect.ts"; const prefix = "/_dot_importer_/"; -const resolvePlugin:ESBuild.Plugin = { +const resolvePlugin =(fullPathDir:string):ESBuild.Plugin=>({ name: "resolve-plugin", setup(build) { build.onResolve({ filter: /^(\.\/|\.\.\/).*/ }, (args)=> { - let resolveRoot = args.importer||Root; + let resolveRoot = args.importer||fullPathDir; if(resolveRoot.startsWith(prefix)) { resolveRoot = resolveRoot.substring(prefix.length); @@ -21,33 +21,31 @@ const resolvePlugin:ESBuild.Plugin = { }); build.onLoad({ filter: /.*/, namespace:"http" }, async(args)=> { const fetchPath = args.path.substring(prefix.length); + console.log("fetch path", fetchPath); const result = await fetch(fetchPath); const contents = await result.text(); return { contents, loader: `tsx` }; }); }, -}; +}); await ESBuild.initialize({ worker: false }); export type ImportMap = Parameters[0]; export type BuildOptions = ESBuild.BuildOptions; /** * + * @param directory Full file:// or http(s):// path to the directory containing assets you want to build (needed to resolve relative imports) * @param buildOptions ESBuild "build" options (will be merged with "reasonable defaults") for docs: https://esbuild.github.io/api/#general-options - * @param importMap + * @param importMap An object to act as the import map ({imports:Record}). If this is left blank, a configuration will be scanned for in the "directory" * @returns build result */ -export default async function(buildOptions={} as BuildOptions, importMap:ImportMap|string|false = false):Promise> +export default async function(directory:string, buildOptions={} as BuildOptions, importMap?:ImportMap):Promise> { - if(importMap === false) + if(!importMap) { - importMap = await Introspect() as ImportMap; + importMap = await Introspect(directory) as ImportMap } - else if(typeof importMap === "string") - { - importMap = await Introspect(importMap) as ImportMap; - } - + console.log("using import map", importMap); const configuration:ESBuild.BuildOptions = { entryPoints: ["entry"], bundle: true, @@ -57,7 +55,7 @@ export default async function(buildOptions={} as BuildOptions, importMap:ImportM jsxImportSource: "react", ...buildOptions, plugins: [ - resolvePlugin, + resolvePlugin(directory), Mapper.importmapPlugin(importMap) as ESBuild.Plugin, ...buildOptions.plugins||[] ] diff --git a/serve.tsx b/serve.tsx index 8c1a098..3e819f0 100644 --- a/serve.tsx +++ b/serve.tsx @@ -1,6 +1,7 @@ -import bundler from "./mod.ts"; import mime from "https://esm.sh/mime@4.0.3"; import { parseArgs } from "jsr:@std/cli/parse-args"; +import bundler, {type ESBuild, type ImportMap} from "./mod.ts"; +import Introspect from "./introspect.ts"; if(Deno.mainModule == import.meta.url) { @@ -8,10 +9,31 @@ if(Deno.mainModule == import.meta.url) } type ServeArgs = {path?:string, html?:string, port?:string|number}; -function serve(settings:ServeArgs):void +async function serve(settings:ServeArgs):Promise { + // etag hash + const ETag = new Date().getTime().toString(); + + // extra parsing + const ExtensionsJS = ["tsx", "ts", "jsx", "js", "jsx", "mjs"]; + const PathExtensionType=(inPath:string)=> + { + const path = inPath.endsWith("/") ? inPath.substring(0, inPath.length-1) : inPath; + const posDot = path.lastIndexOf("."); + const extension = (posDot > path.lastIndexOf("/")) ? path.substring(posDot+1) : null; + let type:string|null = null + if(extension) + { + type = (ExtensionsJS.includes(extension)) ? "application/javascript" : mime.getType(extension); + } + + return {path, extension, type}; + } + + // base path let basePath = ""; - const SetDirectory =(inPath:string)=>{ + let DenoConfig:ImportMap + const SetDirectory =async(inPath:string)=>{ if(inPath.startsWith("http://")||inPath.startsWith("https://")||inPath.startsWith("file://")) { basePath = inPath; @@ -25,109 +47,105 @@ function serve(settings:ServeArgs):void basePath = basePath + "/"; } console.log("Base Path:", basePath); + + DenoConfig = await Introspect(basePath) as ImportMap; + console.log("found config", DenoConfig); } + await SetDirectory(settings.path||"./"); - SetDirectory(settings.path||"./"); - + // bundler const transpiled:Map = new Map(); - const ServeJSCode =(inText:string)=> new Response(inText, {headers:{"content-type":"application/javascript"}}); - const ServeStatic =async(inPath:string)=> + const defaultConfig:ESBuild.BuildOptions = { entryPoints:[] as string[], outdir:"/", entryNames: `[dir][name]`, splitting: true }; + const bundle =async(pathName:string, pathBase:string, buildConfig:ESBuild.BuildOptions = {}):Promise=> { + const lookupKey = pathName.substring(0, pathName.lastIndexOf("."))+".js"; + const lookup = transpiled.get(lookupKey); + if(lookup) + { + return lookup; + } + buildConfig = {...defaultConfig, ...buildConfig} + buildConfig.entryPoints = [pathBase+pathName]; try { - const handle = await fetch(basePath+inPath.substring(1)); - return new Response(handle.body, {headers:{"content-type": mime.getType(inPath)??""}}) - } - catch(_e) - { - console.log("404", inPath); - return resp404; + const results = await bundler(basePath, buildConfig, DenoConfig); + if(results.outputFiles) + { + results.outputFiles.forEach(output=>{transpiled.set(output.path, output.text); console.log("building", output.path);}) + return results.outputFiles[0].text; + } } + catch(_e){return;} }; - + + // index html serving const resp404 = new Response("404", {status:404}); - let respIndex:false|Response|Promise = false; + const resp304 = new Response(null, {status:304}); + let respIndexBody:false|string = false; if(settings.html) { - if(settings.html.indexOf("<") != -1) + if(settings.html.indexOf("<") != -1) // if html is actual markup instead of a url { - respIndex= new Response(settings.html, {headers:{"content-type":"text-html"}}); + respIndexBody = settings.html; } else { - respIndex = ServeStatic(settings.html); + const load = await fetch(basePath+settings.html); + respIndexBody = await load.text(); } } - - Deno.serve({port:parseInt(settings.port as string)||8000}, async(req)=>{ - const url = new URL(req.url); - const index = url.pathname.lastIndexOf("."); - if(index > -1) - { - const ext = url.pathname.substring(index+1); - if(ext === "ts" || ext == "tsx" || ext == "js" || ext == "jsx") - { - if(ext == "js") - { - const lookup = transpiled.get(url.pathname); - if(lookup) - { - return ServeJSCode(lookup); - } - } - else - { - const lookup = transpiled.get(url.pathname.substring(0, index)+".js"); - if(lookup) - { - return ServeJSCode(lookup); - } - } - try - { - const results = await bundler({ - entryPoints:[basePath+url.pathname], - outdir:"/", - entryNames: `[dir][name]`, - splitting: true - }, basePath); - if(results.outputFiles) - { - results.outputFiles.forEach(output=>transpiled.set(output.path, output.text)) - return ServeJSCode(results.outputFiles[0].text); - } - else - { - throw(new Error("no output")); - } - } - catch(_e) - { - return resp404; - } - } - else - { - return ServeStatic(url.pathname); - } - + ///// go + Deno.serve({port:parseInt(settings.port as string)||8000}, async(req)=> + { + const checkHash = req.headers.get("if-none-match") + if(checkHash === ETag){ + return resp304; } - if(respIndex) + const url = new URL(req.url); + const {path, type} = PathExtensionType(url.pathname); + const headers = {"content-type":type||"", ETag, "Cache-Control":"max-age=3600"} + + // serve javascript + if(type === "application/javascript") { - return respIndex; + const checkBuild = await bundle(path, basePath); + if(checkBuild) + { + return new Response(checkBuild, {headers}); + } + } + + // serve static + if(type) + { + try{ + const handle = await fetch(basePath+path.substring(1)); + return new Response(handle.body, {headers}); + } + catch(_e){ + return resp404; + } + } + + // serve routes/implied index html files + if(respIndexBody) + { + return new Response(respIndexBody, {headers}); } else { - let indexLookup = url.pathname; - if(!indexLookup.endsWith("/")) - { - indexLookup = indexLookup+"/"; + try{ + const handle = await fetch(basePath+path.substring(1)); + headers["content-type"] = "text/html" + return new Response(handle.body, {headers}); + } + catch(_e){ + return resp404; } - return ServeStatic(indexLookup+"index.html"); } - + }); }