add script to update VSCode config with proper excludes (#110161)

Co-authored-by: spalger <spalger@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Spencer 2021-08-30 16:43:28 -07:00 committed by GitHub
parent 8a6cf06f15
commit 1500534a51
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 725 additions and 68 deletions

View file

@ -429,6 +429,7 @@
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/generator": "^7.12.11",
"@babel/parser": "^7.12.11",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-export-namespace-from": "^7.12.1",

View file

@ -57,6 +57,7 @@ RUNTIME_DEPS = [
"@npm//load-json-file",
"@npm//markdown-it",
"@npm//normalize-path",
"@npm//prettier",
"@npm//rxjs",
"@npm//tar",
"@npm//tree-kill",
@ -81,6 +82,7 @@ TYPES_DEPS = [
"@npm//@types/markdown-it",
"@npm//@types/node",
"@npm//@types/normalize-path",
"@npm//@types/prettier",
"@npm//@types/react",
"@npm//@types/tar",
"@npm//@types/testing-library__jest-dom",

View file

@ -32,3 +32,4 @@ export * from './plugins';
export * from './streams';
export * from './babel';
export * from './extract';
export * from './vscode_config';

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export * from './update_vscode_config_cli';

View file

@ -0,0 +1,40 @@
/*
* 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.
*/
export interface ManagedConfigKey {
key: string;
value: Record<string, any>;
}
/**
* Defines the keys which we overrite in user's vscode config for the workspace. We currently
* only support object values because that's all we needed to support, but support for non object
* values should be easy to add.
*/
export const MANAGED_CONFIG_KEYS: ManagedConfigKey[] = [
{
key: 'files.watcherExclude',
value: {
['**/.eslintcache']: true,
['**/.es']: true,
['**/.yarn-local-mirror']: true,
['**/.chromium']: true,
['**/packages/kbn-pm/dist/index.js']: true,
['**/bazel-*']: true,
['**/node_modules']: true,
['**/target']: true,
['**/*.log']: true,
},
},
{
key: 'search.exclude',
value: {
['**/packages/kbn-pm/dist/index.js']: true,
},
},
];

View file

@ -0,0 +1,340 @@
/*
* 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 dedent from 'dedent';
import { updateVscodeConfig } from './update_vscode_config';
import { ManagedConfigKey } from './managed_config_keys';
// avoid excessive escaping in snapshots
expect.addSnapshotSerializer({ test: (v) => typeof v === 'string', print: (v) => `${v}` });
const TEST_KEYS: ManagedConfigKey[] = [
{
key: 'key',
value: {
hello: true,
world: [1, 2, 3],
},
},
];
const run = (json?: string) => updateVscodeConfig(TEST_KEYS, '', json);
it('updates the passed JSON with the managed settings', () => {
expect(run(`{}`)).toMatchInlineSnapshot(`
// @managed
{
"key": {
// @managed
"hello": true,
// @managed
"world": [1, 2, 3]
}
}
`);
});
it('initialized empty or undefined json values', () => {
expect(run('')).toMatchInlineSnapshot(`
// @managed
{
"key": {
// @managed
"hello": true,
// @managed
"world": [1, 2, 3]
}
}
`);
expect(run()).toMatchInlineSnapshot(`
// @managed
{
"key": {
// @managed
"hello": true,
// @managed
"world": [1, 2, 3]
}
}
`);
});
it('replaces conflicting managed keys which do not have object values', () => {
expect(run(`{ "key": false }`)).toMatchInlineSnapshot(`
// @managed
{
"key": {
// @managed
"hello": true,
// @managed
"world": [1, 2, 3]
}
}
`);
});
it(`throws if the JSON file doesn't contain an object`, () => {
expect(() => run('[]')).toThrowErrorMatchingInlineSnapshot(
`expected VSCode config to contain a JSON object`
);
expect(() => run('1')).toThrowErrorMatchingInlineSnapshot(
`expected VSCode config to contain a JSON object`
);
expect(() => run('"foo"')).toThrowErrorMatchingInlineSnapshot(
`expected VSCode config to contain a JSON object`
);
});
it('persists comments in the original file', () => {
const newJson = run(`
/**
* This is a top level comment
*/
{
"a": "bar",
// this is just test data
"b": "box"
}
`);
expect(newJson).toMatchInlineSnapshot(`
// @managed
/**
* This is a top level comment
*/
{
"a": "bar",
// this is just test data
"b": "box",
"key": {
// @managed
"hello": true,
// @managed
"world": [1, 2, 3]
}
}
`);
});
it('overrides old values for managed keys', () => {
const newJson = run(`
{
"foo": 0,
"bar": "some other config",
"complex": "some other config",
}
`);
expect(newJson).toMatchInlineSnapshot(`
// @managed
{
"foo": 0,
"bar": "some other config",
"complex": "some other config",
"key": {
// @managed
"hello": true,
// @managed
"world": [1, 2, 3]
}
}
`);
});
it('does not modify files starting with // SELF MANAGED', () => {
const newJson = run(dedent`
// self managed
{
"invalid": "I know what I am doing",
}
`);
expect(newJson).toMatchInlineSnapshot(`
// self managed
{
"invalid": "I know what I am doing",
}
`);
});
it('does not modify properties with leading `// self managed` comment', () => {
const newJson = run(dedent`
{
// self managed
"key": {
"world": [5]
}
}
`);
expect(newJson).toMatchInlineSnapshot(`
// @managed
{
// self managed
"key": {
"world": [5]
}
}
`);
});
it('does not modify child properties with leading `// self managed` comment', () => {
const newJson = run(dedent`
{
"key": {
// self managed
"world": [5]
}
}
`);
expect(newJson).toMatchInlineSnapshot(`
// @managed
{
"key": {
// self managed
"world": [5],
// @managed
"hello": true
}
}
`);
});
it('does not modify unknown child properties', () => {
const newJson = run(dedent`
{
"key": {
"foo": "bar",
// self managed
"world": [5],
}
}
`);
expect(newJson).toMatchInlineSnapshot(`
// @managed
{
"key": {
"foo": "bar",
// self managed
"world": [5],
// @managed
"hello": true
}
}
`);
});
it('removes managed properties which are no longer managed', () => {
const newJson = run(dedent`
{
"key": {
// @managed
"foo": "bar",
// self managed
"world": [5],
}
}
`);
expect(newJson).toMatchInlineSnapshot(`
// @managed
{
"key": {
// self managed
"world": [5],
// @managed
"hello": true
}
}
`);
});
it('wipes out child keys which conflict with newly managed child keys', () => {
const newJson = run(dedent`
{
"key": {
// some user specified comment
"world": [5],
}
}
`);
expect(newJson).toMatchInlineSnapshot(`
// @managed
{
"key": {
// @managed
"hello": true,
// @managed
"world": [1, 2, 3]
}
}
`);
});
it('correctly formats info text when specified', () => {
const newJson = updateVscodeConfig(TEST_KEYS, 'info users\nshould know', `{}`);
expect(newJson).toMatchInlineSnapshot(`
/**
* @managed
*
* info users
* should know
*/
{
"key": {
// @managed
"hello": true,
// @managed
"world": [1, 2, 3]
}
}
`);
});
it('allows "// self managed" comments conflicting with "// @managed" comments to win', () => {
const newJson = run(dedent`
{
"key": {
// @managed
// self managed
"hello": ["world"]
}
}
`);
expect(newJson).toMatchInlineSnapshot(`
// @managed
{
"key": {
// self managed
"hello": ["world"],
// @managed
"world": [1, 2, 3]
}
}
`);
});

View file

@ -0,0 +1,210 @@
/*
* 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 { parseExpression } from '@babel/parser';
import * as t from '@babel/types';
import generate from '@babel/generator';
import Prettier from 'prettier';
import { ManagedConfigKey } from './managed_config_keys';
type BasicObjectProp = t.ObjectProperty & {
key: t.StringLiteral;
};
const isBasicObjectProp = (n: t.Node): n is BasicObjectProp =>
n.type === 'ObjectProperty' && n.key.type === 'StringLiteral';
const isManaged = (node?: t.Node) =>
!!node?.leadingComments?.some(
(c) => c.type === 'CommentLine' && c.value.trim().toLocaleLowerCase() === '@managed'
);
const isSelfManaged = (node?: t.Node) =>
!!node?.leadingComments?.some(
(c) => c.type === 'CommentLine' && c.value.trim().toLocaleLowerCase() === 'self managed'
);
const remove = <T>(arr: T[], value: T) => {
const index = arr.indexOf(value);
if (index > -1) {
arr.splice(index, 1);
}
};
const createManagedChildProp = (key: string, value: any) => {
const childProp = t.objectProperty(t.stringLiteral(key), parseExpression(JSON.stringify(value)));
t.addComment(childProp, 'leading', ' @managed', true);
return childProp;
};
const createManagedProp = (key: string, value: Record<string, any>) => {
return t.objectProperty(
t.stringLiteral(key),
t.objectExpression(Object.entries(value).map(([k, v]) => createManagedChildProp(k, v)))
);
};
/**
* Adds a new setting to the settings.json file. Used when there is no existing key
*
* @param ast AST of the entire settings.json file
* @param key the key name to add
* @param value managed value which should be set at `key`
*/
const addManagedProp = (ast: t.ObjectExpression, key: string, value: Record<string, any>) => {
ast.properties.push(createManagedProp(key, value));
};
/**
* Replace an existing setting in the settings.json file with the `managedValue`, ignoring its
* type, used when the value of the existing setting is not an ObjectExpression
*
* @param ast AST of the entire settings.json file
* @param existing node which should be replaced
* @param value managed value which should replace the current value, regardless of its type
*/
const replaceManagedProp = (
ast: t.ObjectExpression,
existing: BasicObjectProp,
value: Record<string, any>
) => {
remove(ast.properties, existing);
addManagedProp(ast, existing.key.value, value);
};
/**
* Merge the managed value in to the value already in the settings.json file. Any property which is
* labeled with a `// self managed` comment is untouched, any property which is `// @managed` but
* no longer in the `managedValue` is removed, and any properties in the `managedValue` are either
* added or updated based on their existence in the AST.
*
* @param properties Object expression properties list which we will merge with ("key": <value>)
* @param managedValue the managed value that should be merged into the existing values
*/
const mergeManagedProperties = (
properties: t.ObjectExpression['properties'],
managedValue: Record<string, any>
) => {
// iterate through all the keys in the managed `value` and either add them to the
// prop, update their value, or ignore them because they are "// self managed"
for (const [key, value] of Object.entries(managedValue)) {
const existing = properties.filter(isBasicObjectProp).find((p) => p.key.value === key);
if (!existing) {
// add the new managed prop
properties.push(createManagedChildProp(key, value));
continue;
}
if (isSelfManaged(existing)) {
// strip "// @managed" comment if conflicting with "// self managed"
existing.leadingComments = (existing.leadingComments ?? []).filter(
(c) => c.value.trim() !== '@managed'
);
continue;
}
if (isManaged(existing)) {
// the prop already exists and is still managed, so update it's value
existing.value = parseExpression(JSON.stringify(value));
continue;
}
// take over the unmanaged child prop by deleting the previous prop and replacing it
// with a brand new one
remove(properties, existing);
properties.push(createManagedChildProp(key, value));
}
// iterate through the props to find "// @managed" props which are no longer in
// the `managedValue` and remove them
for (const prop of properties) {
if (
isBasicObjectProp(prop) &&
isManaged(prop) &&
!Object.prototype.hasOwnProperty.call(managedValue, prop.key.value)
) {
remove(properties, prop);
}
}
};
/**
* Update the settings.json file used by VSCode in the Kibana repository. If the file starts
* with the comment "// self managed" then it is not touched. If a top-level keys is prefixed with
* `// self managed` then all the properties of that setting are left untouched. And finally, if
* a specific child property of a setting like `search.exclude` is prefixed with `// self managed`
* then it is left untouched.
*
* We don't just use `JSON.parse()` and `JSON.stringify()` in order to support this customization and
* also to support users using comments in this file, which is very useful for temporarily disabling settings.
*
* After the config file is updated it is formatted with prettier.
*
* @param keys The config keys which are managed
* @param infoText The text which should be written to the top of the file to educate users how to customize the settings
* @param json The settings file as a string
*/
export function updateVscodeConfig(keys: ManagedConfigKey[], infoText: string, json?: string) {
json = json || '{}';
const ast = parseExpression(json);
if (ast.type !== 'ObjectExpression') {
throw new Error(`expected VSCode config to contain a JSON object`);
}
if (isSelfManaged(ast)) {
return json;
}
for (const { key, value } of keys) {
const existingProp = ast.properties.filter(isBasicObjectProp).find((p) => p.key.value === key);
if (isSelfManaged(existingProp)) {
continue;
}
if (existingProp && existingProp.value.type === 'ObjectExpression') {
// setting exists and is an object so merge properties of `value` with it
mergeManagedProperties(existingProp.value.properties, value);
continue;
}
if (existingProp) {
// setting exists but its value is not an object expression so replace it
replaceManagedProp(ast, existingProp, value);
continue;
}
// setting isn't in config file so create it
addManagedProp(ast, key, value);
}
ast.leadingComments = [
(infoText
? {
type: 'CommentBlock',
value: `*
* @managed
*
* ${infoText.split(/\r?\n/).join('\n * ')}
`,
}
: {
type: 'CommentLine',
value: ' @managed',
}) as t.CommentBlock,
...(ast.leadingComments ?? [])?.filter((c) => !c.value.includes('@managed')),
];
return Prettier.format(generate(ast).code, {
endOfLine: 'auto',
filepath: 'settings.json',
});
}

View file

@ -0,0 +1,48 @@
/*
* 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 Fs from 'fs/promises';
import { REPO_ROOT } from '@kbn/utils';
import dedent from 'dedent';
import { run } from '../run';
import { MANAGED_CONFIG_KEYS } from './managed_config_keys';
import { updateVscodeConfig } from './update_vscode_config';
export function runUpdateVscodeConfigCli() {
run(async ({ log }) => {
const path = Path.resolve(REPO_ROOT, '.vscode/settings.json');
let json;
try {
json = await Fs.readFile(path, 'utf-8');
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
const updatedJson = updateVscodeConfig(
MANAGED_CONFIG_KEYS,
dedent`
Some settings in this file are managed by @kbn/dev-utils. When a setting is managed it is preceeded
with a comment "// @managed" comment. Replace that with "// self managed" and the scripts will not
touch that value. Put a "// self managed" comment at the top of the file, or above a group of settings
to disable management of that entire section.
`,
json
);
await Fs.mkdir(Path.dirname(path), { recursive: true });
await Fs.writeFile(path, updatedJson);
log.success('updated', path);
});
}

View file

@ -446,78 +446,33 @@ describe('OptimizerConfig::create()', () => {
}
`);
expect(findKibanaPlatformPlugins.mock).toMatchInlineSnapshot(`
Object {
"calls": Array [
Array [
Symbol(parsed plugin scan dirs),
Symbol(parsed plugin paths),
],
expect(findKibanaPlatformPlugins.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Symbol(parsed plugin scan dirs),
Symbol(parsed plugin paths),
],
"instances": Array [
[Window],
],
"invocationCallOrder": Array [
25,
],
"results": Array [
Object {
"type": "return",
"value": Symbol(new platform plugins),
},
],
}
]
`);
expect(filterById.mock).toMatchInlineSnapshot(`
Object {
"calls": Array [
Array [
Array [],
Symbol(focused bundles),
],
expect(filterById.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Array [],
Symbol(focused bundles),
],
"instances": Array [
[Window],
],
"invocationCallOrder": Array [
28,
],
"results": Array [
Object {
"type": "return",
"value": Symbol(filtered bundles),
},
],
}
]
`);
expect(getPluginBundles.mock).toMatchInlineSnapshot(`
Object {
"calls": Array [
Array [
Symbol(new platform plugins),
Symbol(parsed repo root),
Symbol(parsed output root),
Symbol(limits),
],
expect(getPluginBundles.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Symbol(new platform plugins),
Symbol(parsed repo root),
Symbol(parsed output root),
Symbol(limits),
],
"instances": Array [
[Window],
],
"invocationCallOrder": Array [
26,
],
"results": Array [
Object {
"type": "return",
"value": Array [
Symbol(bundle1),
Symbol(bundle2),
],
},
],
}
]
`);
});
});

View file

@ -8968,9 +8968,17 @@ const BootstrapCommand = {
// NOTE: We don't probably need this anymore, is actually not being used
await Object(_utils_link_project_executables__WEBPACK_IMPORTED_MODULE_2__["linkProjectExecutables"])(projects, projectGraph); // Build typescript references
await Object(_utils_link_project_executables__WEBPACK_IMPORTED_MODULE_2__["linkProjectExecutables"])(projects, projectGraph); // Update vscode settings
await Object(_utils_child_process__WEBPACK_IMPORTED_MODULE_1__["spawnStreaming"])('node', ['scripts/build_ts_refs', '--ignore-type-failures', '--info'], {
await Object(_utils_child_process__WEBPACK_IMPORTED_MODULE_1__["spawnStreaming"])(process.execPath, ['scripts/update_vscode_config'], {
cwd: kbn.getAbsolute(),
env: process.env
}, {
prefix: '[vscode]',
debug: false
}); // Build typescript references
await Object(_utils_child_process__WEBPACK_IMPORTED_MODULE_1__["spawnStreaming"])(process.execPath, ['scripts/build_ts_refs', '--ignore-type-failures', '--info'], {
cwd: kbn.getAbsolute(),
env: process.env
}, {

View file

@ -102,9 +102,20 @@ export const BootstrapCommand: ICommand = {
// NOTE: We don't probably need this anymore, is actually not being used
await linkProjectExecutables(projects, projectGraph);
// Update vscode settings
await spawnStreaming(
process.execPath,
['scripts/update_vscode_config'],
{
cwd: kbn.getAbsolute(),
env: process.env,
},
{ prefix: '[vscode]', debug: false }
);
// Build typescript references
await spawnStreaming(
'node',
process.execPath,
['scripts/build_ts_refs', '--ignore-type-failures', '--info'],
{
cwd: kbn.getAbsolute(),

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
require('../src/setup_node_env');
require('@kbn/dev-utils').runUpdateVscodeConfigCli();

View file

@ -110,6 +110,15 @@
jsesc "^2.5.1"
source-map "^0.5.0"
"@babel/generator@^7.12.11":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.5.tgz#848d7b9f031caca9d0cd0af01b063f226f52d785"
integrity sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==
dependencies:
"@babel/types" "^7.14.5"
jsesc "^2.5.1"
source-map "^0.5.0"
"@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.10.4", "@babel/helper-annotate-as-pure@^7.12.10":
version "7.12.10"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz#54ab9b000e60a93644ce17b3f37d313aaf1d115d"
@ -300,6 +309,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
"@babel/helper-validator-identifier@^7.14.5":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz#d0f0e277c512e0c938277faa85a3968c9a44c0e8"
integrity sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==
"@babel/helper-validator-option@^7.12.1", "@babel/helper-validator-option@^7.12.11":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz#d66cb8b7a3e7fe4c6962b32020a131ecf0847f4f"
@ -1217,6 +1231,14 @@
lodash "^4.17.19"
to-fast-properties "^2.0.0"
"@babel/types@^7.14.5":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.5.tgz#3bb997ba829a2104cedb20689c4a5b8121d383ff"
integrity sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==
dependencies:
"@babel/helper-validator-identifier" "^7.14.5"
to-fast-properties "^2.0.0"
"@base2/pretty-print-object@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.0.tgz#860ce718b0b73f4009e153541faff2cb6b85d047"