Implement package linter (#148496)

This PR implements a linter like the TS Project linter, except for
packages in the repo. It does this by extracting the reusable bits from
the TS Project linter and reusing them for the project linter. The only
rule that exists for packages right now is that the "name" in the
package.json file matches the "id" in Kibana.jsonc. The goal is to use a
rule to migrate kibana.json files on the future.

Additionally, a new rule for validating the indentation of tsconfig.json
files was added.

Validating and fixing violations is what has triggered review by so many
teams, but we plan to treat those review requests as notifications of
the changes and not as blockers for merging.

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Spencer 2023-01-09 17:49:29 -06:00 committed by GitHub
parent 039ed991d8
commit d6be4a4b06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
218 changed files with 3917 additions and 1650 deletions

View file

@ -7,6 +7,7 @@ export DISABLE_BOOTSTRAP_VALIDATION=false
.buildkite/scripts/steps/checks/precommit_hook.sh
.buildkite/scripts/steps/checks/ts_projects.sh
.buildkite/scripts/steps/checks/packages.sh
.buildkite/scripts/steps/checks/bazel_packages.sh
.buildkite/scripts/steps/checks/verify_notice.sh
.buildkite/scripts/steps/checks/plugin_list_docs.sh

View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
source .buildkite/scripts/common/util.sh
echo --- Lint packages
cmd="node scripts/lint_packages"
if is_pr && ! is_auto_commit_disabled; then
cmd="$cmd --fix"
fi
eval "$cmd"
check_for_changed_files "$cmd" true

View file

@ -4,8 +4,8 @@ set -euo pipefail
source .buildkite/scripts/common/util.sh
echo --- Run TS Project Linter
cmd="node scripts/ts_project_linter"
echo --- Lint TS projects
cmd="node scripts/lint_ts_projects"
if is_pr && ! is_auto_commit_disabled; then
cmd="$cmd --fix"
fi

View file

@ -1,8 +1,6 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"incremental": false,
"composite": false,
"outDir": "target/types",
"types": ["node", "mocha"],
"paths": {

View file

@ -8,11 +8,7 @@
require('@kbn/babel-register').install();
const Path = require('path');
const Fs = require('fs');
const normalizePath = require('normalize-path');
const { discoverPackageManifestPaths, Jsonc } = require('@kbn/bazel-packages');
const { getPackages } = require('@kbn/repo-packages');
const { REPO_ROOT } = require('@kbn/repo-info');
const APACHE_2_0_LICENSE_HEADER = `
@ -124,10 +120,9 @@ const VENN_DIAGRAM_HEADER = `
`;
/** Packages which should not be included within production code. */
const DEV_PACKAGE_DIRS = discoverPackageManifestPaths(REPO_ROOT).flatMap((path) => {
const manifest = Jsonc.parse(Fs.readFileSync(path, 'utf8'));
return !!manifest.devOnly ? normalizePath(Path.relative(REPO_ROOT, Path.dirname(path))) : [];
});
const DEV_PACKAGE_DIRS = getPackages(REPO_ROOT).flatMap((pkg) =>
pkg.isDevOnly ? pkg.normalizedRepoRelativeDir : []
);
/** Directories (at any depth) which include dev-only code. */
const DEV_DIRECTORIES = [
@ -1700,13 +1695,6 @@ module.exports = {
},
},
/**
* Prettier disables all conflicting rules, listing as last override so it takes precedence
*/
{
files: ['**/*'],
rules: require('eslint-config-prettier').rules,
},
/**
* Enterprise Search Prettier override
* Lints unnecessary backticks - @see https://github.com/prettier/eslint-config-prettier/blob/main/README.md#forbid-unnecessary-backticks
@ -1728,5 +1716,23 @@ module.exports = {
'@kbn/imports/no_unresolvable_imports': 'off',
},
},
/**
* Code inside .buildkite runs separately from everything else in CI, before bootstrap, with ts-node. It needs a few tweaks because of this.
*/
{
files: 'packages/kbn-{package-*,repo-*,dep-*}/**/*',
rules: {
'max-classes-per-file': 'off',
},
},
],
};
/**
* Prettier disables all conflicting rules, listing as last override so it takes precedence
* people kept ignoring that this was last so it's now defined outside of the overrides list
*/
/** eslint-disable-next-line */
module.exports.overrides.push({ files: ['**/*'], rules: require('eslint-config-prettier').rules });
/** PLEASE DON'T PUT THINGS AFTER THIS */

13
.github/CODEOWNERS vendored
View file

@ -894,7 +894,6 @@ packages/kbn-babel-plugin-package-imports @elastic/kibana-operations
packages/kbn-babel-preset @elastic/kibana-operations
packages/kbn-babel-register @elastic/kibana-operations
packages/kbn-babel-transform @elastic/kibana-operations
packages/kbn-bazel-packages @elastic/kibana-operations
packages/kbn-bazel-runner @elastic/kibana-operations
packages/kbn-cases-components @elastic/response-ops
packages/kbn-chart-icons @elastic/kibana-visualizations
@ -942,13 +941,17 @@ packages/kbn-hapi-mocks @elastic/kibana-core
packages/kbn-health-gateway-server @elastic/kibana-core
packages/kbn-i18n @elastic/kibana-core
packages/kbn-i18n-react @elastic/kibana-core
packages/kbn-import-locator @elastic/kibana-operations
packages/kbn-import-resolver @elastic/kibana-operations
packages/kbn-interpreter @elastic/kibana-visualizations
packages/kbn-io-ts-utils @elastic/apm-ui
packages/kbn-jest-serializers @elastic/kibana-operations
packages/kbn-journeys @elastic/kibana-operations
packages/kbn-json-ast @elastic/kibana-operations
packages/kbn-kibana-manifest-schema @elastic/kibana-operations
packages/kbn-language-documentation-popover @elastic/kibana-visualizations
packages/kbn-lint-packages-cli @elastic/kibana-operations
packages/kbn-lint-ts-projects-cli @elastic/kibana-operations
packages/kbn-logging @elastic/kibana-core
packages/kbn-logging-mocks @elastic/kibana-core
packages/kbn-managed-vscode-config @elastic/kibana-operations
@ -958,15 +961,18 @@ packages/kbn-monaco @elastic/kibana-global-experience
packages/kbn-optimizer @elastic/kibana-operations
packages/kbn-optimizer-webpack-helpers @elastic/kibana-operations
packages/kbn-osquery-io-ts-types @elastic/security-asset-management
packages/kbn-package-map @elastic/kibana-operations
packages/kbn-peggy @elastic/kibana-operations
packages/kbn-peggy-loader @elastic/kibana-operations
packages/kbn-performance-testing-dataset-extractor @elastic/kibana-performance-testing
packages/kbn-picomatcher @elastic/kibana-operations
packages/kbn-plugin-discovery @elastic/kibana-operations
packages/kbn-plugin-generator @elastic/kibana-operations
packages/kbn-plugin-helpers @elastic/kibana-operations
packages/kbn-react-field @elastic/kibana-app-services
packages/kbn-repo-file-maps @elastic/kibana-operations
packages/kbn-repo-info @elastic/kibana-operations
packages/kbn-repo-linter @elastic/kibana-operations
packages/kbn-repo-packages @elastic/kibana-operations
packages/kbn-repo-path @elastic/kibana-operations
packages/kbn-repo-source-classifier @elastic/kibana-operations
packages/kbn-repo-source-classifier-cli @elastic/kibana-operations
@ -990,6 +996,7 @@ packages/kbn-securitysolution-t-grid @elastic/security-solution-platform
packages/kbn-securitysolution-utils @elastic/security-solution-platform
packages/kbn-server-http-tools @elastic/kibana-core
packages/kbn-server-route-repository @elastic/apm-ui
packages/kbn-set-map @elastic/kibana-operations
packages/kbn-shared-svg @elastic/apm-ui
packages/kbn-shared-ux-utility @elastic/kibana-global-experience
packages/kbn-slo-schema @elastic/actionable-observability
@ -1006,8 +1013,6 @@ packages/kbn-test-subj-selector @elastic/kibana-operations
packages/kbn-timelion-grammar @elastic/kibana-visualizations
packages/kbn-tinymath @elastic/kibana-visualizations
packages/kbn-tooling-log @elastic/kibana-operations
packages/kbn-ts-project-linter @elastic/kibana-operations
packages/kbn-ts-project-linter-cli @elastic/kibana-operations
packages/kbn-ts-projects @elastic/kibana-operations
packages/kbn-ts-type-check-cli @elastic/kibana-operations
packages/kbn-typed-react-router-config @elastic/apm-ui

3
.gitignore vendored
View file

@ -110,7 +110,8 @@ elastic-agent-*
fleet-server-*
elastic-agent.yml
fleet-server.yml
/packages/kbn-package-map/package-map.json
/packages/*/package-map.json
/packages/*/config-paths.json
/packages/kbn-synthetic-package-map/
**/.synthetics/
**/.journeys/

View file

@ -14,9 +14,10 @@ import { haveNodeModulesBeenManuallyDeleted, removeYarnIntegrityFileIfExists } f
import { setupRemoteCache } from './setup_remote_cache.mjs';
import { sortPackageJson } from './sort_package_json.mjs';
import { regeneratePackageMap } from './regenerate_package_map.mjs';
import { regenerateTsconfigPaths } from './regenerate_tsconfig_paths.mjs';
import { regenerateBaseTsconfig } from './regenerate_base_tsconfig.mjs';
import { packageDiscovery, pluginDiscovery } from './discovery.mjs';
import { validatePackageJson } from './validate_package_json.mjs';
import { discovery } from './discovery.mjs';
import { updatePackageJson } from './update_package_json.mjs';
/** @type {import('../../lib/command').Command} */
export const command = {
@ -61,13 +62,33 @@ export const command = {
const forceInstall =
args.getBooleanValue('force-install') ?? (await haveNodeModulesBeenManuallyDeleted());
await Bazel.tryRemovingBazeliskFromYarnGlobal(log);
const [{ packages, plugins, tsConfigsPaths }] = await Promise.all([
// discover the location of packages, plugins, etc
await time('discovery', discovery),
// Install bazel machinery tools if needed
await Bazel.ensureInstalled(log);
(async () => {
await Bazel.tryRemovingBazeliskFromYarnGlobal(log);
// Setup remote cache settings in .bazelrc.cache if needed
await setupRemoteCache(log);
// Install bazel machinery tools if needed
await Bazel.ensureInstalled(log);
// Setup remote cache settings in .bazelrc.cache if needed
await setupRemoteCache(log);
})(),
]);
// generate the package map and package.json file, if necessary
await Promise.all([
time('regenerate package map', async () => {
await regeneratePackageMap(packages, plugins, log);
}),
time('regenerate tsconfig map', async () => {
await regenerateTsconfigPaths(tsConfigsPaths, log);
}),
time('update package json', async () => {
await updatePackageJson(packages, log);
}),
]);
// Bootstrap process for Bazel packages
// Bazel is now managing dependencies so yarn install
@ -85,34 +106,16 @@ export const command = {
});
}
// discover the location of packages and plugins
const [plugins, packages] = await Promise.all([
time('plugin discovery', pluginDiscovery),
time('package discovery', packageDiscovery),
]);
// generate the package map which powers the resolver and several other features
// needed as an input to the bazel builds
await time('regenerate package map', async () => {
await regeneratePackageMap(packages, plugins, log);
});
await time('pre-build webpack bundles for packages', async () => {
await Bazel.buildWebpackBundles(log, { offline, quiet });
});
await time('regenerate tsconfig.base.json', async () => {
await regenerateBaseTsconfig();
});
await Promise.all([
time('sort package json', async () => {
await sortPackageJson();
time('regenerate tsconfig.base.json', async () => {
await regenerateBaseTsconfig();
}),
time('validate package json', async () => {
// now that deps are installed we can import `@kbn/yarn-lock-validator`
const { kibanaPackageJson } = External['@kbn/repo-info']();
await validatePackageJson(kibanaPackageJson, log);
time('sort package json', async () => {
await sortPackageJson(log);
}),
validate
? time('validate dependencies', async () => {

View file

@ -6,33 +6,84 @@
* Side Public License, v 1.
*/
import Path from 'path';
import Fs from 'fs';
import ChildProcess from 'child_process';
import { promisify } from 'util';
import { REPO_ROOT } from '../../lib/paths.mjs';
const execAsync = promisify(ChildProcess.execFile);
// we need to run these in order to generate the pkg map which is used by things
// like `@kbn/babel-register`, so we have to import the JS files directory and can't
// rely on `@kbn/babel-register`.
export async function packageDiscovery() {
const { discoverBazelPackages } = await import(
// eslint-disable-next-line @kbn/imports/uniform_imports
'../../../../packages/kbn-bazel-packages/index.js'
);
return await discoverBazelPackages(REPO_ROOT);
}
export async function pluginDiscovery() {
export async function discovery() {
const { getPluginSearchPaths, simpleKibanaPlatformPluginDiscovery } = await import(
// eslint-disable-next-line @kbn/imports/uniform_imports
'../../../../packages/kbn-plugin-discovery/index.js'
);
const searchPaths = getPluginSearchPaths({
rootDir: REPO_ROOT,
examples: true,
oss: false,
testPlugins: true,
const { Package } = await import(
// we need to run this before we install node modules, so it can't rely on @kbn/* imports
// eslint-disable-next-line @kbn/imports/uniform_imports
'../../../../packages/kbn-repo-packages/index.js'
);
const proc = await execAsync('git', ['ls-files', '-comt', '--exclude-standard'], {
cwd: REPO_ROOT,
encoding: 'utf8',
maxBuffer: Infinity,
});
return simpleKibanaPlatformPluginDiscovery(searchPaths, []);
const paths = new Map();
/** @type {Map<string, Set<string>>} */
const filesByName = new Map();
for (const raw of proc.stdout.split('\n')) {
const line = raw.trim();
if (!line) {
continue;
}
const repoRel = line.slice(2); // trim the single char status and separating space from the line
const name = repoRel.split('/').pop();
if (name !== 'kibana.jsonc' && name !== 'tsconfig.json') {
continue;
}
const existingPath = paths.get(repoRel);
const path = existingPath ?? Path.resolve(REPO_ROOT, repoRel);
if (!existingPath) {
paths.set(repoRel, path);
}
let files = filesByName.get(name);
if (!files) {
files = new Set();
filesByName.set(name, files);
}
if (line.startsWith('C ')) {
// this line indicates that the previous path is changed in the working
// tree, so we need to determine if it was deleted and remove it if so
if (!Fs.existsSync(path)) {
files.delete(path);
}
} else {
files.add(path);
}
}
return {
plugins: simpleKibanaPlatformPluginDiscovery(
getPluginSearchPaths({
rootDir: REPO_ROOT,
examples: true,
oss: false,
testPlugins: true,
}),
[]
),
tsConfigsPaths: Array.from(filesByName.get('tsconfig.json') ?? new Set()),
packages: Array.from(filesByName.get('kibana.jsonc') ?? new Set())
.map((path) => Package.fromManifest(REPO_ROOT, path))
.sort((a, b) => a.id.localeCompare(b.id)),
};
}

View file

@ -13,7 +13,7 @@ import { REPO_ROOT } from '../../lib/paths.mjs';
import External from '../../lib/external_packages.js';
export async function regenerateBaseTsconfig() {
const pkgMap = External['@kbn/package-map']().readPackageMap();
const pkgMap = External['@kbn/repo-packages']().readPackageMap();
const tsconfigPath = Path.resolve(REPO_ROOT, 'tsconfig.base.json');
const lines = (await Fsp.readFile(tsconfigPath, 'utf-8')).split('\n');

View file

@ -16,18 +16,12 @@ import { REPO_ROOT } from '../../lib/paths.mjs';
/**
*
* @param {import('@kbn/bazel-packages').BazelPackage[]} packages
* @param {import('@kbn/repo-packages').Package[]} packages
* @param {import('@kbn/plugin-discovery').KibanaPlatformPlugin[]} plugins
* @param {import('@kbn/some-dev-log').SomeDevLog} log
*/
export async function regeneratePackageMap(packages, plugins, log) {
// clean up old version of package map package
Fs.rmSync(Path.resolve(REPO_ROOT, 'packages/kbn-synthetic-package-map'), {
recursive: true,
force: true,
});
const path = Path.resolve(REPO_ROOT, 'packages/kbn-package-map/package-map.json');
const path = Path.resolve(REPO_ROOT, 'packages/kbn-repo-packages/package-map.json');
const existingContent = Fs.existsSync(path) ? await Fsp.readFile(path, 'utf8') : undefined;
/** @type {Array<[string, string]>} */
@ -52,6 +46,6 @@ export async function regeneratePackageMap(packages, plugins, log) {
if (content !== existingContent) {
await Fsp.writeFile(path, content);
log.warning('updated package map, many caches may be invalidated');
log.warning('updated package map');
}
}

View file

@ -0,0 +1,32 @@
/*
* 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';
import Fsp from 'fs/promises';
import { REPO_ROOT } from '../../lib/paths.mjs';
/**
* @param {string[]} tsconfigPaths
* @param {import('@kbn/some-dev-log').SomeDevLog} log
*/
export async function regenerateTsconfigPaths(tsconfigPaths, log) {
const path = Path.resolve(REPO_ROOT, 'packages/kbn-ts-projects/config-paths.json');
const existingContent = Fs.existsSync(path) ? await Fsp.readFile(path, 'utf8') : undefined;
const entries = [...tsconfigPaths]
.map((abs) => Path.relative(REPO_ROOT, abs))
.sort((a, b) => a.localeCompare(b));
const content = JSON.stringify(entries, null, 2);
if (content !== existingContent) {
await Fsp.writeFile(path, content);
log.warning('updated tsconfig.json paths');
}
}

View file

@ -7,15 +7,23 @@
*/
import Path from 'path';
import Fs from 'fs';
import Fsp from 'fs/promises';
import { REPO_ROOT } from '../../lib/paths.mjs';
import External from '../../lib/external_packages.js';
export async function sortPackageJson() {
/**
*
* @param {import('@kbn/some-dev-log').SomeDevLog} log
*/
export async function sortPackageJson(log) {
const { sortPackageJson } = External['@kbn/sort-package-json']();
const path = Path.resolve(REPO_ROOT, 'package.json');
const json = Fs.readFileSync(path, 'utf8');
Fs.writeFileSync(path, sortPackageJson(json));
const json = await Fsp.readFile(path, 'utf8');
const sorted = sortPackageJson(json);
if (sorted !== json) {
await Fsp.writeFile(path, sorted, 'utf8');
log.success('sorted package.json');
}
}

View file

@ -0,0 +1,86 @@
/*
* 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 Fsp from 'fs/promises';
import Path from 'path';
import { REPO_ROOT } from '../../lib/paths.mjs';
/**
* @param {import('@kbn/repo-info').KibanaPackageJson['dependencies']} depsObj
* @param {Map<string, string>} actual
* @param {Map<string, string>} expected
*/
function updatePkgEntries(depsObj, actual, expected) {
let changes = false;
const keys = new Set([...actual.keys(), ...expected.keys()]);
for (const key of keys) {
const a = actual.get(key);
const e = expected.get(key);
// if expected and actual match then we don't need to do anything
if (a === e) {
continue;
}
changes = true;
// if expected is undefined then this key shouldn't be set
if (e === undefined) {
delete depsObj[key];
continue;
}
// otherwise we just need to update/add this key/value
depsObj[key] = e;
}
return changes;
}
/**
* @param {import('@kbn/repo-packages').Package[]} pkgs
* @param {import('@kbn/some-dev-log').SomeDevLog} log
*/
export async function updatePackageJson(pkgs, log) {
const path = Path.resolve(REPO_ROOT, 'package.json');
/** @type {import('@kbn/repo-info').KibanaPackageJson} */
const pkgJson = JSON.parse(await Fsp.readFile(path, 'utf8'));
let changes = false;
const typesInProd = Object.keys(pkgJson.dependencies).filter((id) => id.startsWith('@types/'));
for (const t of typesInProd) {
changes = true;
pkgJson.devDependencies[t] = pkgJson.dependencies[t];
delete pkgJson.dependencies[t];
}
changes ||= updatePkgEntries(
pkgJson.dependencies,
new Map(Object.entries(pkgJson.dependencies).filter(([k]) => k.startsWith('@kbn/'))),
new Map(
pkgs
.filter((p) => !p.isDevOnly)
.map((p) => [p.manifest.id, `link:${p.normalizedRepoRelativeDir}`])
)
);
changes ||= updatePkgEntries(
pkgJson.devDependencies,
new Map(Object.entries(pkgJson.devDependencies).filter(([k]) => k.startsWith('@kbn/'))),
new Map(
pkgs
.filter((p) => p.isDevOnly)
.map((p) => [p.manifest.id, `link:${p.normalizedRepoRelativeDir}`])
)
);
if (changes) {
await Fsp.writeFile(path, JSON.stringify(pkgJson, null, 2));
log.warning('updated package.json');
}
}

View file

@ -40,8 +40,8 @@ export const command = {
const exclude = args.getStringValues('exclude') ?? [];
const include = args.getStringValues('include') ?? [];
const { discoverBazelPackages } = External['@kbn/bazel-packages']();
const packages = await discoverBazelPackages(REPO_ROOT);
const { getPackages } = External['@kbn/repo-packages']();
const packages = getPackages(REPO_ROOT);
for (const { manifest, pkg, normalizedRepoRelativeDir } of packages) {
if (
exclude.includes(manifest.id) ||

View file

@ -6,9 +6,9 @@
* Side Public License, v 1.
*/
module.exports = {
['@kbn/bazel-packages']() {
['@kbn/repo-packages']() {
require('@kbn/babel-register').install();
return require('@kbn/bazel-packages');
return require('@kbn/repo-packages');
},
['@kbn/ci-stats-reporter']() {
@ -26,11 +26,6 @@ module.exports = {
return require('@kbn/sort-package-json');
},
['@kbn/package-map']() {
require('@kbn/babel-register').install();
return require('@kbn/package-map');
},
['@kbn/get-repo-files']() {
require('@kbn/babel-register').install();
return require('@kbn/get-repo-files');

View file

@ -16,11 +16,11 @@ import External from './external_packages.js';
* Attempt to load the package map, if bootstrap hasn't run successfully
* this might fail.
* @param {import('@kbn/some-dev-log').SomeDevLog} log
* @returns {Promise<import('@kbn/package-map').PackageMap>}
* @returns {Promise<import('@kbn/repo-packages').PackageMap>}
*/
async function tryToGetPackageMap(log) {
try {
const { readPackageMap } = External['@kbn/package-map']();
const { readPackageMap } = External['@kbn/repo-packages']();
return readPackageMap();
} catch (error) {
log.warning('unable to load package map, unable to clean target directories in packages');

View file

@ -15,14 +15,13 @@
],
"kbn_references": [
"@kbn/babel-register",
"@kbn/bazel-packages",
"@kbn/repo-info",
"@kbn/yarn-lock-validator",
"@kbn/get-repo-files",
"@kbn/sort-package-json",
{ "path": "../src/dev/tsconfig.json" },
"@kbn/ci-stats-reporter",
"@kbn/package-map",
"@kbn/ts-projects"
"@kbn/ts-projects",
"@kbn/repo-packages"
]
}

View file

@ -148,8 +148,8 @@
"@kbn/config": "link:packages/kbn-config",
"@kbn/config-mocks": "link:packages/kbn-config-mocks",
"@kbn/config-schema": "link:packages/kbn-config-schema",
"@kbn/content-management-content-editor": "link:bazel-bin/packages/content-management/content_editor",
"@kbn/content-management-table-list": "link:bazel-bin/packages/content-management/table_list",
"@kbn/content-management-content-editor": "link:packages/content-management/content_editor",
"@kbn/content-management-table-list": "link:packages/content-management/table_list",
"@kbn/core-analytics-browser": "link:packages/core/analytics/core-analytics-browser",
"@kbn/core-analytics-browser-internal": "link:packages/core/analytics/core-analytics-browser-internal",
"@kbn/core-analytics-browser-mocks": "link:packages/core/analytics/core-analytics-browser-mocks",
@ -179,7 +179,6 @@
"@kbn/core-chrome-browser-internal": "link:packages/core/chrome/core-chrome-browser-internal",
"@kbn/core-chrome-browser-mocks": "link:packages/core/chrome/core-chrome-browser-mocks",
"@kbn/core-config-server-internal": "link:packages/core/config/core-config-server-internal",
"@kbn/core-config-server-mocks": "link:packages/core/config/core-config-server-mocks",
"@kbn/core-deprecations-browser": "link:packages/core/deprecations/core-deprecations-browser",
"@kbn/core-deprecations-browser-internal": "link:packages/core/deprecations/core-deprecations-browser-internal",
"@kbn/core-deprecations-browser-mocks": "link:packages/core/deprecations/core-deprecations-browser-mocks",
@ -193,7 +192,6 @@
"@kbn/core-doc-links-server": "link:packages/core/doc-links/core-doc-links-server",
"@kbn/core-doc-links-server-internal": "link:packages/core/doc-links/core-doc-links-server-internal",
"@kbn/core-doc-links-server-mocks": "link:packages/core/doc-links/core-doc-links-server-mocks",
"@kbn/core-elasticsearch-client-server": "link:packages/core/elasticsearch/core-elasticsearch-client-server",
"@kbn/core-elasticsearch-client-server-internal": "link:packages/core/elasticsearch/core-elasticsearch-client-server-internal",
"@kbn/core-elasticsearch-client-server-mocks": "link:packages/core/elasticsearch/core-elasticsearch-client-server-mocks",
"@kbn/core-elasticsearch-server": "link:packages/core/elasticsearch/core-elasticsearch-server",
@ -361,6 +359,7 @@
"@kbn/plugin-discovery": "link:packages/kbn-plugin-discovery",
"@kbn/react-field": "link:packages/kbn-react-field",
"@kbn/repo-info": "link:packages/kbn-repo-info",
"@kbn/repo-packages": "link:packages/kbn-repo-packages",
"@kbn/rison": "link:packages/kbn-rison",
"@kbn/rule-data-utils": "link:packages/kbn-rule-data-utils",
"@kbn/safer-lodash-set": "link:packages/kbn-safer-lodash-set",
@ -381,6 +380,7 @@
"@kbn/securitysolution-utils": "link:packages/kbn-securitysolution-utils",
"@kbn/server-http-tools": "link:packages/kbn-server-http-tools",
"@kbn/server-route-repository": "link:packages/kbn-server-route-repository",
"@kbn/set-map": "link:packages/kbn-set-map",
"@kbn/shared-svg": "link:packages/kbn-shared-svg",
"@kbn/shared-ux-avatar-solution": "link:packages/shared-ux/avatar/solution",
"@kbn/shared-ux-avatar-user-profile-components": "link:packages/shared-ux/avatar/user_profile/impl",
@ -425,9 +425,10 @@
"@kbn/shared-ux-prompt-no-data-views-mocks": "link:packages/shared-ux/prompt/no_data_views/mocks",
"@kbn/shared-ux-prompt-no-data-views-types": "link:packages/shared-ux/prompt/no_data_views/types",
"@kbn/shared-ux-prompt-not-found": "link:packages/shared-ux/prompt/not_found",
"@kbn/shared-ux-router": "link:packages/shared-ux/router/impl",
"@kbn/shared-ux-router-mocks": "link:packages/shared-ux/router/mocks",
"@kbn/shared-ux-services": "link:packages/kbn-shared-ux-services",
"@kbn/shared-ux-storybook": "link:packages/kbn-shared-ux-storybook",
"@kbn/shared-ux-router-types": "link:packages/shared-ux/router/types",
"@kbn/shared-ux-storybook-config": "link:packages/shared-ux/storybook/config",
"@kbn/shared-ux-storybook-mock": "link:packages/shared-ux/storybook/mock",
"@kbn/shared-ux-utility": "link:packages/kbn-shared-ux-utility",
"@kbn/slo-schema": "link:packages/kbn-slo-schema",
@ -748,7 +749,6 @@
"@kbn/babel-preset": "link:packages/kbn-babel-preset",
"@kbn/babel-register": "link:packages/kbn-babel-register",
"@kbn/babel-transform": "link:packages/kbn-babel-transform",
"@kbn/bazel-packages": "link:packages/kbn-bazel-packages",
"@kbn/bazel-runner": "link:packages/kbn-bazel-runner",
"@kbn/ci-stats-core": "link:packages/kbn-ci-stats-core",
"@kbn/ci-stats-performance-metrics": "link:packages/kbn-ci-stats-performance-metrics",
@ -774,20 +774,26 @@
"@kbn/ftr-screenshot-filename": "link:packages/kbn-ftr-screenshot-filename",
"@kbn/generate": "link:packages/kbn-generate",
"@kbn/get-repo-files": "link:packages/kbn-get-repo-files",
"@kbn/import-locator": "link:packages/kbn-import-locator",
"@kbn/import-resolver": "link:packages/kbn-import-resolver",
"@kbn/jest-serializers": "link:packages/kbn-jest-serializers",
"@kbn/journeys": "link:packages/kbn-journeys",
"@kbn/json-ast": "link:packages/kbn-json-ast",
"@kbn/kibana-manifest-schema": "link:packages/kbn-kibana-manifest-schema",
"@kbn/lint-packages-cli": "link:packages/kbn-lint-packages-cli",
"@kbn/lint-ts-projects-cli": "link:packages/kbn-lint-ts-projects-cli",
"@kbn/managed-vscode-config": "link:packages/kbn-managed-vscode-config",
"@kbn/managed-vscode-config-cli": "link:packages/kbn-managed-vscode-config-cli",
"@kbn/optimizer": "link:packages/kbn-optimizer",
"@kbn/optimizer-webpack-helpers": "link:packages/kbn-optimizer-webpack-helpers",
"@kbn/package-map": "link:packages/kbn-package-map",
"@kbn/peggy": "link:packages/kbn-peggy",
"@kbn/peggy-loader": "link:packages/kbn-peggy-loader",
"@kbn/performance-testing-dataset-extractor": "link:packages/kbn-performance-testing-dataset-extractor",
"@kbn/picomatcher": "link:packages/kbn-picomatcher",
"@kbn/plugin-generator": "link:packages/kbn-plugin-generator",
"@kbn/plugin-helpers": "link:packages/kbn-plugin-helpers",
"@kbn/repo-file-maps": "link:packages/kbn-repo-file-maps",
"@kbn/repo-linter": "link:packages/kbn-repo-linter",
"@kbn/repo-path": "link:packages/kbn-repo-path",
"@kbn/repo-source-classifier": "link:packages/kbn-repo-source-classifier",
"@kbn/repo-source-classifier-cli": "link:packages/kbn-repo-source-classifier-cli",
@ -801,8 +807,6 @@
"@kbn/test-jest-helpers": "link:packages/kbn-test-jest-helpers",
"@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector",
"@kbn/tooling-log": "link:packages/kbn-tooling-log",
"@kbn/ts-project-linter": "link:packages/kbn-ts-project-linter",
"@kbn/ts-project-linter-cli": "link:packages/kbn-ts-project-linter-cli",
"@kbn/ts-projects": "link:packages/kbn-ts-projects",
"@kbn/ts-type-check-cli": "link:packages/kbn-ts-type-check-cli",
"@kbn/web-worker-stub": "link:packages/kbn-web-worker-stub",
@ -931,6 +935,7 @@
"@types/pbf": "3.0.2",
"@types/pdfmake": "^0.2.2",
"@types/pegjs": "^0.10.1",
"@types/picomatch": "^2.3.0",
"@types/pidusage": "^2.0.2",
"@types/pixelmatch": "^5.2.4",
"@types/pngjs": "^3.4.0",
@ -1116,6 +1121,7 @@
"openapi-types": "^10.0.0",
"pbf": "3.2.1",
"peggy": "^1.2.0",
"picomatch": "^2.3.1",
"pidusage": "^3.0.2",
"pirates": "^4.0.1",
"piscina": "^3.2.0",
@ -1172,4 +1178,4 @@
"xmlbuilder": "13.0.2",
"yargs": "^15.4.1"
}
}
}

View file

@ -10,7 +10,7 @@
},
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.tsx",
],
"kbn_references": [
"@kbn/core-mount-utils-browser",

View file

@ -9,7 +9,7 @@
},
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.tsx",
],
"kbn_references": [
"@kbn/utility-types",

View file

@ -9,7 +9,7 @@
},
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.tsx",
],
"kbn_references": [
"@kbn/i18n"

View file

@ -10,7 +10,7 @@
},
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.tsx",
],
"kbn_references": [
"@kbn/i18n",

View file

@ -9,7 +9,7 @@
},
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.tsx",
],
"kbn_references": [
"@kbn/std",

View file

@ -9,7 +9,7 @@
},
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.tsx",
],
"kbn_references": [
"@kbn/core-lifecycle-browser",

View file

@ -9,7 +9,7 @@
},
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.tsx",
],
"kbn_references": [
"@kbn/core-theme-browser",

View file

@ -10,7 +10,7 @@
},
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.tsx",
],
"kbn_references": [
"@kbn/logging-mocks",

View file

@ -10,7 +10,7 @@
},
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.tsx",
],
"kbn_references": [
"@kbn/core-application-common",

View file

@ -19,7 +19,7 @@ BUNDLER_DEPS = [
"@npm//@babel/helper-plugin-utils",
"@npm//normalize-path",
"//packages/kbn-repo-info",
"//packages/kbn-package-map",
"//packages/kbn-repo-packages",
]
js_library(

View file

@ -11,10 +11,10 @@ const Path = require('path');
const T = require('@babel/types');
const normalizePath = require('normalize-path');
const { declare } = require('@babel/helper-plugin-utils');
const KbnSyntheticPackageMap = require('@kbn/package-map');
const { readPackageMap } = require('@kbn/repo-packages');
const { REPO_ROOT } = require('@kbn/repo-info');
const PKG_MAP = KbnSyntheticPackageMap.readPackageMap();
const PKG_MAP = readPackageMap();
/**
* @param {unknown} v

View file

@ -17,6 +17,6 @@
],
"kbn_references": [
"@kbn/repo-info",
"@kbn/package-map"
"@kbn/repo-packages"
]
}

View file

@ -36,7 +36,7 @@ BUNDLER_DEPS = [
"@npm//pirates",
"@npm//lmdb",
"@npm//source-map-support",
"//packages/kbn-package-map",
"//packages/kbn-repo-packages",
"//packages/kbn-repo-info",
"//packages/kbn-babel-transform",
]

View file

@ -10,7 +10,7 @@ const Fs = require('fs');
const Path = require('path');
const Crypto = require('crypto');
const { readHashOfPackageMap } = require('@kbn/package-map');
const { readHashOfPackageMap } = require('@kbn/repo-packages');
const babel = require('@babel/core');
const peggy = require('@kbn/peggy');
const { REPO_ROOT, UPSTREAM_BRANCH } = require('@kbn/repo-info');

View file

@ -13,10 +13,10 @@
"**/*.ts",
],
"kbn_references": [
"@kbn/package-map",
"@kbn/repo-info",
"@kbn/babel-transform",
"@kbn/peggy",
"@kbn/repo-packages",
],
"exclude": [
"target/**/*",

View file

@ -1,3 +0,0 @@
# @kbn/bazel-packages
APIs for dealing with bazel packages in the Kibana repo

View file

@ -1,32 +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.
*/
/** @typedef {import('./src/bazel_package').BazelPackage} BazelPackage */
/** @typedef {import('./src/types').KibanaPackageManifest} KibanaPackageManifest */
/** @typedef {import('./src/types').KibanaPackageType} KibanaPackageType */
/** @typedef {import('./src/types').ParsedPackageJson} ParsedPackageJson */
const { BAZEL_PACKAGE_DIRS, getAllBazelPackageDirs } = require('./src/bazel_package_dirs');
const { discoverPackageManifestPaths, discoverBazelPackages } = require('./src/discover_packages');
const {
parsePackageManifest,
readPackageManifest,
validatePackageManifest,
} = require('./src/parse_package_manifest');
const Jsonc = require('./src/jsonc');
module.exports = {
BAZEL_PACKAGE_DIRS,
getAllBazelPackageDirs,
discoverPackageManifestPaths,
discoverBazelPackages,
parsePackageManifest,
readPackageManifest,
validatePackageManifest,
Jsonc,
};

View file

@ -1,97 +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.
*/
const { inspect } = require('util');
const Path = require('path');
const { readPackageJson } = require('./parse_package_json');
const { readPackageManifest } = require('./parse_package_manifest');
/**
* Representation of a Bazel Package in the Kibana repository
* @class
* @property {string} normalizedRepoRelativeDir
* @property {import('./types').KibanaPackageManifest} manifest
* @property {import('./types').ParsedPackageJson | undefined} pkg
*/
class BazelPackage {
/**
* Create a BazelPackage object from a package directory. Reads some files from the package and returns
* a Promise for a BazelPackage instance.
* @param {string} repoRoot
* @param {string} path
*/
static async fromManifest(repoRoot, path) {
const manifest = readPackageManifest(path);
const dir = Path.dirname(path);
return new BazelPackage(
Path.relative(repoRoot, dir),
manifest,
readPackageJson(Path.resolve(dir, 'package.json'))
);
}
/**
* Sort a list of bazek packages
* @param {BazelPackage[]} pkgs
*/
static sort(pkgs) {
return pkgs.slice().sort(BazelPackage.sorter);
}
/**
* Sort an array of bazel packages
* @param {BazelPackage} a
* @param {BazelPackage} b
*/
static sorter(a, b) {
return a.normalizedRepoRelativeDir.localeCompare(b.normalizedRepoRelativeDir);
}
constructor(
/**
* Relative path from the root of the repository to the package directory
* @type {string}
*/
normalizedRepoRelativeDir,
/**
* Parsed kibana.jsonc manifest from the package
* @type {import('./types').KibanaPackageManifest}
*/
manifest,
/**
* Parsed package.json file from the package
* @type {import('./types').ParsedPackageJson | undefined}
*/
pkg
) {
this.normalizedRepoRelativeDir = normalizedRepoRelativeDir;
this.manifest = manifest;
this.pkg = pkg;
}
/**
* Returns true if the package is not intended to be in the build
*/
isDevOnly() {
return !!this.manifest.devOnly;
}
/**
* Custom inspect handler so that logging variables in scripts/generate doesn't
* print all the BUILD.bazel files
*/
[inspect.custom]() {
return `BazelPackage<${this.normalizedRepoRelativeDir}>`;
}
}
module.exports = {
BazelPackage,
};

View file

@ -1,44 +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.
*/
const { expandWildcards } = require('./find_files');
/**
* This is a list of repo-relative paths to directories containing packages. Do not
* include `**` in these, one or two `*` segments is acceptable, we need this search
* to be super fast so please avoid deep recursive searching.
*
* eg. src/vis_editors => would find a package at src/vis_editors/foo/package.json
* src/vis_editors/* => would find a package at src/vis_editors/foo/bar/package.json
*/
const BAZEL_PACKAGE_DIRS = [
'packages',
'packages/shared-ux',
'packages/shared-ux/*',
'packages/shared-ux/*/*',
'packages/analytics',
'packages/analytics/shippers',
'packages/analytics/shippers/elastic_v3',
'packages/core/*',
'packages/home',
'packages/content-management',
'x-pack/packages/ml',
];
/**
* Resolve all the BAZEL_PACKAGE_DIRS to absolute paths
* @param {string} repoRoot
*/
function getAllBazelPackageDirs(repoRoot) {
return expandWildcards(repoRoot, BAZEL_PACKAGE_DIRS);
}
module.exports = {
BAZEL_PACKAGE_DIRS,
getAllBazelPackageDirs,
};

View file

@ -1,39 +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.
*/
const { BazelPackage } = require('./bazel_package');
const { getAllBazelPackageDirs } = require('./bazel_package_dirs');
const { findPackages } = require('./find_files');
const { asyncMapWithLimit } = require('./async');
/**
* Returns an array of all the package manifest paths in the repository
* @param {string} repoRoot
*/
function discoverPackageManifestPaths(repoRoot) {
return getAllBazelPackageDirs(repoRoot)
.flatMap((packageDir) => findPackages(packageDir, 'kibana.jsonc'))
.sort((a, b) => a.localeCompare(b));
}
/**
* Resolves to an array of BazelPackage instances which parse the manifest files,
* package.json files, and provide useful metadata about each package.
* @param {string} repoRoot
*/
async function discoverBazelPackages(repoRoot) {
return BazelPackage.sort(
await asyncMapWithLimit(
discoverPackageManifestPaths(repoRoot),
100,
async (path) => await BazelPackage.fromManifest(repoRoot, path)
)
);
}
module.exports = { discoverPackageManifestPaths, discoverBazelPackages };

View file

@ -115,6 +115,7 @@ export class CliDevMode {
}
const { watchPaths, ignorePaths } = getServerWatchPaths({
runExamples: cliArgs.runExamples,
pluginPaths: config.plugins.additionalPluginPaths,
pluginScanDirs: config.plugins.pluginSearchPaths,
});

View file

@ -10,13 +10,48 @@ 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'),
@ -33,14 +68,9 @@ it('produces the right watch and ignore list', () => {
<absolute path>/src/plugins,
<absolute path>/test/plugin_functional/plugins,
<absolute path>/x-pack/plugins,
<absolute path>/packages,
<absolute path>/packages/shared-ux,
<absolute path>/packages/analytics,
<absolute path>/packages/analytics/shippers,
<absolute path>/packages/analytics/shippers/elastic_v3,
<absolute path>/packages/home,
<absolute path>/packages/content-management,
<absolute path>/x-pack/packages/ml,
<absolute path>/packages/plugin-server,
<absolute path>/packages/shared-common,
<absolute path>/packages/shared-server,
]
`);
@ -89,4 +119,22 @@ it('produces the right watch and ignore list', () => {
<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,
],
},
],
]
`);
});

View file

@ -7,24 +7,24 @@
*/
import Path from 'path';
import Fs from 'fs';
import { REPO_ROOT } from '@kbn/repo-info';
import { BAZEL_PACKAGE_DIRS } from '@kbn/bazel-packages';
import { getPackages, getPluginPackagesFilter } from '@kbn/repo-packages';
interface Options {
runExamples: boolean;
pluginPaths: string[];
pluginScanDirs: string[];
}
export type WatchPaths = ReturnType<typeof getServerWatchPaths>;
export function getServerWatchPaths({ pluginPaths, pluginScanDirs }: Options) {
export function getServerWatchPaths(opts: Options) {
const fromRoot = (p: string) => Path.resolve(REPO_ROOT, p);
const pluginInternalDirsIgnore = pluginScanDirs
const pluginInternalDirsIgnore = opts.pluginScanDirs
.map((scanDir) => Path.resolve(scanDir, '*'))
.concat(pluginPaths)
.concat(opts.pluginPaths)
.reduce(
(acc: string[], path) => [
...acc,
@ -38,19 +38,33 @@ export function getServerWatchPaths({ pluginPaths, pluginScanDirs }: Options) {
[]
);
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('src/legacy/server'),
fromRoot('src/legacy/utils'),
fromRoot('config'),
...pluginPaths,
...pluginScanDirs,
...BAZEL_PACKAGE_DIRS,
].map((path) => Path.resolve(path))
)
).filter((path) => Fs.existsSync(fromRoot(path)));
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)([\\\/]|$)/,

View file

@ -20,8 +20,8 @@
"@kbn/ci-stats-reporter",
"@kbn/jest-serializers",
"@kbn/stdio-dev-helpers",
"@kbn/bazel-packages",
"@kbn/tooling-log",
"@kbn/repo-packages",
],
"exclude": [
"target/**/*",

View file

@ -10,7 +10,7 @@ import Fsp from 'fs/promises';
import Path from 'path';
import { REPO_ROOT } from '@kbn/repo-info';
import { discoverBazelPackages } from '@kbn/bazel-packages';
import { getPackages } from '@kbn/repo-packages';
import type { GenerateCommand } from '../generate_command';
@ -33,7 +33,7 @@ export const CodeownersCommand: GenerateCommand = {
async run({ log }) {
const coPath = Path.resolve(REPO_ROOT, REL);
const codeowners = await Fsp.readFile(coPath, 'utf8');
const pkgs = await discoverBazelPackages(REPO_ROOT);
const pkgs = getPackages(REPO_ROOT);
let genStart = codeowners.indexOf(GENERATED_START);
if (genStart === -1) {

View file

@ -13,9 +13,7 @@ import normalizePath from 'normalize-path';
import globby from 'globby';
import { ESLint } from 'eslint';
import micromatch from 'micromatch';
import { REPO_ROOT } from '@kbn/repo-info';
import { BAZEL_PACKAGE_DIRS } from '@kbn/bazel-packages';
import { createFailError, createFlagError, isFailError } from '@kbn/dev-cli-errors';
import { sortPackageJson } from '@kbn/sort-package-json';
@ -38,10 +36,7 @@ export const PackageCommand: GenerateCommand = {
--dev Generate a package which is intended for dev-only use and can access things like devDependencies
--web Build webpack-compatible version of sources for this package. If your package is intended to be
used in the browser and Node.js then you need to opt-into these sources being created.
--dir Specify where this package will be written. The path must be a direct child of one of the
directories selected by the BAZEL_PACKAGE_DIRS const in @kbn/bazel-packages.
Valid locations for packages:
${BAZEL_PACKAGE_DIRS.map((dir) => ` ./${dir}/*\n`).join('')}
--dir Specify where this package will be written.
defaults to [./packages/{kebab-case-version-of-name}]
--force If the --dir already exists, delete it before generation
--owner Github username of the owner for this package, if this is not specified then you will be asked for
@ -74,13 +69,6 @@ ${BAZEL_PACKAGE_DIRS.map((dir) => ` ./${dir}/*\n`).join
const packageDir = flags.dir
? Path.resolve(`${flags.dir}`)
: Path.resolve(ROOT_PKG_DIR, pkgId.slice(1).replace('/', '-'));
const relContainingDir = Path.relative(REPO_ROOT, Path.dirname(packageDir));
if (!micromatch.isMatch(relContainingDir, BAZEL_PACKAGE_DIRS)) {
throw createFlagError(
'Invalid --dir selection. To setup a new --dir option extend the `BAZEL_PACKAGE_DIRS` const in `@kbn/bazel-packages` and make sure to rebuild.'
);
}
const normalizedRepoRelativeDir = normalizePath(Path.relative(REPO_ROOT, packageDir));
try {

View file

@ -11,12 +11,12 @@
"**/*.ts"
],
"kbn_references": [
"@kbn/bazel-packages",
"@kbn/sort-package-json",
"@kbn/dev-cli-runner",
"@kbn/repo-info",
"@kbn/dev-cli-errors",
"@kbn/tooling-log",
"@kbn/repo-packages",
],
"exclude": [
"target/**/*",

View file

@ -0,0 +1,3 @@
# @kbn/import-locator
ImportLocator is extracted from nx and used to very quickly find import statements in JS/TS code.

View file

@ -11,21 +11,19 @@
import Fsp from 'fs/promises';
import Ts from 'typescript';
import { RepoPath } from '@kbn/repo-path';
import { stripSourceCode } from './strip_source_code';
const EMPTY = new Set<string>();
export class TypeScriptImportLocator {
export class ImportLocator {
private readonly scanner: Ts.Scanner;
constructor() {
this.scanner = Ts.createScanner(Ts.ScriptTarget.Latest, false, Ts.LanguageVariant.JSX);
}
async get(path: RepoPath): Promise<Set<string>> {
const content = await Fsp.readFile(path.abs, 'utf8');
get(path: string, content: string): Set<string> {
const strippedContent = stripSourceCode(this.scanner, content);
if (strippedContent === '') {
return EMPTY;
@ -33,7 +31,7 @@ export class TypeScriptImportLocator {
const imports = new Set<string>();
const queue: Ts.Node[] = [
Ts.createSourceFile(path.abs, strippedContent, Ts.ScriptTarget.Latest, true),
Ts.createSourceFile(path, strippedContent, Ts.ScriptTarget.Latest, true),
];
const addNodeToQueue = (n: Ts.Node) => {
queue.push(n);
@ -76,4 +74,9 @@ export class TypeScriptImportLocator {
return imports;
}
async read(path: string): Promise<Set<string>> {
const content = await Fsp.readFile(path, 'utf8');
return this.get(path, content);
}
}

View file

@ -9,5 +9,5 @@
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-bazel-packages'],
roots: ['<rootDir>/packages/kbn-import-locator'],
};

View file

@ -1,6 +1,6 @@
{
"type": "shared-common",
"id": "@kbn/ts-project-linter",
"id": "@kbn/import-locator",
"owner": "@elastic/kibana-operations",
"devOnly": true
}

View file

@ -0,0 +1,7 @@
{
"name": "@kbn/import-locator",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0",
"main": "./import_locator"
}

View file

@ -3,13 +3,15 @@
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"include": [
"index.d.ts"
"**/*.ts",
],
"exclude": [
"target/**/*",
]
"target/**/*"
],
"kbn_references": []
}

View file

@ -10,9 +10,9 @@ import Path from 'path';
import Fs from 'fs';
import Resolve from 'resolve';
import { readPackageManifest, type KibanaPackageManifest } from '@kbn/bazel-packages';
import { readPackageManifest, type KibanaPackageManifest } from '@kbn/repo-packages';
import { REPO_ROOT } from '@kbn/repo-info';
import { readPackageMap, PackageMap } from '@kbn/package-map';
import { readPackageMap, PackageMap } from '@kbn/repo-packages';
import { safeStat, readFileSync } from './helpers/fs';
import { ResolveResult } from './resolve_result';

View file

@ -11,10 +11,9 @@
"**/*.ts"
],
"kbn_references": [
"@kbn/bazel-packages",
"@kbn/package-map",
"@kbn/repo-info",
"@kbn/jest-serializers",
"@kbn/repo-packages",
],
"exclude": [
"target/**/*",

View file

@ -0,0 +1,3 @@
# @kbn/json-ast
Tools for parsing and mutating JSON files without rewriting the whole file. JSON-C is also supported so that we can update kibana.jsonc and tsconfig.json files.

View file

@ -6,6 +6,13 @@
* Side Public License, v 1.
*/
export { removeCompilerOption, setCompilerOption } from './compiler_options';
export { setExclude } from './exclude';
export { addReferences, removeReferences, replaceReferences } from './references';
export { removeCompilerOption, setCompilerOption } from './src/compiler_options';
export {
addReferences,
removeReferences,
replaceReferences,
removeAllReferences,
} from './src/references';
export { setExtends } from './src/extends';
export { setProp } from './src/props';
export { snip } from './src/snip';

View file

@ -9,5 +9,5 @@
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-ts-project-linter'],
roots: ['<rootDir>/packages/kbn-json-ast'],
};

View file

@ -1,6 +1,6 @@
{
"type": "shared-common",
"id": "@kbn/ts-project-linter-cli",
"id": "@kbn/json-ast",
"owner": "@elastic/kibana-operations",
"devOnly": true
}

View file

@ -1,5 +1,5 @@
{
"name": "@kbn/bazel-packages",
"name": "@kbn/json-ast",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"

View file

@ -273,7 +273,6 @@ describe('setCompilerOptions()', () => {
`,
'skipLibCheck'
),
'outDir',
'foo/bar'
)
@ -289,4 +288,89 @@ describe('setCompilerOptions()', () => {
}"
`);
});
it('handles non-existent compiler options', () => {
expect(
setCompilerOption(
dedent`
{
"extends": "../../tsconfig.base.json",
"include": [
"expect.d.ts"
]
}
`,
'outDir',
'foo/bar'
)
).toMatchInlineSnapshot(`
"{
\\"extends\\": \\"../../tsconfig.base.json\\",
\\"compilerOptions\\": {
\\"outDir\\": \\"foo/bar\\"
},
\\"include\\": [
\\"expect.d.ts\\"
]
}"
`);
expect(
setCompilerOption(
dedent`
{
"include": [
"expect.d.ts"
],
"extends": "../../tsconfig.base.json"
}
`,
'outDir',
'foo/bar'
)
).toMatchInlineSnapshot(`
"{
\\"include\\": [
\\"expect.d.ts\\"
],
\\"extends\\": \\"../../tsconfig.base.json\\",
\\"compilerOptions\\": {
\\"outDir\\": \\"foo/bar\\"
}
}"
`);
expect(
setCompilerOption(
dedent`
{}
`,
'outDir',
'foo/bar'
)
).toMatchInlineSnapshot(`
"{
\\"compilerOptions\\": {
\\"outDir\\": \\"foo/bar\\"
}
}"
`);
expect(
setCompilerOption(
dedent`
{
"foo": "bar"
}
`,
'outDir',
'foo/bar'
)
).toMatchInlineSnapshot(`
"{
\\"compilerOptions\\": {
\\"outDir\\": \\"foo/bar\\"
},
\\"foo\\": \\"bar\\"
}"
`);
});
});

View file

@ -6,15 +6,17 @@
* Side Public License, v 1.
*/
import { Jsonc } from '@kbn/bazel-packages';
import { Jsonc } from '@kbn/repo-packages';
import { T } from './babel';
import { getAst } from './ast';
import { getEnds, getExpandedEnds } from './ends';
import { getProp, getEndOfLastProp } from './props';
import { snip } from './snip';
import { redentJson, stringify } from './json';
export function getCompilerOptions(source: string) {
const compilerOptions = getProp(getAst(source), 'compilerOptions');
export function getCompilerOptions(ast: T.ObjectExpression) {
const compilerOptions = getProp(ast, 'compilerOptions');
if (!compilerOptions) {
throw new Error('unable to find compilerOptions property');
}
@ -26,7 +28,29 @@ export function getCompilerOptions(source: string) {
}
export function setCompilerOption(source: string, name: string, value: any) {
const compilerOptions = getCompilerOptions(source);
const ast = getAst(source);
if (!getProp(ast, 'compilerOptions')) {
const firstProp = ast.properties.at(0);
if (!firstProp) {
return stringify({ compilerOptions: { [name]: value } });
}
const extendsProp = getProp(ast, 'extends');
if (extendsProp) {
const after = getEnds(extendsProp)[1];
return snip(source, [
[after, after, `,\n "compilerOptions": ${redentJson({ [name]: value })}`],
]);
}
const before = getEnds(firstProp)[0];
return snip(source, [
[before, before, `"compilerOptions": ${redentJson({ [name]: value })},\n `],
]);
}
const compilerOptions = getCompilerOptions(ast);
const existing = getProp(compilerOptions, name);
if (existing) {
@ -41,21 +65,7 @@ export function setCompilerOption(source: string, name: string, value: any) {
// convert to multiline
const orig = (Jsonc.parse(source) as any).compilerOptions;
const [start, end] = getEnds(compilerOptions);
return (
source.slice(0, start) +
JSON.stringify(
{
...orig,
[name]: value,
},
null,
2
)
.split('\n')
.map((l, i) => (i === 0 ? l : ` ${l}`))
.join('\n') +
source.slice(end)
);
return source.slice(0, start) + redentJson({ ...orig, [name]: value }) + source.slice(end);
}
const endOfLastProp = getEndOfLastProp(compilerOptions);
@ -64,11 +74,12 @@ export function setCompilerOption(source: string, name: string, value: any) {
left = left.slice(0, -1);
}
const right = source.slice(endOfLastProp);
return left + `,\n ${JSON.stringify(name)}: ${JSON.stringify(value)}` + right;
return left + `,\n ${JSON.stringify(name)}: ${redentJson(value, ' ')}` + right;
}
export function removeCompilerOption(source: string, name: string) {
const compilerOptions = getCompilerOptions(source);
const ast = getAst(source);
const compilerOptions = getCompilerOptions(ast);
const culprit = getProp(compilerOptions, name);
if (!culprit) {

View file

@ -8,66 +8,61 @@
import dedent from 'dedent';
import { setExclude } from './exclude';
import { setExtends } from './extends';
describe('setExclude()', () => {
it('overwrites previous formatting', () => {
describe('setExtends()', () => {
it('overrides the value of the extends key', () => {
expect(
setExclude(
setExtends(
dedent`
{
"exclude": [1, 2,
"foo"
]
"foo": "bar",
"extends": "foo",
"x": 1
}
`,
['1', 'bar']
'new value'
)
).toMatchInlineSnapshot(`
"{
\\"exclude\\": [
\\"1\\",
\\"bar\\",
]
\\"foo\\": \\"bar\\",
\\"extends\\": \\"new value\\",
\\"x\\": 1
}"
`);
});
it('adds the property at the end if it does not exist', () => {
it('adds missing values at the top of the object', () => {
expect(
setExclude(
setExtends(
dedent`
{
"foo": 1
"foo": "bar",
"x": 1
}
`,
['1', 'bar']
'new value'
)
).toMatchInlineSnapshot(`
"{
\\"foo\\": 1,
\\"exclude\\": [
\\"1\\",
\\"bar\\",
]
\\"extends\\": \\"new value\\",
\\"foo\\": \\"bar\\",
\\"x\\": 1
}"
`);
});
it('supports setting on an empty object', () => {
expect(
setExclude(
setExtends(
dedent`
{
"foo": 1,
}
{}
`,
['1', 'bar']
'new value'
)
).toMatchInlineSnapshot(`
"{
\\"foo\\": 1,
\\"exclude\\": [
\\"1\\",
\\"bar\\",
],
\\"extends\\": \\"new value\\"
}"
`);
});

View file

@ -0,0 +1,15 @@
/*
* 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 { setProp } from './props';
export function setExtends(jsonc: string, value: string) {
return setProp(jsonc, 'extends', value, {
insertAtTop: true,
});
}

View file

@ -0,0 +1,51 @@
/*
* 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 { redent, redentJson, stringify } from './json';
describe('redent()', () => {
it('indents all but the first line of a string', () => {
expect(redent('a\nb\nc', ' ')).toMatchInlineSnapshot(`
"a
b
c"
`);
});
});
describe('redentJson()', () => {
it('indents all but the first line of the JSON representation of a value', () => {
expect(redentJson({ a: 1, b: 2, foo: [1, 2, 3] }, ' ')).toMatchInlineSnapshot(`
"{
\\"a\\": 1,
\\"b\\": 2,
\\"foo\\": [
1,
2,
3
]
}"
`);
});
});
describe('stringify()', () => {
it('stringifies value into pretty JSON', () => {
expect(stringify({ a: 1, b: 2, foo: [1, 2, 3] })).toMatchInlineSnapshot(`
"{
\\"a\\": 1,
\\"b\\": 2,
\\"foo\\": [
1,
2,
3
]
}"
`);
});
});

View file

@ -6,13 +6,17 @@
* Side Public License, v 1.
*/
export type PackageMap = Map<string, string>;
export function redent(text: string, spaces: string) {
return text
.split('\n')
.map((l, i) => (i === 0 ? l : `${spaces}${l}`))
.join('\n');
}
/**
* Read the package map from disk
*/
export function readPackageMap(): PackageMap;
/**
* Read the package map and calculate a cache key/hash of the package map
*/
export function readHashOfPackageMap(): string;
export function stringify(obj: any) {
return JSON.stringify(obj, null, 2);
}
export function redentJson(obj: any, spaces: string = ' ') {
return redent(stringify(obj), spaces);
}

View file

@ -0,0 +1,65 @@
/*
* 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 { getAst } from './ast';
import { stringify, redentJson } from './json';
import { snip } from './snip';
import { T } from './babel';
import { getEnds } from './ends';
export function getProp(obj: T.ObjectExpression, name: string) {
return obj.properties.find((p): p is T.ObjectProperty & { key: T.StringLiteral } => {
return T.isObjectProperty(p) && T.isStringLiteral(p.key) && p.key.value === name;
});
}
export function getEndOfLastProp(obj: T.ObjectExpression) {
if (obj.properties.length === 0) {
throw new Error('object has no properties');
}
return obj.properties.reduce((acc, prop) => Math.max(acc, getEnds(prop)[1]), 0);
}
export function setProp(
/** the jsonc to modify */
source: string,
/** The key to set */
key: string,
/** the value of the key */
value: any,
opts?: {
/** by default, if the key isn't already in the json, it will be added at the bottom. Set this to true to add the key at the top instead */
insertAtTop?: boolean;
/** In order to set the property an object other than the root object, parse the source and pass the node of the desired object here (make sure to also pass spaces) */
node?: T.ObjectExpression;
/** This overrides the default " " spacing used for multi line or new properties that are added */
spaces?: string;
}
) {
const ast = opts?.node ?? getAst(source);
const prop = getProp(ast, key);
const spaces = opts?.spaces ?? ' ';
const newPropJson = `${stringify(key)}: ${redentJson(value, spaces)}`;
if (!prop) {
if (!ast.properties.length) {
return `{\n${spaces}${newPropJson}\n}`;
}
if (opts?.insertAtTop) {
const [start] = getEnds(ast.properties[0]);
return snip(source, [[start, start, `${newPropJson},\n${spaces}`]]);
}
const endOfLastProp = getEndOfLastProp(ast);
return snip(source, [[endOfLastProp, endOfLastProp, `,\n${spaces}${newPropJson}`]]);
}
return snip(source, [[...getEnds(prop), newPropJson]]);
}

View file

@ -56,6 +56,15 @@ export function addReferences(source: string, refsToAdd: string[]) {
return source.slice(0, start) + refsSrc + source.slice(end);
}
export function removeAllReferences(source: string) {
const ast = getAst(source);
const existing = getProp(ast, PROP);
if (!existing) {
return source;
}
return snip(source, [getExpandedEnds(source, existing)]);
}
export function removeReferences(source: string, refs: string[]) {
const ast = getAst(source);

View file

@ -6,7 +6,11 @@
* Side Public License, v 1.
*/
type Snip = [number, number] | [number, number, string];
type Snip = [start: number, end: number] | [start: number, end: number, replacement: string];
/**
* Replace or remove specific points of the source code
*/
export function snip(source: string, snips: Snip[]) {
const queue = snips
.map((s): Snip => {

View file

@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.ts",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/repo-packages",
]
}

View file

@ -0,0 +1,3 @@
# @kbn/package-linter-cli
CLI for running the package linter, which just validates a couple rules for each package.

View file

@ -9,5 +9,5 @@
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-ts-project-linter-cli'],
roots: ['<rootDir>/packages/kbn-lint-packages-cli'],
};

View file

@ -0,0 +1,6 @@
{
"type": "shared-common",
"id": "@kbn/lint-packages-cli",
"owner": "@elastic/kibana-operations",
"devOnly": true
}

View file

@ -0,0 +1,7 @@
{
"name": "@kbn/lint-packages-cli",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0",
"main": "./run_lint_packages_cli"
}

View file

@ -0,0 +1,13 @@
/*
* 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 type { PackageRule } from '@kbn/repo-linter';
import { matchingPackageNameRule } from './matching_package_name';
export const RULES: PackageRule[] = [matchingPackageNameRule];

View file

@ -0,0 +1,26 @@
/*
* 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 { PackageRule } from '@kbn/repo-linter';
import { setProp } from '@kbn/json-ast';
export const matchingPackageNameRule = PackageRule.create('matchingPackageName', {
async check({ pkg }) {
if (pkg.pkg && pkg.pkg?.name !== pkg.manifest.id) {
this.err(
'The "name" in the package.json file must match the "id" from the kibana.jsonc file',
{
'package.json': (source) =>
setProp(source, 'name', pkg.manifest.id, {
insertAtTop: true,
}),
}
);
}
},
});

View file

@ -0,0 +1,95 @@
/*
* 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 { run } from '@kbn/dev-cli-runner';
import { createFailError } from '@kbn/dev-cli-errors';
import { getRepoFiles } from '@kbn/get-repo-files';
import { PackageFileMap } from '@kbn/repo-file-maps';
import { getPackages } from '@kbn/repo-packages';
import { REPO_ROOT } from '@kbn/repo-info';
import { TS_PROJECTS } from '@kbn/ts-projects';
import { runLintRules, PackageLintTarget } from '@kbn/repo-linter';
import { RULES } from './rules';
const kebabCase = (input: string) =>
input
.replace(/([a-z])([A-Z])/, '$1 $2')
.split(/\W+/)
.filter((f) => !!f)
.join('-')
.toLowerCase();
function getFilter(input: string) {
const repoRel = Path.relative(REPO_ROOT, Path.resolve(input));
return ({ pkg }: PackageLintTarget) =>
pkg.name === input ||
pkg.name === `@kbn/${input}` ||
pkg.name === `@kbn/${kebabCase(input)}` ||
pkg.normalizedRepoRelativeDir === input ||
repoRel.startsWith(pkg.normalizedRepoRelativeDir + '/');
}
run(
async ({ log, flagsReader }) => {
const filter = flagsReader.getPositionals();
const packages = getPackages(REPO_ROOT);
const allTargets = packages
.map(
(p) =>
new PackageLintTarget(
p,
TS_PROJECTS.find((ts) => ts.repoRelDir === p.normalizedRepoRelativeDir)
)
)
.sort((a, b) => b.repoRel.length - a.repoRel.length);
const toLint = Array.from(
new Set(
!filter.length
? allTargets
: filter.map((input) => {
const pkg = allTargets.find(getFilter(input));
if (!pkg) {
throw createFailError(
`unable to find a package matching [${input}]. Supply either a package id/name or path to a package`
);
}
return pkg;
})
)
).sort((a, b) => a.repoRel.localeCompare(b.repoRel));
const fileMap = new PackageFileMap(packages, await getRepoFiles());
const { lintingErrorCount } = await runLintRules(log, toLint, RULES, {
fix: flagsReader.boolean('fix'),
getFiles: (target) => fileMap.getFiles(target.pkg),
});
if (!lintingErrorCount) {
log.success('All packages linted successfully');
} else {
throw createFailError('see above errors');
}
},
{
usage: `node scripts/lint_packages [...packages]`,
flags: {
boolean: ['fix'],
alias: { f: 'fix' },
help: `
--fix Automatically fix some issues in tsconfig.json files
`,
},
description: 'Validate packages using a set of rules that evolve over time.',
}
);

View file

@ -14,11 +14,14 @@
"target/**/*"
],
"kbn_references": [
"@kbn/repo-linter",
"@kbn/dev-cli-runner",
"@kbn/dev-cli-errors",
"@kbn/repo-path",
"@kbn/get-repo-files",
"@kbn/repo-packages",
"@kbn/repo-info",
"@kbn/ts-projects",
"@kbn/ts-project-linter",
"@kbn/repo-file-maps",
"@kbn/json-ast",
]
}

View file

@ -0,0 +1,3 @@
# @kbn/lint-ts-project-cli
CLI for linting typescript projects in the repo

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-lint-ts-projects-cli'],
};

View file

@ -0,0 +1,6 @@
{
"type": "shared-common",
"id": "@kbn/lint-ts-projects-cli",
"owner": "@elastic/kibana-operations",
"devOnly": true
}

View file

@ -1,5 +1,5 @@
{
"name": "@kbn/ts-project-linter-cli",
"name": "@kbn/lint-ts-projects-cli",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0",

View file

@ -6,8 +6,9 @@
* Side Public License, v 1.
*/
import { Rule } from '../lib/rule';
import { removeCompilerOption } from '../ast';
import { removeCompilerOption } from '@kbn/json-ast';
import { TsProjectRule } from '@kbn/repo-linter';
const NAMES = [
'declaration',
@ -16,23 +17,28 @@ const NAMES = [
'skipLibCheck',
'target',
'paths',
'incremental',
'composite',
'rootDir',
];
export const forbiddenCompilerOptions = Rule.create('forbiddenCompilerOptions', {
check({ config, repoRel }) {
export const forbiddenCompilerOptions = TsProjectRule.create('forbiddenCompilerOptions', {
check({ repoRel, tsProject }) {
for (const optName of NAMES) {
if (repoRel === '.buildkite/tsconfig.json' && optName === 'paths') {
// allow "paths" in this specific config file
continue;
}
const value = config.compilerOptions?.[optName];
const value = tsProject.config.compilerOptions?.[optName];
if (value === undefined) {
continue;
}
this.err(`specifying the "${optName}" compiler option is forbidden`, (source) => {
return removeCompilerOption(source, optName);
this.err(`specifying the "${optName}" compiler option is forbidden`, {
['tsconfig.json']: (source) => {
return removeCompilerOption(source, optName);
},
});
}
},

View file

@ -6,20 +6,24 @@
* Side Public License, v 1.
*/
import type { TsProjectRule } from '@kbn/repo-linter';
import { forbiddenCompilerOptions } from './forbidden_compiler_options';
import { refPkgsIds } from './reference_pkg_ids';
import { requiredCompilerOptions } from './required_compiler_options';
import { validBaseConfig } from './valid_base_config';
import { validBaseTsconfig } from './valid_base_tsconfig';
import { requiredExcludes } from './required_excludes';
import { requiredFileSelectors } from './required_file_selectors';
import { referenceUsedPkgs } from './reference_used_pkgs';
import { tsconfigIndentation } from './tsconfig_indentation';
export const PROJECT_LINTER_RULES = [
export const RULES: TsProjectRule[] = [
forbiddenCompilerOptions,
refPkgsIds,
requiredCompilerOptions,
validBaseConfig,
validBaseTsconfig,
requiredExcludes,
requiredFileSelectors,
referenceUsedPkgs,
tsconfigIndentation,
];

View file

@ -9,13 +9,13 @@
import Path from 'path';
import { REPO_ROOT } from '@kbn/repo-info';
import { readPackageMap } from '@kbn/package-map';
import { readPackageMap } from '@kbn/repo-packages';
import { replaceReferences } from '@kbn/json-ast';
import { Rule } from '../lib/rule';
import { replaceReferences } from '../ast';
import { TsProjectRule } from '@kbn/repo-linter';
export const refPkgsIds = Rule.create('refPkgIds', {
check(proj) {
export const refPkgsIds = TsProjectRule.create('refPkgIds', {
check({ tsProject }) {
const dirsToPkgIds = this.getCache(() => {
const pkgMap = readPackageMap();
return new Map(Array.from(pkgMap).map(([k, v]) => [v, k]));
@ -26,12 +26,12 @@ export const refPkgsIds = Rule.create('refPkgIds', {
const replaceWithPkgId: Array<[string, string]> = [];
for (const ref of proj.config.kbn_references ?? []) {
for (const ref of tsProject.config.kbn_references ?? []) {
if (typeof ref === 'string' || ref.force === true) {
continue;
}
const refPath = Path.resolve(proj.directory, ref.path);
const refPath = this.resolve(ref.path);
const pkgIdJson = getPkgId(refPath);
if (pkgIdJson) {
replaceWithPkgId.push([ref.path, pkgIdJson]);
@ -48,8 +48,10 @@ export const refPkgsIds = Rule.create('refPkgIds', {
return {
msg: `kbn_references must use pkgIds to refer to other packages (use --fix to autofix, or add "force": true to ignore):\n${list}`,
fix(source) {
return replaceReferences(source, replaceWithPkgId);
fixes: {
'tsconfig.json': (source) => {
return replaceReferences(source, replaceWithPkgId);
},
},
};
},

View file

@ -0,0 +1,133 @@
/*
* 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 { asyncForEachWithLimit } from '@kbn/std';
import { addReferences, removeReferences, removeAllReferences } from '@kbn/json-ast';
import { TS_PROJECTS, type RefableTsProject } from '@kbn/ts-projects';
import { parseKbnImportReq } from '@kbn/repo-packages';
import { TsProjectRule } from '@kbn/repo-linter';
import { ImportLocator } from '@kbn/import-locator';
function createCache() {
const importable = new Map<string, Set<RefableTsProject>>();
for (const proj of TS_PROJECTS) {
if (!proj.isRefable()) {
continue;
}
const req = parseKbnImportReq(proj.rootImportReq);
if (!req) {
continue;
}
const pkgId = req.pkgId;
const existing = importable.get(pkgId);
if (existing) {
existing.add(proj);
} else {
importable.set(pkgId, new Set([proj]));
}
}
return {
importLocator: new ImportLocator(),
tsProjectsByRootImportReq: new Map(
TS_PROJECTS.flatMap((p) => (p.isRefable() ? [[p.rootImportReq, p]] : []))
),
importableTsProjects: new Map(
Array.from(importable, ([k, v]) => {
const projects = Array.from(v).sort((a, b) => b.directory.localeCompare(a.directory));
return [k, projects.length === 1 ? projects[0] : projects];
})
),
};
}
export const referenceUsedPkgs = TsProjectRule.create('referenceUsedPkgs', {
async check({ tsProject }) {
if (tsProject.isTypeCheckDisabled()) {
if (!tsProject.config.kbn_references) {
return;
}
return {
msg: 'type checking is disabled, so kbn_references is unnecessary and not kept up-to-date.',
fixes: {
'tsconfig.json': (source) => removeAllReferences(source),
},
};
}
const { importLocator, importableTsProjects, tsProjectsByRootImportReq } =
this.getCache(createCache);
const usedTsProjects = new Set<RefableTsProject>();
await asyncForEachWithLimit(this.getAllFiles(), 30, async (path) => {
const reqs = Array.from(await importLocator.read(path.abs)).flatMap(
(req) => parseKbnImportReq(req) ?? []
);
for (const req of reqs) {
const options = importableTsProjects.get(req.pkgId);
if (!options) {
continue;
}
if (!Array.isArray(options)) {
usedTsProjects.add(options);
continue;
}
for (const opt of options) {
if (req.full.startsWith(opt.rootImportReq)) {
usedTsProjects.add(opt);
break;
}
}
}
});
const refs = new Set(
(tsProject.config.kbn_references ?? []).flatMap((r) =>
typeof r === 'string' ? tsProjectsByRootImportReq.get(r) || r : []
)
);
const missing = new Set<RefableTsProject>();
const extra = new Set<RefableTsProject | string>(refs);
for (const used of usedTsProjects) {
extra.delete(used);
if (!refs.has(used)) {
missing.add(used);
}
}
if (missing.size) {
const ids = Array.from(missing, (p) => p.rootImportReq);
const list = ids.map((id) => ` - ${id}`).join('\n');
this.err(
`the following packages are referenced in the code of this package but not listed in kbn_references:\n${list}`,
{
'tsconfig.json': (source) => addReferences(source, ids),
}
);
}
if (extra.size) {
const ids = Array.from(extra, (p) => (typeof p === 'string' ? p : p.rootImportReq));
const list = ids.map((id) => ` - ${id}`).join('\n');
this.err(
`the following packages are listed in kbn_references but there are no detectable references in the code:\n${list}`,
{
'tsconfig.json': (source) => removeReferences(source, ids),
}
);
}
},
});

View file

@ -6,18 +6,19 @@
* Side Public License, v 1.
*/
import { Rule } from '../lib/rule';
import { setCompilerOption } from '../ast';
import { setCompilerOption } from '@kbn/json-ast';
import { TsProjectRule } from '@kbn/repo-linter';
const REQUIRED: Array<[string, string]> = [['outDir', 'target/types']];
export const requiredCompilerOptions = Rule.create('requiredCompilerOptions', {
check({ config }) {
export const requiredCompilerOptions = TsProjectRule.create('requiredCompilerOptions', {
check({ tsProject }) {
for (const [key, value] of REQUIRED) {
if (config.compilerOptions?.[key] !== value) {
this.err(`the value of the compiler option "${key}" must be set to "${value}"`, (source) =>
setCompilerOption(source, key, value)
);
if (tsProject.config.compilerOptions?.[key] !== value) {
this.err(`the value of the compiler option "${key}" must be set to "${value}"`, {
'tsconfig.json': (source) => setCompilerOption(source, key, value),
});
}
}
},

View file

@ -0,0 +1,46 @@
/*
* 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 { setProp } from '@kbn/json-ast';
import { TsProjectRule } from '@kbn/repo-linter';
const REQUIRED_EXCLUDES = ['target/**/*'];
export const requiredExcludes = TsProjectRule.create('requiredExcludes', {
check({ tsProject }) {
const existing = tsProject.config.exclude;
if (!existing) {
return {
msg: `excludes must be defined and include "${REQUIRED_EXCLUDES.join('", "')}"`,
fixes: {
'tsconfig.json': (source) => setProp(source, 'exclude', REQUIRED_EXCLUDES),
},
};
}
const missing = REQUIRED_EXCLUDES.filter((re) => !existing.includes(re));
if (missing.length) {
return {
msg: `excludes must include "${REQUIRED_EXCLUDES.join('", "')}"`,
fixes: {
'tsconfig.json': (source) =>
setProp(source, 'exclude', [
...(missing.includes('target/**/*')
? existing.filter((e) => {
const normalized = e.startsWith('./') ? e.slice(2) : e;
return normalized === 'target' || normalized.startsWith('target/');
})
: existing),
...missing,
]),
},
};
}
},
});

View file

@ -6,11 +6,11 @@
* Side Public License, v 1.
*/
import { Rule } from '../lib/rule';
import { TsProjectRule } from '@kbn/repo-linter';
export const requiredFileSelectors = Rule.create('requiredFileSelectors', {
check(project) {
if (!project.config.include || project.config.files) {
export const requiredFileSelectors = TsProjectRule.create('requiredFileSelectors', {
check({ tsProject }) {
if (tsProject.config.files || !tsProject.config.include) {
return {
msg: 'every ts project must use the "include" key (and not the "files" key) to select the files for that project',
};

View file

@ -0,0 +1,107 @@
/*
* 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 { TsProjectRule } from '@kbn/repo-linter';
import { snip } from '@kbn/json-ast';
const INDENT = ' ';
class Scanner {
public pos = 0;
constructor(public readonly text: string) {}
scanToClosing(final: string) {
while (this.cont()) {
const char = this.text[this.pos++];
if (char === '\\') {
this.pos++; // ignore next char
continue;
}
if (char === final) {
return;
}
}
throw new Error(`expected to find closing "${final}"`);
}
collectAll(match: string) {
let matched = '';
while (this.cont()) {
if (this.text[this.pos] === match) {
matched += match;
this.pos++;
} else {
break;
}
}
return matched;
}
cont() {
return this.pos < this.text.length;
}
peek() {
return this.text[this.pos];
}
}
function getIndentationSnips(text: string) {
const scanner = new Scanner(text);
let depth = 0;
const snips: Array<[from: number, to: number, expected: string]> = [];
while (scanner.cont()) {
const char = scanner.text[scanner.pos++];
if (char === '{' || char === '[') {
depth += 1;
continue;
}
if (char === '}' || char === ']') {
depth -= 1;
continue;
}
if (char === '"') {
scanner.scanToClosing('"');
continue;
}
if (char === '\n') {
const indent = scanner.collectAll(' ');
const next = scanner.peek();
const expected = INDENT.repeat(
next === '\n' ? 0 : next === '}' || next === ']' ? depth - 1 : depth
);
if (indent !== expected) {
snips.push([scanner.pos - indent.length, scanner.pos, expected]);
}
}
}
return snips;
}
export const tsconfigIndentation = TsProjectRule.create('tsconfigIndentation', {
check() {
const content = this.get('tsconfig.json');
const fixes = getIndentationSnips(content);
if (fixes.length || !content.endsWith('\n')) {
this.err('file should use two space indentation', {
'tsconfig.json': (source) => {
const fixed = snip(source, getIndentationSnips(source));
return fixed.endsWith('\n') ? fixed : fixed + '\n';
},
});
}
},
});

View file

@ -9,11 +9,12 @@
import Path from 'path';
import { REPO_ROOT } from '@kbn/repo-info';
import { Project } from '@kbn/ts-projects';
import { TsProject } from '@kbn/ts-projects';
import { setExtends } from '@kbn/json-ast';
import { Rule } from '../lib/rule';
import { TsProjectRule } from '@kbn/repo-linter';
function getBaseConfigRels(proj: Project): string[] {
function getBaseConfigRels(proj: TsProject): string[] {
const base = proj.getBase();
if (!base) {
return [];
@ -21,15 +22,27 @@ function getBaseConfigRels(proj: Project): string[] {
return [base.repoRel, ...getBaseConfigRels(base)];
}
export const validBaseConfig = Rule.create('validBaseConfig', {
check(proj) {
const baseConfigRels = getBaseConfigRels(proj);
export const validBaseTsconfig = TsProjectRule.create('validBaseTsconfig', {
check({ tsProject }) {
const configRel = Path.relative(REPO_ROOT, tsProject.path);
if (configRel !== 'tsconfig.base.json' && !tsProject.config.extends) {
return {
msg: `This tsconfig requires an "extends" setting`,
fixes: {
'tsconfig.json': (source) =>
setExtends(
source,
Path.relative(tsProject.directory, Path.resolve(REPO_ROOT, 'tsconfig.base.json'))
),
},
};
}
const baseConfigRels = getBaseConfigRels(tsProject);
if (baseConfigRels[0] === 'tsconfig.json') {
return `This tsconfig extends the root tsconfig.json file and shouldn't. The root tsconfig.json file is not a valid base config, you probably want to point to the tsconfig.base.json file.`;
}
const configRel = Path.relative(REPO_ROOT, proj.path);
if (configRel !== 'tsconfig.base.json' && !baseConfigRels.includes('tsconfig.base.json')) {
return `This tsconfig does not extend the tsconfig.base.json file either directly or indirectly. The TS config setup for the repo expects every tsconfig file to extend this base config file.`;
}

View file

@ -0,0 +1,176 @@
/*
* 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 { run } from '@kbn/dev-cli-runner';
import { createFailError } from '@kbn/dev-cli-errors';
import { RepoPath } from '@kbn/repo-path';
import { getRepoFiles } from '@kbn/get-repo-files';
import { SomeDevLog } from '@kbn/some-dev-log';
import { PackageFileMap, TsProjectFileMap } from '@kbn/repo-file-maps';
import { getPackages } from '@kbn/repo-packages';
import { REPO_ROOT } from '@kbn/repo-info';
import { TS_PROJECTS, TsProject } from '@kbn/ts-projects';
import { runLintRules, TsProjectLintTarget } from '@kbn/repo-linter';
import { RULES } from './rules';
function getFilter(input: string) {
const abs = Path.resolve(input);
return ({ tsProject }: TsProjectLintTarget) =>
tsProject.name === input ||
tsProject.repoRel === input ||
tsProject.repoRelDir === input ||
tsProject.path === abs ||
tsProject.directory === abs ||
abs.startsWith(tsProject.directory + '/') ||
tsProject.pkgInfo?.repoRel === input ||
(tsProject.pkgInfo && Path.resolve(REPO_ROOT, tsProject.pkgInfo.repoRel) === abs) ||
(tsProject.pkgInfo && abs.startsWith(Path.resolve(REPO_ROOT, tsProject.pkgInfo.repoRel) + '/'));
}
function validateProjectOwnership(
allTargets: TsProjectLintTarget[],
allFiles: Iterable<RepoPath>,
fileMap: TsProjectFileMap,
log: SomeDevLog
) {
let failed = false;
const isInMultipleTsProjects = new Map<string, Set<TsProject>>();
const pathsToTsProject = new Map<string, TsProject>();
for (const proj of allTargets) {
for (const path of fileMap.getFiles(proj.tsProject)) {
const existing = pathsToTsProject.get(path.repoRel);
if (!existing) {
pathsToTsProject.set(path.repoRel, proj.tsProject);
continue;
}
if (path.isTypeScriptAmbient()) {
continue;
}
const multi = isInMultipleTsProjects.get(path.repoRel);
if (multi) {
multi.add(proj.tsProject);
} else {
isInMultipleTsProjects.set(path.repoRel, new Set([existing, proj.tsProject]));
}
}
}
if (isInMultipleTsProjects.size) {
failed = true;
const details = Array.from(isInMultipleTsProjects)
.map(
([repoRel, list]) =>
` - ${repoRel}:\n${Array.from(list)
.map((p) => ` - ${p.repoRel}`)
.join('\n')}`
)
.join('\n');
log.error(
`The following files belong to multiple tsconfig.json files listed in packages/kbn-ts-projects/projects.ts\n${details}`
);
}
const isNotInTsProject: RepoPath[] = [];
for (const path of allFiles) {
if (!path.isTypeScript()) {
continue;
}
const proj = pathsToTsProject.get(path.repoRel);
if (proj === undefined && !path.repoRel.includes('__fixtures__')) {
isNotInTsProject.push(path);
}
}
if (isNotInTsProject.length) {
failed = true;
log.error(
`The following files do not belong to a tsconfig.json file, or that tsconfig.json file is not listed in packages/kbn-ts-projects/projects.ts\n${isNotInTsProject
.map((file) => ` - ${file.repoRel}`)
.join('\n')}`
);
}
return failed;
}
run(
async ({ log, flagsReader }) => {
const filter = flagsReader.getPositionals();
const packages = getPackages(REPO_ROOT);
const allTargets = Array.from(TS_PROJECTS, (p) => new TsProjectLintTarget(p)).sort(
(a, b) => b.repoRel.length - a.repoRel.length
);
const toLint = Array.from(
new Set(
!filter.length
? allTargets
: filter.map((input) => {
const pkg = allTargets.find(getFilter(input));
if (!pkg) {
throw createFailError(
`unable to find a package matching [${input}]. Supply either a package id/name or path to a package`
);
}
return pkg;
})
)
).sort((a, b) => a.repoRel.localeCompare(b.repoRel));
const skipRefs =
flagsReader.boolean('refs-check') === false || flagsReader.boolean('no-refs-check') === true;
const allFiles = await getRepoFiles();
const fileMap = new TsProjectFileMap(new PackageFileMap(packages, allFiles), TS_PROJECTS);
const { lintingErrorCount } = await runLintRules(
log,
toLint,
RULES.filter((r) => r.name !== 'referenceUsedPkgs' || skipRefs === false),
{
fix: flagsReader.boolean('fix'),
getFiles: (target) => fileMap.getFiles(target.tsProject),
}
);
const failed =
lintingErrorCount > 0 ||
(filter.length > 0 ? false : validateProjectOwnership(allTargets, allFiles, fileMap, log));
if (failed) {
throw createFailError('see above errors');
} else {
log.success('All TS projects linted successfully');
}
},
{
usage: `node scripts/package_linter [...packages]`,
flags: {
boolean: ['fix', 'refs-check', 'no-refs-check'],
alias: { f: 'fix', R: 'no-refs-check' },
default: { 'refs-check': true },
help: `
--no-lint Disables linting rules, only validting that every file is a member of just one project
--fix Automatically fix some issues in tsconfig.json files
-R, --no-refs-check Disables the reference checking rules, making the linting much faster, but less accruate
`,
},
description:
'Validate packages using a set of rules that evolve over time. The vast majority of violations can be auto-fixed, so running with `--fix` is recommended.',
}
);

View file

@ -0,0 +1,31 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.ts",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/dev-cli-runner",
"@kbn/dev-cli-errors",
"@kbn/repo-path",
"@kbn/get-repo-files",
"@kbn/ts-projects",
"@kbn/repo-packages",
"@kbn/repo-info",
"@kbn/repo-linter",
"@kbn/json-ast",
"@kbn/std",
"@kbn/import-locator",
"@kbn/repo-file-maps",
"@kbn/some-dev-log",
]
}

View file

@ -12,7 +12,7 @@ import { createAbsolutePathSerializer } from '@kbn/jest-serializers';
import { getOptimizerCacheKey } from './optimizer_cache_key';
import { OptimizerConfig } from './optimizer_config';
jest.mock('@kbn/package-map', () => {
jest.mock('@kbn/repo-packages', () => {
return {
readHashOfPackageMap() {
return '<hash of package map>';

Some files were not shown because too many files have changed in this diff Show more