mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	feat(ct): only rebuild when necessary (#14026)
This commit is contained in:
		
							parent
							
								
									5aa82dc5e4
								
							
						
					
					
						commit
						46e82e8fea
					
				@ -16,3 +16,4 @@ test-results/
 | 
			
		||||
tests/components/
 | 
			
		||||
examples/
 | 
			
		||||
DEPS
 | 
			
		||||
.cache/
 | 
			
		||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -28,4 +28,4 @@ test-results
 | 
			
		||||
.env
 | 
			
		||||
/tests/installation/output/
 | 
			
		||||
/tests/installation/.registry.json
 | 
			
		||||
/playwright/out/
 | 
			
		||||
.cache/
 | 
			
		||||
 | 
			
		||||
@ -23,8 +23,10 @@ import { parse, traverse, types as t } from '../babelBundle';
 | 
			
		||||
import type { ComponentInfo } from '../tsxTransform';
 | 
			
		||||
import { collectComponentUsages, componentInfo } from '../tsxTransform';
 | 
			
		||||
import type { FullConfig } from '../types';
 | 
			
		||||
import { assert } from 'playwright-core/lib/utils';
 | 
			
		||||
 | 
			
		||||
let previewServer: PreviewServer;
 | 
			
		||||
const VERSION = 1;
 | 
			
		||||
 | 
			
		||||
export function createPlugin(
 | 
			
		||||
  registerSourceFile: string,
 | 
			
		||||
@ -37,42 +39,68 @@ export function createPlugin(
 | 
			
		||||
      const use = config.projects[0].use as any;
 | 
			
		||||
      const viteConfig: InlineConfig = use.viteConfig || {};
 | 
			
		||||
      const port = use.vitePort || 3100;
 | 
			
		||||
 | 
			
		||||
      configDir = configDirectory;
 | 
			
		||||
 | 
			
		||||
      process.env.PLAYWRIGHT_VITE_COMPONENTS_BASE_URL = `http://localhost:${port}/playwright/index.html`;
 | 
			
		||||
 | 
			
		||||
      viteConfig.root = viteConfig.root || configDir;
 | 
			
		||||
      const rootDir = viteConfig.root || configDir;
 | 
			
		||||
      const outDir = viteConfig?.build?.outDir || path.join(rootDir, 'playwright', '.cache');
 | 
			
		||||
      const templateDir = path.join(rootDir, 'playwright');
 | 
			
		||||
 | 
			
		||||
      const buildInfoFile = path.join(outDir, 'metainfo.json');
 | 
			
		||||
      let buildInfo: BuildInfo;
 | 
			
		||||
      try {
 | 
			
		||||
        buildInfo = JSON.parse(await fs.promises.readFile(buildInfoFile, 'utf-8')) as BuildInfo;
 | 
			
		||||
        assert(buildInfo.version === VERSION);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        buildInfo = {
 | 
			
		||||
          version: VERSION,
 | 
			
		||||
          components: [],
 | 
			
		||||
          tests: {},
 | 
			
		||||
          sources: {},
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const componentRegistry: ComponentRegistry = new Map();
 | 
			
		||||
      // 1. Re-parse changed tests and collect required components.
 | 
			
		||||
      const hasNewTests = await checkNewTests(suite, buildInfo, componentRegistry);
 | 
			
		||||
      // 2. Check if the set of required components has changed.
 | 
			
		||||
      const hasNewComponents = await checkNewComponents(buildInfo, componentRegistry);
 | 
			
		||||
      // 3. Check component sources.
 | 
			
		||||
      const sourcesDirty = hasNewComponents || await checkSources(buildInfo);
 | 
			
		||||
 | 
			
		||||
      viteConfig.root = rootDir;
 | 
			
		||||
      viteConfig.preview = { port };
 | 
			
		||||
      viteConfig.build = {
 | 
			
		||||
        outDir
 | 
			
		||||
      };
 | 
			
		||||
      const { build, preview } = require('vite');
 | 
			
		||||
      if (sourcesDirty) {
 | 
			
		||||
        viteConfig.plugins = viteConfig.plugins || [
 | 
			
		||||
          frameworkPluginFactory()
 | 
			
		||||
        ];
 | 
			
		||||
      const files = new Set<string>();
 | 
			
		||||
      for (const project of suite.suites) {
 | 
			
		||||
        for (const file of project.suites)
 | 
			
		||||
          files.add(file.location!.file);
 | 
			
		||||
      }
 | 
			
		||||
        const registerSource = await fs.promises.readFile(registerSourceFile, 'utf-8');
 | 
			
		||||
      viteConfig.plugins.push(vitePlugin(registerSource, [...files]));
 | 
			
		||||
        viteConfig.plugins.push(vitePlugin(registerSource, buildInfo, componentRegistry));
 | 
			
		||||
        viteConfig.configFile = viteConfig.configFile || false;
 | 
			
		||||
        viteConfig.define = viteConfig.define || {};
 | 
			
		||||
        viteConfig.define.__VUE_PROD_DEVTOOLS__ = true;
 | 
			
		||||
        viteConfig.css = viteConfig.css || {};
 | 
			
		||||
        viteConfig.css.devSourcemap = true;
 | 
			
		||||
      viteConfig.preview = { port };
 | 
			
		||||
        viteConfig.build = {
 | 
			
		||||
          ...viteConfig.build,
 | 
			
		||||
          target: 'esnext',
 | 
			
		||||
          minify: false,
 | 
			
		||||
          rollupOptions: {
 | 
			
		||||
            treeshake: false,
 | 
			
		||||
            input: {
 | 
			
		||||
            index: path.join(viteConfig.root, 'playwright', 'index.html')
 | 
			
		||||
              index: path.join(templateDir, 'index.html')
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          sourcemap: true,
 | 
			
		||||
        outDir: viteConfig?.build?.outDir || path.join(viteConfig.root, 'playwright', 'out')
 | 
			
		||||
        };
 | 
			
		||||
      const { build, preview } = require('vite');
 | 
			
		||||
        await build(viteConfig);
 | 
			
		||||
      }
 | 
			
		||||
      if (hasNewTests || hasNewComponents || sourcesDirty)
 | 
			
		||||
        await fs.promises.writeFile(buildInfoFile, JSON.stringify(buildInfo, undefined, 2));
 | 
			
		||||
      previewServer = await preview(viteConfig);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@ -87,18 +115,87 @@ export function createPlugin(
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const imports: Map<string, ComponentInfo> = new Map();
 | 
			
		||||
type BuildInfo = {
 | 
			
		||||
  version: number,
 | 
			
		||||
  sources: {
 | 
			
		||||
    [key: string]: {
 | 
			
		||||
      timestamp: number;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  components: ComponentInfo[];
 | 
			
		||||
  tests: {
 | 
			
		||||
    [key: string]: {
 | 
			
		||||
      timestamp: number;
 | 
			
		||||
      components: string[];
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function vitePlugin(registerSource: string, files: string[]): Plugin {
 | 
			
		||||
  return {
 | 
			
		||||
    name: 'playwright:component-index',
 | 
			
		||||
type ComponentRegistry = Map<string, ComponentInfo>;
 | 
			
		||||
 | 
			
		||||
    configResolved: async config => {
 | 
			
		||||
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;
 | 
			
		||||
      if (sourceInfo.timestamp !== timestamp)
 | 
			
		||||
        return true;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
      for (const file of files) {
 | 
			
		||||
        const text = await fs.promises.readFile(file, 'utf-8');
 | 
			
		||||
async function checkNewTests(suite: Suite, buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Promise<boolean> {
 | 
			
		||||
  const testFiles = new Set<string>();
 | 
			
		||||
  for (const project of suite.suites) {
 | 
			
		||||
    for (const file of project.suites)
 | 
			
		||||
      testFiles.add(file.location!.file);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let hasNewTests = false;
 | 
			
		||||
  for (const testFile of testFiles) {
 | 
			
		||||
    const timestamp = (await fs.promises.stat(testFile)).mtimeMs;
 | 
			
		||||
    if (buildInfo.tests[testFile]?.timestamp !== timestamp) {
 | 
			
		||||
      const components = await parseTestFile(testFile);
 | 
			
		||||
      for (const component of components)
 | 
			
		||||
        componentRegistry.set(component.fullName, component);
 | 
			
		||||
      buildInfo.tests[testFile] = { timestamp, components: components.map(c => c.fullName) };
 | 
			
		||||
      hasNewTests = true;
 | 
			
		||||
    } else {
 | 
			
		||||
      // The test has not changed, populate component registry from the buildInfo.
 | 
			
		||||
      for (const componentName of buildInfo.tests[testFile].components) {
 | 
			
		||||
        const component = buildInfo.components.find(c => c.fullName === componentName)!;
 | 
			
		||||
        componentRegistry.set(component.fullName, component);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return hasNewTests;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function checkNewComponents(buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Promise<boolean> {
 | 
			
		||||
  const newComponents = [...componentRegistry.keys()];
 | 
			
		||||
  const oldComponents = new Set(buildInfo.components.map(c => c.fullName));
 | 
			
		||||
 | 
			
		||||
  let hasNewComponents = false;
 | 
			
		||||
  for (const c of newComponents) {
 | 
			
		||||
    if (!oldComponents.has(c)) {
 | 
			
		||||
      hasNewComponents = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (!hasNewComponents)
 | 
			
		||||
    return false;
 | 
			
		||||
  buildInfo.components = newComponents.map(n => componentRegistry.get(n)!);
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function parseTestFile(testFile: string): Promise<ComponentInfo[]> {
 | 
			
		||||
  const text = await fs.promises.readFile(testFile, 'utf-8');
 | 
			
		||||
  const ast = parse(text, { errorRecovery: true, plugins: ['typescript', 'jsx'], sourceType: 'module' });
 | 
			
		||||
        const components = collectComponentUsages(ast);
 | 
			
		||||
  const componentUsages = collectComponentUsages(ast);
 | 
			
		||||
  const result: ComponentInfo[] = [];
 | 
			
		||||
 | 
			
		||||
  traverse(ast, {
 | 
			
		||||
    enter: p => {
 | 
			
		||||
@ -108,20 +205,36 @@ function vitePlugin(registerSource: string, files: string[]): Plugin {
 | 
			
		||||
          return;
 | 
			
		||||
 | 
			
		||||
        for (const specifier of importNode.specifiers) {
 | 
			
		||||
                if (!components.names.has(specifier.local.name))
 | 
			
		||||
          if (!componentUsages.names.has(specifier.local.name))
 | 
			
		||||
            continue;
 | 
			
		||||
          if (t.isImportNamespaceSpecifier(specifier))
 | 
			
		||||
            continue;
 | 
			
		||||
                const info = componentInfo(specifier, importNode.source.value, file);
 | 
			
		||||
                imports.set(info.fullName, info);
 | 
			
		||||
          result.push(componentInfo(specifier, importNode.source.value, testFile));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
function vitePlugin(registerSource: string, buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Plugin {
 | 
			
		||||
  buildInfo.sources = {};
 | 
			
		||||
  return {
 | 
			
		||||
    name: 'playwright:component-index',
 | 
			
		||||
 | 
			
		||||
    transform: async (content, id) => {
 | 
			
		||||
      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.
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!id.endsWith('playwright/index.ts') && !id.endsWith('playwright/index.tsx') && !id.endsWith('playwright/index.js'))
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
@ -129,7 +242,7 @@ function vitePlugin(registerSource: string, files: string[]): Plugin {
 | 
			
		||||
      const lines = [content, ''];
 | 
			
		||||
      lines.push(registerSource);
 | 
			
		||||
 | 
			
		||||
      for (const [alias, value] of imports) {
 | 
			
		||||
      for (const [alias, value] of componentRegistry) {
 | 
			
		||||
        const importPath = value.isModuleOrAlias ? value.importPath : './' + path.relative(folder, value.importPath).replace(/\\/g, '/');
 | 
			
		||||
        if (value.importedName)
 | 
			
		||||
          lines.push(`import { ${value.importedName} as ${alias} } from '${importPath}';`);
 | 
			
		||||
@ -137,7 +250,7 @@ function vitePlugin(registerSource: string, files: string[]): Plugin {
 | 
			
		||||
          lines.push(`import ${alias} from '${importPath}';`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      lines.push(`register({ ${[...imports.keys()].join(',\n  ')} });`);
 | 
			
		||||
      lines.push(`register({ ${[...componentRegistry.keys()].join(',\n  ')} });`);
 | 
			
		||||
      return {
 | 
			
		||||
        code: lines.join('\n'),
 | 
			
		||||
        map: { mappings: '' }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user