oven-sh / bun

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one
https://bun.sh
Other
74.26k stars 2.77k forks source link

Bun.build: equivalent of globalNames #9685

Open raikasdev opened 7 months ago

raikasdev commented 7 months ago

What is the problem this feature would solve?

Allow using global variables instead of bundling dependencies in environments like the WordPress Block Editor, where React is available already at window.React, etc. Discord thread

What is the feature you are proposing to solve the problem?

Allow mapping dependencies to global variables and not bundle them.

What alternatives have you considered?

No response

tyleregeto commented 7 months ago

Would love to see this as well. I have been investigating bun as a potential replacement to our current bundling solution, and bumped into this.

azekeprofit commented 7 months ago

You can do it as a plugin:

import type { BunPlugin } from "bun";

// port of a https://github.com/a-b-r-o-w-n/esbuild-plugin-globals to Bun

type GlobalResolveFunc = (moduleName: string) => string | undefined;
type PluginGlobalsOptions = {
    [key: string]: string | GlobalResolveFunc;
};

const generateResolveFilter = (globals:PluginGlobalsOptions) => {
    const moduleNames = Object.keys(globals);
    return new RegExp(`^(${moduleNames.join("|")})$`);
};
const generateExport = (globals:PluginGlobalsOptions, name:string) => {
    const match = Object.entries(globals).find(([pattern]) => {
        return new RegExp(`^${pattern}$`).test(name);
    });
    if (match) {
        const output = typeof match[1] === "function" ? match[1](name) : match[1];
        return output ? `module.exports = ${output}` : undefined;
    }
};
const pluginGlobals = (globals:PluginGlobalsOptions = {}) => {
    const filter = generateResolveFilter(globals);
    return {
        name: "globals",
        setup(build) {
            build.onResolve({ filter }, (args) => {
                return { path: args.path, namespace: "globals" };
            });
            build.onLoad({ filter: /.*/, namespace: "globals" }, (args) => {
                const name = args.path;
                const contents = generateExport(globals, name);
                if (contents) {
                    return { contents };
                }
                return null;
            });
        },
    } as BunPlugin;
};
export default pluginGlobals;

then your build script goes like this

import { build } from "bun";
import globalsPlugin from "./globalsPlugin";

build({
  entrypoints: ["src/app.tsx"],
  format: "esm",
  outdir: "dist",
  splitting: true,
  minify: false,
  plugins: [
    globalsPlugin({
      react: "react",
    }),
  ],
})
DylanLukes commented 3 weeks ago

It would be nice to avoid declaring globals requiring any Bunfig changes (or creating a Bunfig at all), and to follow TS/JS conventions.

At least for TypeScript projects, just making use of how globals are declared in TypeScript already seems best. That is, put in (for example) global.d.ts something like:

export {} // (needed so this is considered a module)

import type * as JQuery from 'jquery';

declare global {
  interface Window {
    $: JQuery;
  }
}

For vanilla JavaScript inputs, I'd propose recognizing ESLint's /* global ... */ as that's the most common idiom I know. Another consideration would be JSDoc's @global tag. One potential drawback of the ESLint convention is that someone reasonably might specify their globals in eslint.config.js and then get confused as to why Bun isn't recognizing them as globals.

DylanLukes commented 3 weeks ago

Ok did a quick experiment, and (to the original question) as best as I can tell this works perfectly well in Bun 1.1.30.

No type checker or build errors, the output looks correct (and works), etc. This only requires bun add @types/jquery, the implementation is not needed.

example.ts:

import type * as JQuery from 'jquery';

export {}

declare global {
    interface Window {
        $: JQuery
    }
}

$(() => {
    $("#title").text("Text set by jQuery!");
    console.log("It works!");
});

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    <script src="example.js"></script>
  </head>
  <body>
    <h1 id="title">It didn't work.</h1>
  </body>
</html>

Built with bun build --target browser --format iife example.ts > example.js.

Built Output IIFE
(() => {
  var __defProp = Object.defineProperty;
  var __getOwnPropNames = Object.getOwnPropertyNames;
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  var __hasOwnProp = Object.prototype.hasOwnProperty;
  var __moduleCache = /* @__PURE__ */ new WeakMap;
  var __toCommonJS = (from) => {
    var entry = __moduleCache.get(from), desc;
    if (entry)
      return entry;
    entry = __defProp({}, "__esModule", { value: true });
    if (from && typeof from === "object" || typeof from === "function")
      __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
        get: () => from[key],
        enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
      }));
    __moduleCache.set(from, entry);
    return entry;
  };

  // index.ts
  var exports_bun_global_names = {};  // author note: folder/project was named bun-global-names
  $(() => {
    $("#title").text("Text set by jQuery!");
    console.log("It works!");
  });
})();

This seems to work fine for me?