-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: inline import maps + stacked plugin support (#47)
This commit splits the loader plugin into two - the sync resolver, and the loader. This allows inserting other esbuild plugins between these two. Additionally a `configPath` option has been added which can be used to specify a deno.json file that can have an inline import map, or can have a referenced import map that will be loaded.
- Loading branch information
1 parent
8031f71
commit a51bece
Showing
19 changed files
with
708 additions
and
191 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import * as esbuild from "https://deno.land/x/esbuild@v0.17.11/mod.js"; | ||
import { denoPlugins } from "../mod.ts"; | ||
|
||
import { get } from "https://deno.land/x/emoji@0.2.1/mod.ts"; | ||
|
||
const EMOJI_PLUGIN: esbuild.Plugin = { | ||
name: "emoji", | ||
setup(build) { | ||
build.onResolve({ filter: /.*/, namespace: "emoji" }, (args) => { | ||
return { path: args.path, namespace: "emoji" }; | ||
}); | ||
|
||
build.onLoad({ filter: /.*/, namespace: "emoji" }, (args) => { | ||
return { | ||
contents: `export default "${get(args.path)}";`, | ||
loader: "ts", | ||
}; | ||
}); | ||
}, | ||
}; | ||
|
||
const importMap = { | ||
imports: { | ||
"wave-emoji": "emoji:waving_hand", | ||
}, | ||
}; | ||
const importMapURL = `data:application/json,${JSON.stringify(importMap)}`; | ||
|
||
const res = await esbuild.build({ | ||
plugins: [...denoPlugins({ importMapURL }), EMOJI_PLUGIN], | ||
entryPoints: ["wave-emoji"], | ||
write: false, | ||
}); | ||
console.log(res.outputFiles[0].text); // export default "\u{1F44B}"; | ||
const { default: emoji } = await import( | ||
"data:text/javascript," + res.outputFiles[0].text | ||
); | ||
console.log(emoji); // 👋 | ||
esbuild.stop(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,124 +1,63 @@ | ||
import { esbuild } from "./deps.ts"; | ||
|
||
import { | ||
esbuild, | ||
fromFileUrl, | ||
denoResolverPlugin, | ||
type DenoResolverPluginOptions, | ||
type ImportMap, | ||
type Scopes, | ||
type SpecifierMap, | ||
} from "./src/plugin_deno_resolver.ts"; | ||
export { | ||
denoResolverPlugin, | ||
DenoResolverPluginOptions, | ||
ImportMap, | ||
resolveImportMap, | ||
resolveModuleSpecifier, | ||
toFileUrl, | ||
} from "./deps.ts"; | ||
import { NativeLoader } from "./src/native_loader.ts"; | ||
import { PortableLoader } from "./src/portable_loader.ts"; | ||
import { Loader } from "./src/shared.ts"; | ||
Scopes, | ||
SpecifierMap, | ||
}; | ||
|
||
export interface DenoPluginOptions { | ||
/** | ||
* Specify the URL to an import map to use when resolving import specifiers. | ||
* The URL must be fetchable with `fetch`. | ||
*/ | ||
importMapURL?: URL; | ||
import { | ||
DEFAULT_LOADER, | ||
denoLoaderPlugin, | ||
type DenoLoaderPluginOptions, | ||
} from "./src/plugin_deno_loader.ts"; | ||
export { DEFAULT_LOADER, denoLoaderPlugin, DenoLoaderPluginOptions }; | ||
|
||
export { | ||
type EsbuildResolution, | ||
esbuildResolutionToURL, | ||
urlToEsbuildResolution, | ||
} from "./src/shared.ts"; | ||
|
||
export interface DenoPluginsOptions { | ||
/** | ||
* Specify which loader to use. By default this will use the `native` loader, | ||
* unless the `--allow-run` permission has not been given. | ||
* | ||
* - `native`: Shells out to the Deno execuatble under the hood to load | ||
* files. Requires --allow-read and --allow-run. | ||
* - `portable`: Do module downloading and caching with only Web APIs. | ||
* Requires --allow-read and/or --allow-net. | ||
* See {@link denoLoaderPlugin} for more information on the different loaders. | ||
*/ | ||
loader?: "native" | "portable"; | ||
} | ||
|
||
/** The default loader to use. */ | ||
export const DEFAULT_LOADER: "native" | "portable" = | ||
await Deno.permissions.query({ name: "run" }) | ||
.then((res) => res.state !== "granted") | ||
? "portable" | ||
: "native"; | ||
|
||
export function denoPlugin(options: DenoPluginOptions = {}): esbuild.Plugin { | ||
const loader = options.loader ?? DEFAULT_LOADER; | ||
return { | ||
name: "deno", | ||
setup(build) { | ||
let loaderImpl: Loader; | ||
let importMap: ImportMap | null = null; | ||
|
||
build.onStart(async function onStart() { | ||
if (options.importMapURL !== undefined) { | ||
const resp = await fetch(options.importMapURL.href); | ||
const txt = await resp.text(); | ||
importMap = resolveImportMap(JSON.parse(txt), options.importMapURL); | ||
} else { | ||
importMap = null; | ||
} | ||
switch (loader) { | ||
case "native": | ||
loaderImpl = new NativeLoader({ | ||
importMapURL: options.importMapURL, | ||
}); | ||
break; | ||
case "portable": | ||
loaderImpl = new PortableLoader(); | ||
} | ||
}); | ||
|
||
build.onResolve({ filter: /.*/ }, async function onResolve( | ||
args: esbuild.OnResolveArgs, | ||
): Promise<esbuild.OnResolveResult | null | undefined> { | ||
// Resolve to an absolute specifier using import map and referrer. | ||
const resolveDir = args.resolveDir | ||
? `${toFileUrl(args.resolveDir).href}/` | ||
: ""; | ||
const referrer = args.importer | ||
? `${args.namespace}:${args.importer}` | ||
: resolveDir; | ||
let resolved: URL; | ||
if (importMap !== null) { | ||
const res = resolveModuleSpecifier( | ||
args.path, | ||
importMap, | ||
new URL(referrer) || undefined, | ||
); | ||
resolved = new URL(res); | ||
} else { | ||
resolved = new URL(args.path, referrer); | ||
} | ||
|
||
// Once we have an absolute path, let the loader resolver figure out | ||
// what to do with it. | ||
const res = await loaderImpl.resolve(resolved); | ||
|
||
switch (res.kind) { | ||
case "esm": { | ||
const { specifier } = res; | ||
if (specifier.protocol === "file:") { | ||
const path = fromFileUrl(specifier); | ||
return { path, namespace: "file" }; | ||
} else { | ||
const path = specifier.href.slice(specifier.protocol.length); | ||
return { path, namespace: specifier.protocol.slice(0, -1) }; | ||
} | ||
} | ||
} | ||
}); | ||
/** | ||
* Specify the path to a deno.json config file to use. This is equivalent to | ||
* the `--config` flag to the Deno executable. This path must be absolute. | ||
*/ | ||
configPath?: string; | ||
/** | ||
* Specify a URL to an import map file to use when resolving import | ||
* specifiers. This is equivalent to the `--import-map` flag to the Deno | ||
* executable. This URL may be remote or a local file URL. | ||
* | ||
* If this option is not specified, the deno.json config file is consulted to | ||
* determine what import map to use, if any. | ||
*/ | ||
importMapURL?: string; | ||
} | ||
|
||
function onLoad( | ||
args: esbuild.OnLoadArgs, | ||
): Promise<esbuild.OnLoadResult | null> { | ||
let specifier; | ||
if (args.namespace === "file") { | ||
specifier = toFileUrl(args.path).href; | ||
} else { | ||
specifier = `${args.namespace}:${args.path}`; | ||
} | ||
return loaderImpl.loadEsm(specifier); | ||
} | ||
// TODO(lucacasonato): once https://github.com/evanw/esbuild/pull/2968 is fixed, remove the catch all "file" handler | ||
// build.onLoad({ filter: /.*\.json/, namespace: "file" }, onLoad); | ||
build.onLoad({ filter: /.*/, namespace: "file" }, onLoad); | ||
build.onLoad({ filter: /.*/, namespace: "http" }, onLoad); | ||
build.onLoad({ filter: /.*/, namespace: "https" }, onLoad); | ||
build.onLoad({ filter: /.*/, namespace: "data" }, onLoad); | ||
}, | ||
}; | ||
export function denoPlugins( | ||
opts: DenoLoaderPluginOptions = {}, | ||
): esbuild.Plugin[] { | ||
return [ | ||
denoResolverPlugin(opts), | ||
denoLoaderPlugin(opts), | ||
]; | ||
} |
Oops, something went wrong.