chore(ct): use sticky test server if available (#29136)

This commit is contained in:
Pavel Feldman 2024-01-25 08:36:13 -08:00 committed by GitHub
parent f5de6e5538
commit f7fb1e4d4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 302 additions and 179 deletions

18
package-lock.json generated
View File

@ -8299,7 +8299,8 @@
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {
"playwright": "cli.js"
"playwright": "cli.js",
"pw-react": "cli.js"
},
"engines": {
"node": ">=16"
@ -8314,7 +8315,8 @@
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {
"playwright": "cli.js"
"playwright": "cli.js",
"pw-react17": "cli.js"
},
"engines": {
"node": ">=16"
@ -8329,7 +8331,8 @@
"vite-plugin-solid": "^2.7.0"
},
"bin": {
"playwright": "cli.js"
"playwright": "cli.js",
"pw-solid": "cli.js"
},
"devDependencies": {
"solid-js": "^1.7.0"
@ -8347,7 +8350,8 @@
"@sveltejs/vite-plugin-svelte": "^3.0.1"
},
"bin": {
"playwright": "cli.js"
"playwright": "cli.js",
"pw-svelte": "cli.js"
},
"devDependencies": {
"svelte": "^4.2.8"
@ -8365,7 +8369,8 @@
"@vitejs/plugin-vue": "^4.2.1"
},
"bin": {
"playwright": "cli.js"
"playwright": "cli.js",
"pw-vue": "cli.js"
},
"engines": {
"node": ">=16"
@ -8380,7 +8385,8 @@
"@vitejs/plugin-vue2": "^2.2.0"
},
"bin": {
"playwright": "cli.js"
"playwright": "cli.js",
"pw-vue2": "cli.js"
},
"devDependencies": {
"vue": "^2.7.14"

View File

@ -169,6 +169,37 @@ export function createHttpsServer(...args: any[]): https.Server {
return server;
}
export async function isURLAvailable(url: URL, ignoreHTTPSErrors: boolean, onLog?: (data: string) => void, onStdErr?: (data: string) => void) {
let statusCode = await httpStatusCode(url, ignoreHTTPSErrors, onLog, onStdErr);
if (statusCode === 404 && url.pathname === '/') {
const indexUrl = new URL(url);
indexUrl.pathname = '/index.html';
statusCode = await httpStatusCode(indexUrl, ignoreHTTPSErrors, onLog, onStdErr);
}
return statusCode >= 200 && statusCode < 404;
}
async function httpStatusCode(url: URL, ignoreHTTPSErrors: boolean, onLog?: (data: string) => void, onStdErr?: (data: string) => void): Promise<number> {
return new Promise(resolve => {
onLog?.(`HTTP GET: ${url}`);
httpRequest({
url: url.toString(),
headers: { Accept: '*/*' },
rejectUnauthorized: !ignoreHTTPSErrors
}, res => {
res.resume();
const statusCode = res.statusCode ?? 0;
onLog?.(`HTTP Status: ${statusCode}`);
resolve(statusCode);
}, error => {
if ((error as NodeJS.ErrnoException).code === 'DEPTH_ZERO_SELF_SIGNED_CERT')
onStdErr?.(`[WebServer] Self-signed certificate detected. Try adding ignoreHTTPSErrors: true to config.webServer.`);
onLog?.(`Error while checking if ${url} is available: ${error.message}`);
resolve(0);
});
});
}
function decorateServer(server: http.Server | http.Server) {
const sockets = new Set<net.Socket>();
server.on('connection', socket => {

View File

@ -4,6 +4,9 @@ generated/indexSource.ts
[viteDevPlugin.ts]
generated/indexSource.ts
[devServer.ts]
generated/indexSource.ts
[mount.ts]
generated/serializers.ts
injected/**

View File

@ -0,0 +1,92 @@
/**
* 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 type { FullConfigInternal } from 'playwright/lib/common/config';
import { ConfigLoader, resolveConfigFile } from 'playwright/lib/common/configLoader';
import { Watcher } from 'playwright/lib/fsWatcher';
import { restartWithExperimentalTsEsm } from 'playwright/lib/program';
import { Runner } from 'playwright/lib/runner/runner';
import type { PluginContext } from 'rollup';
import { source as injectedSource } from './generated/indexSource';
import { createConfig, populateComponentsFromTests, resolveDirs, transformIndexFile } from './viteUtils';
import type { ComponentRegistry } from './viteUtils';
export async function runDevServer(configFile: string, registerSourceFile: string, frameworkPluginFactory: () => Promise<any>) {
const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd();
const resolvedConfigFile = resolveConfigFile(configFileOrDirectory);
if (restartWithExperimentalTsEsm(resolvedConfigFile))
return;
const configLoader = new ConfigLoader();
let config: FullConfigInternal;
if (resolvedConfigFile)
config = await configLoader.loadConfigFile(resolvedConfigFile);
else
config = await configLoader.loadEmptyConfig(configFileOrDirectory);
const runner = new Runner(config);
await runner.loadAllTests(true);
const componentRegistry: ComponentRegistry = new Map();
await populateComponentsFromTests(componentRegistry);
const dirs = resolveDirs(config.configDir, config.config);
const registerSource = injectedSource + '\n' + await fs.promises.readFile(registerSourceFile, 'utf-8');
const viteConfig = await createConfig(dirs, config.config, frameworkPluginFactory, false);
viteConfig.plugins.push({
name: 'playwright:component-index',
async transform(this: PluginContext, content: string, id: string) {
return transformIndexFile(id, content, dirs.templateDir, registerSource, componentRegistry);
},
});
const { createServer } = await import('vite');
const devServer = await createServer(viteConfig);
await devServer.listen();
const protocol = viteConfig.server.https ? 'https:' : 'http:';
// eslint-disable-next-line no-console
console.log(`Test Server listening on ${protocol}//${viteConfig.server.host || 'localhost'}:${viteConfig.server.port}`);
const projectDirs = new Set<string>();
const projectOutputs = new Set<string>();
for (const p of config.projects) {
projectDirs.add(p.project.testDir);
projectOutputs.add(p.project.outputDir);
}
const globalWatcher = new Watcher('deep', async () => {
const registry: ComponentRegistry = new Map();
await populateComponentsFromTests(registry);
// compare componentRegistry to registry key sets.
if (componentRegistry.size === registry.size && [...componentRegistry.keys()].every(k => registry.has(k)))
return;
// eslint-disable-next-line no-console
console.log('List of components changed');
componentRegistry.clear();
for (const [k, v] of registry)
componentRegistry.set(k, v);
const id = path.join(dirs.templateDir, 'index');
const modules = [...devServer.moduleGraph.urlToModuleMap.values()];
const rootModule = modules.find(m => m.file?.startsWith(id + '.ts') || m.file?.startsWith(id + '.js'));
if (rootModule)
devServer.moduleGraph.onFileChange(rootModule.file!);
});
globalWatcher.update([...projectDirs], [...projectOutputs], false);
}

View File

@ -14,4 +14,27 @@
* limitations under the License.
*/
import type { Command } from 'playwright-core/lib/utilsBundle';
import { program } from 'playwright/lib/program';
import { runDevServer } from './devServer';
export { program } from 'playwright/lib/program';
let registerSourceFile: string;
let frameworkPluginFactory: () => Promise<any>;
export function initializePlugin(registerSource: string, factory: () => Promise<any>) {
registerSourceFile = registerSource;
frameworkPluginFactory = factory;
}
function addDevServerCommand(program: Command) {
const command = program.command('dev-server');
command.description('start dev server');
command.option('-c, --config <file>', `Configuration file. Can be used to specify additional configuration for the output report.`);
command.action(options => {
runDevServer(options.config, registerSourceFile, frameworkPluginFactory);
});
}
addDevServerCommand(program);

View File

@ -1,64 +0,0 @@
/**
* 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);
},
};
}

View File

@ -18,7 +18,7 @@ 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 { assert, calculateSha1, getPlaywrightVersion, isURLAvailable } 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';
@ -30,7 +30,7 @@ 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';
import { createConfig, hasJSComponents, populateComponentsFromTests, resolveDirs, resolveEndpoint, transformIndexFile } from './viteUtils';
const log = debug('pw:vite');
@ -51,6 +51,19 @@ export function createPlugin(
},
begin: async (suite: Suite) => {
{
// Detect a running dev server and use it if available.
const endpoint = resolveEndpoint(config);
const protocol = endpoint.https ? 'https:' : 'http:';
const url = new URL(`${protocol}//${endpoint.host}:${endpoint.port}`);
if (process.env.PW_CT_DEV && await isURLAvailable(url, true)) {
// eslint-disable-next-line no-console
console.log(`Test Server is already running at ${url.toString()}, using it.\n`);
process.env.PLAYWRIGHT_TEST_BASE_URL = url.toString();
return;
}
}
const dirs = resolveDirs(configDir, config);
const buildInfoFile = path.join(dirs.outDir, 'metainfo.json');

View File

@ -53,14 +53,24 @@ export function resolveDirs(configDir: string, config: FullConfig): ComponentDir
};
}
export function resolveEndpoint(config: FullConfig) {
const use = config.projects[0].use as CtConfig;
const baseURL = new URL(use.baseURL || 'http://localhost');
return {
https: baseURL.protocol.startsWith('https:') ? {} : undefined,
host: baseURL.hostname,
port: use.ctPort || Number(baseURL.port) || 3100
};
}
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 endpoint = resolveEndpoint(config);
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 = {
@ -76,16 +86,8 @@ export async function createConfig(dirs: ComponentDirs, config: FullConfig, fram
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
},
preview: endpoint,
server: endpoint,
// Vite preview server will otherwise always return the index.html with 200.
appType: 'mpa',
};

View File

@ -15,5 +15,8 @@
* limitations under the License.
*/
const { program } = require('@playwright/experimental-ct-core/lib/program');
const path = require('path');
const { program, initializePlugin } = require('@playwright/experimental-ct-core/lib/program');
initializePlugin(path.join(__dirname, 'registerSource.mjs'), () => import('@vitejs/plugin-react').then(plugin => plugin.default()))
program.parse(process.argv);

View File

@ -33,6 +33,7 @@
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {
"playwright": "cli.js"
"playwright": "cli.js",
"pw-react": "cli.js"
}
}

View File

@ -33,6 +33,7 @@
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {
"playwright": "cli.js"
"playwright": "cli.js",
"pw-react17": "cli.js"
}
}

View File

@ -36,6 +36,7 @@
"solid-js": "^1.7.0"
},
"bin": {
"playwright": "cli.js"
"playwright": "cli.js",
"pw-solid": "cli.js"
}
}

View File

@ -36,6 +36,7 @@
"svelte": "^4.2.8"
},
"bin": {
"playwright": "cli.js"
"playwright": "cli.js",
"pw-svelte": "cli.js"
}
}

View File

@ -33,6 +33,7 @@
"@vitejs/plugin-vue": "^4.2.1"
},
"bin": {
"playwright": "cli.js"
"playwright": "cli.js",
"pw-vue": "cli.js"
}
}

View File

@ -36,6 +36,7 @@
"vue": "^2.7.14"
},
"bin": {
"playwright": "cli.js"
"playwright": "cli.js",
"pw-vue2": "cli.js"
}
}

View File

@ -19,9 +19,12 @@
"default": "./index.js"
},
"./package.json": "./package.json",
"./lib/common/configLoader": "./lib/common/configLoader.js",
"./lib/fsWatcher": "./lib/fsWatcher.js",
"./lib/program": "./lib/program.js",
"./lib/transform/babelBundle": "./lib/transform/babelBundle.js",
"./lib/transform/compilationCache": "./lib/transform/compilationCache.js",
"./lib/runner/runner": "./lib/runner/runner.js",
"./lib/transform/esmLoader": "./lib/transform/esmLoader.js",
"./lib/transform/transform": "./lib/transform/transform.js",
"./lib/internalsForTest": "./lib/internalsForTest.js",

View File

@ -0,0 +1,71 @@
/**
* 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 { chokidar } from './utilsBundle';
import type { FSWatcher } from 'chokidar';
export type FSEvent = { event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', file: string };
export class Watcher {
private _onChange: (events: FSEvent[]) => void;
private _watchedFiles: string[] = [];
private _ignoredFolders: string[] = [];
private _collector: FSEvent[] = [];
private _fsWatcher: FSWatcher | undefined;
private _throttleTimer: NodeJS.Timeout | undefined;
private _mode: 'flat' | 'deep';
constructor(mode: 'flat' | 'deep', onChange: (events: FSEvent[]) => void) {
this._mode = mode;
this._onChange = onChange;
}
update(watchedFiles: string[], ignoredFolders: string[], reportPending: boolean) {
if (JSON.stringify([this._watchedFiles, this._ignoredFolders]) === JSON.stringify(watchedFiles, ignoredFolders))
return;
if (reportPending)
this._reportEventsIfAny();
this._watchedFiles = watchedFiles;
this._ignoredFolders = ignoredFolders;
void this._fsWatcher?.close();
this._fsWatcher = undefined;
this._collector.length = 0;
clearTimeout(this._throttleTimer);
this._throttleTimer = undefined;
if (!this._watchedFiles.length)
return;
this._fsWatcher = chokidar.watch(watchedFiles, { ignoreInitial: true, ignored: this._ignoredFolders }).on('all', async (event, file) => {
if (this._throttleTimer)
clearTimeout(this._throttleTimer);
if (this._mode === 'flat' && event !== 'add' && event !== 'change')
return;
if (this._mode === 'deep' && event !== 'add' && event !== 'change' && event !== 'unlink' && event !== 'addDir' && event !== 'unlinkDir')
return;
this._collector.push({ event, file });
this._throttleTimer = setTimeout(() => this._reportEventsIfAny(), 250);
});
}
private _reportEventsIfAny() {
if (this._collector.length)
this._onChange(this._collector.slice());
this._collector.length = 0;
}
}

View File

@ -17,7 +17,7 @@ import path from 'path';
import net from 'net';
import { colors, debug } from 'playwright-core/lib/utilsBundle';
import { raceAgainstDeadline, launchProcess, httpRequest, monotonicTime } from 'playwright-core/lib/utils';
import { raceAgainstDeadline, launchProcess, monotonicTime, isURLAvailable } from 'playwright-core/lib/utils';
import type { FullConfig } from '../../types/testReporter';
import type { TestRunnerPlugin } from '.';
@ -155,37 +155,6 @@ async function isPortUsed(port: number): Promise<boolean> {
return await innerIsPortUsed('127.0.0.1') || await innerIsPortUsed('::1');
}
async function isURLAvailable(url: URL, ignoreHTTPSErrors: boolean, onStdErr: ReporterV2['onStdErr']) {
let statusCode = await httpStatusCode(url, ignoreHTTPSErrors, onStdErr);
if (statusCode === 404 && url.pathname === '/') {
const indexUrl = new URL(url);
indexUrl.pathname = '/index.html';
statusCode = await httpStatusCode(indexUrl, ignoreHTTPSErrors, onStdErr);
}
return statusCode >= 200 && statusCode < 404;
}
async function httpStatusCode(url: URL, ignoreHTTPSErrors: boolean, onStdErr: ReporterV2['onStdErr']): Promise<number> {
return new Promise(resolve => {
debugWebServer(`HTTP GET: ${url}`);
httpRequest({
url: url.toString(),
headers: { Accept: '*/*' },
rejectUnauthorized: !ignoreHTTPSErrors
}, res => {
res.resume();
const statusCode = res.statusCode ?? 0;
debugWebServer(`HTTP Status: ${statusCode}`);
resolve(statusCode);
}, error => {
if ((error as NodeJS.ErrnoException).code === 'DEPTH_ZERO_SELF_SIGNED_CERT')
onStdErr?.(`[WebServer] Self-signed certificate detected. Try adding ignoreHTTPSErrors: true to config.webServer.`);
debugWebServer(`Error while checking if ${url} is available: ${error.message}`);
resolve(0);
});
});
}
async function waitFor(waitFn: () => Promise<boolean>, cancellationToken: { canceled: boolean }) {
const logScale = [100, 250, 500];
while (!cancellationToken.canceled) {
@ -201,7 +170,7 @@ async function waitFor(waitFn: () => Promise<boolean>, cancellationToken: { canc
function getIsAvailableFunction(url: string, checkPortOnly: boolean, ignoreHTTPSErrors: boolean, onStdErr: ReporterV2['onStdErr']) {
const urlObject = new URL(url);
if (!checkPortOnly)
return () => isURLAvailable(urlObject, ignoreHTTPSErrors, onStdErr);
return () => isURLAvailable(urlObject, ignoreHTTPSErrors, debugWebServer, onStdErr);
const port = urlObject.port;
return () => isPortUsed(+port);
}

View File

@ -271,7 +271,7 @@ function resolveReporter(id: string) {
return require.resolve(id, { paths: [process.cwd()] });
}
function restartWithExperimentalTsEsm(configFile: string | null): boolean {
export function restartWithExperimentalTsEsm(configFile: string | null): boolean {
const nodeVersion = +process.versions.node.split('.')[0];
// New experimental loader is only supported on Node 16+.
if (nodeVersion < 16)

View File

@ -8,3 +8,4 @@
../util.ts
../utilsBundle.ts
../isomorphic/folders.ts
../fsWatcher.ts

View File

@ -104,6 +104,24 @@ export class Runner {
return status;
}
async loadAllTests(outOfProcess?: boolean): Promise<FullResult['status']> {
const config = this._config;
const reporter = new InternalReporter(new Multiplexer([]));
const taskRunner = createTaskRunnerForList(config, reporter, outOfProcess ? 'out-of-process' : 'in-process', { failOnLoadErrors: true });
const testRun = new TestRun(config, reporter);
reporter.onConfigure(config.config);
const taskStatus = await taskRunner.run(testRun, 0);
let status: FullResult['status'] = testRun.failureTracker.result();
if (status === 'passed' && taskStatus !== 'passed')
status = taskStatus;
const modifiedResult = await reporter.onEnd({ status });
if (modifiedResult && modifiedResult.status)
status = modifiedResult.status;
await reporter.onExit();
return status;
}
async watchAllTests(): Promise<FullResult['status']> {
const config = this._config;
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));

View File

@ -23,13 +23,12 @@ import { InternalReporter } from '../reporters/internalReporter';
import { TeleReporterEmitter } from '../reporters/teleEmitter';
import { createReporters } from './reporters';
import { TestRun, createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
import { chokidar } from '../utilsBundle';
import type { FSWatcher } from 'chokidar';
import { open } from 'playwright-core/lib/utilsBundle';
import ListReporter from '../reporters/list';
import type { OpenTraceViewerOptions, Transport } from 'playwright-core/lib/server/trace/viewer/traceViewer';
import { Multiplexer } from '../reporters/multiplexer';
import { SigIntWatcher } from './sigIntWatcher';
import { Watcher } from '../fsWatcher';
class UIMode {
private _config: FullConfigInternal;
@ -258,59 +257,6 @@ function chunkToPayload(type: 'stdout' | 'stderr', chunk: Buffer | string): Stdi
return { type, text: chunk };
}
type FSEvent = { event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', file: string };
class Watcher {
private _onChange: (events: FSEvent[]) => void;
private _watchedFiles: string[] = [];
private _ignoredFolders: string[] = [];
private _collector: FSEvent[] = [];
private _fsWatcher: FSWatcher | undefined;
private _throttleTimer: NodeJS.Timeout | undefined;
private _mode: 'flat' | 'deep';
constructor(mode: 'flat' | 'deep', onChange: (events: FSEvent[]) => void) {
this._mode = mode;
this._onChange = onChange;
}
update(watchedFiles: string[], ignoredFolders: string[], reportPending: boolean) {
if (JSON.stringify([this._watchedFiles, this._ignoredFolders]) === JSON.stringify(watchedFiles, ignoredFolders))
return;
if (reportPending)
this._reportEventsIfAny();
this._watchedFiles = watchedFiles;
this._ignoredFolders = ignoredFolders;
void this._fsWatcher?.close();
this._fsWatcher = undefined;
this._collector.length = 0;
clearTimeout(this._throttleTimer);
this._throttleTimer = undefined;
if (!this._watchedFiles.length)
return;
this._fsWatcher = chokidar.watch(watchedFiles, { ignoreInitial: true, ignored: this._ignoredFolders }).on('all', async (event, file) => {
if (this._throttleTimer)
clearTimeout(this._throttleTimer);
if (this._mode === 'flat' && event !== 'add' && event !== 'change')
return;
if (this._mode === 'deep' && event !== 'add' && event !== 'change' && event !== 'unlink' && event !== 'addDir' && event !== 'unlinkDir')
return;
this._collector.push({ event, file });
this._throttleTimer = setTimeout(() => this._reportEventsIfAny(), 250);
});
}
private _reportEventsIfAny() {
if (this._collector.length)
this._onChange(this._collector.slice());
this._collector.length = 0;
}
}
function hasSomeBrowsers(): boolean {
for (const browserName of ['chromium', 'webkit', 'firefox']) {
try {