/** * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import crypto from 'crypto'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { sourceMapSupport } from '../utilsBundle'; import { sanitizeForFilePath } from '../util'; export type MemoryCache = { codePath: string; sourceMapPath: string; moduleUrl?: string; }; const version = 13; const DEFAULT_CACHE_DIR_WIN32 = path.join(os.tmpdir(), `playwright-transform-cache`); const DEFAULT_CACHE_DIR_POSIX = path.join(os.tmpdir(), `playwright-transform-cache-` + sanitizeForFilePath(os.userInfo().username)); const cacheDir = process.env.PWTEST_CACHE_DIR || (process.platform === 'win32' ? DEFAULT_CACHE_DIR_WIN32 : DEFAULT_CACHE_DIR_POSIX); const sourceMaps: Map = new Map(); const memoryCache = new Map(); // Dependencies resolved by the loader. const fileDependencies = new Map>(); // Dependencies resolved by the external bundler. const externalDependencies = new Map>(); Error.stackTraceLimit = 200; sourceMapSupport.install({ environment: 'node', handleUncaughtExceptions: false, retrieveSourceMap(source) { if (!sourceMaps.has(source)) return null; const sourceMapPath = sourceMaps.get(source)!; if (!fs.existsSync(sourceMapPath)) return null; return { map: JSON.parse(fs.readFileSync(sourceMapPath, 'utf-8')), url: source }; } }); function _innerAddToCompilationCache(filename: string, options: { codePath: string, sourceMapPath: string, moduleUrl?: string }) { sourceMaps.set(options.moduleUrl || filename, options.sourceMapPath); memoryCache.set(filename, options); } export function getFromCompilationCache(filename: string, code: string, moduleUrl?: string): { cachedCode?: string, addToCache?: (code: string, map?: any) => void } { // First check the memory cache by filename, this cache will always work in the worker, // because we just compiled this file in the loader. const cache = memoryCache.get(filename); if (cache?.codePath) return { cachedCode: fs.readFileSync(cache.codePath, 'utf-8') }; // Then do the disk cache, this cache works between the Playwright Test runs. const isModule = !!moduleUrl; const cachePath = calculateCachePath(code, filename, isModule); const codePath = cachePath + '.js'; const sourceMapPath = cachePath + '.map'; if (fs.existsSync(codePath)) { _innerAddToCompilationCache(filename, { codePath, sourceMapPath, moduleUrl }); return { cachedCode: fs.readFileSync(codePath, 'utf8') }; } return { addToCache: (code: string, map: any) => { fs.mkdirSync(path.dirname(cachePath), { recursive: true }); if (map) fs.writeFileSync(sourceMapPath, JSON.stringify(map), 'utf8'); fs.writeFileSync(codePath, code, 'utf8'); _innerAddToCompilationCache(filename, { codePath, sourceMapPath, moduleUrl }); } }; } export function serializeCompilationCache(): any { return { sourceMaps: [...sourceMaps.entries()], memoryCache: [...memoryCache.entries()], fileDependencies: [...fileDependencies.entries()].map(([filename, deps]) => ([filename, [...deps]])), externalDependencies: [...externalDependencies.entries()].map(([filename, deps]) => ([filename, [...deps]])), }; } export function clearCompilationCache() { sourceMaps.clear(); memoryCache.clear(); } export function addToCompilationCache(payload: any) { for (const entry of payload.sourceMaps) sourceMaps.set(entry[0], entry[1]); for (const entry of payload.memoryCache) memoryCache.set(entry[0], entry[1]); for (const entry of payload.fileDependencies) fileDependencies.set(entry[0], new Set(entry[1])); for (const entry of payload.externalDependencies) externalDependencies.set(entry[0], new Set(entry[1])); } function calculateCachePath(content: string, filePath: string, isModule: boolean): string { const hash = crypto.createHash('sha1') .update(process.env.PW_TEST_SOURCE_TRANSFORM || '') .update(isModule ? 'esm' : 'no_esm') .update(content) .update(filePath) .update(String(version)) .digest('hex'); const fileName = path.basename(filePath, path.extname(filePath)).replace(/\W/g, '') + '_' + hash; return path.join(cacheDir, hash[0] + hash[1], fileName); } // Since ESM and CJS collect dependencies differently, // we go via the global state to collect them. let depsCollector: Set | undefined; export function startCollectingFileDeps() { depsCollector = new Set(); } export function stopCollectingFileDeps(filename: string) { if (!depsCollector) return; depsCollector.delete(filename); for (const dep of depsCollector) { if (belongsToNodeModules(dep)) depsCollector.delete(dep); } fileDependencies.set(filename, depsCollector); depsCollector = undefined; } export function currentFileDepsCollector(): Set | undefined { return depsCollector; } export function setExternalDependencies(filename: string, deps: string[]) { const depsSet = new Set(deps.filter(dep => !belongsToNodeModules(dep) && dep !== filename)); externalDependencies.set(filename, depsSet); } export function fileDependenciesForTest() { return fileDependencies; } export function collectAffectedTestFiles(dependency: string, testFileCollector: Set) { testFileCollector.add(dependency); for (const [testFile, deps] of fileDependencies) { if (deps.has(dependency)) testFileCollector.add(testFile); } for (const [testFile, deps] of externalDependencies) { if (deps.has(dependency)) testFileCollector.add(testFile); } } export function dependenciesForTestFile(filename: string): Set { return fileDependencies.get(filename) || new Set(); } // These two are only used in the dev mode, they are specifically excluding // files from packages/playwright*. In production mode, node_modules covers // that. const kPlaywrightInternalPrefix = path.resolve(__dirname, '../../../playwright'); const kPlaywrightCoveragePrefix = path.resolve(__dirname, '../../../../tests/config/coverage.js'); export function belongsToNodeModules(file: string) { if (file.includes(`${path.sep}node_modules${path.sep}`)) return true; if (file.startsWith(kPlaywrightInternalPrefix)) return true; if (file.startsWith(kPlaywrightCoveragePrefix)) return true; return false; }