import * as ESBuild from "https://deno.land/x/esbuild@v0.25.0/wasm.js"; await ESBuild.initialize({ worker: false }); /** @typedef {{imports?:Record, scopes?:Record>}} 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} options * @returns {Promise>} 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 }