From 4dfb7454eb61637c2e71e58204af940e501d15bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B3=96=E9=A5=BC?= Date: Mon, 11 Aug 2025 10:26:52 +0800 Subject: [PATCH] fix: add environment check to web-router script injection - Add isVercelEnvironment() function to detect Vercel deployment - Conditionally inject script tags only in Vercel environments - Match behavior of Next.js and SvelteKit frameworks - Ensure consistency across all frameworks in the flags package --- .changeset/fix-web-router-env-check.md | 5 ++ .../src/web-router/html-transform.test.ts | 49 ++++++++++++- packages/flags/src/web-router/index.ts | 73 +++++++++++-------- 3 files changed, 94 insertions(+), 33 deletions(-) create mode 100644 .changeset/fix-web-router-env-check.md diff --git a/.changeset/fix-web-router-env-check.md b/.changeset/fix-web-router-env-check.md new file mode 100644 index 00000000..d72e588d --- /dev/null +++ b/.changeset/fix-web-router-env-check.md @@ -0,0 +1,5 @@ +--- +'@web-widget/flags-kit': patch +--- + +Fix web-router environment check to only inject flag values script tags in Vercel environments, matching Next.js/SvelteKit behavior. diff --git a/packages/flags/src/web-router/html-transform.test.ts b/packages/flags/src/web-router/html-transform.test.ts index 26fb10a4..5bdd221a 100644 --- a/packages/flags/src/web-router/html-transform.test.ts +++ b/packages/flags/src/web-router/html-transform.test.ts @@ -1,7 +1,20 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { createFlagScriptInjectionTransform } from './html-transform'; +// Mock process.env for testing +const originalEnv = process.env; + describe('createFlagScriptInjectionTransform', () => { + beforeEach(() => { + // Reset environment variables before each test + process.env = { ...originalEnv }; + }); + + afterEach(() => { + // Restore original environment variables + process.env = originalEnv; + }); + it('should inject script before tag', async () => { const html = '

Hello

'; const scriptContent = async () => '{"flag1":true}'; @@ -60,8 +73,35 @@ describe('createFlagScriptInjectionTransform', () => { ); }); - it('should not inject when no tag', async () => { - const html = '

Hello

'; + it('should handle empty script content', async () => { + const html = '

Hello

'; + const scriptContent = async () => ''; + + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(html)); + controller.close(); + }, + }); + + const transform = createFlagScriptInjectionTransform(scriptContent); + const transformedStream = stream.pipeThrough(transform); + + const reader = transformedStream.getReader(); + let result = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + result += new TextDecoder().decode(value); + } + + // Should not inject script tag when content is empty + expect(result).toBe('

Hello

'); + }); + + it('should handle HTML without body tag', async () => { + const html = 'Test'; const scriptContent = async () => '{"flag3":true}'; const stream = new ReadableStream({ @@ -83,6 +123,7 @@ describe('createFlagScriptInjectionTransform', () => { result += new TextDecoder().decode(value); } - expect(result).toBe(html); + // Should not inject script tag when there's no body tag + expect(result).toBe('Test'); }); }); diff --git a/packages/flags/src/web-router/index.ts b/packages/flags/src/web-router/index.ts index d25cfdce..85f5a020 100644 --- a/packages/flags/src/web-router/index.ts +++ b/packages/flags/src/web-router/index.ts @@ -666,37 +666,43 @@ export function createHandle({ if (result && result instanceof Response && result.body) { const contentType = result.headers.get('content-type')?.toLowerCase(); if (contentType && contentType.includes('text/html')) { - const transformStream = createFlagScriptInjectionTransform(async () => { - const store = context().state._flag; - if (!store || Object.keys(store.usedFlags).length === 0) return ''; - - // This is for reporting which flags were used when this page was generated, - // so the value shows up in Vercel Toolbar, without the client ever being - // aware of this feature flag. - const flagValues = await resolveObjectPromises(store.usedFlags); - - // Create a serialized representation of the flag values using the same - // signing mechanism as Next.js, rather than encryption. This ensures - // deterministic output across server restarts. - const flagsArray = Object.keys(flagValues).map((key) => ({ - key, - options: undefined, - })); - const serializedFlagValues = await serialize( - flagValues, - flagsArray, - secret!, // secret is guaranteed to be defined at this point + // Only inject flag values script in Vercel environments for consistency with other frameworks + if (isVercelEnvironment()) { + const transformStream = createFlagScriptInjectionTransform( + async () => { + const store = context().state._flag; + if (!store || Object.keys(store.usedFlags).length === 0) + return ''; + + // This is for reporting which flags were used when this page was generated, + // so the value shows up in Vercel Toolbar, without the client ever being + // aware of this feature flag. + const flagValues = await resolveObjectPromises(store.usedFlags); + + // Create a serialized representation of the flag values using the same + // signing mechanism as Next.js, rather than encryption. This ensures + // deterministic output across server restarts. + const flagsArray = Object.keys(flagValues).map((key) => ({ + key, + options: undefined, + })); + const serializedFlagValues = await serialize( + flagValues, + flagsArray, + secret!, // secret is guaranteed to be defined at this point + ); + + return safeJsonStringify(serializedFlagValues); + }, ); + const modifiedBody = result.body.pipeThrough(transformStream); - return safeJsonStringify(serializedFlagValues); - }); - const modifiedBody = result.body.pipeThrough(transformStream); - - return new Response(modifiedBody, { - status: result.status, - statusText: result.statusText, - headers: result.headers, - }); + return new Response(modifiedBody, { + status: result.status, + statusText: result.statusText, + headers: result.headers, + }); + } } } @@ -704,6 +710,15 @@ export function createHandle({ }; } +/** + * Checks if the current environment is Vercel (preview or production) + * This ensures consistency with other frameworks that only inject flag values in Vercel environments + */ +function isVercelEnvironment(): boolean { + // Check for Vercel-specific environment variables + return !!(process.env.VERCEL || process.env.VERCEL_ENV); +} + async function handleWellKnownFlagsRoute( headers: Headers, secret: string,