-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(dev): make browser Node polyfills opt-in #7269
Changes from 3 commits
d8edc34
2c7174b
fd6060c
6aac9e0
a837a9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
--- | ||
"@remix-run/dev": minor | ||
--- | ||
|
||
The `serverNodeBuiltinsPolyfill` option (along with the newly added `browserNodeBuiltinsPolyfill`) now supports defining global polyfills in addition to module polyfills. | ||
|
||
For example, to polyfill Node's `Buffer` global: | ||
|
||
```js | ||
module.exports = { | ||
serverNodeBuiltinsPolyfill: { | ||
globals: { | ||
Buffer: true, | ||
}, | ||
// You'll probably need to polyfill the "buffer" module | ||
// too since the global polyfill imports this: | ||
modules: { | ||
buffer: true, | ||
}, | ||
}, | ||
}; | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
--- | ||
"@remix-run/dev": major | ||
--- | ||
|
||
Remove default Node.js polyfills. | ||
|
||
Any Node.js polyfills (or empty polyfills) that are required for your browser code must be configured via the `browserNodeBuiltinsPolyfill` option in `remix.config.js`. | ||
|
||
```js | ||
exports.browserNodeBuiltinsPolyfill = { | ||
modules: { | ||
buffer: true, | ||
fs: "empty", | ||
}, | ||
globals: { | ||
Buffer: true, | ||
}, | ||
}; | ||
``` | ||
|
||
If you're targeting a non-Node.js server platform, any Node.js polyfills (or empty polyfills) that are required for your server code must be configured via the `serverNodeBuiltinsPolyfill` option in `remix.config.js`. | ||
|
||
```js | ||
exports.serverNodeBuiltinsPolyfill = { | ||
modules: { | ||
buffer: true, | ||
fs: "empty", | ||
}, | ||
globals: { | ||
Buffer: true, | ||
}, | ||
}; | ||
``` |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
import * as path from "node:path"; | ||
import { builtinModules as nodeBuiltins } from "node:module"; | ||
import * as esbuild from "esbuild"; | ||
import { nodeModulesPolyfillPlugin } from "esbuild-plugins-node-modules-polyfill"; | ||
|
||
import type { RemixConfig } from "../../config"; | ||
import { type Manifest } from "../../manifest"; | ||
|
@@ -13,6 +12,7 @@ import { absoluteCssUrlsPlugin } from "../plugins/absoluteCssUrlsPlugin"; | |
import { emptyModulesPlugin } from "../plugins/emptyModules"; | ||
import { mdxPlugin } from "../plugins/mdx"; | ||
import { externalPlugin } from "../plugins/external"; | ||
import { browserNodeBuiltinsPolyfillPlugin } from "./plugins/browserNodeBuiltinsPolyfill"; | ||
import { cssBundlePlugin } from "../plugins/cssBundlePlugin"; | ||
import { cssModulesPlugin } from "../plugins/cssModuleImports"; | ||
import { cssSideEffectImportsPlugin } from "../plugins/cssSideEffectImports"; | ||
|
@@ -27,6 +27,7 @@ type Compiler = { | |
// produce ./public/build/ | ||
compile: () => Promise<{ | ||
metafile: esbuild.Metafile; | ||
outputFiles: esbuild.OutputFile[]; | ||
hmr?: Manifest["hmr"]; | ||
}>; | ||
cancel: () => Promise<void>; | ||
|
@@ -94,15 +95,7 @@ const createEsbuildConfig = ( | |
emptyModulesPlugin(ctx, /^@remix-run\/(deno|cloudflare|node)(\/.*)?$/, { | ||
includeNodeModules: true, | ||
}), | ||
nodeModulesPolyfillPlugin({ | ||
// For the browser build, we replace any Node built-ins that don't have a | ||
// polyfill with an empty module. This ensures the build can pass without | ||
// having to mark all Node built-ins as external which can result in other | ||
// issues, e.g. https://github.com/remix-run/remix/issues/5521. We then | ||
// rely on tree-shaking to remove all unused polyfills and fallbacks. | ||
fallback: "empty", | ||
}), | ||
externalPlugin(/^node:.*/, { sideEffects: false }), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since esbuild-plugins-node-modules-polyfill is configured to manage fallbacks, this usage of |
||
browserNodeBuiltinsPolyfillPlugin(ctx), | ||
]; | ||
|
||
if (ctx.options.mode === "development") { | ||
|
@@ -156,11 +149,12 @@ export const create = async ( | |
): Promise<Compiler> => { | ||
let compiler = await esbuild.context({ | ||
...createEsbuildConfig(ctx, refs), | ||
write: false, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to manually write the result of the browser build to disk so that esbuild-plugins-node-modules-polyfill can check the build output in memory for any missing/unconfigured polyfills after tree-shaking has occured. This is important in the context of Remix since route files often depend on Node builtins regardless of whether they're used in browser code. If If you're interested, here's the PR where I implemented this: imranbarbhuiya/esbuild-plugins-node-modules-polyfill#152 |
||
metafile: true, | ||
}); | ||
|
||
let compile = async () => { | ||
let { metafile } = await compiler.rebuild(); | ||
let { metafile, outputFiles } = await compiler.rebuild(); | ||
writeMetafile(ctx, "metafile.js.json", metafile); | ||
|
||
let hmr: Manifest["hmr"] | undefined = undefined; | ||
|
@@ -181,7 +175,7 @@ export const create = async ( | |
}; | ||
} | ||
|
||
return { metafile, hmr }; | ||
return { metafile, hmr, outputFiles }; | ||
}; | ||
|
||
return { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export { create as createCompiler } from "./compiler"; | ||
export { write } from "./write"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { nodeModulesPolyfillPlugin } from "esbuild-plugins-node-modules-polyfill"; | ||
|
||
import type { Context } from "../../context"; | ||
|
||
export const browserNodeBuiltinsPolyfillPlugin = (ctx: Context) => | ||
nodeModulesPolyfillPlugin({ | ||
// Rename plugin to improve error message attribution | ||
name: "browser-node-builtins-polyfill-plugin", | ||
// Only pass through the "modules" and "globals" options to ensure we | ||
// don't leak the full plugin API to Remix consumers. | ||
modules: ctx.config.browserNodeBuiltinsPolyfill?.modules ?? {}, | ||
globals: ctx.config.browserNodeBuiltinsPolyfill?.globals ?? {}, | ||
// Mark any unpolyfilled Node builtins in the build output as errors. | ||
fallback: "error", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added support for built-time errors for unpolyfilled/unconfigured modules here: imranbarbhuiya/esbuild-plugins-node-modules-polyfill#152 |
||
formatError({ moduleName, importer, polyfillExists }) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So that we could customise errors for Remix consumers who don't have direct access to the esbuild plugin, I added support for custom error formatting here: imranbarbhuiya/esbuild-plugins-node-modules-polyfill#153. |
||
let normalizedModuleName = moduleName.replace("node:", ""); | ||
let modulesConfigKey = /^[a-z_]+$/.test(normalizedModuleName) | ||
? normalizedModuleName | ||
: JSON.stringify(normalizedModuleName); | ||
|
||
return { | ||
text: (polyfillExists | ||
? [ | ||
`Node builtin "${moduleName}" (imported by "${importer}") must be polyfilled for the browser. `, | ||
`You can enable this polyfill in your Remix config, `, | ||
`e.g. \`browserNodeBuiltinsPolyfill: { modules: { ${modulesConfigKey}: true } }\``, | ||
] | ||
: [ | ||
`Node builtin "${moduleName}" (imported by "${importer}") doesn't have a browser polyfill available. `, | ||
`You can stub it out with an empty object in your Remix config `, | ||
`e.g. \`browserNodeBuiltinsPolyfill: { modules: { ${modulesConfigKey}: "empty" } }\` `, | ||
"but note that this may cause runtime errors if the module is used in your browser code.", | ||
] | ||
).join(""), | ||
}; | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import * as path from "node:path"; | ||
import type { OutputFile } from "esbuild"; | ||
import fse from "fs-extra"; | ||
|
||
import type { RemixConfig } from "../../config"; | ||
|
||
export async function write(config: RemixConfig, outputFiles: OutputFile[]) { | ||
await fse.ensureDir(path.dirname(config.assetsBuildDirectory)); | ||
|
||
for (let file of outputFiles) { | ||
await fse.ensureDir(path.dirname(file.path)); | ||
await fse.writeFile(file.path, file.contents); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { nodeModulesPolyfillPlugin } from "esbuild-plugins-node-modules-polyfill"; | ||
|
||
import type { Context } from "../../context"; | ||
|
||
export const serverNodeBuiltinsPolyfillPlugin = (ctx: Context) => | ||
nodeModulesPolyfillPlugin({ | ||
// Rename plugin to improve error message attribution | ||
name: "server-node-builtins-polyfill-plugin", | ||
// Only pass through the "modules" and "globals" options to ensure we | ||
// don't leak the full plugin API to Remix consumers. | ||
modules: ctx.config.serverNodeBuiltinsPolyfill?.modules ?? {}, | ||
globals: ctx.config.serverNodeBuiltinsPolyfill?.globals ?? {}, | ||
// Since the server environment may provide its own Node polyfills, | ||
// we don't define any fallback behavior here and allow all Node | ||
// builtins to be marked as external | ||
fallback: "none", | ||
MichaelDeBoey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As explained in my other comment, we now need to manage writing the output of the JS build to disk.