chore: extract a dev variation of the ct plugin (#29126)
This commit is contained in:
		
							parent
							
								
									8e607d509f
								
							
						
					
					
						commit
						8898a537e0
					
				|  | @ -7,3 +7,4 @@ | |||
| !types/** | ||||
| !index.d.ts | ||||
| !index.js | ||||
| !plugin.js | ||||
|  |  | |||
|  | @ -21,7 +21,7 @@ | |||
|     }, | ||||
|     "./cli": "./cli.js", | ||||
|     "./lib/mount": "./lib/mount.js", | ||||
|     "./lib/vitePlugin": "./lib/vitePlugin.js", | ||||
|     "./plugin": "./plugin.js", | ||||
|     "./types/component": { | ||||
|       "types": "./types/component.d.ts" | ||||
|     } | ||||
|  |  | |||
|  | @ -0,0 +1,17 @@ | |||
| #!/usr/bin/env node
 | ||||
| /** | ||||
|  * 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. | ||||
|  */ | ||||
| module.exports = require('./lib/vitePlugin'); | ||||
|  | @ -1,6 +1,9 @@ | |||
| [vitePlugin.ts] | ||||
| generated/indexSource.ts | ||||
| 
 | ||||
| [viteDevPlugin.ts] | ||||
| generated/indexSource.ts | ||||
| 
 | ||||
| [mount.ts] | ||||
| generated/serializers.ts | ||||
| injected/** | ||||
|  |  | |||
|  | @ -0,0 +1,64 @@ | |||
| /** | ||||
|  * 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 fs from 'fs'; | ||||
| import type { FullConfig } from 'playwright/test'; | ||||
| import type { PluginContext } from 'rollup'; | ||||
| import type { Plugin } from 'vite'; | ||||
| import type { TestRunnerPlugin } from '../../playwright/src/plugins'; | ||||
| import { source as injectedSource } from './generated/indexSource'; | ||||
| import type { ImportInfo } from './tsxTransform'; | ||||
| import type { ComponentRegistry } from './viteUtils'; | ||||
| import { createConfig, hasJSComponents, populateComponentsFromTests, resolveDirs, transformIndexFile } from './viteUtils'; | ||||
| 
 | ||||
| export function createPlugin( | ||||
|   registerSourceFile: string, | ||||
|   frameworkPluginFactory?: () => Promise<Plugin>): TestRunnerPlugin { | ||||
|   let configDir: string; | ||||
|   let config: FullConfig; | ||||
|   return { | ||||
|     name: 'playwright-vite-plugin', | ||||
| 
 | ||||
|     setup: async (configObject: FullConfig, configDirectory: string) => { | ||||
|       config = configObject; | ||||
|       configDir = configDirectory; | ||||
|     }, | ||||
| 
 | ||||
|     begin: async () => { | ||||
|       const registerSource = injectedSource + '\n' + await fs.promises.readFile(registerSourceFile, 'utf-8'); | ||||
|       const componentRegistry: ComponentRegistry = new Map(); | ||||
|       await populateComponentsFromTests(componentRegistry); | ||||
|       const dirs = resolveDirs(configDir, config); | ||||
|       const viteConfig = await createConfig(dirs, config, frameworkPluginFactory, hasJSComponents([...componentRegistry.values()])); | ||||
|       viteConfig.plugins.push(vitePlugin(registerSource, dirs.templateDir, componentRegistry)); | ||||
|       const { createServer } = await import('vite'); | ||||
|       const devServer = await createServer(viteConfig); | ||||
|       await devServer.listen(); | ||||
|       const protocol = viteConfig.server.https ? 'https:' : 'http:'; | ||||
|       process.env.PLAYWRIGHT_TEST_BASE_URL = `${protocol}//${viteConfig.server.host || 'localhost'}:${viteConfig.server.port}`; | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function vitePlugin(registerSource: string, templateDir: string, importInfos: Map<string, ImportInfo>): Plugin { | ||||
|   return { | ||||
|     name: 'playwright:component-index', | ||||
| 
 | ||||
|     async transform(this: PluginContext, content, id) { | ||||
|       return transformIndexFile(id, content, templateDir, registerSource, importInfos); | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
|  | @ -14,38 +14,29 @@ | |||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| import type { Suite } from 'playwright/types/testReporter'; | ||||
| import type { PlaywrightTestConfig as BasePlaywrightTestConfig, FullConfig } from 'playwright/test'; | ||||
| import type http from 'http'; | ||||
| import type { InlineConfig, Plugin, ResolveFn, ResolvedConfig, UserConfig } from 'vite'; | ||||
| import type { TestRunnerPlugin } from '../../playwright/src/plugins'; | ||||
| import type { AddressInfo } from 'net'; | ||||
| import type { PluginContext } from 'rollup'; | ||||
| import { debug } from 'playwright-core/lib/utilsBundle'; | ||||
| import fs from 'fs'; | ||||
| import type http from 'http'; | ||||
| import type { AddressInfo } from 'net'; | ||||
| import path from 'path'; | ||||
| import { assert, calculateSha1, getPlaywrightVersion } from 'playwright-core/lib/utils'; | ||||
| import { debug } from 'playwright-core/lib/utilsBundle'; | ||||
| import { internalDependenciesForTestFile, setExternalDependencies } from 'playwright/lib/transform/compilationCache'; | ||||
| import { stoppable } from 'playwright/lib/utilsBundle'; | ||||
| import { assert, calculateSha1 } from 'playwright-core/lib/utils'; | ||||
| import { getPlaywrightVersion } from 'playwright-core/lib/utils'; | ||||
| import { getUserData, internalDependenciesForTestFile, setExternalDependencies } from 'playwright/lib/transform/compilationCache'; | ||||
| import type { FullConfig } from 'playwright/test'; | ||||
| import type { Suite } from 'playwright/types/testReporter'; | ||||
| import type { PluginContext } from 'rollup'; | ||||
| import type { Plugin, ResolveFn, ResolvedConfig } from 'vite'; | ||||
| import type { TestRunnerPlugin } from '../../playwright/src/plugins'; | ||||
| import { source as injectedSource } from './generated/indexSource'; | ||||
| import type { ImportInfo } from './tsxTransform'; | ||||
| import type { ComponentRegistry } from './viteUtils'; | ||||
| import { createConfig, hasJSComponents, populateComponentsFromTests, resolveDirs, transformIndexFile } from './viteUtils'; | ||||
| 
 | ||||
| const log = debug('pw:vite'); | ||||
| 
 | ||||
| let stoppableServer: any; | ||||
| const playwrightVersion = getPlaywrightVersion(); | ||||
| 
 | ||||
| type CtConfig = BasePlaywrightTestConfig['use'] & { | ||||
|   ctPort?: number; | ||||
|   ctTemplateDir?: string; | ||||
|   ctCacheDir?: string; | ||||
|   ctViteConfig?: InlineConfig | (() => Promise<InlineConfig>); | ||||
| }; | ||||
| 
 | ||||
| const importReactRE = /(^|\n|;)import\s+(\*\s+as\s+)?React(,|\s+)/; | ||||
| const compiledReactRE = /(const|var)\s+React\s*=/; | ||||
| 
 | ||||
| export function createPlugin( | ||||
|   registerSourceFile: string, | ||||
|   frameworkPluginFactory?: () => Promise<Plugin>): TestRunnerPlugin { | ||||
|  | @ -60,50 +51,8 @@ export function createPlugin( | |||
|     }, | ||||
| 
 | ||||
|     begin: async (suite: Suite) => { | ||||
|       // We are going to have 3 config files:
 | ||||
|       // - the defaults that user config overrides (baseConfig)
 | ||||
|       // - the user config (userConfig)
 | ||||
|       // - frameworks overrides (frameworkOverrides);
 | ||||
| 
 | ||||
|       const use = config.projects[0].use as CtConfig; | ||||
|       const baseURL = new URL(use.baseURL || 'http://localhost'); | ||||
|       const relativeTemplateDir = use.ctTemplateDir || 'playwright'; | ||||
| 
 | ||||
|       // FIXME: use build plugin to determine html location to resolve this.
 | ||||
|       // TemplateDir must be relative, otherwise we can't move the final index.html into its target location post-build.
 | ||||
|       // This regressed in https://github.com/microsoft/playwright/pull/26526
 | ||||
|       const templateDir = path.join(configDir, relativeTemplateDir); | ||||
| 
 | ||||
|       // Compose base config from the playwright config only.
 | ||||
|       const baseConfig: InlineConfig = { | ||||
|         root: templateDir, | ||||
|         configFile: false, | ||||
|         publicDir: path.join(configDir, 'public'), | ||||
|         define: { | ||||
|           __VUE_PROD_DEVTOOLS__: true, | ||||
|         }, | ||||
|         css: { | ||||
|           devSourcemap: true, | ||||
|         }, | ||||
|         build: { | ||||
|           outDir: use.ctCacheDir ? path.resolve(configDir, use.ctCacheDir) : path.resolve(templateDir, '.cache') | ||||
|         }, | ||||
|         preview: { | ||||
|           https: baseURL.protocol.startsWith('https:') ? {} : undefined, | ||||
|           host: baseURL.hostname, | ||||
|           port: use.ctPort || Number(baseURL.port) || 3100 | ||||
|         }, | ||||
|         // Vite preview server will otherwise always return the index.html with 200.
 | ||||
|         appType: 'mpa', | ||||
|       }; | ||||
| 
 | ||||
|       // Vite 5 refuses to support CJS.
 | ||||
|       const { version: viteVersion, build, preview, mergeConfig } = await import('vite'); | ||||
| 
 | ||||
|       // Apply user config on top of the base config. This could have changed root and build.outDir.
 | ||||
|       const userConfig = typeof use.ctViteConfig === 'function' ? await use.ctViteConfig() : (use.ctViteConfig || {}); | ||||
|       const baseAndUserConfig = mergeConfig(baseConfig, userConfig); | ||||
|       const buildInfoFile = path.join(baseAndUserConfig.build.outDir, 'metainfo.json'); | ||||
|       const dirs = resolveDirs(configDir, config); | ||||
|       const buildInfoFile = path.join(dirs.outDir, 'metainfo.json'); | ||||
| 
 | ||||
|       let buildExists = false; | ||||
|       let buildInfo: BuildInfo; | ||||
|  | @ -111,6 +60,8 @@ export function createPlugin( | |||
|       const registerSource = injectedSource + '\n' + await fs.promises.readFile(registerSourceFile, 'utf-8'); | ||||
|       const registerSourceHash = calculateSha1(registerSource); | ||||
| 
 | ||||
|       const { version: viteVersion, build, preview, mergeConfig } = await import('vite'); | ||||
| 
 | ||||
|       try { | ||||
|         buildInfo = JSON.parse(await fs.promises.readFile(buildInfoFile, 'utf-8')) as BuildInfo; | ||||
|         assert(buildInfo.version === playwrightVersion); | ||||
|  | @ -145,52 +96,38 @@ export function createPlugin( | |||
|       // 4. Update component info.
 | ||||
|       buildInfo.components = [...componentRegistry.values()]; | ||||
| 
 | ||||
|       const frameworkOverrides: UserConfig = { plugins: [] }; | ||||
| 
 | ||||
|       // React heuristic. If we see a component in a file with .js extension,
 | ||||
|       // consider it a potential JSX-in-JS scenario and enable JSX loader for all
 | ||||
|       // .js files.
 | ||||
|       if (hasJSComponents(buildInfo.components)) { | ||||
|         log('jsx-in-js detected'); | ||||
|         frameworkOverrides.esbuild = { | ||||
|           loader: 'jsx', | ||||
|           include: /.*\.jsx?$/, | ||||
|           exclude: [], | ||||
|         }; | ||||
|         frameworkOverrides.optimizeDeps = { | ||||
|           esbuildOptions: { | ||||
|             loader: { '.js': 'jsx' }, | ||||
|           } | ||||
|         }; | ||||
|       } | ||||
| 
 | ||||
|       // We assume that any non-empty plugin list includes `vite-react` or similar.
 | ||||
|       if (frameworkPluginFactory && !baseAndUserConfig.plugins?.length) | ||||
|         frameworkOverrides.plugins = [await frameworkPluginFactory()]; | ||||
| 
 | ||||
|       // But only add out own plugin when we actually build / transform.
 | ||||
|       const depsCollector = new Map<string, string[]>(); | ||||
|       if (sourcesDirty) | ||||
|         frameworkOverrides.plugins!.push(vitePlugin(registerSource, templateDir, buildInfo, componentRegistry, depsCollector)); | ||||
| 
 | ||||
|       frameworkOverrides.build = { | ||||
|         target: 'esnext', | ||||
|         minify: false, | ||||
|         rollupOptions: { | ||||
|           treeshake: false, | ||||
|           input: { | ||||
|             index: path.join(templateDir, 'index.html') | ||||
|           }, | ||||
|         }, | ||||
|         sourcemap: true, | ||||
|       }; | ||||
| 
 | ||||
|       const finalConfig = mergeConfig(baseAndUserConfig, frameworkOverrides); | ||||
|       const jsxInJS = hasJSComponents(buildInfo.components); | ||||
|       const viteConfig = await createConfig(dirs, config, frameworkPluginFactory, jsxInJS); | ||||
| 
 | ||||
|       if (sourcesDirty) { | ||||
|         // Only add out own plugin when we actually build / transform.
 | ||||
|         log('build'); | ||||
|         await build(finalConfig); | ||||
|         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.
 | ||||
|         for (const projectSuite of suite.suites) { | ||||
|           for (const fileSuite of projectSuite.suites) { | ||||
|             // For every test file...
 | ||||
|             const testFile = fileSuite.location!.file; | ||||
|             const deps = new Set<string>(); | ||||
|             // Collect its JS dependencies (helpers).
 | ||||
|             for (const file of [testFile, ...(internalDependenciesForTestFile(testFile) || [])]) { | ||||
|               // For each helper, get all the imported components.
 | ||||
|               for (const componentFile of componentsByImportingFile.get(file) || []) { | ||||
|                 // For each component, get all the dependencies.
 | ||||
|                 for (const d of depsCollector.get(componentFile) || []) | ||||
|                   deps.add(d); | ||||
|               } | ||||
|             } | ||||
|             // Now we have test file => all components along with dependencies.
 | ||||
|             setExternalDependencies(testFile, [...deps]); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (hasNewComponents || sourcesDirty) { | ||||
|  | @ -198,32 +135,13 @@ export function createPlugin( | |||
|         await fs.promises.writeFile(buildInfoFile, JSON.stringify(buildInfo, undefined, 2)); | ||||
|       } | ||||
| 
 | ||||
|       for (const projectSuite of suite.suites) { | ||||
|         for (const fileSuite of projectSuite.suites) { | ||||
|           // For every test file...
 | ||||
|           const testFile = fileSuite.location!.file; | ||||
|           const deps = new Set<string>(); | ||||
|           // Collect its JS dependencies (helpers).
 | ||||
|           for (const file of [testFile, ...(internalDependenciesForTestFile(testFile) || [])]) { | ||||
|             // For each helper, get all the imported components.
 | ||||
|             for (const componentFile of componentsByImportingFile.get(file) || []) { | ||||
|               // For each component, get all the dependencies.
 | ||||
|               for (const d of depsCollector.get(componentFile) || []) | ||||
|                 deps.add(d); | ||||
|             } | ||||
|           } | ||||
|           // Now we have test file => all components along with dependencies.
 | ||||
|           setExternalDependencies(testFile, [...deps]); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       const previewServer = await preview(finalConfig); | ||||
|       const previewServer = await preview(viteConfig); | ||||
|       stoppableServer = stoppable(previewServer.httpServer as http.Server, 0); | ||||
|       const isAddressInfo = (x: any): x is AddressInfo => x?.address; | ||||
|       const address = previewServer.httpServer.address(); | ||||
|       if (isAddressInfo(address)) { | ||||
|         const protocol = finalConfig.preview.https ? 'https:' : 'http:'; | ||||
|         process.env.PLAYWRIGHT_TEST_BASE_URL = `${protocol}//${finalConfig.preview.host}:${address.port}`; | ||||
|         const protocol = viteConfig.preview.https ? 'https:' : 'http:'; | ||||
|         process.env.PLAYWRIGHT_TEST_BASE_URL = `${protocol}//${viteConfig.preview.host}:${address.port}`; | ||||
|       } | ||||
|     }, | ||||
| 
 | ||||
|  | @ -249,8 +167,6 @@ type BuildInfo = { | |||
|   } | ||||
| }; | ||||
| 
 | ||||
| type ComponentRegistry = Map<string, ImportInfo>; | ||||
| 
 | ||||
| async function checkSources(buildInfo: BuildInfo): Promise<boolean> { | ||||
|   for (const [source, sourceInfo] of Object.entries(buildInfo.sources)) { | ||||
|     try { | ||||
|  | @ -267,15 +183,6 @@ async function checkSources(buildInfo: BuildInfo): Promise<boolean> { | |||
|   return false; | ||||
| } | ||||
| 
 | ||||
| async function populateComponentsFromTests(componentRegistry: ComponentRegistry, componentsByImportingFile: Map<string, string[]>) { | ||||
|   const importInfos: Map<string, ImportInfo[]> = await getUserData('playwright-ct-core'); | ||||
|   for (const [file, importList] of importInfos) { | ||||
|     for (const importInfo of importList) | ||||
|       componentRegistry.set(importInfo.id, importInfo); | ||||
|     componentsByImportingFile.set(file, importList.filter(i => !i.isModuleOrAlias).map(i => i.importPath)); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function checkNewComponents(buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Promise<boolean> { | ||||
|   const newComponents = [...componentRegistry.keys()]; | ||||
|   const oldComponents = new Map(buildInfo.components.map(c => [c.id, c])); | ||||
|  | @ -314,35 +221,7 @@ function vitePlugin(registerSource: string, templateDir: string, buildInfo: Buil | |||
|           // Silent if can't read the file.
 | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       // Vite React plugin will do this for .jsx files, but not .js files.
 | ||||
|       if (id.endsWith('.js') && content.includes('React.createElement') && !content.match(importReactRE) && !content.match(compiledReactRE)) { | ||||
|         const code = `import React from 'react';\n${content}`; | ||||
|         return { code, map: { mappings: '' } }; | ||||
|       } | ||||
| 
 | ||||
|       const indexTs = path.join(templateDir, 'index.ts'); | ||||
|       const indexTsx = path.join(templateDir, 'index.tsx'); | ||||
|       const indexJs = path.join(templateDir, 'index.js'); | ||||
|       const indexJsx = path.join(templateDir, 'index.jsx'); | ||||
|       const idResolved = path.resolve(id); | ||||
|       if (!idResolved.endsWith(indexTs) && !idResolved.endsWith(indexTsx) && !idResolved.endsWith(indexJs) && !idResolved.endsWith(indexJsx)) | ||||
|         return; | ||||
| 
 | ||||
|       const folder = path.dirname(id); | ||||
|       const lines = [content, '']; | ||||
|       lines.push(registerSource); | ||||
| 
 | ||||
|       for (const value of importInfos.values()) { | ||||
|         const importPath = value.isModuleOrAlias ? value.importPath : './' + path.relative(folder, value.importPath).replace(/\\/g, '/'); | ||||
|         lines.push(`const ${value.id} = () => import('${importPath}').then((mod) => mod.${value.remoteName || 'default'});`); | ||||
|       } | ||||
| 
 | ||||
|       lines.push(`__pwRegistry.initialize({ ${[...importInfos.keys()].join(',\n  ')} });`); | ||||
|       return { | ||||
|         code: lines.join('\n'), | ||||
|         map: { mappings: '' } | ||||
|       }; | ||||
|       return transformIndexFile(id, content, templateDir, registerSource, importInfos); | ||||
|     }, | ||||
| 
 | ||||
|     async writeBundle(this: PluginContext) { | ||||
|  | @ -371,12 +250,3 @@ function collectViteModuleDependencies(context: PluginContext, id: string, deps: | |||
|   for (const importedId of module?.dynamicallyImportedIds || []) | ||||
|     collectViteModuleDependencies(context, importedId, deps); | ||||
| } | ||||
| 
 | ||||
| function hasJSComponents(components: ImportInfo[]): boolean { | ||||
|   for (const component of components) { | ||||
|     const extname = path.extname(component.importPath); | ||||
|     if (extname === '.js' || !extname && fs.existsSync(component.importPath + '.js')) | ||||
|       return true; | ||||
|   } | ||||
|   return false; | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,189 @@ | |||
| /** | ||||
|  * 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 fs from 'fs'; | ||||
| import path from 'path'; | ||||
| import { debug } from 'playwright-core/lib/utilsBundle'; | ||||
| import { getUserData } from 'playwright/lib/transform/compilationCache'; | ||||
| import type { PlaywrightTestConfig as BasePlaywrightTestConfig, FullConfig } from 'playwright/test'; | ||||
| import type { InlineConfig, Plugin, TransformResult, UserConfig } from 'vite'; | ||||
| import type { ImportInfo } from './tsxTransform'; | ||||
| 
 | ||||
| const log = debug('pw:vite'); | ||||
| 
 | ||||
| export type CtConfig = BasePlaywrightTestConfig['use'] & { | ||||
|   ctPort?: number; | ||||
|   ctTemplateDir?: string; | ||||
|   ctCacheDir?: string; | ||||
|   ctViteConfig?: InlineConfig | (() => Promise<InlineConfig>); | ||||
| }; | ||||
| 
 | ||||
| export type ComponentRegistry = Map<string, ImportInfo>; | ||||
| export type ComponentDirs = { | ||||
|   configDir: string; | ||||
|   outDir: string; | ||||
|   templateDir: string; | ||||
| }; | ||||
| 
 | ||||
| export function resolveDirs(configDir: string, config: FullConfig): ComponentDirs { | ||||
|   const use = config.projects[0].use as CtConfig; | ||||
|   // FIXME: use build plugin to determine html location to resolve this.
 | ||||
|   // TemplateDir must be relative, otherwise we can't move the final index.html into its target location post-build.
 | ||||
|   // This regressed in https://github.com/microsoft/playwright/pull/26526
 | ||||
|   const relativeTemplateDir = use.ctTemplateDir || 'playwright'; | ||||
|   const templateDir = path.join(configDir, relativeTemplateDir); | ||||
|   const outDir = use.ctCacheDir ? path.resolve(configDir, use.ctCacheDir) : path.resolve(templateDir, '.cache'); | ||||
|   return { | ||||
|     configDir, | ||||
|     outDir, | ||||
|     templateDir | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export async function createConfig(dirs: ComponentDirs, config: FullConfig, frameworkPluginFactory: (() => Promise<Plugin>) | undefined, supportJsxInJs: boolean) { | ||||
|   // We are going to have 3 config files:
 | ||||
|   // - the defaults that user config overrides (baseConfig)
 | ||||
|   // - the user config (userConfig)
 | ||||
|   // - frameworks overrides (frameworkOverrides);
 | ||||
| 
 | ||||
|   const use = config.projects[0].use as CtConfig; | ||||
|   const baseURL = new URL(use.baseURL || 'http://localhost'); | ||||
| 
 | ||||
|   // Compose base config from the playwright config only.
 | ||||
|   const baseConfig: InlineConfig = { | ||||
|     root: dirs.templateDir, | ||||
|     configFile: false, | ||||
|     publicDir: path.join(dirs.configDir, 'public'), | ||||
|     define: { | ||||
|       __VUE_PROD_DEVTOOLS__: true, | ||||
|     }, | ||||
|     css: { | ||||
|       devSourcemap: true, | ||||
|     }, | ||||
|     build: { | ||||
|       outDir: dirs.outDir | ||||
|     }, | ||||
|     preview: { | ||||
|       https: baseURL.protocol.startsWith('https:') ? {} : undefined, | ||||
|       host: baseURL.hostname, | ||||
|       port: use.ctPort || Number(baseURL.port) || 3100 | ||||
|     }, | ||||
|     server: { | ||||
|       https: baseURL.protocol.startsWith('https:') ? {} : undefined, | ||||
|       host: baseURL.hostname, | ||||
|       port: use.ctPort || Number(baseURL.port) || 3100 | ||||
|     }, | ||||
|     // Vite preview server will otherwise always return the index.html with 200.
 | ||||
|     appType: 'mpa', | ||||
|   }; | ||||
| 
 | ||||
|   // Vite 5 refuses to support CJS.
 | ||||
|   const { mergeConfig } = await import('vite'); | ||||
| 
 | ||||
|   // Apply user config on top of the base config. This could have changed root and build.outDir.
 | ||||
|   const userConfig = typeof use.ctViteConfig === 'function' ? await use.ctViteConfig() : (use.ctViteConfig || {}); | ||||
|   const baseAndUserConfig = mergeConfig(baseConfig, userConfig); | ||||
| 
 | ||||
|   const frameworkOverrides: UserConfig = { plugins: [] }; | ||||
| 
 | ||||
|   // React heuristic. If we see a component in a file with .js extension,
 | ||||
|   // consider it a potential JSX-in-JS scenario and enable JSX loader for all
 | ||||
|   // .js files.
 | ||||
|   if (supportJsxInJs) { | ||||
|     log('jsx-in-js detected'); | ||||
|     frameworkOverrides.esbuild = { | ||||
|       loader: 'jsx', | ||||
|       include: /.*\.jsx?$/, | ||||
|       exclude: [], | ||||
|     }; | ||||
|     frameworkOverrides.optimizeDeps = { | ||||
|       esbuildOptions: { | ||||
|         loader: { '.js': 'jsx' }, | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   frameworkOverrides.build = { | ||||
|     target: 'esnext', | ||||
|     minify: false, | ||||
|     rollupOptions: { | ||||
|       treeshake: false, | ||||
|       input: { | ||||
|         index: path.join(dirs.templateDir, 'index.html') | ||||
|       }, | ||||
|     }, | ||||
|     sourcemap: true, | ||||
|   }; | ||||
| 
 | ||||
|   // We assume that any non-empty plugin list includes `vite-react` or similar.
 | ||||
|   if (frameworkPluginFactory && !baseAndUserConfig.plugins?.length) | ||||
|     frameworkOverrides.plugins = [await frameworkPluginFactory()]; | ||||
| 
 | ||||
|   return mergeConfig(baseAndUserConfig, frameworkOverrides); | ||||
| } | ||||
| 
 | ||||
| export async function populateComponentsFromTests(componentRegistry: ComponentRegistry, componentsByImportingFile?: Map<string, string[]>) { | ||||
|   const importInfos: Map<string, ImportInfo[]> = await getUserData('playwright-ct-core'); | ||||
|   for (const [file, importList] of importInfos) { | ||||
|     for (const importInfo of importList) | ||||
|       componentRegistry.set(importInfo.id, importInfo); | ||||
|     if (componentsByImportingFile) | ||||
|       componentsByImportingFile.set(file, importList.filter(i => !i.isModuleOrAlias).map(i => i.importPath)); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export function hasJSComponents(components: ImportInfo[]): boolean { | ||||
|   for (const component of components) { | ||||
|     const extname = path.extname(component.importPath); | ||||
|     if (extname === '.js' || !extname && fs.existsSync(component.importPath + '.js')) | ||||
|       return true; | ||||
|   } | ||||
|   return false; | ||||
| } | ||||
| 
 | ||||
| const importReactRE = /(^|\n|;)import\s+(\*\s+as\s+)?React(,|\s+)/; | ||||
| const compiledReactRE = /(const|var)\s+React\s*=/; | ||||
| 
 | ||||
| export function transformIndexFile(id: string, content: string, templateDir: string, registerSource: string, importInfos: Map<string, ImportInfo>): TransformResult  | null { | ||||
|   // Vite React plugin will do this for .jsx files, but not .js files.
 | ||||
|   if (id.endsWith('.js') && content.includes('React.createElement') && !content.match(importReactRE) && !content.match(compiledReactRE)) { | ||||
|     const code = `import React from 'react';\n${content}`; | ||||
|     return { code, map: { mappings: '' } }; | ||||
|   } | ||||
| 
 | ||||
|   const indexTs = path.join(templateDir, 'index.ts'); | ||||
|   const indexTsx = path.join(templateDir, 'index.tsx'); | ||||
|   const indexJs = path.join(templateDir, 'index.js'); | ||||
|   const indexJsx = path.join(templateDir, 'index.jsx'); | ||||
|   const idResolved = path.resolve(id); | ||||
|   if (!idResolved.endsWith(indexTs) && !idResolved.endsWith(indexTsx) && !idResolved.endsWith(indexJs) && !idResolved.endsWith(indexJsx)) | ||||
|     return null; | ||||
| 
 | ||||
|   const folder = path.dirname(id); | ||||
|   const lines = [content, '']; | ||||
|   lines.push(registerSource); | ||||
| 
 | ||||
|   for (const value of importInfos.values()) { | ||||
|     const importPath = value.isModuleOrAlias ? value.importPath : './' + path.relative(folder, value.importPath).replace(/\\/g, '/'); | ||||
|     lines.push(`const ${value.id} = () => import('${importPath}').then((mod) => mod.${value.remoteName || 'default'});`); | ||||
|   } | ||||
| 
 | ||||
|   lines.push(`__pwRegistry.initialize({ ${[...importInfos.keys()].join(',\n  ')} });`); | ||||
|   return { | ||||
|     code: lines.join('\n'), | ||||
|     map: { mappings: '' } | ||||
|   }; | ||||
| } | ||||
|  | @ -19,7 +19,7 @@ const path = require('path'); | |||
| 
 | ||||
| const plugin = () => { | ||||
|   // Only fetch upon request to avoid resolution in workers.
 | ||||
|   const { createPlugin } = require('@playwright/experimental-ct-core/lib/vitePlugin'); | ||||
|   const { createPlugin } = require('@playwright/experimental-ct-core/plugin'); | ||||
|   return createPlugin( | ||||
|     path.join(__dirname, 'registerSource.mjs'), | ||||
|     () => import('@vitejs/plugin-react').then(plugin => plugin.default())); | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ const path = require('path'); | |||
| 
 | ||||
| const plugin = () => { | ||||
|   // Only fetch upon request to avoid resolution in workers.
 | ||||
|   const { createPlugin } = require('@playwright/experimental-ct-core/lib/vitePlugin'); | ||||
|   const { createPlugin } = require('@playwright/experimental-ct-core/plugin'); | ||||
|   return createPlugin( | ||||
|     path.join(__dirname, 'registerSource.mjs'), | ||||
|     () => import('@vitejs/plugin-react').then(plugin => plugin.default())); | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ const path = require('path'); | |||
| 
 | ||||
| const plugin = () => { | ||||
|   // Only fetch upon request to avoid resolution in workers.
 | ||||
|   const { createPlugin } = require('@playwright/experimental-ct-core/lib/vitePlugin'); | ||||
|   const { createPlugin } = require('@playwright/experimental-ct-core/plugin'); | ||||
|   return createPlugin( | ||||
|     path.join(__dirname, 'registerSource.mjs'), | ||||
|     () => import('vite-plugin-solid').then(plugin => plugin.default())); | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ const path = require('path'); | |||
| 
 | ||||
| const plugin = () => { | ||||
|   // Only fetch upon request to avoid resolution in workers.
 | ||||
|   const { createPlugin } = require('@playwright/experimental-ct-core/lib/vitePlugin'); | ||||
|   const { createPlugin } = require('@playwright/experimental-ct-core/plugin'); | ||||
|   return createPlugin( | ||||
|     path.join(__dirname, 'registerSource.mjs'), | ||||
|     () => import('@sveltejs/vite-plugin-svelte').then(plugin => plugin.svelte())); | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ const path = require('path'); | |||
| 
 | ||||
| const plugin = () => { | ||||
|   // Only fetch upon request to avoid resolution in workers.
 | ||||
|   const { createPlugin } = require('@playwright/experimental-ct-core/lib/vitePlugin'); | ||||
|   const { createPlugin } = require('@playwright/experimental-ct-core/plugin'); | ||||
|   return createPlugin( | ||||
|     path.join(__dirname, 'registerSource.mjs'), | ||||
|     () => import('@vitejs/plugin-vue').then(plugin => plugin.default())); | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ const path = require('path'); | |||
| 
 | ||||
| const plugin = () => { | ||||
|   // Only fetch upon request to avoid resolution in workers.
 | ||||
|   const { createPlugin } = require('@playwright/experimental-ct-core/lib/vitePlugin'); | ||||
|   const { createPlugin } = require('@playwright/experimental-ct-core/plugin'); | ||||
|   return createPlugin( | ||||
|     path.join(__dirname, 'registerSource.mjs'), | ||||
|     () => import('@vitejs/plugin-vue2').then(plugin => plugin.default())); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue