Merge pull request 'fixes' (#1) from fixes into master

Reviewed-on: #1
This commit is contained in:
SethTrowbridge 2025-02-28 17:10:47 -05:00
commit 9ec3854ebd
17 changed files with 252 additions and 115 deletions

33
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,33 @@
{
// 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": [
{
"name": "Attach",
"port": 9229,
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node"
},
{
"request": "launch",
"name": "Launch Program",
"type": "node",
"program": "${workspaceFolder}/main.ts",
"cwd": "${workspaceFolder}",
"env": {},
"runtimeExecutable": "C:\\Users\\strowbridge\\.deno\\bin\\deno.EXE",
"runtimeArgs": [
"run",
"--unstable",
"--inspect-wait",
"--allow-all"
],
"attachSimplePort": 9229
}
]
}

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);
}
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)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,39 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--
<script type="module">
import * as PREACT from "./bundle.js";
console.log(PREACT);
</script>
-->
<!-- -->
<script type="module">
import Bundle, {ESBuild} from "../mod.js";
const {outputFiles} = await Bundle({
esBuild:{
entryPoints:["react"],
},
importMap:{
imports: {
"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);
}
</script>
</body>
</html>

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);
})

136
mod.js Normal file
View File

@ -0,0 +1,136 @@
import * as ESBuild from "https://deno.land/x/esbuild@v0.25.0/wasm.js";
await ESBuild.initialize({ worker: false });
/** @typedef {{imports?:Record<string, string>, scopes?:Record<string, Record<string, string>>}} ImportMap */
/** @typedef {{fileSystem:string, esBuild:ESBuild.BuildOptions, importMap:ImportMap}} BuildOptions */
/**
* Resolves a module specifier against an import map
* @param {string} specifier - The module specifier to resolve (bare specifier or absolute URL)
* @param {ImportMap} 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, 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;
}
/**
*
* @param {string} fullPathDir
* @param {ImportMap} importMap
* @returns {ESBuild.Plugin}
*/
export function resolvePlugin(fullPathDir, importMap)
{
return {
name: "resolve-plugin",
setup(build) {
build.onResolve( {/* `/`, `./`, and `../` */ filter:/^(\/|\.\/|\.\.\/).*/}, args=>
{
const resolveRoot = args.importer||fullPathDir;
const out = { path:new URL(args.path, resolveRoot).href, namespace:"FULLPATH" };
return out;
} );
build.onResolve({filter:/.*/}, args=>
{
const resolveRoot = args.importer||fullPathDir;
const check = resolveImportMap(args.path, importMap, resolveRoot);
if(check)
{
console.log("import remap", args.path, "=>", check);
const out = { path:new URL(check, resolveRoot).href, namespace:"FULLPATH" };
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` };
return out;
} );
},
}
};
/**
* Perform a bundle
* @param {Partial<BuildOptions>} options
* @returns {Promise<ESBuild.BuildResult<ESBuild.BuildOptions>>} build result
*/
export default async function Build(options)
{
const configuration = {
entryPoints: ["entry"],
bundle: true,
minify: true,
format: "esm",
jsx: "automatic",
jsxImportSource: "react",
...options.esBuild||{},
plugins: [
resolvePlugin(options.fileSystem||import.meta.resolve("./"), options.importMap||{}),
...options.esBuild.plugins||[]
]
};
const result = await ESBuild.build(configuration);
return result;
}
export { ESBuild }

24
mod.test.tsx Normal file
View File

@ -0,0 +1,24 @@
import * as Test from "https://deno.land/std@0.224.0/assert/mod.ts";
import Bundle from "./mod.js";
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...
console.log(item.text.substring(0, 200));
}
Deno.test("check", ()=>{
Test.assert(outputFiles?.length == 1);
Test.assert(outputFiles[0].text.length > 1000);
})

67
mod.ts
View File

@ -1,67 +0,0 @@
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";
const prefix = "/_dot_importer_/";
const resolvePlugin =(fullPathDir:string):ESBuild.Plugin=>({
name: "resolve-plugin",
setup(build) {
build.onResolve({ filter: /^(\.\/|\.\.\/).*/ }, (args)=>
{
let resolveRoot = args.importer||fullPathDir;
if(resolveRoot.startsWith(prefix))
{
resolveRoot = resolveRoot.substring(prefix.length);
}
const output:ESBuild.OnResolveResult = {
path:prefix + new URL(args.path, resolveRoot).href,
namespace:"http",
}
return output;
});
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>>
{
if(!importMap)
{
importMap = await Introspect(directory) as 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),
Mapper.importmapPlugin(importMap) as ESBuild.Plugin,
...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)
@ -105,6 +105,9 @@ async function serve(settings:ServeArgs):Promise<void>
const url = new URL(req.url);
const {path, type} = PathExtensionType(url.pathname);
console.log(req.url);
const headers = {"content-type":type||"", ETag, "Cache-Control":"max-age=3600"}
// serve javascript