in-browser working

This commit is contained in:
Seth Trowbridge 2025-02-28 16:39:49 -05:00
parent f15e392806
commit 296516b844
5 changed files with 197 additions and 5 deletions

1
clean/bundle.js Normal file

File diff suppressed because one or more lines are too long

39
clean/index.html Normal file
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>

136
clean/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 }

View File

@ -1,9 +1,25 @@
import * as Test from "https://deno.land/std@0.224.0/assert/mod.ts"; import * as Test from "https://deno.land/std@0.224.0/assert/mod.ts";
import Bundle, {ESBuild} from "./mod.ts";
import Bundle from "./mod.ts";
const {outputFiles} = await Bundle(
// directory to work from
import.meta.resolve('./test/'),
const path = await import.meta.resolve("./test/"); // ESBuild configuration (entry points are relative to "directory"):
const {outputFiles} = await Bundle(path, {entryPoints:["./app.tsx"]}); {entryPoints:["entry"]},
// import map (if omitted, will scan for a deno configuration within "directory" and use that)
{"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", ()=>{ Deno.test("check", ()=>{
Test.assert(outputFiles?.length == 1); Test.assert(outputFiles?.length == 1);

4
mod.ts
View File

@ -7,7 +7,7 @@ import * as ESBuild from "https://deno.land/x/esbuild@v0.25.0/wasm.js";
* @param {string} baseURL - The base URL for context (especially for scopes) * @param {string} baseURL - The base URL for context (especially for scopes)
* @returns {string} The resolved URL * @returns {string} The resolved URL
*/ */
function resolveImportMap(specifier, importMap, baseURL) { function resolveImportMap(specifier:string, importMap:ImportMap, baseURL) {
// Check for prefix matches in the main imports // Check for prefix matches in the main imports
const result = checkPrefixMatch(specifier, importMap.imports); const result = checkPrefixMatch(specifier, importMap.imports);
@ -107,7 +107,7 @@ 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 {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 {ESBuild.BuildOptions} [buildOptions={}] ESBuild "build" options (will be merged with "reasonable defaults") for docs: https://esbuild.github.io/api/#general-options
* @param {ImportMap|null} [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" * @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 * @returns {Promise<ESBuild.BuildResult<ESBuild.BuildOptions>>} build result
*/ */
export default async function Build(directory, buildOptions={}, importMap = {}) export default async function Build(directory, buildOptions={}, importMap = {})