mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-20 22:02:19 +03:00
fix(build): stop the client bundle from reaching server-only modules, and make the guard find them (#13436)
Fixes the production build break from `node:fs` reaching client bundles (`oauth.ts → cursorAgentCliVersion.ts` through the codebuddy-cn registry) and widens the client-bundle guard so it finds any Node builtin, not just the one that broke.
Maintainer rework before merge (kept the idea, no default behavior change):
- The guard was 11× slower (3.8s → ~40s) because resolved edges were not cached; with resolved edges and per-file verdicts cached it runs in ~4.6s.
- Bare builtins that Next's client build polyfills (`path`, `os`, `crypto`, `buffer`, … from Next's own `resolve.fallback` list) are allowed consistently; `node:` imports are always flagged; a drift test fails if Next stops polyfilling an allowlisted name.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- **fix(build):** the production build no longer breaks when a client component reaches a server-only module, and the client-bundle guard now discovers server-only modules instead of matching a fixed list ([#13436](https://github.com/diegosouzapw/OmniRoute/pull/13436)) — thanks @maxmad64bis
|
||||
@@ -1,7 +1,10 @@
|
||||
import { PROVIDER_ID_TO_ALIAS, PROVIDER_MODELS } from "../config/providerModels.ts";
|
||||
import { ALIAS_TO_PROVIDER_ID, resolveProviderAlias } from "./providerAlias.ts";
|
||||
import { resolveWildcardAlias } from "./wildcardRouter.ts";
|
||||
import { getRegisteredProviderEffortBaseModelId } from "../utils/registeredEffortVariants.ts";
|
||||
|
||||
export { resolveProviderAlias };
|
||||
|
||||
type ProviderModelAliasMap = Record<string, Record<string, string>>;
|
||||
type ModelAliasValue = string | { provider?: string; model?: string };
|
||||
type ModelAliasMap = Record<string, ModelAliasValue>;
|
||||
@@ -27,38 +30,6 @@ export function stripContextWindowSuffix(
|
||||
return modelStr.replace(CONTEXT_WINDOW_SUFFIX_RE, "").trimEnd();
|
||||
}
|
||||
|
||||
// Derive alias→provider mapping from the single source of truth (PROVIDER_ID_TO_ALIAS)
|
||||
// This prevents the two maps from drifting out of sync
|
||||
const ALIAS_TO_PROVIDER_ID: Record<string, string> = {};
|
||||
for (const [id, alias] of Object.entries(PROVIDER_ID_TO_ALIAS)) {
|
||||
if (ALIAS_TO_PROVIDER_ID[alias]) {
|
||||
console.log(
|
||||
`[MODEL] Warning: alias "${alias}" maps to both "${ALIAS_TO_PROVIDER_ID[alias]}" and "${id}". Using "${id}".`
|
||||
);
|
||||
}
|
||||
ALIAS_TO_PROVIDER_ID[alias] = id;
|
||||
}
|
||||
// Manual alias overrides — maps slug-style prefixes to canonical provider IDs.
|
||||
// These live outside the registry because they represent multiple providers
|
||||
// or backward-compatible slug changes, not a single provider's display name.
|
||||
// opencode/ → opencode-zen (the main free/open tier; opencode-go is a separate paid tier)
|
||||
ALIAS_TO_PROVIDER_ID["opencode"] = "opencode-zen";
|
||||
// xiaomi/ is the user-visible prefix for MiMo models; register it so
|
||||
// parseModel("xiaomi/mimo-v2-flash") resolves provider = "xiaomi-mimo" instead
|
||||
// of falling through to the identity fallback ("xiaomi").
|
||||
ALIAS_TO_PROVIDER_ID["xiaomi"] = "xiaomi-mimo";
|
||||
// llamacpp/ is the user-visible alias for the llama-cpp self-hosted provider.
|
||||
// The canonical ID is "llama-cpp" (with a hyphen), but the catalog and user-facing
|
||||
// prefix is "llamacpp". Register it so parseModel("llamacpp/<model>") resolves
|
||||
// provider = "llama-cpp" instead of the identity fallback ("llamacpp").
|
||||
ALIAS_TO_PROVIDER_ID["llamacpp"] = "llama-cpp";
|
||||
// agy/ is the short alias for antigravity provider.
|
||||
ALIAS_TO_PROVIDER_ID["agy"] = "antigravity";
|
||||
// aq/ is the user-visible prefix for the Amazon Q (AWS Builder ID) provider.
|
||||
// The canonical provider ID is "amazon-q". Register it so parseModel("aq/<model>")
|
||||
// resolves provider = "amazon-q" instead of falling through to the identity fallback.
|
||||
ALIAS_TO_PROVIDER_ID["aq"] = "amazon-q";
|
||||
|
||||
// Provider-scoped legacy model aliases. Used to normalize provider/model inputs
|
||||
// and keep backward compatibility when upstream IDs change.
|
||||
const PROVIDER_MODEL_ALIASES: ProviderModelAliasMap = {
|
||||
@@ -180,31 +151,6 @@ interface ProviderConnectionLike {
|
||||
is_active?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve provider alias to provider ID
|
||||
*/
|
||||
export function resolveProviderAlias(aliasOrId: string | null | undefined): string | null {
|
||||
if (typeof aliasOrId !== "string") return null;
|
||||
// Follow the alias chain transitively so intermediate alias-only hops resolve
|
||||
// to the final target, but STOP as soon as a hop lands on a registered
|
||||
// provider id (#2901): "oc" must resolve to the no-auth "opencode" provider,
|
||||
// NOT continue through the manual "opencode" → "opencode-zen" slug override —
|
||||
// that override is for user-typed `opencode/` prefixes only. Without this
|
||||
// boundary the no-auth provider becomes unreachable by any prefix.
|
||||
// Guarded against infinite loops with both a depth limit and a seen-set.
|
||||
let current = aliasOrId;
|
||||
const seen = new Set<string>();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const next = ALIAS_TO_PROVIDER_ID[current];
|
||||
if (!next || next === current) return current;
|
||||
if (next in PROVIDER_ID_TO_ALIAS) return next;
|
||||
if (seen.has(next)) return next;
|
||||
seen.add(next);
|
||||
current = next;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* #474 — Resolve a bare model name to the selected connection's `defaultModel`.
|
||||
*
|
||||
|
||||
58
open-sse/services/providerAlias.ts
Normal file
58
open-sse/services/providerAlias.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { PROVIDER_ID_TO_ALIAS } from "../config/providerModels.ts";
|
||||
|
||||
// Derive alias→provider mapping from the single source of truth (PROVIDER_ID_TO_ALIAS)
|
||||
// This prevents the two maps from drifting out of sync
|
||||
export const ALIAS_TO_PROVIDER_ID: Record<string, string> = {};
|
||||
for (const [id, alias] of Object.entries(PROVIDER_ID_TO_ALIAS)) {
|
||||
if (ALIAS_TO_PROVIDER_ID[alias]) {
|
||||
console.log(
|
||||
`[MODEL] Warning: alias "${alias}" maps to both "${ALIAS_TO_PROVIDER_ID[alias]}" and "${id}". Using "${id}".`
|
||||
);
|
||||
}
|
||||
ALIAS_TO_PROVIDER_ID[alias] = id;
|
||||
}
|
||||
// Manual alias overrides — maps slug-style prefixes to canonical provider IDs.
|
||||
// These live outside the registry because they represent multiple providers
|
||||
// or backward-compatible slug changes, not a single provider's display name.
|
||||
// opencode/ → opencode-zen (the main free/open tier; opencode-go is a separate paid tier)
|
||||
ALIAS_TO_PROVIDER_ID["opencode"] = "opencode-zen";
|
||||
// xiaomi/ is the user-visible prefix for MiMo models; register it so
|
||||
// parseModel("xiaomi/mimo-v2-flash") resolves provider = "xiaomi-mimo" instead
|
||||
// of falling through to the identity fallback ("xiaomi").
|
||||
ALIAS_TO_PROVIDER_ID["xiaomi"] = "xiaomi-mimo";
|
||||
// llamacpp/ is the user-visible alias for the llama-cpp self-hosted provider.
|
||||
// The canonical ID is "llama-cpp" (with a hyphen), but the catalog and user-facing
|
||||
// prefix is "llamacpp". Register it so parseModel("llamacpp/<model>") resolves
|
||||
// provider = "llama-cpp" instead of the identity fallback ("llamacpp").
|
||||
ALIAS_TO_PROVIDER_ID["llamacpp"] = "llama-cpp";
|
||||
// agy/ is the short alias for antigravity provider.
|
||||
ALIAS_TO_PROVIDER_ID["agy"] = "antigravity";
|
||||
// aq/ is the user-visible prefix for the Amazon Q (AWS Builder ID) provider.
|
||||
// The canonical provider ID is "amazon-q". Register it so parseModel("aq/<model>")
|
||||
// resolves provider = "amazon-q" instead of falling through to the identity fallback.
|
||||
ALIAS_TO_PROVIDER_ID["aq"] = "amazon-q";
|
||||
|
||||
/**
|
||||
* Resolve provider alias to provider ID
|
||||
*/
|
||||
export function resolveProviderAlias(aliasOrId: string | null | undefined): string | null {
|
||||
if (typeof aliasOrId !== "string") return null;
|
||||
// Follow the alias chain transitively so intermediate alias-only hops resolve
|
||||
// to the final target, but STOP as soon as a hop lands on a registered
|
||||
// provider id (#2901): "oc" must resolve to the no-auth "opencode" provider,
|
||||
// NOT continue through the manual "opencode" → "opencode-zen" slug override —
|
||||
// that override is for user-typed `opencode/` prefixes only. Without this
|
||||
// boundary the no-auth provider becomes unreachable by any prefix.
|
||||
// Guarded against infinite loops with both a depth limit and a seen-set.
|
||||
let current = aliasOrId;
|
||||
const seen = new Set<string>();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const next = ALIAS_TO_PROVIDER_ID[current];
|
||||
if (!next || next === current) return current;
|
||||
if (next in PROVIDER_ID_TO_ALIAS) return next;
|
||||
if (seen.has(next)) return next;
|
||||
seen.add(next);
|
||||
current = next;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
@@ -19,12 +19,9 @@ import {
|
||||
} from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { CURSOR_AGENT_CLI_VERSION } from "./cursorAgentCliVersionPin.ts";
|
||||
|
||||
/**
|
||||
* Pinned Agent CLI build id used when no local install is found (typical
|
||||
* headless OmniRoute). Bump when refreshing Cursor CLI impersonation.
|
||||
*/
|
||||
export const CURSOR_AGENT_CLI_VERSION = "2026.07.08-0c04a8a";
|
||||
export { CURSOR_AGENT_CLI_VERSION };
|
||||
|
||||
const VERSION_ID_RE = /^\d{4}\.\d{2}\.\d{2}-[0-9a-f]+$/;
|
||||
const CACHE_TTL_MS = 60 * 60 * 1000;
|
||||
|
||||
7
open-sse/utils/cursorAgentCliVersionPin.ts
Normal file
7
open-sse/utils/cursorAgentCliVersionPin.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// Import-free pin so client bundles can read the version without pulling node-only detection code.
|
||||
|
||||
/**
|
||||
* Pinned Agent CLI build id used when no local install is found (typical
|
||||
* headless OmniRoute). Bump when refreshing Cursor CLI impersonation.
|
||||
*/
|
||||
export const CURSOR_AGENT_CLI_VERSION = "2026.07.08-0c04a8a";
|
||||
@@ -1,6 +1,6 @@
|
||||
import { normalizeComboModels, type ComboStep } from "./steps";
|
||||
import { resolveComboTargetModelStr } from "../../../open-sse/services/combo/opencodeTargetAlias.ts";
|
||||
import { resolveProviderAlias } from "../../../open-sse/services/model.ts";
|
||||
import { resolveProviderAlias } from "../../../open-sse/services/providerAlias.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
GROK_BUILD_TOKEN_URL,
|
||||
} from "@omniroute/open-sse/config/grokBuild.ts";
|
||||
import { resolvePublicCred } from "@omniroute/open-sse/utils/publicCreds.ts";
|
||||
import { CURSOR_AGENT_CLI_VERSION } from "@omniroute/open-sse/utils/cursorAgentCliVersion.ts";
|
||||
import { CURSOR_AGENT_CLI_VERSION } from "@omniroute/open-sse/utils/cursorAgentCliVersionPin.ts";
|
||||
import { buildGitLabOAuthEndpoints, GITLAB_DUO_DEFAULT_BASE_URL } from "../gitlab";
|
||||
|
||||
/**
|
||||
@@ -375,7 +375,7 @@ export const KIRO_CONFIG = {
|
||||
// Cursor stores credentials in SQLite database: state.vscdb
|
||||
// Keys: cursorAuth/accessToken, cursorAuth/refreshToken, storage.serviceMachineId
|
||||
// Deep-control PKCE + refresh aligned with OpenCodex (lidge-jun/opencodex src/oauth/cursor.ts).
|
||||
// clientVersion pin lives in open-sse/utils/cursorAgentCliVersion.ts — single source of truth.
|
||||
// clientVersion pin lives in open-sse/utils/cursorAgentCliVersionPin.ts — single source of truth.
|
||||
export const CURSOR_CONFIG = {
|
||||
// API endpoints
|
||||
apiEndpoint: "https://api2.cursor.sh",
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { builtinModules } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
/**
|
||||
@@ -26,6 +27,20 @@ import { fileURLToPath } from "node:url";
|
||||
* - **Dynamic `import()` is not followed.** It does not actually break a bundle edge (that was
|
||||
* tried for #10692 and failed), but it does move the module into a chunk the browser only
|
||||
* fetches on demand, which is a legitimate boundary for a lazily-used server path.
|
||||
*
|
||||
* A reached module counts as server-only when it statically imports a Node builtin the
|
||||
* production bundler cannot resolve for the browser. The pinned list below (the original
|
||||
* #10692 chain) stays explicit so it keeps failing loudly even if the discovery logic
|
||||
* changes; everything else is found by walking the graph and checking each visited file
|
||||
* for a builtin import.
|
||||
*
|
||||
* Builtins Next ships a browser polyfill for are tolerated when imported by their BARE name
|
||||
* (`path`, `os`, `crypto`, `buffer`, …): Next's client build maps exactly those names to
|
||||
* `next/dist/compiled/*` shims (`resolve.fallback` for the client compiler in
|
||||
* `node_modules/next/dist/build/webpack-config.js`), so flagging them would cry wolf the
|
||||
* same way counting `import type` did. The `node:` scheme is never tolerated — the client
|
||||
* build has no fallback for it (`UnhandledSchemeError` on `node:fs` / `node:os` / `node:path`
|
||||
* is what broke the build this guard was widened for).
|
||||
*/
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
@@ -39,6 +54,45 @@ const SERVER_ONLY = new Set([
|
||||
"open-sse/utils/tlsClient.ts",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Bare builtin names Next's client build polyfills (the client `resolve.fallback` map in
|
||||
* `next/dist/build/webpack-config.js`). Kept in sync by the drift test at the bottom.
|
||||
*/
|
||||
const NEXT_CLIENT_POLYFILLED_BUILTINS = new Set([
|
||||
"assert",
|
||||
"buffer",
|
||||
"constants",
|
||||
"crypto",
|
||||
"domain",
|
||||
"events",
|
||||
"http",
|
||||
"https",
|
||||
"os",
|
||||
"path",
|
||||
"process",
|
||||
"punycode",
|
||||
"querystring",
|
||||
"stream",
|
||||
"string_decoder",
|
||||
"sys",
|
||||
"timers",
|
||||
"tty",
|
||||
"util",
|
||||
"vm",
|
||||
"zlib",
|
||||
]);
|
||||
|
||||
const NODE_BUILTINS = new Set(
|
||||
builtinModules.map((name) => name.replace(/^node:/, "")).filter((bare) => !bare.startsWith("_"))
|
||||
);
|
||||
|
||||
/** True when `specifier` names a Node builtin the browser bundle cannot resolve. */
|
||||
function isBrowserForbiddenBuiltin(specifier: string): boolean {
|
||||
if (specifier.startsWith("node:")) return true; // no client fallback for the scheme
|
||||
const root = specifier.split("/")[0]; // `fs/promises` → `fs`
|
||||
return NODE_BUILTINS.has(root) && !NEXT_CLIENT_POLYFILLED_BUILTINS.has(root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-`"use client"` entry points that still end up in a client bundle because client
|
||||
* components import them. Kept explicit so the original #10692 chain stays pinned even if the
|
||||
@@ -60,6 +114,9 @@ function resolveSpecifier(fromFile: string, specifier: string): string | null {
|
||||
} else if (specifier.startsWith("@omniroute/open-sse")) {
|
||||
const rest = specifier.slice("@omniroute/open-sse".length).replace(/^\//, "");
|
||||
base = path.join(REPO_ROOT, "open-sse", rest);
|
||||
} else if (specifier.startsWith("@omniroute/browser-pool")) {
|
||||
const rest = specifier.slice("@omniroute/browser-pool".length).replace(/^\//, "");
|
||||
base = path.join(REPO_ROOT, "packages/browser-pool/src", rest);
|
||||
} else if (specifier.startsWith("@/")) {
|
||||
base = path.join(REPO_ROOT, "src", specifier.slice(2));
|
||||
} else {
|
||||
@@ -118,29 +175,53 @@ function staticSpecifiers(source: string): string[] {
|
||||
}
|
||||
|
||||
const specifierCache = new Map<string, string[]>();
|
||||
function edgesOf(file: string): string[] {
|
||||
function specifiersOf(file: string): string[] {
|
||||
const cached = specifierCache.get(file);
|
||||
if (cached) return cached;
|
||||
const absolute = path.join(REPO_ROOT, file);
|
||||
let edges: string[] = [];
|
||||
let specs: string[] = [];
|
||||
if (fs.existsSync(absolute)) {
|
||||
edges = staticSpecifiers(fs.readFileSync(absolute, "utf8"))
|
||||
.map((specifier) => resolveSpecifier(file, specifier))
|
||||
.filter((resolved): resolved is string => resolved !== null);
|
||||
specs = staticSpecifiers(fs.readFileSync(absolute, "utf8"));
|
||||
}
|
||||
specifierCache.set(file, edges);
|
||||
specifierCache.set(file, specs);
|
||||
return specs;
|
||||
}
|
||||
// Resolved edges and verdicts are cached per file: the BFS runs once per client entry and
|
||||
// re-visits the same shared modules thousands of times, so re-resolving specifiers
|
||||
// (fs.existsSync/statSync per candidate) on every visit made the guard ~6x slower.
|
||||
const edgeCache = new Map<string, string[]>();
|
||||
function edgesOf(file: string): string[] {
|
||||
const cached = edgeCache.get(file);
|
||||
if (cached) return cached;
|
||||
const edges = specifiersOf(file)
|
||||
.map((specifier) => resolveSpecifier(file, specifier))
|
||||
.filter((resolved): resolved is string => resolved !== null);
|
||||
edgeCache.set(file, edges);
|
||||
return edges;
|
||||
}
|
||||
|
||||
const serverOnlyVerdictCache = new Map<string, boolean>();
|
||||
/** True when the file is pinned server-only or itself imports a browser-forbidden builtin. */
|
||||
function isServerOnly(file: string): boolean {
|
||||
const cached = serverOnlyVerdictCache.get(file);
|
||||
if (cached !== undefined) return cached;
|
||||
const verdict = SERVER_ONLY.has(file) || specifiersOf(file).some(isBrowserForbiddenBuiltin);
|
||||
serverOnlyVerdictCache.set(file, verdict);
|
||||
return verdict;
|
||||
}
|
||||
|
||||
/** BFS over static imports; returns the first path reaching a server-only module. */
|
||||
function findServerOnlyPath(entry: string): string[] | null {
|
||||
const seen = new Set<string>([entry]);
|
||||
if (isServerOnly(entry)) return [entry];
|
||||
const queue: Array<string[]> = [[entry]];
|
||||
while (queue.length > 0) {
|
||||
const trail = queue.shift()!;
|
||||
for (const resolved of edgesOf(trail[trail.length - 1])) {
|
||||
if (seen.has(resolved)) continue;
|
||||
if (SERVER_ONLY.has(resolved)) return [...trail, resolved];
|
||||
if (isServerOnly(resolved)) {
|
||||
return [...trail, resolved];
|
||||
}
|
||||
seen.add(resolved);
|
||||
queue.push([...trail, resolved]);
|
||||
}
|
||||
@@ -163,7 +244,9 @@ function walk(dir: string, acc: string[] = []): string[] {
|
||||
|
||||
function clientEntryPoints(): string[] {
|
||||
return walk(path.join(REPO_ROOT, "src")).filter((file) =>
|
||||
/^\s*["']use client["']/m.test(fs.readFileSync(path.join(REPO_ROOT, file), "utf8").slice(0, 200))
|
||||
/^\s*["']use client["']/m.test(
|
||||
fs.readFileSync(path.join(REPO_ROOT, file), "utf8").slice(0, 200)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -184,3 +267,32 @@ test("no client entry point statically reaches server-only code", () => {
|
||||
"carries no runtime edge."
|
||||
);
|
||||
});
|
||||
|
||||
test("builtin classification: node: scheme always forbidden, bare polyfilled names tolerated", () => {
|
||||
for (const specifier of ["node:fs", "node:path", "node:os", "node:crypto", "fs", "fs/promises"]) {
|
||||
assert.equal(isBrowserForbiddenBuiltin(specifier), true, specifier);
|
||||
}
|
||||
for (const specifier of ["child_process", "net", "tls", "module", "worker_threads"]) {
|
||||
assert.equal(isBrowserForbiddenBuiltin(specifier), true, specifier);
|
||||
}
|
||||
for (const specifier of ["path", "os", "crypto", "buffer", "events", "util", "stream"]) {
|
||||
assert.equal(isBrowserForbiddenBuiltin(specifier), false, specifier);
|
||||
}
|
||||
for (const specifier of ["react", "@/lib/db/core", "./local", "zod"]) {
|
||||
assert.equal(isBrowserForbiddenBuiltin(specifier), false, specifier);
|
||||
}
|
||||
});
|
||||
|
||||
test("the polyfilled-builtin allowlist matches Next's client resolve.fallback", () => {
|
||||
const webpackConfig = fs.readFileSync(
|
||||
path.join(REPO_ROOT, "node_modules/next/dist/build/webpack-config.js"),
|
||||
"utf8"
|
||||
);
|
||||
for (const name of NEXT_CLIENT_POLYFILLED_BUILTINS) {
|
||||
assert.match(
|
||||
webpackConfig,
|
||||
new RegExp(`\\b${name}: require\\.resolve\\(`),
|
||||
`Next no longer polyfills "${name}" for the client — drop it from the allowlist`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user