edgeone动态拦截生成

This commit is contained in:
mtvpls
2026-07-11 10:34:19 +08:00
parent f55a1f50de
commit 984f763c66
+225 -23
View File
@@ -80,22 +80,168 @@ const runtimeEnvKeys = [
'NEXT_PUBLIC_BASE_PATH',
];
const edgeOneMiddlewareSkipPaths = [
'/login',
'/register',
'/oidc-register',
'/qr-login',
'/warning',
'/tv/login',
'/api/login',
'/api/register',
'/api/logout',
'/api/auth/oidc',
'/api/auth/qr',
'/api/auth/refresh',
'/api/server-config',
'/api/tvbox/subscribe',
];
/**
* Read Next.js middleware matcher sources (originalSource strings).
* Prefer build manifest, then compiled edge function, then source middleware.ts.
*/
function getMiddlewareMatcherSources() {
const sources = [];
const manifestPath = join(process.cwd(), '.next', 'server', 'middleware-manifest.json');
try {
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
const middlewareEntries = manifest?.middleware || {};
for (const entry of Object.values(middlewareEntries)) {
for (const matcher of entry?.matchers || []) {
if (typeof matcher?.originalSource === 'string' && matcher.originalSource) {
sources.push(matcher.originalSource);
} else if (typeof matcher?.regexp === 'string' && matcher.regexp) {
sources.push(matcher.regexp);
}
}
}
if (sources.length > 0) {
return [...new Set(sources)];
}
} catch {
// fall through
}
const edgeFunctionPath = join(process.cwd(), '.edgeone', 'edge-functions', 'index.js');
try {
const code = readFileSync(edgeFunctionPath, 'utf8');
// compiled form: matcher:["/((?!...).*)"] or matcher:["..."]
const matcherArrayMatch = code.match(/matcher:\s*(\[(?:[^\[\]"']|"[^"]*"|'[^']*')*\])/);
if (matcherArrayMatch) {
try {
const parsed = JSON.parse(matcherArrayMatch[1].replace(/'/g, '"'));
if (Array.isArray(parsed)) {
for (const item of parsed) {
if (typeof item === 'string' && item) {
sources.push(item);
}
}
}
} catch {
// ignore parse errors
}
}
if (sources.length > 0) {
return [...new Set(sources)];
}
} catch {
// fall through
}
const middlewareSourcePath = join(process.cwd(), 'src', 'middleware.ts');
try {
const source = readFileSync(middlewareSourcePath, 'utf8');
// export const config = { matcher: [ '...' ] } or matcher: '...'
const blockMatch = source.match(
/export\s+const\s+config\s*=\s*\{[\s\S]*?matcher\s*:\s*(\[[\s\S]*?\]|['"`][^'"`]+['"`])/
);
if (blockMatch) {
const raw = blockMatch[1].trim();
if (raw.startsWith('[')) {
const stringLiterals = [...raw.matchAll(/['"`]([^'"`]+)['"`]/g)].map((m) => m[1]);
sources.push(...stringLiterals);
} else {
const single = raw.slice(1, -1);
if (single) {
sources.push(single);
}
}
}
} catch {
// ignore
}
return [...new Set(sources.filter(Boolean))];
}
/**
* Convert Next.js middleware matcher originalSource exclusions into path prefixes.
* Supports the common form: /((?!a|b|c/).*)
*/
function extractSkipPathsFromMatcherSource(matcherSource) {
if (typeof matcherSource !== 'string' || !matcherSource) {
return [];
}
const skipPaths = new Set();
// Negative-lookahead exclusion list inside /((?!... ).*)
const negativeLookahead = matcherSource.match(/\(\?\!([^)]+)\)/);
if (negativeLookahead) {
for (const part of negativeLookahead[1].split('|')) {
const token = part.trim();
if (!token) {
continue;
}
// Keep trailing slash semantics (api/cron/, tvbox/) for prefix matching.
const normalized = token.startsWith('/') ? token : `/${token}`;
skipPaths.add(normalized);
}
}
return [...skipPaths];
}
function resolveEdgeOneMiddlewareSkipPaths() {
const matcherSources = getMiddlewareMatcherSources();
const skipPaths = new Set();
for (const source of matcherSources) {
for (const path of extractSkipPathsFromMatcherSource(source)) {
skipPaths.add(path);
}
}
const resolved = [...skipPaths].sort((a, b) => a.localeCompare(b));
if (resolved.length === 0) {
console.warn(
'[edgeone-build] Unable to derive middleware skip paths from matcher; edge function will rely on compiled config.matcher only'
);
} else {
console.log(
`[edgeone-build] Derived ${resolved.length} middleware skip path(s) from matcher: ${resolved.join(', ')}`
);
}
return resolved;
}
function isNextStyleComplexMatcher(source) {
return (
typeof source === 'string' &&
(source.includes('(?') ||
source.includes('[^') ||
source.includes('.*') ||
source.includes('|') ||
source.endsWith('$'))
);
}
/**
* EdgeOne platform-level middleware.matcher uses path-to-regexp style sources
* (e.g. /:path*, /api/:path*). Next.js negative-lookahead matchers are only safe
* inside the compiled edge function (matchesPath). For complex Next matchers we
* keep a catch-all outer intercept and rely on runtime matcher + derived skip paths.
*/
function resolveEdgeOneMiddlewareMatchers() {
const matcherSources = getMiddlewareMatcherSources();
if (matcherSources.length === 0) {
return [{ source: '/:path*' }];
}
if (matcherSources.some((source) => isNextStyleComplexMatcher(source))) {
console.log(
'[edgeone-build] Next.js complex middleware matcher detected; edge-functions config keeps /:path* and enforces exclusions at runtime'
);
return [{ source: '/:path*' }];
}
return matcherSources.map((source) => ({ source }));
}
function getRuntimeEnvLiteral() {
const env = {};
@@ -170,6 +316,16 @@ function replaceEnvLiterals(code, envLiteral) {
return output;
}
function buildMatcherFallbackSnippet(skipPaths) {
const matcherFallbackMarker = '/* edgeone-middleware-matcher-fallback */';
// Mirrors Next negative-lookahead tokens as pathname prefixes (e.g. /login, /api/cron/).
return `${matcherFallbackMarker}
const edgeOneMiddlewareSkipPaths = ${JSON.stringify(skipPaths)};
if (edgeOneMiddlewareSkipPaths.some((path) => pathname === path || pathname.startsWith(path))) {
return null;
}`;
}
function patchEdgeFunctionEnvInjection() {
const edgeFunctionPath = join(process.cwd(), '.edgeone', 'edge-functions', 'index.js');
let code;
@@ -181,6 +337,7 @@ function patchEdgeFunctionEnvInjection() {
const marker = '/* edgeone-process-env-injected */';
const envLiteral = getRuntimeEnvLiteral();
const edgeOneMiddlewareSkipPaths = resolveEdgeOneMiddlewareSkipPaths();
code = replaceEnvLiterals(code, envLiteral);
@@ -197,7 +354,9 @@ function patchEdgeFunctionEnvInjection() {
const middlewareSignature = 'async function executeMiddleware({request}) {';
const middlewareMarker = '/* edgeone-middleware-env-injected */';
if (!code.includes(middlewareMarker) && !code.includes(middlewareSignature)) {
console.warn('[edgeone-build] Unable to patch middleware env injection: target not found');
if (!code.includes('async function executeMiddleware({request, env})')) {
console.warn('[edgeone-build] Unable to patch middleware env injection: target not found');
}
} else if (!code.includes(middlewareMarker)) {
code = code.replace(
middlewareSignature,
@@ -209,19 +368,61 @@ function patchEdgeFunctionEnvInjection() {
const matcherFallbackTarget = ` if (!matchesPath(pathname, config.matcher)) {
return null;
}`;
if (!code.includes(matcherFallbackMarker) && !code.includes(matcherFallbackTarget)) {
console.warn('[edgeone-build] Unable to patch middleware matcher fallback: target not found');
const fallbackSnippet = buildMatcherFallbackSnippet(edgeOneMiddlewareSkipPaths);
// Replace an existing dynamic/static fallback block so rebuilds stay in sync with middleware.
const existingFallbackRe =
/\/\* edgeone-middleware-matcher-fallback \*\/[\s\S]*?if \(edgeOneMiddlewareSkipPaths\.some\([\s\S]*?\) \{\s*return null;\s*\}/;
if (existingFallbackRe.test(code)) {
code = code.replace(existingFallbackRe, fallbackSnippet);
} else if (code.includes(matcherFallbackTarget)) {
code = code.replace(matcherFallbackTarget, `${matcherFallbackTarget}\n\n ${fallbackSnippet}`);
} else if (!code.includes(matcherFallbackMarker)) {
code = code.replace(
matcherFallbackTarget,
`${matcherFallbackTarget}\n\n ${matcherFallbackMarker}\n const edgeOneMiddlewareSkipPaths = ${JSON.stringify(edgeOneMiddlewareSkipPaths)};\n if (edgeOneMiddlewareSkipPaths.some((path) => pathname.startsWith(path))) {\n return null;\n }`
);
console.warn('[edgeone-build] Unable to patch middleware matcher fallback: target not found');
}
writeFileSync(edgeFunctionPath, code);
console.log('[edgeone-build] Patched edge function process.env injection');
}
/**
* Write EdgeOne edge-function route interception matcher from Next.js middleware config.
* EdgeOne outer matcher decides when the edge function is invoked; keep it aligned with
* middleware originalSource so exclusions are not a hardcoded catch-all forever.
*/
function patchEdgeFunctionMiddlewareConfig() {
const configPath = join(process.cwd(), '.edgeone', 'edge-functions', 'config.json');
let raw;
try {
raw = readFileSync(configPath, 'utf8');
} catch {
console.warn('[edgeone-build] edge-functions config.json not found, skip middleware matcher patch');
return;
}
let config;
try {
config = JSON.parse(raw);
} catch (error) {
console.warn('[edgeone-build] Failed to parse edge-functions config.json:', error);
return;
}
const matchers = resolveEdgeOneMiddlewareMatchers();
config.routes = Array.isArray(config.routes) ? config.routes : [];
config.middleware = {
...(config.middleware && typeof config.middleware === 'object' ? config.middleware : {}),
runtime: config.middleware?.runtime || 'edge',
matcher: matchers,
};
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
console.log(
`[edgeone-build] Patched edge-functions middleware matcher from Next.js middleware (${matchers.length} rule(s))`
);
}
/**
* Fix /api returning HTTP 404 while body is correct and function logs show 200.
*
@@ -461,6 +662,7 @@ child.on('exit', (code, signal) => {
}
patchEdgeFunctionEnvInjection();
patchEdgeFunctionMiddlewareConfig();
patchSsrNodeRoutes();
patchSsrNodeHandlerStatus();