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",
"version": "0.1.0",
"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": {
"serve": "deno run -A --no-lock serve.tsx --path=./test --html=index.html",
"jsapi": "deno run -A --no-lock jsapi.tsx"
"go": "deno run -A bundler-inc.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; };
/** A `file://` url version of Deno.cwd() (contains trailing slash) */
export const Root:string = new URL(`file://${Deno.cwd().replaceAll("\\", "/")}`).toString() + "/";
export default async function HuntConfig(directory=Root):Promise<{
imports: JsonLike;
compilerOptions: JsonLike;
}>
/** A `file://` url version of Deno.cwd() */
export const Root = new URL(`file://${Deno.cwd().replaceAll("\\", "/")}`).toString() + "/";
export default async function HuntConfig()
{
let path:string, json:JsonLike|undefined;
console.log("searchig in directory", directory)
const loadJSON =async(inPath:string)=>{
try{
const path = new URL(inPath, directory);
console.log("looking at", path.href);
const resp = await fetch(path);
if(inPath.endsWith("./.jsonc"))
const resp = await fetch(Root + inPath);
if(inPath.endsWith(".jsonc"))
{
const text = await resp.text();
json = JSONC(text) as JsonLike;
@ -35,20 +30,20 @@ export default async function HuntConfig(directory=Root):Promise<{
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)

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 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 resolvePlugin =(fullPathDir:string):ESBuild.Plugin=>({
const resolvePlugin:ESBuild.Plugin = {
name: "resolve-plugin",
setup(build) {
build.onResolve({ filter: /^(\.\/|\.\.\/).*/ }, (args)=>
{
let resolveRoot = args.importer||fullPathDir;
let resolveRoot = args.importer||Root;
if(resolveRoot.startsWith(prefix))
{
resolveRoot = resolveRoot.substring(prefix.length);
@ -21,31 +21,23 @@ const resolvePlugin =(fullPathDir:string):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<typeof Mapper.importmapPlugin>[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 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>>
export default async function(buildOptions={} as BuildOptions, importMap:ImportMap|false = false):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 = {
entryPoints: ["entry"],
bundle: true,
@ -55,7 +47,7 @@ export default async function(directory:string, buildOptions={} as BuildOptions,
jsxImportSource: "react",
...buildOptions,
plugins: [
resolvePlugin(directory),
resolvePlugin,
Mapper.importmapPlugin(importMap) as ESBuild.Plugin,
...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 { 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)
{
serve(parseArgs(Deno.args) as ServeArgs);
}
export type ServeArgs = {path?:string, html?:string, port?:string|number};
async function serve(settings:ServeArgs):Promise<void>
type ServeArgs = {path?:string, html?:string, port?:string|number};
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 DenoConfig:ImportMap
const SetDirectory =async(inPath:string)=>{
const SetDirectory =(inPath:string)=>{
if(inPath.startsWith("http://")||inPath.startsWith("https://")||inPath.startsWith("file://"))
{
basePath = inPath;
@ -47,105 +27,108 @@ async function serve(settings:ServeArgs):Promise<void>
basePath = 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 defaultConfig:ESBuild.BuildOptions = { entryPoints:[] as string[], outdir:"/", entryNames: `[dir][name]`, splitting: true };
const bundle =async(pathName:string, pathBase:string, buildConfig:ESBuild.BuildOptions = {}):Promise<string|void>=>
const ServeJSCode =(inText:string)=> new Response(inText, {headers:{"content-type":"application/javascript"}});
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
{
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;
}
const handle = await fetch(basePath+inPath.substring(1));
return new Response(handle.body, {headers:{"content-type": mime.getType(inPath)??""}})
}
catch(_e)
{
return resp404;
}
catch(_e){return;}
};
// index html serving
const resp404 = new Response("404", {status:404});
const resp304 = new Response(null, {status:304});
let respIndexBody:false|string = false;
let respIndex:false|Response|Promise<Response> = false;
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
{
const load = await fetch(basePath+settings.html);
respIndexBody = await load.text();
respIndex = ServeStatic(settings.html);
}
}
///// 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;
}
Deno.serve({port:parseInt(settings.port as string)||8000}, async(req)=>{
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")
const index = url.pathname.lastIndexOf(".");
if(index > -1)
{
const checkBuild = await bundle(path, basePath);
if(checkBuild)
const ext = url.pathname.substring(index+1);
if(ext === "ts" || ext == "tsx" || ext == "js" || ext == "jsx")
{
return new Response(checkBuild, {headers});
}
}
// serve static
if(type)
{
try{
const handle = await fetch(basePath+path.substring(1));
return new Response(handle.body, {headers});
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
});
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){
return resp404;
}
else
{
return ServeStatic(url.pathname);
}
}
// serve routes/implied index html files
if(respIndexBody)
if(respIndex)
{
return new Response(respIndexBody, {headers});
return respIndex;
}
else
{
try{
const handle = await fetch(basePath+path.substring(1));
headers["content-type"] = "text/html"
return new Response(handle.body, {headers});
}
catch(_e){
return resp404;
let indexLookup = url.pathname;
if(!indexLookup.endsWith("/"))
{
indexLookup = indexLookup+"/";
}
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);