2022-04-21 16:30:17 -08:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
2025-02-19 15:32:12 +01:00
|
|
|
import fs from 'fs';
|
|
|
|
import path from 'path';
|
2025-02-07 14:44:00 -08:00
|
|
|
|
2024-07-23 10:19:58 +02:00
|
|
|
import { setExternalDependencies } from 'playwright/lib/transform/compilationCache';
|
2025-02-07 14:44:00 -08:00
|
|
|
import { resolveHook } from 'playwright/lib/transform/transform';
|
|
|
|
import { removeDirAndLogToConsole } from 'playwright/lib/util';
|
2023-09-08 14:23:35 -07:00
|
|
|
import { stoppable } from 'playwright/lib/utilsBundle';
|
2025-02-11 17:19:27 -08:00
|
|
|
import { isURLAvailable } from 'playwright-core/lib/utils';
|
|
|
|
import { assert, calculateSha1, getPlaywrightVersion } from 'playwright-core/lib/utils';
|
2025-02-07 14:44:00 -08:00
|
|
|
import { debug } from 'playwright-core/lib/utilsBundle';
|
|
|
|
|
|
|
|
import { runDevServer } from './devServer';
|
2024-01-14 08:41:40 -08:00
|
|
|
import { source as injectedSource } from './generated/indexSource';
|
2025-02-07 14:44:00 -08:00
|
|
|
import { createConfig, frameworkConfig, hasJSComponents, populateComponentsFromTests, resolveDirs, resolveEndpoint, transformIndexFile } from './viteUtils';
|
|
|
|
|
2024-01-12 20:02:27 -08:00
|
|
|
import type { ImportInfo } from './tsxTransform';
|
2024-01-23 12:55:28 -08:00
|
|
|
import type { ComponentRegistry } from './viteUtils';
|
2025-02-07 14:44:00 -08:00
|
|
|
import type { TestRunnerPlugin } from '../../playwright/src/plugins';
|
|
|
|
import type http from 'http';
|
|
|
|
import type { AddressInfo } from 'net';
|
|
|
|
import type { FullConfig, Suite } from 'playwright/types/testReporter';
|
|
|
|
import type { PluginContext } from 'rollup';
|
|
|
|
import type { Plugin, ResolveFn, ResolvedConfig } from 'vite';
|
|
|
|
|
2023-08-24 16:19:57 -07:00
|
|
|
|
|
|
|
const log = debug('pw:vite');
|
2022-04-21 16:30:17 -08:00
|
|
|
|
2022-08-08 08:54:56 -07:00
|
|
|
let stoppableServer: any;
|
2022-09-15 15:24:01 -07:00
|
|
|
const playwrightVersion = getPlaywrightVersion();
|
2022-04-21 16:30:17 -08:00
|
|
|
|
2024-02-13 09:34:03 -08:00
|
|
|
export function createPlugin(): TestRunnerPlugin {
|
2022-05-06 11:02:07 -08:00
|
|
|
let configDir: string;
|
2023-01-23 17:44:23 -08:00
|
|
|
let config: FullConfig;
|
2022-04-25 09:40:58 -08:00
|
|
|
return {
|
2022-05-06 11:02:07 -08:00
|
|
|
name: 'playwright-vite-plugin',
|
2022-04-28 11:43:39 -07:00
|
|
|
|
2023-01-23 17:44:23 -08:00
|
|
|
setup: async (configObject: FullConfig, configDirectory: string) => {
|
|
|
|
config = configObject;
|
2022-05-06 11:02:07 -08:00
|
|
|
configDir = configDirectory;
|
2023-01-23 17:44:23 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
begin: async (suite: Suite) => {
|
2024-07-23 10:19:58 +02:00
|
|
|
const result = await buildBundle(config, configDir);
|
2024-02-09 19:02:42 -08:00
|
|
|
if (!result)
|
|
|
|
return;
|
|
|
|
|
|
|
|
const { viteConfig } = result;
|
|
|
|
const { preview } = await import('vite');
|
2024-01-23 12:55:28 -08:00
|
|
|
const previewServer = await preview(viteConfig);
|
2024-01-19 11:13:03 -08:00
|
|
|
stoppableServer = stoppable(previewServer.httpServer as http.Server, 0);
|
2022-05-24 13:54:12 -07:00
|
|
|
const isAddressInfo = (x: any): x is AddressInfo => x?.address;
|
|
|
|
const address = previewServer.httpServer.address();
|
2022-12-29 02:04:23 +01:00
|
|
|
if (isAddressInfo(address)) {
|
2024-01-23 12:55:28 -08:00
|
|
|
const protocol = viteConfig.preview.https ? 'https:' : 'http:';
|
|
|
|
process.env.PLAYWRIGHT_TEST_BASE_URL = `${protocol}//${viteConfig.preview.host}:${address.port}`;
|
2022-12-29 02:04:23 +01:00
|
|
|
}
|
2022-04-25 09:40:58 -08:00
|
|
|
},
|
|
|
|
|
2023-02-06 15:52:14 -08:00
|
|
|
end: async () => {
|
2023-07-20 17:16:22 -07:00
|
|
|
if (stoppableServer)
|
|
|
|
await new Promise(f => stoppableServer.stop(f));
|
2022-04-25 09:40:58 -08:00
|
|
|
},
|
2024-07-23 18:04:17 +02:00
|
|
|
|
|
|
|
populateDependencies: async () => {
|
|
|
|
await buildBundle(config, configDir);
|
|
|
|
},
|
2024-09-05 02:22:27 -07:00
|
|
|
|
|
|
|
startDevServer: async () => {
|
|
|
|
return await runDevServer(config);
|
|
|
|
},
|
2024-09-05 13:50:16 -07:00
|
|
|
|
|
|
|
clearCache: async () => {
|
|
|
|
const configDir = config.configFile ? path.dirname(config.configFile) : config.rootDir;
|
|
|
|
const dirs = await resolveDirs(configDir, config);
|
|
|
|
if (dirs)
|
|
|
|
await removeDirAndLogToConsole(dirs.outDir);
|
|
|
|
},
|
2022-04-21 16:30:17 -08:00
|
|
|
};
|
|
|
|
}
|
2022-05-05 13:26:56 -08:00
|
|
|
|
2022-05-09 08:10:47 -08:00
|
|
|
type BuildInfo = {
|
2022-09-15 15:24:01 -07:00
|
|
|
version: string,
|
|
|
|
viteVersion: string,
|
2022-06-02 17:37:43 -07:00
|
|
|
registerSourceHash: string,
|
2022-05-09 08:10:47 -08:00
|
|
|
sources: {
|
|
|
|
[key: string]: {
|
|
|
|
timestamp: number;
|
|
|
|
}
|
|
|
|
};
|
2024-01-16 19:31:19 -08:00
|
|
|
components: ImportInfo[];
|
|
|
|
deps: {
|
|
|
|
[key: string]: string[];
|
|
|
|
}
|
2022-05-09 08:10:47 -08:00
|
|
|
};
|
|
|
|
|
2024-07-23 10:19:58 +02:00
|
|
|
export async function buildBundle(config: FullConfig, configDir: string): Promise<{ buildInfo: BuildInfo, viteConfig: Record<string, any> } | null> {
|
2024-02-13 09:34:03 -08:00
|
|
|
const { registerSourceFile, frameworkPluginFactory } = frameworkConfig(config);
|
2024-02-09 19:02:42 -08:00
|
|
|
{
|
|
|
|
// Detect a running dev server and use it if available.
|
2024-02-13 09:34:03 -08:00
|
|
|
const endpoint = resolveEndpoint(config);
|
2024-02-09 19:02:42 -08:00
|
|
|
const protocol = endpoint.https ? 'https:' : 'http:';
|
|
|
|
const url = new URL(`${protocol}//${endpoint.host}:${endpoint.port}`);
|
|
|
|
if (await isURLAvailable(url, true)) {
|
|
|
|
// eslint-disable-next-line no-console
|
2024-04-05 08:39:51 -07:00
|
|
|
console.log(`Dev Server is already running at ${url.toString()}, using it.\n`);
|
2024-02-09 19:02:42 -08:00
|
|
|
process.env.PLAYWRIGHT_TEST_BASE_URL = url.toString();
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-13 09:34:03 -08:00
|
|
|
const dirs = await resolveDirs(configDir, config);
|
2024-02-09 19:02:42 -08:00
|
|
|
if (!dirs) {
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
console.log(`Template file playwright/index.html is missing.`);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const buildInfoFile = path.join(dirs.outDir, 'metainfo.json');
|
|
|
|
|
|
|
|
let buildExists = false;
|
|
|
|
let buildInfo: BuildInfo;
|
|
|
|
|
2024-02-13 09:34:03 -08:00
|
|
|
const registerSource = injectedSource + '\n' + await fs.promises.readFile(registerSourceFile, 'utf-8');
|
2024-02-09 19:02:42 -08:00
|
|
|
const registerSourceHash = calculateSha1(registerSource);
|
|
|
|
|
|
|
|
const { version: viteVersion, build, mergeConfig } = await import('vite');
|
|
|
|
|
|
|
|
try {
|
|
|
|
buildInfo = JSON.parse(await fs.promises.readFile(buildInfoFile, 'utf-8')) as BuildInfo;
|
|
|
|
assert(buildInfo.version === playwrightVersion);
|
|
|
|
assert(buildInfo.viteVersion === viteVersion);
|
|
|
|
assert(buildInfo.registerSourceHash === registerSourceHash);
|
|
|
|
buildExists = true;
|
|
|
|
} catch (e) {
|
|
|
|
buildInfo = {
|
|
|
|
version: playwrightVersion,
|
|
|
|
viteVersion,
|
|
|
|
registerSourceHash,
|
|
|
|
components: [],
|
|
|
|
sources: {},
|
|
|
|
deps: {},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
log('build exists:', buildExists);
|
|
|
|
|
|
|
|
const componentRegistry: ComponentRegistry = new Map();
|
|
|
|
const componentsByImportingFile = new Map<string, string[]>();
|
|
|
|
// 1. Populate component registry based on tests' component imports.
|
|
|
|
await populateComponentsFromTests(componentRegistry, componentsByImportingFile);
|
|
|
|
|
|
|
|
// 2. Check if the set of required components has changed.
|
|
|
|
const hasNewComponents = await checkNewComponents(buildInfo, componentRegistry);
|
|
|
|
log('has new components:', hasNewComponents);
|
|
|
|
|
|
|
|
// 3. Check component sources.
|
|
|
|
const sourcesDirty = !buildExists || hasNewComponents || await checkSources(buildInfo);
|
|
|
|
log('sourcesDirty:', sourcesDirty);
|
|
|
|
|
|
|
|
// 4. Update component info.
|
|
|
|
buildInfo.components = [...componentRegistry.values()];
|
|
|
|
|
|
|
|
const jsxInJS = hasJSComponents(buildInfo.components);
|
2024-02-13 09:34:03 -08:00
|
|
|
const viteConfig = await createConfig(dirs, config, frameworkPluginFactory, jsxInJS);
|
2024-02-09 19:02:42 -08:00
|
|
|
|
|
|
|
if (sourcesDirty) {
|
2024-07-23 18:04:17 +02:00
|
|
|
// Only add our own plugin when we actually build / transform.
|
2024-02-09 19:02:42 -08:00
|
|
|
log('build');
|
|
|
|
const depsCollector = new Map<string, string[]>();
|
|
|
|
const buildConfig = mergeConfig(viteConfig, {
|
|
|
|
plugins: [vitePlugin(registerSource, dirs.templateDir, buildInfo, componentRegistry, depsCollector)]
|
|
|
|
});
|
|
|
|
await build(buildConfig);
|
|
|
|
buildInfo.deps = Object.fromEntries(depsCollector.entries());
|
|
|
|
}
|
|
|
|
|
|
|
|
{
|
|
|
|
// Update dependencies based on the vite build.
|
2024-07-23 10:19:58 +02:00
|
|
|
for (const [importingFile, components] of componentsByImportingFile) {
|
|
|
|
const deps = new Set<string>();
|
|
|
|
for (const component of components) {
|
|
|
|
for (const d of buildInfo.deps[component])
|
|
|
|
deps.add(d);
|
2024-02-09 19:02:42 -08:00
|
|
|
}
|
2024-07-23 10:19:58 +02:00
|
|
|
setExternalDependencies(importingFile, [...deps]);
|
2024-02-09 19:02:42 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hasNewComponents || sourcesDirty) {
|
|
|
|
log('write manifest');
|
|
|
|
await fs.promises.writeFile(buildInfoFile, JSON.stringify(buildInfo, undefined, 2));
|
|
|
|
}
|
|
|
|
return { buildInfo, viteConfig };
|
|
|
|
}
|
|
|
|
|
2022-05-09 08:10:47 -08:00
|
|
|
async function checkSources(buildInfo: BuildInfo): Promise<boolean> {
|
|
|
|
for (const [source, sourceInfo] of Object.entries(buildInfo.sources)) {
|
|
|
|
try {
|
|
|
|
const timestamp = (await fs.promises.stat(source)).mtimeMs;
|
2023-08-24 16:19:57 -07:00
|
|
|
if (sourceInfo.timestamp !== timestamp) {
|
|
|
|
log('source has changed:', source);
|
2022-05-09 08:10:47 -08:00
|
|
|
return true;
|
2023-08-24 16:19:57 -07:00
|
|
|
}
|
2022-05-09 08:10:47 -08:00
|
|
|
} catch (e) {
|
2023-08-24 16:19:57 -07:00
|
|
|
log('check source failed:', e);
|
2022-05-09 08:10:47 -08:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function checkNewComponents(buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Promise<boolean> {
|
|
|
|
const newComponents = [...componentRegistry.keys()];
|
2024-01-12 20:02:27 -08:00
|
|
|
const oldComponents = new Map(buildInfo.components.map(c => [c.id, c]));
|
2022-05-09 08:10:47 -08:00
|
|
|
|
|
|
|
let hasNewComponents = false;
|
|
|
|
for (const c of newComponents) {
|
|
|
|
if (!oldComponents.has(c)) {
|
|
|
|
hasNewComponents = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2023-02-09 18:39:20 -08:00
|
|
|
for (const c of oldComponents.values())
|
2024-01-12 20:02:27 -08:00
|
|
|
componentRegistry.set(c.id, c);
|
2023-02-09 18:39:20 -08:00
|
|
|
|
|
|
|
return hasNewComponents;
|
2022-05-09 08:10:47 -08:00
|
|
|
}
|
|
|
|
|
2024-01-16 19:31:19 -08:00
|
|
|
function vitePlugin(registerSource: string, templateDir: string, buildInfo: BuildInfo, importInfos: Map<string, ImportInfo>, depsCollector: Map<string, string[]>): Plugin {
|
2022-05-09 08:10:47 -08:00
|
|
|
buildInfo.sources = {};
|
2023-02-10 08:33:25 -08:00
|
|
|
let moduleResolver: ResolveFn;
|
2022-05-06 11:02:07 -08:00
|
|
|
return {
|
|
|
|
name: 'playwright:component-index',
|
|
|
|
|
2023-02-10 08:33:25 -08:00
|
|
|
configResolved(config: ResolvedConfig) {
|
|
|
|
moduleResolver = config.createResolver();
|
|
|
|
},
|
|
|
|
|
|
|
|
async transform(this: PluginContext, content, id) {
|
2022-05-09 08:10:47 -08:00
|
|
|
const queryIndex = id.indexOf('?');
|
|
|
|
const file = queryIndex !== -1 ? id.substring(0, queryIndex) : id;
|
|
|
|
if (!buildInfo.sources[file]) {
|
|
|
|
try {
|
|
|
|
const timestamp = (await fs.promises.stat(file)).mtimeMs;
|
|
|
|
buildInfo.sources[file] = { timestamp };
|
|
|
|
} catch {
|
|
|
|
// Silent if can't read the file.
|
|
|
|
}
|
2022-05-06 11:02:07 -08:00
|
|
|
}
|
2024-01-23 12:55:28 -08:00
|
|
|
return transformIndexFile(id, content, templateDir, registerSource, importInfos);
|
2022-05-06 11:02:07 -08:00
|
|
|
},
|
2023-02-10 08:33:25 -08:00
|
|
|
|
|
|
|
async writeBundle(this: PluginContext) {
|
2024-01-16 19:31:19 -08:00
|
|
|
for (const importInfo of importInfos.values()) {
|
2024-09-06 12:08:10 -07:00
|
|
|
const importPath = resolveHook(importInfo.filename, importInfo.importSource);
|
2024-02-07 20:39:45 -08:00
|
|
|
if (!importPath)
|
|
|
|
continue;
|
2024-01-16 19:31:19 -08:00
|
|
|
const deps = new Set<string>();
|
2024-02-07 20:39:45 -08:00
|
|
|
const id = await moduleResolver(importPath);
|
2023-02-10 08:33:25 -08:00
|
|
|
if (!id)
|
|
|
|
continue;
|
|
|
|
collectViteModuleDependencies(this, id, deps);
|
2024-02-07 20:39:45 -08:00
|
|
|
depsCollector.set(importPath, [...deps]);
|
2023-02-10 08:33:25 -08:00
|
|
|
}
|
|
|
|
},
|
2022-05-06 11:02:07 -08:00
|
|
|
};
|
|
|
|
}
|
2022-05-24 19:43:28 -07:00
|
|
|
|
2023-02-10 08:33:25 -08:00
|
|
|
function collectViteModuleDependencies(context: PluginContext, id: string, deps: Set<string>) {
|
|
|
|
if (!path.isAbsolute(id))
|
|
|
|
return;
|
2023-02-14 14:55:49 -08:00
|
|
|
const normalizedId = path.normalize(id);
|
|
|
|
if (deps.has(normalizedId))
|
2023-02-10 08:33:25 -08:00
|
|
|
return;
|
2023-02-14 14:55:49 -08:00
|
|
|
deps.add(normalizedId);
|
2023-02-10 08:33:25 -08:00
|
|
|
const module = context.getModuleInfo(id);
|
|
|
|
for (const importedId of module?.importedIds || [])
|
|
|
|
collectViteModuleDependencies(context, importedId, deps);
|
|
|
|
for (const importedId of module?.dynamicallyImportedIds || [])
|
|
|
|
collectViteModuleDependencies(context, importedId, deps);
|
|
|
|
}
|