commit
9ec3854ebd
33
.vscode/launch.json
vendored
Normal file
33
.vscode/launch.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
47
README.md
47
README.md
@ -1,48 +1,27 @@
|
|||||||
# ESBuild Typescript Bunder
|
# 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***.
|
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
|
### Manual Bundle API
|
||||||
```typescript
|
```javascript
|
||||||
import Bundle from "./mod.ts";
|
import Bundle from "./mod.js";
|
||||||
Bundle(
|
|
||||||
// directory to work from
|
|
||||||
"./some_folder",
|
|
||||||
|
|
||||||
// ESBuild configuration (entry points are relative to "directory"):
|
const {outputFiles} = await Bundle({
|
||||||
{entry:["./app.tsx"]},
|
fileSystem:import.meta.resolve('./test_project/'),
|
||||||
|
esBuild:{entryPoints:["entry"]},
|
||||||
// import map (if omitted, will scan for a deno configuration within "directory" and use that)
|
importMap:{"imports": {
|
||||||
{"imports": {
|
"entry": "./app.tsx",
|
||||||
"react": "https://esm.sh/preact@10.22.0/compat",
|
"react": "https://esm.sh/preact@10.22.0/compat",
|
||||||
"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){
|
for(const item of outputFiles){
|
||||||
// do something with the output...
|
// do something with the output...
|
||||||
console.log(item.text);
|
console.log(item.text.substring(0, 200));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Static bundle server
|
(dev server is being reworked)
|
||||||
|
|
||||||
#### 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.
|
|
1
browser_example/bundle.js
Normal file
1
browser_example/bundle.js
Normal file
File diff suppressed because one or more lines are too long
39
browser_example/index.html
Normal file
39
browser_example/index.html
Normal 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>
|
@ -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
136
mod.js
Normal 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
24
mod.test.tsx
Normal 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
67
mod.ts
@ -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 };
|
|
@ -1,6 +1,6 @@
|
|||||||
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 bundler, {type ESBuild, type ImportMap} from "../mod.js";
|
||||||
import Introspect from "./introspect.ts";
|
import Introspect from "./introspect.ts";
|
||||||
|
|
||||||
if(Deno.mainModule == import.meta.url)
|
if(Deno.mainModule == import.meta.url)
|
||||||
@ -105,6 +105,9 @@ async function serve(settings:ServeArgs):Promise<void>
|
|||||||
|
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
const {path, type} = PathExtensionType(url.pathname);
|
const {path, type} = PathExtensionType(url.pathname);
|
||||||
|
|
||||||
|
console.log(req.url);
|
||||||
|
|
||||||
const headers = {"content-type":type||"", ETag, "Cache-Control":"max-age=3600"}
|
const headers = {"content-type":type||"", ETag, "Cache-Control":"max-age=3600"}
|
||||||
|
|
||||||
// serve javascript
|
// serve javascript
|
Loading…
Reference in New Issue
Block a user