add esbuild loader
parent
bc82791626
commit
4f122bbb9c
|
@ -1 +0,0 @@
|
|||
Subproject commit 70c34bb6d77b2dd05255f0e4a813bb62239d62d1
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Luca Casonato
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,21 @@
|
|||
# esbuild_deno_loader
|
||||
|
||||
Deno module resolution for `esbuild`.
|
||||
|
||||
## Example
|
||||
|
||||
This example bundles an entrypoint into a single ESM output.
|
||||
|
||||
```js
|
||||
import * as esbuild from "https://deno.land/x/esbuild@v0.14.51/mod.js";
|
||||
import { denoPlugin } from "https://deno.land/x/esbuild_deno_loader@0.5.2/mod.ts";
|
||||
|
||||
await esbuild.build({
|
||||
plugins: [denoPlugin()],
|
||||
entryPoints: ["https://deno.land/std@0.150.0/hash/sha1.ts"],
|
||||
outfile: "./dist/sha1.esm.js",
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
});
|
||||
esbuild.stop();
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
import type * as esbuild from "https://deno.land/x/esbuild@v0.14.54/mod.d.ts";
|
||||
export type { esbuild };
|
||||
export {
|
||||
fromFileUrl,
|
||||
resolve,
|
||||
toFileUrl,
|
||||
} from "https://deno.land/std@0.150.0/path/mod.ts";
|
||||
export { basename, extname } from "https://deno.land/std@0.150.0/path/mod.ts";
|
||||
export {
|
||||
resolveImportMap,
|
||||
resolveModuleSpecifier,
|
||||
} from "https://deno.land/x/importmap@0.2.1/mod.ts";
|
||||
export type { ImportMap } from "https://deno.land/x/importmap@0.2.1/mod.ts";
|
|
@ -0,0 +1,11 @@
|
|||
import * as esbuild from "https://deno.land/x/esbuild@v0.14.51/mod.js";
|
||||
import { denoPlugin } from "https://deno.land/x/esbuild_deno_loader@0.5.2/mod.ts";
|
||||
|
||||
await esbuild.build({
|
||||
plugins: [denoPlugin()],
|
||||
entryPoints: ["https://deno.land/std@0.150.0/hash/sha1.ts"],
|
||||
outfile: "./dist/sha1.esm.js",
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
});
|
||||
esbuild.stop();
|
|
@ -0,0 +1,11 @@
|
|||
test:
|
||||
deno test -A
|
||||
|
||||
lint:
|
||||
deno lint
|
||||
|
||||
fmt:
|
||||
deno fmt
|
||||
|
||||
fmt/check:
|
||||
deno fmt --check
|
|
@ -0,0 +1,110 @@
|
|||
import {
|
||||
esbuild,
|
||||
ImportMap,
|
||||
resolveImportMap,
|
||||
resolveModuleSpecifier,
|
||||
toFileUrl,
|
||||
} from "./deps.ts";
|
||||
import { load as nativeLoad } from "./src/native_loader.ts";
|
||||
import { load as portableLoad } from "./src/portable_loader.ts";
|
||||
import { ModuleEntry } from "./src/deno.ts";
|
||||
|
||||
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;
|
||||
/**
|
||||
* Specify which loader to use. By default this will use the `native` loader,
|
||||
* unless `Deno.run` is not available.
|
||||
*
|
||||
* - `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-net.
|
||||
*/
|
||||
loader?: "native" | "portable";
|
||||
}
|
||||
|
||||
/** The default loader to use. */
|
||||
export const DEFAULT_LOADER: "native" | "portable" =
|
||||
typeof Deno.run === "function" ? "native" : "portable";
|
||||
|
||||
export function denoPlugin(options: DenoPluginOptions = {}): esbuild.Plugin {
|
||||
const loader = options.loader ?? DEFAULT_LOADER;
|
||||
return {
|
||||
name: "deno",
|
||||
setup(build) {
|
||||
const infoCache = new Map<string, ModuleEntry>();
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
build.onResolve(
|
||||
{ filter: /.*/ },
|
||||
function onResolve(
|
||||
args: esbuild.OnResolveArgs,
|
||||
): esbuild.OnResolveResult | null | undefined {
|
||||
// console.log("To resolve", args.path);
|
||||
const resolveDir = args.resolveDir
|
||||
? `${toFileUrl(args.resolveDir).href}/`
|
||||
: "";
|
||||
const referrer = 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);
|
||||
}
|
||||
// console.log("Resolved", resolved.href);
|
||||
if (build.initialOptions.external) {
|
||||
for (const external of build.initialOptions.external) {
|
||||
if (resolved.href.startsWith(external)) {
|
||||
// console.log("Got external", args.path, resolved.href);
|
||||
return { path: resolved.href, external: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resolved.href.endsWith(".css")) {
|
||||
return {
|
||||
path: resolved.href.substring("file://".length),
|
||||
};
|
||||
}
|
||||
return { path: resolved.href, namespace: "deno" };
|
||||
},
|
||||
);
|
||||
|
||||
build.onLoad(
|
||||
{ filter: /.*/ },
|
||||
function onLoad(
|
||||
args: esbuild.OnLoadArgs,
|
||||
): Promise<esbuild.OnLoadResult | null> {
|
||||
if (args.path.endsWith(".css")) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
const url = new URL(args.path);
|
||||
switch (loader) {
|
||||
case "native":
|
||||
return nativeLoad(infoCache, url, options);
|
||||
case "portable":
|
||||
return portableLoad(url, options);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
// Lifted from https://raw.githubusercontent.com/denoland/deno_graph/89affe43c9d3d5c9165c8089687c107d53ed8fe1/lib/media_type.ts
|
||||
export type MediaType =
|
||||
| "JavaScript"
|
||||
| "Mjs"
|
||||
| "Cjs"
|
||||
| "JSX"
|
||||
| "TypeScript"
|
||||
| "Mts"
|
||||
| "Cts"
|
||||
| "Dts"
|
||||
| "Dmts"
|
||||
| "Dcts"
|
||||
| "TSX"
|
||||
| "Json"
|
||||
| "Wasm"
|
||||
| "TsBuildInfo"
|
||||
| "SourceMap"
|
||||
| "Unknown";
|
||||
|
||||
export interface InfoOutput {
|
||||
roots: string[];
|
||||
modules: ModuleEntry[];
|
||||
redirects: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ModuleEntry {
|
||||
specifier: string;
|
||||
size: number;
|
||||
mediaType?: MediaType;
|
||||
local?: string;
|
||||
checksum?: string;
|
||||
emit?: string;
|
||||
map?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface DenoInfoOptions {
|
||||
importMap?: string;
|
||||
}
|
||||
|
||||
let tempDir: null | string;
|
||||
|
||||
export async function info(
|
||||
specifier: URL,
|
||||
options: DenoInfoOptions,
|
||||
): Promise<InfoOutput> {
|
||||
const cmd = [
|
||||
Deno.execPath(),
|
||||
"info",
|
||||
"--json",
|
||||
];
|
||||
if (options.importMap !== undefined) {
|
||||
cmd.push("--import-map", options.importMap);
|
||||
}
|
||||
cmd.push(specifier.href);
|
||||
|
||||
if (!tempDir) {
|
||||
tempDir = Deno.makeTempDirSync();
|
||||
}
|
||||
|
||||
let proc;
|
||||
|
||||
try {
|
||||
proc = Deno.run({
|
||||
cmd,
|
||||
stdout: "piped",
|
||||
cwd: tempDir,
|
||||
});
|
||||
const raw = await proc.output();
|
||||
const status = await proc.status();
|
||||
if (!status.success) {
|
||||
throw new Error(`Failed to call 'deno info' on '${specifier.href}'`);
|
||||
}
|
||||
const txt = new TextDecoder().decode(raw);
|
||||
return JSON.parse(txt);
|
||||
} finally {
|
||||
try {
|
||||
proc?.stdout.close();
|
||||
} catch (err) {
|
||||
if (err instanceof Deno.errors.BadResource) {
|
||||
// ignore the error
|
||||
} else {
|
||||
// deno-lint-ignore no-unsafe-finally
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
proc?.close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import { esbuild, fromFileUrl } from "../deps.ts";
|
||||
import * as deno from "./deno.ts";
|
||||
import { mediaTypeToLoader, transformRawIntoContent } from "./shared.ts";
|
||||
|
||||
export interface LoadOptions {
|
||||
importMapURL?: URL;
|
||||
}
|
||||
|
||||
export async function load(
|
||||
infoCache: Map<string, deno.ModuleEntry>,
|
||||
url: URL,
|
||||
options: LoadOptions
|
||||
): Promise<esbuild.OnLoadResult | null> {
|
||||
switch (url.protocol) {
|
||||
case "http:":
|
||||
case "https:":
|
||||
case "data:":
|
||||
return await loadFromCLI(infoCache, url, options);
|
||||
case "file:": {
|
||||
const res = await loadFromCLI(infoCache, url, options);
|
||||
res.watchFiles = [fromFileUrl(url.href)];
|
||||
return res;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function loadFromCLI(
|
||||
infoCache: Map<string, deno.ModuleEntry>,
|
||||
specifier: URL,
|
||||
options: LoadOptions
|
||||
): Promise<esbuild.OnLoadResult> {
|
||||
const specifierRaw = specifier.href;
|
||||
if (!infoCache.has(specifierRaw)) {
|
||||
const { modules, redirects } = await deno.info(specifier, {
|
||||
importMap: options.importMapURL?.href,
|
||||
});
|
||||
for (const module of modules) {
|
||||
infoCache.set(module.specifier, module);
|
||||
}
|
||||
for (const [specifier, redirect] of Object.entries(redirects)) {
|
||||
const redirected = infoCache.get(redirect);
|
||||
if (!redirected) {
|
||||
throw new TypeError("Unreachable.");
|
||||
}
|
||||
infoCache.set(specifier, redirected);
|
||||
}
|
||||
}
|
||||
|
||||
const module = infoCache.get(specifierRaw);
|
||||
if (!module) {
|
||||
throw new TypeError("Unreachable.");
|
||||
}
|
||||
|
||||
if (module.error) throw new Error(module.error);
|
||||
if (!module.local) throw new Error("Module not downloaded yet.");
|
||||
const mediaType = module.mediaType ?? "Unknown";
|
||||
|
||||
const loader = mediaTypeToLoader(mediaType);
|
||||
|
||||
const raw = await Deno.readFile(module.local);
|
||||
const contents = transformRawIntoContent(raw, mediaType);
|
||||
|
||||
return { contents, loader };
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
import { esbuild, extname, fromFileUrl } from "../deps.ts";
|
||||
import * as deno from "./deno.ts";
|
||||
import { mediaTypeToLoader, transformRawIntoContent } from "./shared.ts";
|
||||
|
||||
export interface LoadOptions {
|
||||
importMapURL?: URL;
|
||||
}
|
||||
|
||||
export async function load(
|
||||
url: URL,
|
||||
_options: LoadOptions,
|
||||
): Promise<esbuild.OnLoadResult | null> {
|
||||
switch (url.protocol) {
|
||||
case "http:":
|
||||
case "https:":
|
||||
case "data:":
|
||||
return await loadWithFetch(url);
|
||||
case "file:": {
|
||||
const res = await loadWithReadFile(url);
|
||||
res.watchFiles = [fromFileUrl(url.href)];
|
||||
return res;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function loadWithFetch(
|
||||
specifier: URL,
|
||||
): Promise<esbuild.OnLoadResult> {
|
||||
const specifierRaw = specifier.href;
|
||||
|
||||
// TODO(lucacasonato): redirects!
|
||||
const resp = await fetch(specifierRaw);
|
||||
if (!resp.ok) {
|
||||
throw new Error(
|
||||
`Encountered status code ${resp.status} while fetching ${specifierRaw}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const contentType = resp.headers.get("content-type");
|
||||
const mediaType = mapContentType(
|
||||
new URL(resp.url || specifierRaw),
|
||||
contentType,
|
||||
);
|
||||
|
||||
const loader = mediaTypeToLoader(mediaType);
|
||||
|
||||
const raw = new Uint8Array(await resp.arrayBuffer());
|
||||
const contents = transformRawIntoContent(raw, mediaType);
|
||||
|
||||
return { contents, loader };
|
||||
}
|
||||
|
||||
async function loadWithReadFile(specifier: URL): Promise<esbuild.OnLoadResult> {
|
||||
const path = fromFileUrl(specifier);
|
||||
|
||||
const mediaType = mapContentType(specifier, null);
|
||||
const loader = mediaTypeToLoader(mediaType);
|
||||
|
||||
const raw = await Deno.readFile(path);
|
||||
const contents = transformRawIntoContent(raw, mediaType);
|
||||
|
||||
return { contents, loader };
|
||||
}
|
||||
|
||||
function mapContentType(
|
||||
specifier: URL,
|
||||
contentType: string | null,
|
||||
): deno.MediaType {
|
||||
if (contentType !== null) {
|
||||
const contentTypes = contentType.split(";");
|
||||
const mediaType = contentTypes[0].toLowerCase();
|
||||
switch (mediaType) {
|
||||
case "application/typescript":
|
||||
case "text/typescript":
|
||||
case "video/vnd.dlna.mpeg-tts":
|
||||
case "video/mp2t":
|
||||
case "application/x-typescript":
|
||||
return mapJsLikeExtension(specifier, "TypeScript");
|
||||
case "application/javascript":
|
||||
case "text/javascript":
|
||||
case "application/ecmascript":
|
||||
case "text/ecmascript":
|
||||
case "application/x-javascript":
|
||||
case "application/node":
|
||||
return mapJsLikeExtension(specifier, "JavaScript");
|
||||
case "text/jsx":
|
||||
return "JSX";
|
||||
case "text/tsx":
|
||||
return "TSX";
|
||||
case "application/json":
|
||||
case "text/json":
|
||||
return "Json";
|
||||
case "application/wasm":
|
||||
return "Wasm";
|
||||
case "text/plain":
|
||||
case "application/octet-stream":
|
||||
return mediaTypeFromSpecifier(specifier);
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
} else {
|
||||
return mediaTypeFromSpecifier(specifier);
|
||||
}
|
||||
}
|
||||
|
||||
function mapJsLikeExtension(
|
||||
specifier: URL,
|
||||
defaultType: deno.MediaType,
|
||||
): deno.MediaType {
|
||||
const path = specifier.pathname;
|
||||
switch (extname(path)) {
|
||||
case ".jsx":
|
||||
return "JSX";
|
||||
case ".mjs":
|
||||
return "Mjs";
|
||||
case ".cjs":
|
||||
return "Cjs";
|
||||
case ".tsx":
|
||||
return "TSX";
|
||||
case ".ts":
|
||||
if (path.endsWith(".d.ts")) {
|
||||
return "Dts";
|
||||
} else {
|
||||
return defaultType;
|
||||
}
|
||||
case ".mts": {
|
||||
if (path.endsWith(".d.mts")) {
|
||||
return "Dmts";
|
||||
} else {
|
||||
return defaultType == "JavaScript" ? "Mjs" : "Mts";
|
||||
}
|
||||
}
|
||||
case ".cts": {
|
||||
if (path.endsWith(".d.cts")) {
|
||||
return "Dcts";
|
||||
} else {
|
||||
return defaultType == "JavaScript" ? "Cjs" : "Cts";
|
||||
}
|
||||
}
|
||||
default:
|
||||
return defaultType;
|
||||
}
|
||||
}
|
||||
|
||||
function mediaTypeFromSpecifier(specifier: URL): deno.MediaType {
|
||||
const path = specifier.pathname;
|
||||
switch (extname(path)) {
|
||||
case "":
|
||||
if (path.endsWith("/.tsbuildinfo")) {
|
||||
return "TsBuildInfo";
|
||||
} else {
|
||||
return "Unknown";
|
||||
}
|
||||
case ".ts":
|
||||
if (path.endsWith(".d.ts")) {
|
||||
return "Dts";
|
||||
} else {
|
||||
return "TypeScript";
|
||||
}
|
||||
case ".mts":
|
||||
if (path.endsWith(".d.mts")) {
|
||||
return "Dmts";
|
||||
} else {
|
||||
return "Mts";
|
||||
}
|
||||
case ".cts":
|
||||
if (path.endsWith(".d.cts")) {
|
||||
return "Dcts";
|
||||
} else {
|
||||
return "Cts";
|
||||
}
|
||||
case ".tsx":
|
||||
return "TSX";
|
||||
case ".js":
|
||||
return "JavaScript";
|
||||
case ".jsx":
|
||||
return "JSX";
|
||||
case ".mjs":
|
||||
return "Mjs";
|
||||
case ".cjs":
|
||||
return "Cjs";
|
||||
case ".json":
|
||||
return "Json";
|
||||
case ".wasm":
|
||||
return "Wasm";
|
||||
case ".tsbuildinfo":
|
||||
return "TsBuildInfo";
|
||||
case ".map":
|
||||
return "SourceMap";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { esbuild } from "../deps.ts";
|
||||
import { MediaType } from "./deno.ts";
|
||||
|
||||
export function mediaTypeToLoader(mediaType: MediaType): esbuild.Loader {
|
||||
switch (mediaType) {
|
||||
case "JavaScript":
|
||||
case "Mjs":
|
||||
return "js";
|
||||
case "JSX":
|
||||
return "jsx";
|
||||
case "TypeScript":
|
||||
case "Mts":
|
||||
return "ts";
|
||||
case "TSX":
|
||||
return "tsx";
|
||||
case "Json":
|
||||
return "js";
|
||||
default:
|
||||
throw new Error(`Unhandled media type ${mediaType}.`);
|
||||
}
|
||||
}
|
||||
|
||||
export function transformRawIntoContent(
|
||||
raw: Uint8Array,
|
||||
mediaType: MediaType
|
||||
): string | Uint8Array {
|
||||
switch (mediaType) {
|
||||
case "Json":
|
||||
return jsonToESM(raw);
|
||||
default:
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
function jsonToESM(source: Uint8Array): string {
|
||||
const sourceString = new TextDecoder().decode(source);
|
||||
let json = JSON.stringify(JSON.parse(sourceString), null, 2);
|
||||
json = json.replaceAll(`"__proto__":`, `["__proto__"]:`);
|
||||
return `export default ${json};`;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import * as esbuild from "https://deno.land/x/esbuild@v0.14.51/mod.js";
|
||||
export { esbuild };
|
||||
export {
|
||||
assert,
|
||||
assertEquals,
|
||||
} from "https://deno.land/std@0.150.0/testing/asserts.ts";
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"hello": "world",
|
||||
"__proto__": {
|
||||
"sky": "universe"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "mod";
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"imports": {
|
||||
"mod": "./mod.ts"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
const bool = "asd";
|
||||
export { bool };
|
|
@ -0,0 +1,11 @@
|
|||
function createElement(fn) {
|
||||
return fn();
|
||||
}
|
||||
|
||||
const React = { createElement };
|
||||
|
||||
function Asd() {
|
||||
return "foo";
|
||||
}
|
||||
|
||||
export default <Asd />;
|
|
@ -0,0 +1,2 @@
|
|||
const bool = "asd";
|
||||
export { bool };
|
|
@ -0,0 +1,4 @@
|
|||
let bool: string;
|
||||
bool = "asd";
|
||||
bool = "asd2";
|
||||
export { bool };
|
|
@ -0,0 +1,4 @@
|
|||
let bool: string;
|
||||
bool = "asd";
|
||||
bool = "asd2";
|
||||
export { bool };
|
|
@ -0,0 +1,11 @@
|
|||
function createElement(fn: () => string) {
|
||||
return fn();
|
||||
}
|
||||
|
||||
const React = { createElement };
|
||||
|
||||
function Asd() {
|
||||
return "foo";
|
||||
}
|
||||
|
||||
export default <Asd />;
|
Loading…
Reference in New Issue