Skip to content

Commit

Permalink
feat: don't error on unrecognized media types in loader
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 committed Sep 27, 2024
1 parent f4cf2e7 commit 14c93ed
Show file tree
Hide file tree
Showing 7 changed files with 62 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
2 changes: 1 addition & 1 deletion src/esbuild_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ 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 14c93ed

Please sign in to comment.