Compare commits

..

No commits in common. "3aa0b3a7ff59f79229f694cd8dac06ac70a3569c" and "5b1646cf40f0b156d192ff57071e24cb53ca46a7" have entirely different histories.

9 changed files with 127 additions and 199 deletions

View File

@ -1,48 +0,0 @@
# ESBuild Typescript Bunder
## Supports import maps and runs on Deno Deploy
Uses the WASM version of ESBuild for use on Deno Deploy and adds some plugins to allow for import maps. ***Unfortunately, `npm:*` and `jsr:*` specifiers are not supported currently***.
### Manual Bundle API
```typescript
import Bundle from "./mod.ts";
Bundle(
// directory to work from
"./some_folder",
// ESBuild configuration (entry points are relative to "directory"):
{entry:["./app.tsx"]},
// import map (if omitted, will scan for a deno configuration within "directory" and use that)
{"imports": {
"react": "https://esm.sh/preact@10.22.0/compat",
"react/": "https://esm.sh/preact@10.22.0/compat/"
}}
);
if(outputFiles){
for(const item of outputFiles){
// do something with the output...
console.log(item.text);
}
}
```
### Static bundle server
#### API
```typescript
import Serve from "./serve.tsx";
Serve({path:"./some_folder", html:"index.html", port:8080});
```
#### CLI
```
deno run -A <hosted-location>/serve.tsx --path=./some_folder --html=index.html --port=8080
```
Accepts three optional settings:
- `path` A directory to serve content from. Defaults to the current working directory.
- `html` A path to an html file or actual html string to *always use* when the server would serve up a non-file route. If `html` is a path, it is assumed to be relative to `path`. If not specified, routes that don't match files will 404.
- `port` Port number (default is 8000).
When requests to typescript files are made to the server, it will treat that file as an entrypoint and return the corresponding plain javascript bundle.

View File

@ -2,8 +2,19 @@
"name": "@ttf/wasm-bundle", "name": "@ttf/wasm-bundle",
"version": "0.1.0", "version": "0.1.0",
"exports": "./mod.ts", "exports": "./mod.ts",
"imports": {
"react": "https://esm.sh/preact@10.22.0/compat",
"react/": "https://esm.sh/preact@10.22.0/compat/",
"other": "./other.tsx",
"entry": "./app.tsx",
"config": "./deno.json"
},
"tasks": { "tasks": {
"serve": "deno run -A --no-lock serve.tsx --path=./test --html=index.html", "go": "deno run -A bundler-inc.tsx",
"jsapi": "deno run -A --no-lock jsapi.tsx" "serve": "deno run -A serve.tsx"
},
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
} }
} }

18
deno.lock Normal file
View File

@ -0,0 +1,18 @@
{
"version": "3",
"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/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"
}
}

View File

@ -2,21 +2,16 @@ import { parse as JSONC } from "https://deno.land/x/jsonct@v0.1.0/mod.ts";
type JsonLike = { [key: string]: string | string[] | JsonLike; }; type JsonLike = { [key: string]: string | string[] | JsonLike; };
/** A `file://` url version of Deno.cwd() (contains trailing slash) */ /** A `file://` url version of Deno.cwd() */
export const Root:string = new URL(`file://${Deno.cwd().replaceAll("\\", "/")}`).toString() + "/"; export const Root = new URL(`file://${Deno.cwd().replaceAll("\\", "/")}`).toString() + "/";
export default async function HuntConfig(directory=Root):Promise<{ export default async function HuntConfig()
imports: JsonLike;
compilerOptions: JsonLike;
}>
{ {
let path:string, json:JsonLike|undefined; let path:string, json:JsonLike|undefined;
console.log("searchig in directory", directory)
const loadJSON =async(inPath:string)=>{ const loadJSON =async(inPath:string)=>{
try{ try{
const path = new URL(inPath, directory); const resp = await fetch(Root + inPath);
console.log("looking at", path.href); if(inPath.endsWith(".jsonc"))
const resp = await fetch(path);
if(inPath.endsWith("./.jsonc"))
{ {
const text = await resp.text(); const text = await resp.text();
json = JSONC(text) as JsonLike; json = JSONC(text) as JsonLike;
@ -35,20 +30,20 @@ export default async function HuntConfig(directory=Root):Promise<{
try// look for `deno.json` try// look for `deno.json`
{ {
json = await loadJSON("./deno.json"); json = await loadJSON("deno.json");
} }
catch(_e) catch(_e)
{ {
try // look for `deno.jsonc` try // look for `deno.jsonc`
{ {
json = await loadJSON("./deno.jsonc"); json = await loadJSON("deno.jsonc");
} }
catch(_e) catch(_e)
{ {
try // look in the vscode plugin settings 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 : ""; path = json ? json["deno.config"] as string : "";
json = undefined; json = undefined;
if(path) if(path)

View File

@ -1,11 +0,0 @@
import * as Test from "https://deno.land/std@0.224.0/assert/mod.ts";
import Bundle, {ESBuild} from "./mod.ts";
const path = await import.meta.resolve("./test/");
const {outputFiles} = await Bundle(path, {entryPoints:["./app.tsx"]});
Deno.test("check", ()=>{
Test.assert(outputFiles?.length == 1);
Test.assert(outputFiles[0].text.length > 1000);
})

26
mod.ts
View File

@ -1,14 +1,14 @@
import * as ESBuild from "https://deno.land/x/esbuild@v0.19.2/wasm.js"; 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 * as Mapper from "https://esm.sh/esbuild-plugin-importmaps@1.0.0"; // https://github.com/andstellar/esbuild-plugin-importmaps
import Introspect from "./introspect.ts"; import Introspect, {Root} from "./introspect.ts";
const prefix = "/_dot_importer_/"; const prefix = "/_dot_importer_/";
const resolvePlugin =(fullPathDir:string):ESBuild.Plugin=>({ const resolvePlugin:ESBuild.Plugin = {
name: "resolve-plugin", name: "resolve-plugin",
setup(build) { setup(build) {
build.onResolve({ filter: /^(\.\/|\.\.\/).*/ }, (args)=> build.onResolve({ filter: /^(\.\/|\.\.\/).*/ }, (args)=>
{ {
let resolveRoot = args.importer||fullPathDir; let resolveRoot = args.importer||Root;
if(resolveRoot.startsWith(prefix)) if(resolveRoot.startsWith(prefix))
{ {
resolveRoot = resolveRoot.substring(prefix.length); resolveRoot = resolveRoot.substring(prefix.length);
@ -21,31 +21,23 @@ const resolvePlugin =(fullPathDir:string):ESBuild.Plugin=>({
}); });
build.onLoad({ filter: /.*/, namespace:"http" }, async(args)=> { build.onLoad({ filter: /.*/, namespace:"http" }, async(args)=> {
const fetchPath = args.path.substring(prefix.length); const fetchPath = args.path.substring(prefix.length);
console.log("fetch path", fetchPath);
const result = await fetch(fetchPath); const result = await fetch(fetchPath);
const contents = await result.text(); const contents = await result.text();
return { contents, loader: `tsx` }; return { contents, loader: `tsx` };
}); });
}, },
}); };
await ESBuild.initialize({ worker: false }); await ESBuild.initialize({ worker: false });
export type ImportMap = Parameters<typeof Mapper.importmapPlugin>[0]; export type ImportMap = Parameters<typeof Mapper.importmapPlugin>[0];
export type BuildOptions = ESBuild.BuildOptions; export type BuildOptions = ESBuild.BuildOptions;
/** export default async function(buildOptions={} as BuildOptions, importMap:ImportMap|false = false):Promise<ESBuild.BuildResult<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 An object to act as the import map ({imports:Record<string, string>}). If this is left blank, a configuration will be scanned for in the "directory"
* @returns build result
*/
export default async function(directory:string, buildOptions={} as BuildOptions, importMap?:ImportMap):Promise<ESBuild.BuildResult<ESBuild.BuildOptions>>
{ {
if(!importMap) if(importMap === false)
{ {
importMap = await Introspect(directory) as ImportMap importMap = await Introspect() as ImportMap;
} }
console.log("using import map", importMap);
const configuration:ESBuild.BuildOptions = { const configuration:ESBuild.BuildOptions = {
entryPoints: ["entry"], entryPoints: ["entry"],
bundle: true, bundle: true,
@ -55,7 +47,7 @@ export default async function(directory:string, buildOptions={} as BuildOptions,
jsxImportSource: "react", jsxImportSource: "react",
...buildOptions, ...buildOptions,
plugins: [ plugins: [
resolvePlugin(directory), resolvePlugin,
Mapper.importmapPlugin(importMap) as ESBuild.Plugin, Mapper.importmapPlugin(importMap) as ESBuild.Plugin,
...buildOptions.plugins||[] ...buildOptions.plugins||[]
] ]

173
serve.tsx
View File

@ -1,39 +1,19 @@
import bundler from "./mod.ts";
import mime from "https://esm.sh/mime@4.0.3"; import mime from "https://esm.sh/mime@4.0.3";
import { parseArgs } from "jsr:@std/cli/parse-args"; 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) if(Deno.mainModule == import.meta.url)
{ {
serve(parseArgs(Deno.args) as ServeArgs); serve(parseArgs(Deno.args) as ServeArgs);
} }
export type ServeArgs = {path?:string, html?:string, port?:string|number}; type ServeArgs = {path?:string, html?:string, port?:string|number};
async function serve(settings:ServeArgs):Promise<void> function serve(settings:ServeArgs):void
{ {
// 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 = ""; let basePath = "";
let DenoConfig:ImportMap const SetDirectory =(inPath:string)=>{
const SetDirectory =async(inPath:string)=>{
if(inPath.startsWith("http://")||inPath.startsWith("https://")||inPath.startsWith("file://")) if(inPath.startsWith("http://")||inPath.startsWith("https://")||inPath.startsWith("file://"))
{ {
basePath = inPath; basePath = inPath;
@ -47,105 +27,108 @@ async function serve(settings:ServeArgs):Promise<void>
basePath = basePath + "/"; basePath = basePath + "/";
} }
console.log("Base Path:", basePath); console.log("Base Path:", basePath);
DenoConfig = await Introspect(basePath) as ImportMap;
console.log("found config", DenoConfig);
} }
await SetDirectory(settings.path||"./");
// bundler SetDirectory(settings.path||"./");
const transpiled:Map<string, string> = new Map(); const transpiled:Map<string, string> = new Map();
const defaultConfig:ESBuild.BuildOptions = { entryPoints:[] as string[], outdir:"/", entryNames: `[dir][name]`, splitting: true }; const ServeJSCode =(inText:string)=> new Response(inText, {headers:{"content-type":"application/javascript"}});
const bundle =async(pathName:string, pathBase:string, buildConfig:ESBuild.BuildOptions = {}):Promise<string|void>=> const ServeStatic =async(inPath:string)=>
{ {
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 try
{ {
const results = await bundler(basePath, buildConfig, DenoConfig); const handle = await fetch(basePath+inPath.substring(1));
if(results.outputFiles) return new Response(handle.body, {headers:{"content-type": mime.getType(inPath)??""}})
{ }
results.outputFiles.forEach(output=>{transpiled.set(output.path, output.text); console.log("building", output.path);}) catch(_e)
return results.outputFiles[0].text; {
} return resp404;
} }
catch(_e){return;}
}; };
// index html serving
const resp404 = new Response("404", {status:404}); const resp404 = new Response("404", {status:404});
const resp304 = new Response(null, {status:304}); let respIndex:false|Response|Promise<Response> = false;
let respIndexBody:false|string = false;
if(settings.html) if(settings.html)
{ {
if(settings.html.indexOf("<") != -1) // if html is actual markup instead of a url if(settings.html.indexOf("<") != -1)
{ {
respIndexBody = settings.html; respIndex= new Response(settings.html, {headers:{"content-type":"text-html"}});
} }
else else
{ {
const load = await fetch(basePath+settings.html); respIndex = ServeStatic(settings.html);
respIndexBody = await load.text();
} }
} }
///// go Deno.serve({port:parseInt(settings.port as string)||8000}, async(req)=>{
Deno.serve({port:parseInt(settings.port as string)||8000}, async(req)=>
{
const checkHash = req.headers.get("if-none-match")
if(checkHash === ETag){
return resp304;
}
const url = new URL(req.url); const url = new URL(req.url);
const {path, type} = PathExtensionType(url.pathname); const index = url.pathname.lastIndexOf(".");
const headers = {"content-type":type||"", ETag, "Cache-Control":"max-age=3600"} if(index > -1)
// serve javascript
if(type === "application/javascript")
{ {
const checkBuild = await bundle(path, basePath); const ext = url.pathname.substring(index+1);
if(checkBuild) if(ext === "ts" || ext == "tsx" || ext == "js" || ext == "jsx")
{ {
return new Response(checkBuild, {headers}); if(ext == "js")
} {
} const lookup = transpiled.get(url.pathname);
if(lookup)
// serve static {
if(type) return ServeJSCode(lookup);
{ }
try{ }
const handle = await fetch(basePath+path.substring(1)); else
return new Response(handle.body, {headers}); {
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
});
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;
}
} }
catch(_e){ else
return resp404; {
} return ServeStatic(url.pathname);
}
} }
// serve routes/implied index html files if(respIndex)
if(respIndexBody)
{ {
return new Response(respIndexBody, {headers}); return respIndex;
} }
else else
{ {
try{ let indexLookup = url.pathname;
const handle = await fetch(basePath+path.substring(1)); if(!indexLookup.endsWith("/"))
headers["content-type"] = "text/html" {
return new Response(handle.body, {headers}); indexLookup = indexLookup+"/";
}
catch(_e){
return resp404;
} }
return ServeStatic(indexLookup+"index.html");
} }
}); });
} }

View File

@ -1,10 +0,0 @@
{
"imports": {
"react": "https://esm.sh/preact@10.22.0/compat",
"react/": "https://esm.sh/preact@10.22.0/compat/"
},
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}

View File

@ -1,2 +0,0 @@
const message:string = "leaf default export!"
export default ()=>console.log(message);