mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
switch out chokidar for @parcel/watcher in dev cli (#148924)
After the recent changes to limit the dev-cli watcher to relevant packages, the watcher started logging tons of unnecessary changes, and in some cases breaking based on the state of the repo. I have seen this happen with Chokidar before, and I'm not convinced we'll be able to fix it, so instead I decided to swap it out with `@parcel/watcher`, which is a conceptually simpler implementation that automatically batches changes and watches an entire directory, rather than tons of unique directories/files. This new implementation is conceptually simpler, and because of the design of the `@parcel/watcher` module I was pushed to reuse the `RepoSourceClassifier` to determine if we should restart the server based on a specific change. This means we now have a single source of truth for test files and the like (the classifier will tell us if a file is a test file, regardless of where it exists in the repo). Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a94a1b620e
commit
99013bdab8
14 changed files with 107 additions and 538 deletions
|
@ -826,6 +826,7 @@
|
|||
"@mapbox/vector-tile": "1.3.1",
|
||||
"@octokit/rest": "^16.35.0",
|
||||
"@openpgp/web-stream-tools": "^0.0.10",
|
||||
"@parcel/watcher": "^2.1.0",
|
||||
"@storybook/addon-a11y": "^6.5.15",
|
||||
"@storybook/addon-actions": "^6.5.15",
|
||||
"@storybook/addon-controls": "^6.5.15",
|
||||
|
@ -1025,7 +1026,6 @@
|
|||
"backport": "^8.9.7",
|
||||
"callsites": "^3.1.0",
|
||||
"chance": "1.0.18",
|
||||
"chokidar": "^3.5.3",
|
||||
"chromedriver": "^108.0.0",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"compression-webpack-plugin": "^4.0.0",
|
||||
|
|
|
@ -23,7 +23,7 @@ objects.
|
|||
|
||||
## `Watcher`
|
||||
|
||||
The `Watcher` manages a [chokidar](https://github.com/paulmillr/chokidar) instance to watch the
|
||||
The `Watcher` manages a [@parcel/watcher](https://github.com/parcel-bundler/watcher) instance to watch the
|
||||
server files, logs about file changes observed and provides an observable to the `DevServer` via
|
||||
its `serverShouldRestart$()` method.
|
||||
|
||||
|
|
|
@ -35,13 +35,6 @@ const { BasePathProxyServer } = jest.requireMock('./base_path_proxy_server');
|
|||
jest.mock('@kbn/ci-stats-reporter');
|
||||
const { CiStatsReporter } = jest.requireMock('@kbn/ci-stats-reporter');
|
||||
|
||||
jest.mock('./get_server_watch_paths', () => ({
|
||||
getServerWatchPaths: jest.fn(() => ({
|
||||
watchPaths: ['<mock watch paths>'],
|
||||
ignorePaths: ['<mock ignore paths>'],
|
||||
})),
|
||||
}));
|
||||
|
||||
const mockBasePathProxy = {
|
||||
targetPort: 9999,
|
||||
basePath: '/foo/bar',
|
||||
|
@ -142,15 +135,9 @@ it('passes correct args to sub-classes', () => {
|
|||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"cwd": <absolute path>,
|
||||
"enabled": true,
|
||||
"ignore": Array [
|
||||
"<mock ignore paths>",
|
||||
],
|
||||
"log": <TestLog>,
|
||||
"paths": Array [
|
||||
"<mock watch paths>",
|
||||
],
|
||||
"repoRoot": <absolute path>,
|
||||
},
|
||||
],
|
||||
]
|
||||
|
|
|
@ -31,7 +31,6 @@ import { DevServer } from './dev_server';
|
|||
import { Watcher } from './watcher';
|
||||
import { BasePathProxyServer } from './base_path_proxy_server';
|
||||
import { shouldRedirectFromOldBasePath } from './should_redirect_from_old_base_path';
|
||||
import { getServerWatchPaths } from './get_server_watch_paths';
|
||||
import { CliDevConfig } from './config';
|
||||
|
||||
// signal that emits undefined once a termination signal has been sent
|
||||
|
@ -114,18 +113,10 @@ export class CliDevMode {
|
|||
this.basePathProxy = new BasePathProxyServer(this.log, config.http, config.dev);
|
||||
}
|
||||
|
||||
const { watchPaths, ignorePaths } = getServerWatchPaths({
|
||||
runExamples: cliArgs.runExamples,
|
||||
pluginPaths: config.plugins.additionalPluginPaths,
|
||||
pluginScanDirs: config.plugins.pluginSearchPaths,
|
||||
});
|
||||
|
||||
this.watcher = new Watcher({
|
||||
enabled: !!cliArgs.watch,
|
||||
log: this.log,
|
||||
cwd: REPO_ROOT,
|
||||
paths: watchPaths,
|
||||
ignore: ignorePaths,
|
||||
repoRoot: REPO_ROOT,
|
||||
});
|
||||
|
||||
this.devServer = new DevServer({
|
||||
|
|
|
@ -1,140 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import Path from 'path';
|
||||
|
||||
import { createAbsolutePathSerializer } from '@kbn/jest-serializers';
|
||||
import { REPO_ROOT } from '@kbn/repo-info';
|
||||
import type { KibanaPackageType } from '@kbn/repo-packages';
|
||||
|
||||
const TYPES = Object.keys(
|
||||
(() => {
|
||||
const asObj: { [k in KibanaPackageType]: true } = {
|
||||
'functional-tests': true,
|
||||
'plugin-browser': true,
|
||||
'plugin-server': true,
|
||||
'shared-browser': true,
|
||||
'shared-common': true,
|
||||
'shared-scss': true,
|
||||
'shared-server': true,
|
||||
'test-helper': true,
|
||||
};
|
||||
return asObj;
|
||||
})()
|
||||
);
|
||||
|
||||
import { getServerWatchPaths } from './get_server_watch_paths';
|
||||
|
||||
jest.mock('@kbn/repo-packages', () => ({
|
||||
getPackages: jest.fn(),
|
||||
getPluginPackagesFilter: jest.fn().mockReturnValue(() => true),
|
||||
}));
|
||||
const mockGetPluginPackagesFilter = jest.requireMock('@kbn/repo-packages').getPluginPackagesFilter;
|
||||
const mockGetPackages = jest.requireMock('@kbn/repo-packages').getPackages;
|
||||
|
||||
expect.addSnapshotSerializer(createAbsolutePathSerializer());
|
||||
|
||||
it('produces the right watch and ignore list', () => {
|
||||
mockGetPackages.mockReturnValue(
|
||||
TYPES.flatMap((type) => ({
|
||||
isPlugin: type.startsWith('plugin-'),
|
||||
directory: Path.resolve(REPO_ROOT, 'packages', type),
|
||||
manifest: {
|
||||
type,
|
||||
},
|
||||
}))
|
||||
);
|
||||
|
||||
const { watchPaths, ignorePaths } = getServerWatchPaths({
|
||||
runExamples: false,
|
||||
pluginPaths: [Path.resolve(REPO_ROOT, 'x-pack/test/plugin_functional/plugins/resolver_test')],
|
||||
pluginScanDirs: [
|
||||
Path.resolve(REPO_ROOT, 'src/plugins'),
|
||||
Path.resolve(REPO_ROOT, 'test/plugin_functional/plugins'),
|
||||
Path.resolve(REPO_ROOT, 'x-pack/plugins'),
|
||||
],
|
||||
});
|
||||
|
||||
expect(watchPaths).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
<absolute path>/src/core,
|
||||
<absolute path>/config,
|
||||
<absolute path>/x-pack/test/plugin_functional/plugins/resolver_test,
|
||||
<absolute path>/src/plugins,
|
||||
<absolute path>/test/plugin_functional/plugins,
|
||||
<absolute path>/x-pack/plugins,
|
||||
<absolute path>/packages/plugin-server,
|
||||
<absolute path>/packages/shared-common,
|
||||
<absolute path>/packages/shared-server,
|
||||
]
|
||||
`);
|
||||
|
||||
expect(ignorePaths).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
/\\[\\\\\\\\\\\\/\\]\\(\\\\\\.\\.\\*\\|node_modules\\|bower_components\\|target\\|public\\|__\\[a-z0-9_\\]\\+__\\|coverage\\)\\(\\[\\\\\\\\\\\\/\\]\\|\\$\\)/,
|
||||
/\\\\\\.\\(test\\|spec\\)\\\\\\.\\(js\\|ts\\|tsx\\)\\$/,
|
||||
/\\\\\\.\\(md\\|sh\\|txt\\)\\$/,
|
||||
/debug\\\\\\.log\\$/,
|
||||
<absolute path>/src/plugins/*/test/**,
|
||||
<absolute path>/src/plugins/*/integration_tests/**,
|
||||
<absolute path>/src/plugins/*/build/**,
|
||||
<absolute path>/src/plugins/*/target/**,
|
||||
<absolute path>/src/plugins/*/scripts/**,
|
||||
<absolute path>/src/plugins/*/docs/**,
|
||||
<absolute path>/test/plugin_functional/plugins/*/test/**,
|
||||
<absolute path>/test/plugin_functional/plugins/*/integration_tests/**,
|
||||
<absolute path>/test/plugin_functional/plugins/*/build/**,
|
||||
<absolute path>/test/plugin_functional/plugins/*/target/**,
|
||||
<absolute path>/test/plugin_functional/plugins/*/scripts/**,
|
||||
<absolute path>/test/plugin_functional/plugins/*/docs/**,
|
||||
<absolute path>/x-pack/plugins/*/test/**,
|
||||
<absolute path>/x-pack/plugins/*/integration_tests/**,
|
||||
<absolute path>/x-pack/plugins/*/build/**,
|
||||
<absolute path>/x-pack/plugins/*/target/**,
|
||||
<absolute path>/x-pack/plugins/*/scripts/**,
|
||||
<absolute path>/x-pack/plugins/*/docs/**,
|
||||
<absolute path>/x-pack/test/plugin_functional/plugins/resolver_test/test/**,
|
||||
<absolute path>/x-pack/test/plugin_functional/plugins/resolver_test/integration_tests/**,
|
||||
<absolute path>/x-pack/test/plugin_functional/plugins/resolver_test/build/**,
|
||||
<absolute path>/x-pack/test/plugin_functional/plugins/resolver_test/target/**,
|
||||
<absolute path>/x-pack/test/plugin_functional/plugins/resolver_test/scripts/**,
|
||||
<absolute path>/x-pack/test/plugin_functional/plugins/resolver_test/docs/**,
|
||||
<absolute path>/x-pack/plugins/screenshotting/chromium,
|
||||
<absolute path>/x-pack/plugins/security_solution/cypress,
|
||||
<absolute path>/x-pack/plugins/apm/scripts,
|
||||
<absolute path>/x-pack/plugins/apm/ftr_e2e,
|
||||
<absolute path>/x-pack/plugins/canvas/canvas_plugin_src,
|
||||
<absolute path>/x-pack/plugins/cases/server/scripts,
|
||||
<absolute path>/x-pack/plugins/lists/scripts,
|
||||
<absolute path>/x-pack/plugins/lists/server/scripts,
|
||||
<absolute path>/x-pack/plugins/security_solution/scripts,
|
||||
<absolute path>/x-pack/plugins/security_solution/server/lib/detection_engine/scripts,
|
||||
<absolute path>/x-pack/plugins/synthetics/e2e,
|
||||
<absolute path>/x-pack/plugins/ux/e2e,
|
||||
<absolute path>/x-pack/plugins/observability/e2e,
|
||||
]
|
||||
`);
|
||||
|
||||
expect(mockGetPluginPackagesFilter.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"examples": false,
|
||||
"parentDirs": Array [
|
||||
<absolute path>/src/plugins,
|
||||
<absolute path>/test/plugin_functional/plugins,
|
||||
<absolute path>/x-pack/plugins,
|
||||
],
|
||||
"paths": Array [
|
||||
<absolute path>/x-pack/test/plugin_functional/plugins/resolver_test,
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import Path from 'path';
|
||||
|
||||
import { REPO_ROOT } from '@kbn/repo-info';
|
||||
import { getPackages, getPluginPackagesFilter } from '@kbn/repo-packages';
|
||||
|
||||
interface Options {
|
||||
runExamples: boolean;
|
||||
pluginPaths: string[];
|
||||
pluginScanDirs: string[];
|
||||
}
|
||||
|
||||
export type WatchPaths = ReturnType<typeof getServerWatchPaths>;
|
||||
|
||||
export function getServerWatchPaths(opts: Options) {
|
||||
const fromRoot = (p: string) => Path.resolve(REPO_ROOT, p);
|
||||
|
||||
const pluginInternalDirsIgnore = opts.pluginScanDirs
|
||||
.map((scanDir) => Path.resolve(scanDir, '*'))
|
||||
.concat(opts.pluginPaths)
|
||||
.reduce(
|
||||
(acc: string[], path) => [
|
||||
...acc,
|
||||
Path.resolve(path, 'test/**'),
|
||||
Path.resolve(path, 'integration_tests/**'),
|
||||
Path.resolve(path, 'build/**'),
|
||||
Path.resolve(path, 'target/**'),
|
||||
Path.resolve(path, 'scripts/**'),
|
||||
Path.resolve(path, 'docs/**'),
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
function getServerPkgDirs() {
|
||||
const pluginFilter = getPluginPackagesFilter({
|
||||
examples: opts.runExamples,
|
||||
paths: opts.pluginPaths,
|
||||
parentDirs: opts.pluginScanDirs,
|
||||
});
|
||||
|
||||
return getPackages(REPO_ROOT).flatMap((p) => {
|
||||
if (p.isPlugin) {
|
||||
return pluginFilter(p) && p.manifest.type === 'plugin-server' ? p.directory : [];
|
||||
}
|
||||
|
||||
return p.manifest.type === 'shared-common' || p.manifest.type === 'shared-server'
|
||||
? p.directory
|
||||
: [];
|
||||
});
|
||||
}
|
||||
|
||||
const watchPaths = Array.from(
|
||||
new Set([
|
||||
fromRoot('src/core'),
|
||||
fromRoot('config'),
|
||||
...opts.pluginPaths.map((path) => Path.resolve(path)),
|
||||
...opts.pluginScanDirs.map((path) => Path.resolve(path)),
|
||||
...getServerPkgDirs(),
|
||||
])
|
||||
);
|
||||
|
||||
const ignorePaths = [
|
||||
/[\\\/](\..*|node_modules|bower_components|target|public|__[a-z0-9_]+__|coverage)([\\\/]|$)/,
|
||||
/\.(test|spec)\.(js|ts|tsx)$/,
|
||||
/\.(md|sh|txt)$/,
|
||||
/debug\.log$/,
|
||||
...pluginInternalDirsIgnore,
|
||||
fromRoot('x-pack/plugins/screenshotting/chromium'),
|
||||
fromRoot('x-pack/plugins/security_solution/cypress'),
|
||||
fromRoot('x-pack/plugins/apm/scripts'),
|
||||
fromRoot('x-pack/plugins/apm/ftr_e2e'), // prevents restarts for APM cypress tests
|
||||
fromRoot('x-pack/plugins/canvas/canvas_plugin_src'), // prevents server from restarting twice for Canvas plugin changes,
|
||||
fromRoot('x-pack/plugins/cases/server/scripts'),
|
||||
fromRoot('x-pack/plugins/lists/scripts'),
|
||||
fromRoot('x-pack/plugins/lists/server/scripts'),
|
||||
fromRoot('x-pack/plugins/security_solution/scripts'),
|
||||
fromRoot('x-pack/plugins/security_solution/server/lib/detection_engine/scripts'),
|
||||
fromRoot('x-pack/plugins/synthetics/e2e'),
|
||||
fromRoot('x-pack/plugins/ux/e2e'),
|
||||
fromRoot('x-pack/plugins/observability/e2e'),
|
||||
];
|
||||
|
||||
return {
|
||||
watchPaths,
|
||||
ignorePaths,
|
||||
};
|
||||
}
|
|
@ -1,207 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import { materialize, toArray } from 'rxjs/operators';
|
||||
|
||||
import { TestLog } from './log';
|
||||
import { Watcher, Options } from './watcher';
|
||||
|
||||
class MockChokidar extends EventEmitter {
|
||||
close = jest.fn();
|
||||
}
|
||||
|
||||
let mockChokidar: MockChokidar | undefined;
|
||||
jest.mock('chokidar');
|
||||
const chokidar = jest.requireMock('chokidar');
|
||||
function isMock(mock: MockChokidar | undefined): asserts mock is MockChokidar {
|
||||
expect(mock).toBeInstanceOf(MockChokidar);
|
||||
}
|
||||
|
||||
chokidar.watch.mockImplementation(() => {
|
||||
mockChokidar = new MockChokidar();
|
||||
return mockChokidar;
|
||||
});
|
||||
|
||||
const subscriptions: Rx.Subscription[] = [];
|
||||
const run = (watcher: Watcher) => {
|
||||
const subscription = watcher.run$.subscribe({
|
||||
error(e) {
|
||||
throw e;
|
||||
},
|
||||
});
|
||||
subscriptions.push(subscription);
|
||||
return subscription;
|
||||
};
|
||||
|
||||
const log = new TestLog();
|
||||
const defaultOptions: Options = {
|
||||
enabled: true,
|
||||
log,
|
||||
paths: ['foo.js', 'bar.js'],
|
||||
ignore: [/^f/],
|
||||
cwd: '/app/repo',
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
if (mockChokidar) {
|
||||
mockChokidar.removeAllListeners();
|
||||
mockChokidar = undefined;
|
||||
}
|
||||
|
||||
for (const sub of subscriptions) {
|
||||
sub.unsubscribe();
|
||||
}
|
||||
|
||||
subscriptions.length = 0;
|
||||
log.messages.length = 0;
|
||||
});
|
||||
|
||||
it('completes restart streams immediately when disabled', () => {
|
||||
const watcher = new Watcher({
|
||||
...defaultOptions,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const restart$ = new Rx.BehaviorSubject<void>(undefined);
|
||||
subscriptions.push(watcher.serverShouldRestart$().subscribe(restart$));
|
||||
|
||||
run(watcher);
|
||||
expect(restart$.isStopped).toBe(true);
|
||||
});
|
||||
|
||||
it('calls chokidar.watch() with expected arguments', () => {
|
||||
const watcher = new Watcher(defaultOptions);
|
||||
expect(chokidar.watch).not.toHaveBeenCalled();
|
||||
run(watcher);
|
||||
expect(chokidar.watch.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Array [
|
||||
"foo.js",
|
||||
"bar.js",
|
||||
],
|
||||
Object {
|
||||
"cwd": "/app/repo",
|
||||
"ignored": Array [
|
||||
/\\^f/,
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('closes chokidar watcher when unsubscribed', () => {
|
||||
const sub = run(new Watcher(defaultOptions));
|
||||
isMock(mockChokidar);
|
||||
expect(mockChokidar.close).not.toHaveBeenCalled();
|
||||
sub.unsubscribe();
|
||||
expect(mockChokidar.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('rethrows chokidar errors', async () => {
|
||||
const watcher = new Watcher(defaultOptions);
|
||||
const promise = Rx.firstValueFrom(watcher.run$.pipe(materialize(), toArray()));
|
||||
|
||||
isMock(mockChokidar);
|
||||
mockChokidar.emit('error', new Error('foo bar'));
|
||||
|
||||
const notifications = await promise;
|
||||
expect(notifications).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Notification {
|
||||
"error": [Error: foo bar],
|
||||
"hasValue": false,
|
||||
"kind": "E",
|
||||
"value": undefined,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('logs the count of add events after the ready event', () => {
|
||||
run(new Watcher(defaultOptions));
|
||||
isMock(mockChokidar);
|
||||
|
||||
mockChokidar.emit('add');
|
||||
mockChokidar.emit('add');
|
||||
mockChokidar.emit('add');
|
||||
mockChokidar.emit('add');
|
||||
mockChokidar.emit('ready');
|
||||
|
||||
expect(log.messages).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"args": Array [
|
||||
"watching for changes",
|
||||
"(4 files)",
|
||||
],
|
||||
"type": "good",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('buffers subsequent changes before logging and notifying serverShouldRestart$', async () => {
|
||||
const watcher = new Watcher(defaultOptions);
|
||||
|
||||
const history: any[] = [];
|
||||
subscriptions.push(
|
||||
watcher
|
||||
.serverShouldRestart$()
|
||||
.pipe(materialize())
|
||||
.subscribe((n) => history.push(n))
|
||||
);
|
||||
|
||||
run(watcher);
|
||||
expect(history).toMatchInlineSnapshot(`Array []`);
|
||||
|
||||
isMock(mockChokidar);
|
||||
mockChokidar.emit('ready');
|
||||
mockChokidar.emit('all', ['add', 'foo.js']);
|
||||
mockChokidar.emit('all', ['add', 'bar.js']);
|
||||
mockChokidar.emit('all', ['delete', 'bar.js']);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
expect(log.messages).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"args": Array [
|
||||
"watching for changes",
|
||||
"(0 files)",
|
||||
],
|
||||
"type": "good",
|
||||
},
|
||||
Object {
|
||||
"args": Array [
|
||||
"restarting server",
|
||||
"due to changes in
|
||||
- \\"foo.js\\"
|
||||
- \\"bar.js\\"",
|
||||
],
|
||||
"type": "warn",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
expect(history).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Notification {
|
||||
"error": undefined,
|
||||
"hasValue": true,
|
||||
"kind": "N",
|
||||
"value": undefined,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
|
@ -7,44 +7,48 @@
|
|||
*/
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import {
|
||||
map,
|
||||
tap,
|
||||
takeUntil,
|
||||
count,
|
||||
share,
|
||||
buffer,
|
||||
debounceTime,
|
||||
ignoreElements,
|
||||
} from 'rxjs/operators';
|
||||
import Chokidar from 'chokidar';
|
||||
import Pw from '@parcel/watcher';
|
||||
import { REPO_ROOT } from '@kbn/repo-info';
|
||||
import { RepoSourceClassifier } from '@kbn/repo-source-classifier';
|
||||
import { ImportResolver } from '@kbn/import-resolver';
|
||||
import { makeMatcher } from '@kbn/picomatcher';
|
||||
|
||||
import { Log } from './log';
|
||||
|
||||
const packageMatcher = makeMatcher(['**/*', '!**/.*']);
|
||||
|
||||
/**
|
||||
* Any non-package code must match this in order to trigger a restart
|
||||
*/
|
||||
const nonPackageMatcher = makeMatcher([
|
||||
'src/**',
|
||||
'!src/{dev,fixtures}/**',
|
||||
'x-pack/plugins/**',
|
||||
'!x-pack/plugins/screenshotting/chromium',
|
||||
'!x-pack/plugins/canvas/canvas_plugin_src',
|
||||
]);
|
||||
|
||||
export interface Options {
|
||||
enabled: boolean;
|
||||
log: Log;
|
||||
paths: string[];
|
||||
ignore: Array<string | RegExp>;
|
||||
cwd: string;
|
||||
repoRoot: string;
|
||||
}
|
||||
|
||||
export class Watcher {
|
||||
public readonly enabled: boolean;
|
||||
|
||||
private readonly log: Log;
|
||||
private readonly paths: string[];
|
||||
private readonly ignore: Array<string | RegExp>;
|
||||
private readonly cwd: string;
|
||||
|
||||
private readonly repoRoot: string;
|
||||
private readonly classifier: RepoSourceClassifier;
|
||||
private readonly restart$ = new Rx.Subject<void>();
|
||||
private readonly resolver: ImportResolver;
|
||||
|
||||
constructor(options: Options) {
|
||||
this.enabled = !!options.enabled;
|
||||
this.log = options.log;
|
||||
this.paths = options.paths;
|
||||
this.ignore = options.ignore;
|
||||
this.cwd = options.cwd;
|
||||
this.repoRoot = options.repoRoot;
|
||||
this.resolver = ImportResolver.create(REPO_ROOT);
|
||||
this.classifier = new RepoSourceClassifier(this.resolver);
|
||||
}
|
||||
|
||||
run$ = new Rx.Observable((subscriber) => {
|
||||
|
@ -54,54 +58,56 @@ export class Watcher {
|
|||
return;
|
||||
}
|
||||
|
||||
const chokidar = Chokidar.watch(this.paths, {
|
||||
cwd: this.cwd,
|
||||
ignored: this.ignore,
|
||||
});
|
||||
const fire = (repoRel: string) => {
|
||||
this.log.warn(`restarting server`, `due to changes in ${repoRel}`);
|
||||
this.restart$.next();
|
||||
};
|
||||
|
||||
subscriber.add(() => {
|
||||
chokidar.close();
|
||||
});
|
||||
Pw.subscribe(
|
||||
this.repoRoot,
|
||||
(error, events) => {
|
||||
if (error) {
|
||||
subscriber.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const error$ = Rx.fromEvent(chokidar, 'error').pipe(
|
||||
map((error) => {
|
||||
throw error;
|
||||
})
|
||||
);
|
||||
for (const event of events) {
|
||||
const pkg = this.resolver.getPackageManifestForPath(event.path);
|
||||
|
||||
const init$ = Rx.fromEvent(chokidar, 'add').pipe(
|
||||
takeUntil(Rx.fromEvent(chokidar, 'ready')),
|
||||
count(),
|
||||
tap((fileCount) => {
|
||||
this.log.good('watching for changes', `(${fileCount} files)`);
|
||||
})
|
||||
);
|
||||
// ignore changes in any devOnly package, these can't power the server so we can ignore them
|
||||
if (pkg?.devOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const change$ = Rx.fromEvent<[string, string]>(chokidar, 'all').pipe(
|
||||
map(([, path]) => path),
|
||||
share()
|
||||
);
|
||||
|
||||
subscriber.add(
|
||||
Rx.merge(
|
||||
error$,
|
||||
Rx.concat(
|
||||
init$,
|
||||
change$.pipe(
|
||||
buffer(change$.pipe(debounceTime(50))),
|
||||
map((changes) => {
|
||||
const paths = Array.from(new Set(changes));
|
||||
const prefix = paths.length > 1 ? '\n - ' : ' ';
|
||||
const fileList = paths.reduce((list, file) => `${list || ''}${prefix}"${file}"`, '');
|
||||
|
||||
this.log.warn(`restarting server`, `due to changes in${fileList}`);
|
||||
this.restart$.next();
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
.pipe(ignoreElements())
|
||||
.subscribe(subscriber)
|
||||
const result = this.classifier.classify(event.path);
|
||||
if (result.type === 'common package' || result.type === 'server package') {
|
||||
return packageMatcher(result.repoRel) && fire(result.repoRel);
|
||||
}
|
||||
if (result.type === 'non-package') {
|
||||
return nonPackageMatcher(result.repoRel) && fire(result.repoRel);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
// some basic high-level ignore statements. Additional filtering is done above
|
||||
// before paths are passed to `fire()`, using the RepoSourceClassifier mostly
|
||||
ignore: [
|
||||
'**/{node_modules,target,public,coverage,__*__}/**',
|
||||
'**/*.{test,spec,story,stories}.*',
|
||||
'**/*.{md,sh,txt}',
|
||||
'**/debug.log',
|
||||
],
|
||||
}
|
||||
).then(
|
||||
(sub) => {
|
||||
this.log.good('watching server files for changes');
|
||||
subscriber.add(() => {
|
||||
sub.unsubscribe();
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
subscriber.error(error);
|
||||
}
|
||||
);
|
||||
|
||||
// complete state subjects when run$ completes
|
||||
|
|
|
@ -21,7 +21,9 @@
|
|||
"@kbn/jest-serializers",
|
||||
"@kbn/stdio-dev-helpers",
|
||||
"@kbn/tooling-log",
|
||||
"@kbn/repo-packages",
|
||||
"@kbn/repo-source-classifier",
|
||||
"@kbn/import-resolver",
|
||||
"@kbn/picomatcher",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -107,6 +107,11 @@ export class ImportResolver {
|
|||
return this.pkgIdForDir(Path.dirname(relative));
|
||||
}
|
||||
|
||||
getPackageManifestForPath(path: string) {
|
||||
const pkgId = this.getPackageIdForPath(path);
|
||||
return pkgId ? this.getPkgManifest(pkgId) : undefined;
|
||||
}
|
||||
|
||||
getAbsolutePackageDir(pkgId: string) {
|
||||
const dir = this.pkgMap.get(pkgId);
|
||||
return dir ? Path.resolve(this.cwd, dir) : null;
|
||||
|
|
|
@ -16,4 +16,6 @@ export interface ModuleId {
|
|||
repoRel: string;
|
||||
/** info about the package the source file is within, in the case the file is found within a package */
|
||||
pkgInfo?: PkgInfo;
|
||||
/** path segments of the dirname of this */
|
||||
dirs: string[];
|
||||
}
|
||||
|
|
|
@ -60,10 +60,12 @@ export class RepoPath {
|
|||
}
|
||||
|
||||
private segs: string[] | undefined;
|
||||
/** get and cache the path segments from the repo-realtive versions of this path */
|
||||
/** get and cache the path segments from the repo-realtive dirname of this path */
|
||||
getSegs() {
|
||||
if (this.segs === undefined) {
|
||||
this.segs = Path.dirname(this.getRepoRel()).split('/');
|
||||
this.segs = Path.dirname(this.getRepoRel())
|
||||
.split('/')
|
||||
.filter((s) => s !== '.');
|
||||
}
|
||||
|
||||
return this.segs;
|
||||
|
|
|
@ -12,6 +12,10 @@ import { ModuleType } from './module_type';
|
|||
import { RANDOM_TEST_FILE_NAMES, TEST_DIR, TEST_TAG } from './config';
|
||||
import { RepoPath } from './repo_path';
|
||||
|
||||
const STATIC_EXTS = new Set(
|
||||
'json|woff|woff2|ttf|eot|svg|ico|png|jpg|gif|jpeg|html|md|txt|tmpl'.split('|').map((e) => `.${e}`)
|
||||
);
|
||||
|
||||
export class RepoSourceClassifier {
|
||||
constructor(private readonly resolver: ImportResolver) {}
|
||||
|
||||
|
@ -124,7 +128,7 @@ export class RepoSourceClassifier {
|
|||
* Determine the "type" of a file
|
||||
*/
|
||||
private getType(path: RepoPath): ModuleType {
|
||||
if (path.getExtname() === '.json') {
|
||||
if (STATIC_EXTS.has(path.getExtname())) {
|
||||
return 'static';
|
||||
}
|
||||
|
||||
|
@ -214,6 +218,7 @@ export class RepoSourceClassifier {
|
|||
type: this.getType(path),
|
||||
repoRel: path.getRepoRel(),
|
||||
pkgInfo: path.getPkgInfo() ?? undefined,
|
||||
dirs: path.getSegs(),
|
||||
};
|
||||
this.ids.set(path, id);
|
||||
return id;
|
||||
|
|
14
yarn.lock
14
yarn.lock
|
@ -5060,6 +5060,16 @@
|
|||
resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.4.0.tgz#facf2c67d6063b9918d5a5e3fdf25f3a30d547b6"
|
||||
integrity sha512-Hzl8soGpmyzja9w3kiFFcYJ7n5HNETpplY6cb67KR4QPlxp4FTTresO06qXHgHDhyIInmbLJXuwARjjpsKYGuQ==
|
||||
|
||||
"@parcel/watcher@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.1.0.tgz#5f32969362db4893922c526a842d8af7a8538545"
|
||||
integrity sha512-8s8yYjd19pDSsBpbkOHnT6Z2+UJSuLQx61pCFM0s5wSRvKCEMDjd/cHY3/GI1szHIWbpXpsJdg3V6ISGGx9xDw==
|
||||
dependencies:
|
||||
is-glob "^4.0.3"
|
||||
micromatch "^4.0.5"
|
||||
node-addon-api "^3.2.1"
|
||||
node-gyp-build "^4.3.0"
|
||||
|
||||
"@pmmmwh/react-refresh-webpack-plugin@^0.5.3":
|
||||
version "0.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz#58f8217ba70069cc6a73f5d7e05e85b458c150e2"
|
||||
|
@ -20448,7 +20458,7 @@ node-abi@^3.3.0:
|
|||
dependencies:
|
||||
semver "^7.3.5"
|
||||
|
||||
node-addon-api@^3.0.0:
|
||||
node-addon-api@^3.0.0, node-addon-api@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
|
||||
integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
|
||||
|
@ -20506,7 +20516,7 @@ node-gyp-build-optional-packages@5.0.3:
|
|||
resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17"
|
||||
integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==
|
||||
|
||||
node-gyp-build@^4.2.2, node-gyp-build@^4.2.3:
|
||||
node-gyp-build@^4.2.2, node-gyp-build@^4.2.3, node-gyp-build@^4.3.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40"
|
||||
integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue