From 0358c1acb25392606b97df777da78bb0f2a777a9 Mon Sep 17 00:00:00 2001 From: ahfoysal Date: Tue, 7 Jul 2026 15:03:40 +0600 Subject: [PATCH] fix(test runner): separate ESM and CJS compilation cache by mode fixes 41662 --- .../src/transform/compilationCache.ts | 16 +++-- tests/playwright-test/loader.spec.ts | 60 +++++++++++++++++++ 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/packages/playwright/src/transform/compilationCache.ts b/packages/playwright/src/transform/compilationCache.ts index 492d0ed090b6f..1bee24ed7a4fc 100644 --- a/packages/playwright/src/transform/compilationCache.ts +++ b/packages/playwright/src/transform/compilationCache.ts @@ -71,6 +71,10 @@ const externalDependencies = new Map>(); const devSourceInfix = path.sep + 'playwright' + path.sep + 'packages' + path.sep; +function getCompilationCacheKey(filename: string, moduleUrl?: string) { + return `${moduleUrl ? 'esm' : 'cjs'}\0${filename}`; +} + export function installSourceMapSupport() { Error.stackTraceLimit = 200; @@ -106,10 +110,11 @@ function identitySourceMap(source: string) { function _innerAddToCompilationCacheAndSerialize(filename: string, entry: MemoryCache) { sourceMaps.set(entry.moduleUrl || filename, entry.sourceMapPath); - memoryCache.set(filename, entry); + const cacheKey = getCompilationCacheKey(filename, entry.moduleUrl); + memoryCache.set(cacheKey, entry); return { sourceMaps: [[entry.moduleUrl || filename, entry.sourceMapPath]], - memoryCache: [[filename, entry]], + memoryCache: [[cacheKey, entry]], fileDependencies: [], externalDependencies: [], }; @@ -122,9 +127,9 @@ type CompilationCacheLookupResult = { }; export function getFromCompilationCache(filename: string, contentHash: string, moduleUrl?: string): CompilationCacheLookupResult { - // First check the memory cache by filename, this cache will always work in the worker, + // First check the memory cache by filename+mode, this cache will always work in the worker, // because we just compiled this file in the loader. - const cache = memoryCache.get(filename); + const cache = memoryCache.get(getCompilationCacheKey(filename, moduleUrl)); if (cache?.codePath) { try { return { cachedCode: fs.readFileSync(cache.codePath, 'utf-8') }; @@ -308,13 +313,14 @@ export function belongsToNodeModules(file: string) { export async function getUserData(pluginName: string): Promise> { const result = new Map(); for (const [fileName, cache] of memoryCache) { + const normalizedFileName = fileName.split('\0', 2)[1] || fileName; if (!cache.dataPath) continue; if (!fs.existsSync(cache.dataPath)) continue; const data = JSON.parse(await fs.promises.readFile(cache.dataPath, 'utf8')); if (data[pluginName]) - result.set(fileName, data[pluginName]); + result.set(normalizedFileName, data[pluginName]); } return result; } diff --git a/tests/playwright-test/loader.spec.ts b/tests/playwright-test/loader.spec.ts index 75e7d85267409..cfa62822260c3 100644 --- a/tests/playwright-test/loader.spec.ts +++ b/tests/playwright-test/loader.spec.ts @@ -952,6 +952,66 @@ test('should resolve extensionless .ts subpath import across a workspace symlink expect(result.exitCode).toBe(0); }); +test('should keep ESM and CJS loader compilation cache separate for workspace modules', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41662' }, +}, async ({ runInlineTest }, testInfo) => { + const baseDir = testInfo.outputPath(); + const symlinkType = process.platform === 'win32' ? 'junction' : 'dir'; + const link = async (target: string, linkPath: string) => { + await fs.promises.mkdir(path.dirname(linkPath), { recursive: true }); + await fs.promises.symlink(path.join(baseDir, target), linkPath, symlinkType); + }; + + // Keep these packages out of a node_modules segment so the loader transform cache path is shared. + await link('packages/shared', path.join(baseDir, 'packages/core/node_modules/@repro/shared')); + await link('packages/core', path.join(baseDir, 'apps/e2e/node_modules/@repro/core')); + + const result = await runInlineTest({ + 'package.json': JSON.stringify({ name: 'repro-root', private: true }), + 'packages/shared/package.json': JSON.stringify({ name: '@repro/shared', private: true, type: 'module' }), + 'packages/shared/lib/text.utils.ts': ` + export function greet(name: string) { + return 'Hello, ' + name; + } + `, + 'packages/core/package.json': JSON.stringify({ + name: '@repro/core', + private: true, + type: 'module', + dependencies: { '@repro/shared': 'workspace:*' }, + }), + 'packages/core/lib/conversations.ts': ` + export { greet } from '@repro/shared/lib/text.utils'; + `, + 'apps/e2e/global-setup.js': ` + const { greet } = require('@repro/core/lib/conversations'); + console.log('setup:' + greet('setup')); + `, + 'apps/e2e/playwright.config.mts': ` + import { defineConfig } from '@playwright/test'; + import { greet } from '@repro/core/lib/conversations'; + + if (greet('config') !== 'Hello, config') + throw new Error('invalid shared module'); + + export default defineConfig({ + testDir: './tests', + globalSetup: './global-setup.js', + }); + `, + 'apps/e2e/tests/basic.spec.ts': ` + import { test, expect } from '@playwright/test'; + import { greet } from '@repro/core/lib/conversations'; + test('greet returns expected string', () => { + expect(greet('world')).toBe('Hello, world'); + }); + `, + }, { config: 'apps/e2e/playwright.config.mts' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + expect(result.output).toContain('setup:Hello, setup'); +}); + test('should support node imports', async ({ runInlineTest }) => { const result = await runInlineTest({ 'playwright.config.ts': 'export default {}',