Skip to content

Instantly share code, notes, and snippets.

@randallb
Created January 10, 2024 02:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save randallb/cf696de5867ecde39fec0d057364680c to your computer and use it in GitHub Desktop.
Save randallb/cf696de5867ecde39fec0d057364680c to your computer and use it in GitHub Desktop.
Deno esbuild plugin
import {
Loader,
PluginBuild,
} from "https://deno.land/x/esbuild@v0.15.15/mod.js";
import { dirname, join } from "https://deno.land/std@0.114.0/path/mod.ts";
import { createLogger } from "packages/logs/mod.ts";
const log = createLogger("esbuildDenoPlugin", "debug");
const logError = createLogger("esbuildDenoPlugin", "error");
const LOCAL_NAMESPACE = "deno-local";
const REMOTE_NAMESPACE = "deno-remote";
const NPM_NAMESPACE = "npm";
const NPM_RELATIVE_DEPENDENCY = "npm_relative_dependency";
const NPM_SCOPED_NAMESPACE = "npm_scoped";
const NPM_PACKAGE_JSON_NAMESPACE = "npm_package_json";
const REMOTE_MODULE_REGEX = /^https?:\/\//;
const cacheLocations = await getCacheLocations();
async function getCacheLocations() {
log("Fetching cache locations...");
const p = Deno.run({
cmd: [
"deno",
"info",
],
stdout: "piped",
});
const { code } = await p.status();
if (code !== 0) {
throw new Error("deno info failed");
}
const decoder = new TextDecoder("utf-8");
const output = decoder.decode(await p.output());
// deno-lint-ignore no-control-regex
const plainText = output.replace(/\x1b\[[0-9;]*m/g, "");
const lines = plainText.split("\n");
const denoDirLocation = lines.find((line) =>
line.startsWith("DENO_DIR location: ")
)?.split(
"DENO_DIR location: ",
)[1];
const remoteModulesCache = lines.find((line) =>
line.startsWith("Remote modules cache: ")
)?.split(
"Remote modules cache: ",
)[1];
const npmModulesCacheRoot =
lines.find((line) => line.startsWith("npm modules cache: "))?.split(
"npm modules cache: ",
)[1] ?? "./.deno/npm";
const npmModulesCache = join(npmModulesCacheRoot, "registry.npmjs.org");
log("Cache locations fetched successfully.");
return {
denoDirLocation,
remoteModulesCache,
npmModulesCache,
};
}
async function checkIfNpmModule(pathWithNamespace: string) {
try {
const packageJsonString = await Deno.readTextFile(
join(Deno.env.get("BFF_ROOT") ?? Deno.cwd(), "package.json"),
);
const packageJson = JSON.parse(packageJsonString);
const dependencies = Object.keys(packageJson.dependencies ?? {});
const path = pathWithNamespace.split("/")[0];
const isPackageJson = dependencies.includes(path);
return isPackageJson;
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
return false;
}
throw error;
}
}
export const esbuildDenoPlugin = {
name: "deno",
setup(build: PluginBuild) {
const { onResolve, onLoad } = build;
onResolve({ filter: REMOTE_MODULE_REGEX }, (args) => {
log(`Resolving remote module: ${args.path}`);
return { path: args.path, namespace: REMOTE_NAMESPACE };
});
onResolve({ filter: /.*/, namespace: REMOTE_NAMESPACE }, (args) => {
const path = new URL(args.path, args.importer).href;
if (path.startsWith("node:")) {
return { path, namespace: "empty" };
}
log(`Resolving remote module: ${args.path}`);
const namespace = REMOTE_NAMESPACE;
return { path, namespace };
});
onResolve({ filter: /.*/, namespace: NPM_NAMESPACE }, async (args) => {
const [importerPackageName, ...rest] = args.importer.split("/");
let path = args.path;
if (path.endsWith(".js")) {
path = path.replace(".js", "");
}
const isRelativeNpmPath = path.startsWith(".") || path.startsWith("/");
if (isRelativeNpmPath) {
const everythingExceptLast = rest.slice(0, -1);
path = everythingExceptLast.join("/") + "/" + path + ".js";
log("resolving relative npm module:", path);
const mainEntrypointPath = await loadCachedNpmModulePath(
importerPackageName,
);
path = path = join(dirname(mainEntrypointPath), path);
return { path, namespace: NPM_RELATIVE_DEPENDENCY };
}
log("resolving npm namespaced module:", path);
return { path, namespace: NPM_PACKAGE_JSON_NAMESPACE };
});
onResolve({ filter: /.*/, namespace: NPM_RELATIVE_DEPENDENCY }, (args) => {
const pathWithExtension = args.path.endsWith(".js")
? args.path
: args.path + ".js";
if (args.path.startsWith(".")) {
log(`Resolving npm filepath dependency: ${args.path}`);
const path = join(dirname(args.importer), `${pathWithExtension}`);
return { path, namespace: NPM_RELATIVE_DEPENDENCY };
}
// Check if the module is scoped
const isScoped = args.path.startsWith("@") && args.path.includes("/");
log(`Resolving npm bare specifier dependency: ${args.path}`);
return {
path: args.path,
namespace: isScoped ? NPM_SCOPED_NAMESPACE : NPM_NAMESPACE,
};
});
onResolve({ filter: /.*/, namespace: NPM_SCOPED_NAMESPACE }, async (
args,
) => {
const [namespace, packageName, ...rest] = args.importer.split("/");
const packageIdentifier = `${namespace}/${packageName}`;
let paths = args.path;
if (paths.startsWith(".")) {
paths = paths.replace(".", "");
}
const mainEntrypointPath = await loadCachedNpmModulePath(
packageIdentifier,
);
const restExceptLast = rest.slice(0, -1);
const path = join(
dirname(mainEntrypointPath),
restExceptLast.join("/"),
paths,
);
log(`Resolving npm scoped namespace dependency: ${args.path}`);
return { path, namespace: NPM_RELATIVE_DEPENDENCY };
});
onResolve({ filter: /.*/, namespace: NPM_PACKAGE_JSON_NAMESPACE }, async (
args,
) => {
if (args.path.startsWith(".")) {
const mainEntrypointPath = await loadCachedNpmModulePath(
args.importer,
);
const path = join(dirname(mainEntrypointPath), args.path);
log(`Resolving npm package_json dependency: ${args.path}`);
return { path, namespace: NPM_RELATIVE_DEPENDENCY };
}
log(`Resolving npm package.json: ${args.path}`);
return { path: args.path, namespace: NPM_PACKAGE_JSON_NAMESPACE };
});
onResolve({ filter: /.*/ }, async (args) => {
if (args.kind === "entry-point") {
return;
}
const isNpmModule = await checkIfNpmModule(args.path);
if (isNpmModule) {
log(`Resolving npm module: ${args.path}`);
if (args.path.includes("posthog-node")) {
return { path: args.path, namespace: "empty" };
}
return { path: args.path, namespace: NPM_PACKAGE_JSON_NAMESPACE };
}
if (args.path.startsWith("npm:") || isNpmModule) {
const path: string = args.path.split("npm:")[1] ?? args.path;
log(`Resolving npm prefixed module: ${args.path}`);
return { path, namespace: NPM_NAMESPACE };
}
if (args.path.endsWith(".graphql")) {
const path =
new URL(import.meta.resolve(`packages/__generated__/${args.path}.ts`))
.pathname;
return {
path,
namespace: LOCAL_NAMESPACE,
};
}
const resolvedPath = import.meta.resolve(args.path);
if (resolvedPath.startsWith("file://")) {
const path = resolvedPath.replace("file://", "");
log(`Resolving local module: ${args.path}`);
return {
path,
namespace: LOCAL_NAMESPACE,
};
}
if (resolvedPath.startsWith("http")) {
const path = new URL(resolvedPath).href;
log(`Resolving remote module: ${args.path}`);
return {
path,
namespace: REMOTE_NAMESPACE,
};
}
if (resolvedPath.startsWith("npm:")) {
const path: string = args.path.split("npm:")[1] ?? args.path;
return { path, namespace: NPM_NAMESPACE };
}
});
function getLoader(extension: string): Loader {
const validExtensions = [".js", ".jsx", ".ts", ".tsx"];
if (validExtensions.includes(extension)) {
return extension as Loader;
}
return "ts";
}
onLoad({ filter: /.*/, namespace: "empty" }, (args) => {
// Creating a Proxy to handle any named import dynamically.
// This will cater to both default and named exports.
const contents = `
const handler = {
get: (target, prop) => undefined
};
const moduleProxy = new Proxy({}, handler);
export default moduleProxy;
export const Buffer = undefined;
`;
const loader = "js"; // JavaScript loader for the content
log(`Handling 'empty' namespace for module: ${args.path}`);
return { contents, loader };
});
onLoad({ filter: /.*/, namespace: REMOTE_NAMESPACE }, async (args) => {
const cachePath = cacheLocations.remoteModulesCache + "/" + args.path;
let contents;
try {
await Deno.stat(cachePath);
contents = await Deno.readTextFile(cachePath);
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
const source = await fetch(args.path);
if (!source.ok) {
throw new Error(
`Failed to fetch ${args.path}: ${source.status} ${source.statusText}`,
);
}
contents = await source.text();
} else {
throw error;
}
}
const pattern = /\/\/# sourceMappingURL=(\S+)/;
const match = contents.match(pattern);
if (match) {
const sourceMapUrl = new URL(match[1], args.path);
const dataurl = await fetchSourceMap(sourceMapUrl);
const comment = `//# sourceMappingURL=${dataurl}`;
contents = contents.replace(pattern, comment);
}
const { pathname } = new URL(args.path);
const ext = pathname.match(/[^.]+$/);
const loader = getLoader(ext ? ext[0] : "");
return { contents, loader };
});
onLoad({ filter: /.*/, namespace: LOCAL_NAMESPACE }, async (args) => {
const source = await Deno.readTextFile(args.path);
const graphqlTags = extractGraphqlTags(source);
let contents = source;
if (graphqlTags.length > 0) {
contents = await replaceTagsWithImports(source, graphqlTags);
}
const ext = args.path.match(/[^.]+$/);
const loader = (ext ? ext[0] : "ts") as Loader;
log(`Loading local module: ${args.path}`);
return { contents, loader };
});
onLoad({ filter: /.*/, namespace: NPM_NAMESPACE }, async (args) => {
// Get the npm identifier from the path
const npmIdentifier = args.path.split("/")[0];
const pathParts = args.path.split("/").slice(1);
const mainEntrypointPath = await loadCachedNpmModulePath(npmIdentifier);
let requestedFile;
if (pathParts.length === 0) {
requestedFile = mainEntrypointPath;
} else {
const mainModuleDir = dirname(mainEntrypointPath);
const requestedFileWithoutExtension = join(mainModuleDir, ...pathParts);
requestedFile = `${requestedFileWithoutExtension}.js`;
}
const ext = requestedFile.split(".").pop();
const loader = getLoader(ext ?? "");
// Read the contents of the requested file
log("trying to load", requestedFile);
let contents;
try {
contents = await Deno.readTextFile(requestedFile);
} catch (e) {
logError(`Could not find module ${npmIdentifier} at ${requestedFile}`);
contents = "";
}
log(`Loading npm module: ${args.path}`);
return { contents, loader };
});
onLoad(
{ filter: /.*/, namespace: NPM_RELATIVE_DEPENDENCY },
async (args) => {
const ext = args.path.match(/[^.]+$/);
const loader = (ext ? ext[0] : "ts") as Loader;
log(`Loading npm relative dependency: ${args.path}`);
// Read the contents of the requested file
const contents = await Deno.readTextFile(args.path);
return { contents, loader };
},
);
onLoad({ filter: /.*/, namespace: NPM_SCOPED_NAMESPACE }, async (args) => {
log(`Loading npm scoped namespace: ${args.path}`);
// Get the scoped npm identifier from the path
const [scope, packageName] = args.path.split("/").slice(0, 2);
const scopedNpmIdentifier = `${scope}/${packageName}`;
const pathParts = args.path.split("/").slice(2);
const mainModuleDir = await loadCachedNpmModulePath(
scopedNpmIdentifier,
);
const requestedFileWithoutExtension = join(
dirname(mainModuleDir),
...pathParts,
);
const requestedFile = `${requestedFileWithoutExtension}.js`;
const ext = requestedFile.split(".").pop();
const loader = getLoader(ext ?? "");
// Read the contents of the requested file
const contents = await Deno.readTextFile(requestedFile);
return { contents, loader };
});
onLoad(
{ filter: /.*/, namespace: NPM_PACKAGE_JSON_NAMESPACE },
async (args) => {
const [npmIdentifier, child] = args.path.split("/");
const mainEntrypointPath = await loadCachedNpmModulePath(npmIdentifier);
let contents = await Deno.readTextFile(mainEntrypointPath);
if (child) {
const entrypointPath = join(dirname(mainEntrypointPath), child);
contents = await Deno.readTextFile(`${entrypointPath}.js`);
}
log(`Loading npm package.json: ${args.path}`);
return { contents, loader: "js" };
},
);
},
};
async function fetchSourceMap(url: URL) {
const map = await fetch(url);
const type = map.headers.get("content-type") ?? undefined;
const buffer = await map.arrayBuffer();
const blob = new Blob([buffer], { type });
const reader = new FileReader();
return new Promise((cb) => {
reader.onload = (e) => cb(e.target?.result);
reader.readAsDataURL(blob);
});
}
async function loadCachedNpmModulePath(
npmIdentifier: string,
retry = true,
): Promise<string> {
const cachePath = `${cacheLocations.npmModulesCache}/${npmIdentifier}`;
const registryPath = `${cachePath}/registry.json`;
let registryJsonString;
try {
registryJsonString = await Deno.readTextFile(registryPath);
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
if (npmIdentifier.includes("/") && retry) {
const [packageName] = npmIdentifier.split("/");
return loadCachedNpmModulePath(packageName, false);
}
if (npmIdentifier.startsWith("npm:") && retry) {
return loadCachedNpmModulePath(npmIdentifier.split(":")[1], false);
}
logError(
`Could not find module ${npmIdentifier} in cache at ${cachePath}`,
);
}
}
if (registryJsonString == null) {
return `infra/utils/empty.ts`;
}
const registryJson = JSON.parse(registryJsonString);
const currentVersion = registryJson["dist-tags"].latest;
const currentVersionPath = `${cachePath}/${currentVersion}`;
try {
const packageJsonString = await Deno.readTextFile(
`${currentVersionPath}/package.json`,
);
const packageJson = JSON.parse(packageJsonString);
const mainFile = packageJson.main;
const mainFilePath = `${currentVersionPath}/${mainFile}`;
return mainFilePath;
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
logError(
`Could not find module ${npmIdentifier} in cache at ${currentVersionPath}`,
);
return `infra/utils/empty.ts`;
}
throw error;
}
}
const extractGraphqlTags = (contents: string) => {
const matches = Array.from(contents.matchAll(/graphql`([\s\S]+?)`/g)).map(
(match) => match[1].trim(),
);
return matches;
};
const replaceTagsWithImports = async (
contents: string,
matches: string[],
) => {
let updatedContents = contents;
const artifactsDirectory = "packages/__generated__";
const replacements: Record<string, string> = {};
for (const match of matches) {
const pattern = /^(?<operationType>\w+)\s+(?<operationName>\w+)/m;
const { _operationType, operationName } = match.match(pattern)?.groups ??
{};
const generatedFileName = `${operationName}.graphql.ts`;
const generatedFilePath = join(artifactsDirectory, generatedFileName);
try {
const filesystemPath =
new URL(import.meta.resolve(generatedFilePath)).pathname;
await Deno.stat(filesystemPath);
const replacement =
`(async () => { const importedModule = await import('${generatedFilePath}'); return importedModule.default; })()`;
replacements[match] = replacement;
} catch (_error) {
logError(
`Generated Relay file not found for query: ${generatedFilePath}, skipping replacement.`,
);
}
}
updatedContents = updatedContents.replace(
/graphql`([\s\S]+?)`/g,
(fullMatch, group) => {
const trimmedGroup = group.trim();
return replacements[trimmedGroup] || fullMatch;
},
);
return updatedContents;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment