From e048958778bf8c43a0a23c0f555c1538acc32f09 Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Fri, 28 Jun 2024 18:33:59 +0100 Subject: [PATCH] feature: alias modules in the worker (#6167) Sometimes, users want to replace modules with other modules. This commonly happens inside a third party dependency itself. As an example, a user might have imported `node-fetch`, which will probably never work in workerd. You can use the alias config to replace any of these imports with a module of your choice. Let's say you make a `fetch-nolyfill.js` ```ts export default fetch; // all this does is export the standard fetch function` ``` You can then configure `wrangler.toml` like so: ```toml [alias] "node-fetch": "./fetch-nolyfill" ``` So any calls to `import fetch from 'node-fetch';` will simply use our nolyfilled version. You can also pass aliases in the cli (for both `dev` and `deploy`). Like: ```bash npx wrangler dev --alias node-fetch:./fetch-nolyfill ``` --- .changeset/friendly-olives-pay.md | 29 +++++++++ .../src/__tests__/configuration.test.ts | 61 +++++++++++++++++++ .../__tests__/navigator-user-agent.test.ts | 2 + packages/wrangler/src/api/dev.ts | 1 + .../api/startDevWorker/BundlerController.ts | 2 + .../wrangler/src/api/startDevWorker/types.ts | 2 + packages/wrangler/src/config/config.ts | 7 +++ packages/wrangler/src/config/validation.ts | 38 ++++++++++++ packages/wrangler/src/deploy/deploy.ts | 2 + packages/wrangler/src/deploy/index.ts | 8 +++ .../wrangler/src/deployment-bundle/bundle.ts | 3 + packages/wrangler/src/dev.tsx | 12 ++++ packages/wrangler/src/dev/dev.tsx | 2 + packages/wrangler/src/dev/start-server.ts | 4 ++ packages/wrangler/src/dev/use-esbuild.ts | 7 +++ .../src/pages/functions/buildPlugin.ts | 1 + .../src/pages/functions/buildWorker.ts | 2 + packages/wrangler/src/versions/index.ts | 8 +++ packages/wrangler/src/versions/upload.ts | 2 + 19 files changed, 193 insertions(+) create mode 100644 .changeset/friendly-olives-pay.md diff --git a/.changeset/friendly-olives-pay.md b/.changeset/friendly-olives-pay.md new file mode 100644 index 000000000000..588ad421ea5a --- /dev/null +++ b/.changeset/friendly-olives-pay.md @@ -0,0 +1,29 @@ +--- +"wrangler": patch +--- + +feature: alias modules in the worker + +Sometimes, users want to replace modules with other modules. This commonly happens inside a third party dependency itself. As an example, a user might have imported `node-fetch`, which will probably never work in workerd. You can use the alias config to replace any of these imports with a module of your choice. + +Let's say you make a `fetch-nolyfill.js` + +```ts +export default fetch; // all this does is export the standard fetch function` +``` + +You can then configure `wrangler.toml` like so: + +```toml +# ... +[alias] +"node-fetch": "./fetch-nolyfill" +``` + +So any calls to `import fetch from 'node-fetch';` will simply use our nolyfilled version. + +You can also pass aliases in the cli (for both `dev` and `deploy`). Like: + +```bash +npx wrangler dev --alias node-fetch:./fetch-nolyfill +``` diff --git a/packages/wrangler/src/__tests__/configuration.test.ts b/packages/wrangler/src/__tests__/configuration.test.ts index ba7d4d795942..d5750d868343 100644 --- a/packages/wrangler/src/__tests__/configuration.test.ts +++ b/packages/wrangler/src/__tests__/configuration.test.ts @@ -384,6 +384,67 @@ describe("normalizeAndValidateConfig()", () => { }); }); + describe("[alias]", () => { + it("errors with a non-object", () => { + const { config: _config, diagnostics } = normalizeAndValidateConfig( + { + alias: "some silly string", + } as unknown as RawConfig, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(true); + + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - Expected alias to be an object, but got string" + `); + }); + + it("errors with non string values", () => { + const { config: _config, diagnostics } = normalizeAndValidateConfig( + { + alias: { + "some-module": 123, + }, + } as unknown as RawConfig, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(true); + + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - Expected alias[\\"some-module\\"] to be a string, but got number" + `); + }); + + it("returns the alias config when valid", () => { + const { config, diagnostics } = normalizeAndValidateConfig( + { + alias: { + "some-module": "./path/to/some-module", + }, + } as unknown as RawConfig, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(false); + + expect(config.alias).toMatchInlineSnapshot(` + Object { + "some-module": "./path/to/some-module", + } + `); + }); + }); + describe("[assets]", () => { it("normalizes a string input to an object", () => { const { config, diagnostics } = normalizeAndValidateConfig( diff --git a/packages/wrangler/src/__tests__/navigator-user-agent.test.ts b/packages/wrangler/src/__tests__/navigator-user-agent.test.ts index c305fcc6c3d1..5e9925c890ed 100644 --- a/packages/wrangler/src/__tests__/navigator-user-agent.test.ts +++ b/packages/wrangler/src/__tests__/navigator-user-agent.test.ts @@ -116,6 +116,7 @@ describe("defineNavigatorUserAgent is respected", () => { serveAssetsFromWorker: false, doBindings: [], define: {}, + alias: {}, checkFetch: false, targetConsumer: "deploy", local: true, @@ -174,6 +175,7 @@ describe("defineNavigatorUserAgent is respected", () => { serveAssetsFromWorker: false, doBindings: [], define: {}, + alias: {}, checkFetch: false, targetConsumer: "deploy", local: true, diff --git a/packages/wrangler/src/api/dev.ts b/packages/wrangler/src/api/dev.ts index 2151634c6446..503923667f8d 100644 --- a/packages/wrangler/src/api/dev.ts +++ b/packages/wrangler/src/api/dev.ts @@ -205,6 +205,7 @@ export async function unstable_dev( upstreamProtocol: undefined, var: undefined, define: undefined, + alias: undefined, jsxFactory: undefined, jsxFragment: undefined, tsconfig: undefined, diff --git a/packages/wrangler/src/api/startDevWorker/BundlerController.ts b/packages/wrangler/src/api/startDevWorker/BundlerController.ts index 23d9ddb58eda..250a471e8b83 100644 --- a/packages/wrangler/src/api/startDevWorker/BundlerController.ts +++ b/packages/wrangler/src/api/startDevWorker/BundlerController.ts @@ -121,6 +121,7 @@ export class BundlerController extends Controller { nodejsCompatMode: config.build.nodejsCompatMode, define: config.build.define, checkFetch: true, + alias: config.build.alias, assets: config.legacy?.assets, // enable the cache when publishing bypassAssetCache: false, @@ -232,6 +233,7 @@ export class BundlerController extends Controller { minify: config.build?.minify, nodejsCompatMode: config.build.nodejsCompatMode, define: config.build.define, + alias: config.build.alias, noBundle: !config.build?.bundle, findAdditionalModules: config.build?.findAdditionalModules, durableObjects: bindings?.durable_objects ?? { bindings: [] }, diff --git a/packages/wrangler/src/api/startDevWorker/types.ts b/packages/wrangler/src/api/startDevWorker/types.ts index d79bb87d7637..b228baa7bee7 100644 --- a/packages/wrangler/src/api/startDevWorker/types.ts +++ b/packages/wrangler/src/api/startDevWorker/types.ts @@ -99,6 +99,8 @@ export interface StartDevWorkerOptions { moduleRules?: Rule[]; /** Replace global identifiers with constant expressions, e.g. { debug: 'true', version: '"1.0.0"' }. Only takes effect if bundle: true. */ define?: Record; + /** Alias modules */ + alias?: Record; /** Whether the bundled worker is minified. Only takes effect if bundle: true. */ minify?: boolean; /** Options controlling a custom build step. */ diff --git a/packages/wrangler/src/config/config.ts b/packages/wrangler/src/config/config.ts index 069c3eb54e02..8d1304897f8d 100644 --- a/packages/wrangler/src/config/config.ts +++ b/packages/wrangler/src/config/config.ts @@ -162,6 +162,12 @@ export interface ConfigFields { } | undefined; + /** + * A map of module aliases. Lets you swap out a module for any others. + * Corresponds with esbuild's `alias` config + */ + alias: { [key: string]: string } | undefined; + /** * By default, wrangler.toml is the source of truth for your environment configuration, like a terraform file. * @@ -336,6 +342,7 @@ export const defaultWranglerConfig: Config = { text_blobs: undefined, data_blobs: undefined, keep_vars: undefined, + alias: undefined, /** INHERITABLE ENVIRONMENT FIELDS **/ account_id: undefined, diff --git a/packages/wrangler/src/config/validation.ts b/packages/wrangler/src/config/validation.ts index 1cb987c377fa..46137b98f17e 100644 --- a/packages/wrangler/src/config/validation.ts +++ b/packages/wrangler/src/config/validation.ts @@ -257,6 +257,7 @@ export function normalizeAndValidateConfig( activeEnv.main ), assets: normalizeAndValidateAssets(diagnostics, configPath, rawConfig), + alias: normalizeAndValidateAliases(diagnostics, configPath, rawConfig), wasm_modules: normalizeAndValidateModulePaths( diagnostics, configPath, @@ -608,6 +609,43 @@ function normalizeAndValidateSite( return undefined; } +/** + * Validate the `alias` configuration + */ +function normalizeAndValidateAliases( + diagnostics: Diagnostics, + configPath: string | undefined, + rawConfig: RawConfig +): Config["alias"] { + if (rawConfig?.alias === undefined) { + return undefined; + } + if ( + ["string", "boolean", "number"].includes(typeof rawConfig?.alias) || + typeof rawConfig?.alias !== "object" + ) { + diagnostics.errors.push( + `Expected alias to be an object, but got ${typeof rawConfig?.alias}` + ); + return undefined; + } + + let isValid = true; + for (const [key, value] of Object.entries(rawConfig?.alias)) { + if (typeof value !== "string") { + diagnostics.errors.push( + `Expected alias["${key}"] to be a string, but got ${typeof value}` + ); + isValid = false; + } + } + if (isValid) { + return rawConfig.alias; + } + + return; +} + /** * Validate the `assets` configuration and return normalized values. */ diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index 22afa5fcf78c..a03020301fe0 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -78,6 +78,7 @@ type Props = { assetPaths: AssetPaths | undefined; vars: Record | undefined; defines: Record | undefined; + alias: Record | undefined; triggers: string[] | undefined; routes: string[] | undefined; legacyEnv: boolean | undefined; @@ -526,6 +527,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m nodejsCompatMode, define: { ...config.define, ...props.defines }, checkFetch: false, + alias: config.alias, assets: config.assets, // enable the cache when publishing bypassAssetCache: false, diff --git a/packages/wrangler/src/deploy/index.ts b/packages/wrangler/src/deploy/index.ts index 29b3b846584f..4bf5b6bbe244 100644 --- a/packages/wrangler/src/deploy/index.ts +++ b/packages/wrangler/src/deploy/index.ts @@ -134,6 +134,12 @@ export function deployOptions(yargs: CommonYargsArgv) { requiresArg: true, array: true, }) + .option("alias", { + describe: "A module pair to be substituted in the script", + type: "string", + requiresArg: true, + array: true, + }) .option("triggers", { describe: "cron schedules to attach", alias: ["schedule", "schedules"], @@ -263,6 +269,7 @@ export async function deployHandler( const cliVars = collectKeyValues(args.var); const cliDefines = collectKeyValues(args.define); + const cliAlias = collectKeyValues(args.alias); const accountId = args.dryRun ? undefined : await requireAuth(config); @@ -293,6 +300,7 @@ export async function deployHandler( compatibilityFlags: args.compatibilityFlags, vars: cliVars, defines: cliDefines, + alias: cliAlias, triggers: args.triggers, jsxFactory: args.jsxFactory, jsxFragment: args.jsxFragment, diff --git a/packages/wrangler/src/deployment-bundle/bundle.ts b/packages/wrangler/src/deployment-bundle/bundle.ts index 82c0c56e4aa9..2d91189e44e2 100644 --- a/packages/wrangler/src/deployment-bundle/bundle.ts +++ b/packages/wrangler/src/deployment-bundle/bundle.ts @@ -74,6 +74,7 @@ export type BundleOptions = { minify?: boolean; nodejsCompatMode?: NodeJSCompatMode; define: Config["define"]; + alias: Config["alias"]; checkFetch: boolean; targetConsumer: "dev" | "deploy"; testScheduled?: boolean; @@ -108,6 +109,7 @@ export async function bundleWorker( tsconfig, minify, nodejsCompatMode, + alias, define, checkFetch, assets, @@ -312,6 +314,7 @@ export async function bundleWorker( ...define, }, }), + alias, loader: { ...COMMON_ESBUILD_OPTIONS.loader, ...(loader || {}), diff --git a/packages/wrangler/src/dev.tsx b/packages/wrangler/src/dev.tsx index c4c6b4ce3f40..a89f05baf4e0 100644 --- a/packages/wrangler/src/dev.tsx +++ b/packages/wrangler/src/dev.tsx @@ -201,6 +201,12 @@ export function devOptions(yargs: CommonYargsArgv) { requiresArg: true, array: true, }) + .option("alias", { + describe: "A module pair to be substituted in the script", + type: "string", + requiresArg: true, + array: true, + }) .option("jsx-factory", { describe: "The function that is called for each JSX element", type: "string", @@ -501,6 +507,7 @@ export async function startDev(args: StartDevOptions) { getInspectorPort, getRuntimeInspectorPort, cliDefines, + cliAlias, localPersistencePath, processEntrypoint, additionalModules, @@ -592,6 +599,7 @@ export async function startDev(args: StartDevOptions) { nodejsCompatMode={nodejsCompatMode} build={configParam.build || {}} define={{ ...configParam.define, ...cliDefines }} + alias={{ ...configParam.alias, ...cliAlias }} initialMode={args.remote ? "remote" : "local"} jsxFactory={args.jsxFactory || configParam.jsx_factory} jsxFragment={args.jsxFragment || configParam.jsx_fragment} @@ -684,6 +692,7 @@ export async function startApiDev(args: StartDevOptions) { getInspectorPort, getRuntimeInspectorPort, cliDefines, + cliAlias, localPersistencePath, processEntrypoint, additionalModules, @@ -754,6 +763,7 @@ export async function startApiDev(args: StartDevOptions) { nodejsCompatMode: nodejsCompatMode, build: configParam.build || {}, define: { ...config.define, ...cliDefines }, + alias: { ...config.alias, ...cliAlias }, initialMode: args.remote ? "remote" : "local", jsxFactory: args.jsxFactory ?? configParam.jsx_factory, jsxFragment: args.jsxFragment ?? configParam.jsx_fragment, @@ -989,6 +999,7 @@ export async function validateDevServerSettings( ); const cliDefines = collectKeyValues(args.define); + const cliAlias = collectKeyValues(args.alias); return { entry, @@ -999,6 +1010,7 @@ export async function validateDevServerSettings( host, routes, cliDefines, + cliAlias, localPersistencePath, processEntrypoint: !!args.processEntrypoint, additionalModules: args.additionalModules ?? [], diff --git a/packages/wrangler/src/dev/dev.tsx b/packages/wrangler/src/dev/dev.tsx index 994bf3bfa987..4a7b647afb63 100644 --- a/packages/wrangler/src/dev/dev.tsx +++ b/packages/wrangler/src/dev/dev.tsx @@ -237,6 +237,7 @@ export type DevProps = { liveReload: boolean; bindings: CfWorkerInit["bindings"]; define: Config["define"]; + alias: Config["alias"]; crons: Config["triggers"]["crons"]; queueConsumers: Config["queues"]["consumers"]; isWorkersSite: boolean; @@ -767,6 +768,7 @@ function DevSession(props: DevSessionProps) { minify: props.minify, nodejsCompatMode: props.nodejsCompatMode, define: props.define, + alias: props.alias, noBundle: props.noBundle, findAdditionalModules: props.findAdditionalModules, assets: props.assetsConfig, diff --git a/packages/wrangler/src/dev/start-server.ts b/packages/wrangler/src/dev/start-server.ts index 1f0bca564a34..1b0e4061bb69 100644 --- a/packages/wrangler/src/dev/start-server.ts +++ b/packages/wrangler/src/dev/start-server.ts @@ -167,6 +167,7 @@ export async function startDevServer( define: props.define, noBundle: props.noBundle, findAdditionalModules: props.findAdditionalModules, + alias: props.alias, assets: props.assetsConfig, testScheduled: props.testScheduled, local: props.local, @@ -343,6 +344,7 @@ async function runEsbuild({ processEntrypoint, additionalModules, rules, + alias, assets, serveAssetsFromWorker, tsconfig, @@ -364,6 +366,7 @@ async function runEsbuild({ processEntrypoint: boolean; additionalModules: CfModule[]; rules: Config["rules"]; + alias: Config["alias"]; assets: Config["assets"]; define: Config["define"]; serveAssetsFromWorker: boolean; @@ -412,6 +415,7 @@ async function runEsbuild({ nodejsCompatMode, define, checkFetch: true, + alias, assets, // disable the cache in dev bypassAssetCache: true, diff --git a/packages/wrangler/src/dev/use-esbuild.ts b/packages/wrangler/src/dev/use-esbuild.ts index 7624e7dfebeb..8e63f818f4a7 100644 --- a/packages/wrangler/src/dev/use-esbuild.ts +++ b/packages/wrangler/src/dev/use-esbuild.ts @@ -44,6 +44,7 @@ export type EsbuildBundleProps = { rules: Config["rules"]; assets: Config["assets"]; define: Config["define"]; + alias: Config["alias"]; serveAssetsFromWorker: boolean; tsconfig: string | undefined; minify: boolean | undefined; @@ -75,6 +76,7 @@ export function runBuild( minify, nodejsCompatMode, define, + alias, noBundle, findAdditionalModules, durableObjects, @@ -94,6 +96,7 @@ export function runBuild( rules: Config["rules"]; assets: Config["assets"]; define: Config["define"]; + alias: Config["alias"]; serveAssetsFromWorker: boolean; tsconfig: string | undefined; minify: boolean | undefined; @@ -210,6 +213,7 @@ export function runBuild( minify, nodejsCompatMode, doBindings: durableObjects.bindings, + alias, define, checkFetch: true, assets, @@ -293,6 +297,7 @@ export function useEsbuild({ tsconfig, minify, nodejsCompatMode, + alias, define, noBundle, findAdditionalModules, @@ -322,6 +327,7 @@ export function useEsbuild({ tsconfig, minify, nodejsCompatMode, + alias, define, noBundle, findAdditionalModules, @@ -351,6 +357,7 @@ export function useEsbuild({ findAdditionalModules, minify, nodejsCompatMode, + alias, define, assets, durableObjects, diff --git a/packages/wrangler/src/pages/functions/buildPlugin.ts b/packages/wrangler/src/pages/functions/buildPlugin.ts index 2e61aa9b14e4..58fe7f939425 100644 --- a/packages/wrangler/src/pages/functions/buildPlugin.ts +++ b/packages/wrangler/src/pages/functions/buildPlugin.ts @@ -50,6 +50,7 @@ export function buildPluginFromFunctions({ // and they document that on their README.md, we should let them. nodejsCompatMode: nodejsCompatMode ?? "v1", define: {}, + alias: {}, doBindings: [], // Pages functions don't support internal Durable Objects external, plugins: [ diff --git a/packages/wrangler/src/pages/functions/buildWorker.ts b/packages/wrangler/src/pages/functions/buildWorker.ts index b0e068342581..34aa335964e0 100644 --- a/packages/wrangler/src/pages/functions/buildWorker.ts +++ b/packages/wrangler/src/pages/functions/buildWorker.ts @@ -75,6 +75,7 @@ export function buildWorkerFromFunctions({ define: { __FALLBACK_SERVICE__: JSON.stringify(fallbackService), }, + alias: {}, doBindings: [], // Pages functions don't support internal Durable Objects external, plugins: [ @@ -229,6 +230,7 @@ export function buildRawWorker({ watch, nodejsCompatMode, define: {}, + alias: {}, doBindings: [], // Pages functions don't support internal Durable Objects external, plugins: [ diff --git a/packages/wrangler/src/versions/index.ts b/packages/wrangler/src/versions/index.ts index 0a9aca2c4fe4..dd463f564289 100644 --- a/packages/wrangler/src/versions/index.ts +++ b/packages/wrangler/src/versions/index.ts @@ -118,6 +118,12 @@ export function versionsUploadOptions(yargs: CommonYargsArgv) { requiresArg: true, array: true, }) + .option("alias", { + describe: "A module pair to be substituted in the script", + type: "string", + requiresArg: true, + array: true, + }) .option("jsx-factory", { describe: "The function that is called for each JSX element", type: "string", @@ -193,6 +199,7 @@ export async function versionsUploadHandler( const cliVars = collectKeyValues(args.var); const cliDefines = collectKeyValues(args.define); + const cliAlias = collectKeyValues(args.alias); const accountId = args.dryRun ? undefined : await requireAuth(config); @@ -211,6 +218,7 @@ export async function versionsUploadHandler( compatibilityFlags: args.compatibilityFlags, vars: cliVars, defines: cliDefines, + alias: cliAlias, jsxFactory: args.jsxFactory, jsxFragment: args.jsxFragment, tsconfig: args.tsconfig, diff --git a/packages/wrangler/src/versions/upload.ts b/packages/wrangler/src/versions/upload.ts index 406311d534f4..4d06151b3585 100644 --- a/packages/wrangler/src/versions/upload.ts +++ b/packages/wrangler/src/versions/upload.ts @@ -52,6 +52,7 @@ type Props = { compatibilityFlags: string[] | undefined; vars: Record | undefined; defines: Record | undefined; + alias: Record | undefined; jsxFactory: string | undefined; jsxFragment: string | undefined; tsconfig: string | undefined; @@ -282,6 +283,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m sourcemap: uploadSourceMaps, nodejsCompatMode, define: { ...config.define, ...props.defines }, + alias: { ...config.alias, ...props.alias }, checkFetch: false, assets: config.assets, // enable the cache when publishing