Skip to content

Commit

Permalink
feat: don't error on unrecognized media types in loader (#142)
Browse files Browse the repository at this point in the history
This now makes it possible to implement your own loaders for file types like `.css` or `.txt`.
  • Loading branch information
lucacasonato authored Sep 27, 2024
1 parent f4cf2e7 commit 6ed22bf
Show file tree
Hide file tree
Showing 7 changed files with 64 additions and 10 deletions.
36 changes: 36 additions & 0 deletions mod_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,42 @@ Deno.test("custom plugin for scheme with import map", async (t) => {
});
});

const TXT_PLUGIN: esbuildNative.Plugin = {
name: "computed",
setup(build) {
build.onLoad({ filter: /.*\.txt$/, namespace: "file" }, async (args) => {
const url = esbuildResolutionToURL(args);
const file = await Deno.readTextFile(new URL(url));
return {
contents: `export default ${JSON.stringify(file)};`,
loader: "js",
};
});
},
};

Deno.test("txt plugin", async (t) => {
+await testLoader(t, LOADERS, async (esbuild, loader) => {
const res = await esbuild.build({
...DEFAULT_OPTS,
plugins: [
denoResolverPlugin(),
TXT_PLUGIN,
denoLoaderPlugin({ loader }),
],
entryPoints: ["./testdata/hello.txt"],
});
assertEquals(res.warnings, []);
assertEquals(res.errors, []);
assertEquals(res.outputFiles.length, 1);
const output = res.outputFiles[0];
assertEquals(output.path, "<stdout>");
const dataURL = `data:application/javascript;base64,${btoa(output.text)}`;
const { default: hello } = await import(dataURL);
assertEquals(hello, "Hello World!");
});
});

Deno.test("uncached data url", async (t) => {
await testLoader(t, LOADERS, async (esbuild, loader) => {
const configPath = join(Deno.cwd(), "testdata", "config_ref.json");
Expand Down
4 changes: 3 additions & 1 deletion src/esbuild_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ export interface PluginBuild {
/** Documentation: https://esbuild.github.io/plugins/#on-load */
onLoad(
options: OnLoadOptions,
callback: (args: OnLoadArgs) => Promise<OnLoadResult | null> | undefined,
callback: (
args: OnLoadArgs,
) => Promise<OnLoadResult | null | undefined> | undefined,
): void;
}

Expand Down
20 changes: 17 additions & 3 deletions src/loader_native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type Loader,
type LoaderResolution,
mapContentType,
mediaTypeFromSpecifier,
mediaTypeToLoader,
parseNpmSpecifier,
} from "./shared.ts";
Expand Down Expand Up @@ -45,7 +46,15 @@ export class NativeLoader implements Loader {
}

const entry = await this.#infoCache.get(specifier.href);
if ("error" in entry) throw new Error(entry.error);
if ("error" in entry) {
if (
specifier.protocol === "file:" &&
mediaTypeFromSpecifier(specifier) === "Unknown"
) {
return { kind: "esm", specifier: new URL(entry.specifier) };
}
throw new Error(entry.error);
}

if (entry.kind === "npm") {
// TODO(lucacasonato): remove parsing once https://github.com/denoland/deno/issues/18043 is resolved
Expand All @@ -66,23 +75,28 @@ export class NativeLoader implements Loader {
return { kind: "esm", specifier: new URL(entry.specifier) };
}

async loadEsm(specifier: URL): Promise<esbuild.OnLoadResult> {
async loadEsm(specifier: URL): Promise<esbuild.OnLoadResult | undefined> {
if (specifier.protocol === "data:") {
const resp = await fetch(specifier);
const contents = new Uint8Array(await resp.arrayBuffer());
const contentType = resp.headers.get("content-type");
const mediaType = mapContentType(specifier, contentType);
const loader = mediaTypeToLoader(mediaType);
if (loader === null) return undefined;
return { contents, loader };
}
const entry = await this.#infoCache.get(specifier.href);
if ("error" in entry) throw new Error(entry.error);
if (
"error" in entry && specifier.protocol !== "file:" &&
mediaTypeFromSpecifier(specifier) !== "Unknown"
) throw new Error(entry.error);

if (!("local" in entry)) {
throw new Error("[unreachable] Not an ESM module.");
}
if (!entry.local) throw new Error("Module not downloaded yet.");
const loader = mediaTypeToLoader(entry.mediaType);
if (loader === null) return undefined;

let contents = await Deno.readFile(entry.local);
const denoCacheMetadata = lastIndexOfNeedle(contents, DENO_CACHE_METADATA);
Expand Down
3 changes: 2 additions & 1 deletion src/loader_portable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export class PortableLoader implements Loader, Disposable {
);
}

async loadEsm(url: URL): Promise<esbuild.OnLoadResult> {
async loadEsm(url: URL): Promise<esbuild.OnLoadResult | undefined> {
let module: Module;
switch (url.protocol) {
case "file:": {
Expand All @@ -162,6 +162,7 @@ export class PortableLoader implements Loader, Disposable {
}

const loader = mediaTypeToLoader(module.mediaType);
if (loader === null) return undefined;

const res: esbuild.OnLoadResult = { contents: module.data, loader };
if (url.protocol === "file:") {
Expand Down
2 changes: 1 addition & 1 deletion src/plugin_deno_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ export function denoLoaderPlugin(

function onLoad(
args: esbuild.OnLoadArgs,
): Promise<esbuild.OnLoadResult | null> | undefined {
): Promise<esbuild.OnLoadResult | null | undefined> | undefined {
if (args.namespace === "file" && isInNodeModules(args.path)) {
// inside node_modules, just let esbuild do it's thing
return undefined;
Expand Down
8 changes: 4 additions & 4 deletions src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type * as esbuild from "./esbuild_types.ts";

export interface Loader {
resolve(specifier: URL): Promise<LoaderResolution>;
loadEsm(specifier: URL): Promise<esbuild.OnLoadResult>;
loadEsm(specifier: URL): Promise<esbuild.OnLoadResult | undefined>;

packageIdFromNameInPackage?(
name: string,
Expand Down Expand Up @@ -39,7 +39,7 @@ export interface LoaderResolutionNode {
path: string;
}

export function mediaTypeToLoader(mediaType: MediaType): esbuild.Loader {
export function mediaTypeToLoader(mediaType: MediaType): esbuild.Loader | null {
switch (mediaType) {
case "JavaScript":
case "Mjs":
Expand All @@ -54,7 +54,7 @@ export function mediaTypeToLoader(mediaType: MediaType): esbuild.Loader {
case "Json":
return "json";
default:
throw new Error(`Unhandled media type ${mediaType}.`);
return null;
}
}

Expand Down Expand Up @@ -242,7 +242,7 @@ function mapJsLikeExtension(
}
}

function mediaTypeFromSpecifier(specifier: URL): MediaType {
export function mediaTypeFromSpecifier(specifier: URL): MediaType {
const path = specifier.pathname;
switch (extname(path)) {
case "":
Expand Down
1 change: 1 addition & 0 deletions testdata/hello.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello World!

0 comments on commit 6ed22bf

Please sign in to comment.