fixes #1

Merged
SethTrowbridge merged 7 commits from fixes into master 2025-02-28 17:10:48 -05:00
19 changed files with 24 additions and 218 deletions
Showing only changes of commit 22ca93fc9a - Show all commits

View File

@ -1,48 +1,27 @@
# ESBuild Typescript Bunder
## Supports import maps and runs on Deno Deploy
## Supports import maps and runs on Deno Deploy and the browser
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",
```javascript
import Bundle from "./mod.js";
// 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": {
const {outputFiles} = await Bundle({
fileSystem:import.meta.resolve('./test_project/'),
esBuild:{entryPoints:["entry"]},
importMap:{"imports": {
"entry": "./app.tsx",
"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);
}
});
for(const item of outputFiles){
// do something with the output...
console.log(item.text.substring(0, 200));
}
```
### 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.
(dev server is being reworked)

View File

@ -14,7 +14,7 @@
<!-- -->
<script type="module">
import Bundle, {ESBuild} from "./mod.js";
import Bundle, {ESBuild} from "../mod.js";
const {outputFiles} = await Bundle({
esBuild:{

View File

@ -1,20 +1,17 @@
import * as Test from "https://deno.land/std@0.224.0/assert/mod.ts";
import Bundle from "./mod.ts";
const {outputFiles} = await Bundle(
// directory to work from
import.meta.resolve('./test/'),
import Bundle from "./mod.js";
// ESBuild configuration (entry points are relative to "directory"):
{entryPoints:["entry"]},
// import map (if omitted, will scan for a deno configuration within "directory" and use that)
{"imports": {
const {outputFiles} = await Bundle({
fileSystem:import.meta.resolve('./test_project/'),
esBuild:{entryPoints:["entry"]},
importMap:{"imports": {
"entry": "./app.tsx",
"react": "https://esm.sh/preact@10.22.0/compat",
"react/": "https://esm.sh/preact@10.22.0/compat/"
}}
);
});
for(const item of outputFiles){
// do something with the output...

133
mod.ts
View File

@ -1,133 +0,0 @@
import * as ESBuild from "https://deno.land/x/esbuild@v0.25.0/wasm.js";
/**
* Resolves a module specifier against an import map
* @param {string} specifier - The module specifier to resolve (bare specifier or absolute URL)
* @param {Object} importMap - The import map object containing imports and scopes
* @param {string} baseURL - The base URL for context (especially for scopes)
* @returns {string} The resolved URL
*/
function resolveImportMap(specifier:string, importMap:ImportMap, baseURL) {
// Check for prefix matches in the main imports
const result = checkPrefixMatch(specifier, importMap.imports);
if (result) {
return result
}
// First check scopes that match the baseURL (scope applies based on baseURL, not specifier)
if (importMap.scopes) {
const scopeKeys = Object.keys(importMap.scopes).sort((a, b) => b.length - a.length);
for (const scopeKey of scopeKeys) {
// Convert scope key to absolute URL and check if baseURL starts with it
const scopeURL = new URL(scopeKey, baseURL).href;
if (baseURL.startsWith(scopeURL)) {
const scopeImports = importMap.scopes[scopeKey];
// Check for prefix match in the scope
const result = checkPrefixMatch(specifier, scopeImports);
if (result) {
return result;
}
}
}
}
}
/**
* Helper function to check for prefix matches
* @param {string} specifier - The specifier to check
* @param {Object} mappings - Object with prefix mappings
* @returns {string|null} The resolved path or null if no match
*/
function checkPrefixMatch(specifier, mappings) {
const check = mappings[specifier];
if(check){ return check; }
const prefixes = Object.keys(mappings)
.filter(key => key.endsWith('/') && specifier.startsWith(key))
.sort((a, b) => b.length - a.length);
for (const prefix of prefixes) {
const remainder = specifier.slice(prefix.length);
return mappings[prefix] + remainder;
}
return null;
}
const resolvePlugin =(fullPathDir:string, importMap):ESBuild.Plugin=>({
name: "resolve-plugin",
setup(build) {
build.onResolve( {/* `/`, `./`, and `../` */ filter:/^(\/|\.\/|\.\.\/).*/}, args=>{
const resolveRoot = args.importer||fullPathDir;
const url = new URL(args.path, resolveRoot).href;
const out = { path:url, namespace:"FULLPATH" };
console.log(`SLASHPATH=>FULLPATH RESOLVE`, {args, out}, "\n");
return out;
} );
build.onResolve({filter:/.*/}, args=>{
const check = resolveImportMap(args.path, importMap, args.importer||fullPathDir);
console.log("pth??", check);
if(check)
{
const resolveRoot = args.importer||fullPathDir;
const out = { path:new URL(check, resolveRoot).href, namespace:"FULLPATH" };
console.log(`IMPORTMAP RESOLVE`, {args, out}, "\n");
return out;
}
})
build.onLoad(
{/* `file://`, `http://`, and `https://` */ filter:/^(file:\/\/|http:\/\/|https:\/\/).*/},
async(args)=>
{
const contents = await fetch(args.path).then(r=>r.text());
const out = { contents, loader: `tsx` };
console.log(`FULLPATH LOAD`, {args, out:{...out, contents:contents.substring(0, 100)}}, "\n");
return out;
} );
},
});
await ESBuild.initialize({ worker: false });
export type ImportMap = {imports?:Record<string, string>, scopes?:Record<string, Record<string, string>>}
export type BuildOptions = ESBuild.BuildOptions;
/**
*
* @param {string} directory Full file:// or http(s):// path to the directory containing assets you want to build (needed to resolve relative imports)
* @param {ESBuild.BuildOptions} [buildOptions={}] ESBuild "build" options (will be merged with "reasonable defaults") for docs: https://esbuild.github.io/api/#general-options
* @param {ImportMap} [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 {Promise<ESBuild.BuildResult<ESBuild.BuildOptions>>} build result
*/
export default async function Build(directory, buildOptions={}, importMap = {})
{
console.log("using import map", importMap);
const configuration:ESBuild.BuildOptions = {
entryPoints: ["entry"],
bundle: true,
minify: true,
format: "esm",
jsx: "automatic",
jsxImportSource: "react",
...buildOptions,
plugins: [
resolvePlugin(directory, importMap),
...buildOptions.plugins||[]
]
};
const result = await ESBuild.build(configuration);
return result;
}
export { ESBuild };

View File

@ -1,6 +1,6 @@
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 bundler, {type ESBuild, type ImportMap} from "../mod.js";
import Introspect from "./introspect.ts";
if(Deno.mainModule == import.meta.url)

View File

@ -1,10 +0,0 @@
import React from "react";
import Message from "./include_a.ts";
type Person = {name:string, age:number};
const me:Person = {name:"seth", age:41};
console.log(Message, React);
export default me;

View File

@ -1,6 +0,0 @@
import * as B from "./include_b.ts";
import * as React from "react";
console.log(React, B);
export default "HELLO";

View File

@ -1 +0,0 @@
export default {};

View File

@ -1,20 +0,0 @@
import Bundle from "../mod.ts";
const {outputFiles} = await Bundle(
// directory to work from
import.meta.resolve('./'),
// ESBuild configuration (entry points are relative to "directory"):
{entryPoints:["entry"]},
// import map (if omitted, will scan for a deno configuration within "directory" and use that)
{"imports": {
"entry": "./entry.ts",
"react": "https://esm.sh/preact@10.22.0/compat",
"react/": "https://esm.sh/preact@10.22.0/compat/"
}}
);
for(const item of outputFiles){
// do something with the output...
console.log(item.text.substring(0, 200));
}