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/precommit_hook.sh
.buildkite/scripts/steps/checks/ts_projects.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/bazel_packages.sh
.buildkite/scripts/steps/checks/verify_notice.sh .buildkite/scripts/steps/checks/verify_notice.sh
.buildkite/scripts/steps/checks/plugin_list_docs.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 source .buildkite/scripts/common/util.sh
echo --- Run TS Project Linter echo --- Lint TS projects
cmd="node scripts/ts_project_linter" cmd="node scripts/lint_ts_projects"
if is_pr && ! is_auto_commit_disabled; then if is_pr && ! is_auto_commit_disabled; then
cmd="$cmd --fix" cmd="$cmd --fix"
fi fi

View file

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

View file

@ -8,11 +8,7 @@
require('@kbn/babel-register').install(); require('@kbn/babel-register').install();
const Path = require('path'); const { getPackages } = require('@kbn/repo-packages');
const Fs = require('fs');
const normalizePath = require('normalize-path');
const { discoverPackageManifestPaths, Jsonc } = require('@kbn/bazel-packages');
const { REPO_ROOT } = require('@kbn/repo-info'); const { REPO_ROOT } = require('@kbn/repo-info');
const APACHE_2_0_LICENSE_HEADER = ` const APACHE_2_0_LICENSE_HEADER = `
@ -124,10 +120,9 @@ const VENN_DIAGRAM_HEADER = `
`; `;
/** Packages which should not be included within production code. */ /** Packages which should not be included within production code. */
const DEV_PACKAGE_DIRS = discoverPackageManifestPaths(REPO_ROOT).flatMap((path) => { const DEV_PACKAGE_DIRS = getPackages(REPO_ROOT).flatMap((pkg) =>
const manifest = Jsonc.parse(Fs.readFileSync(path, 'utf8')); pkg.isDevOnly ? pkg.normalizedRepoRelativeDir : []
return !!manifest.devOnly ? normalizePath(Path.relative(REPO_ROOT, Path.dirname(path))) : []; );
});
/** Directories (at any depth) which include dev-only code. */ /** Directories (at any depth) which include dev-only code. */
const DEV_DIRECTORIES = [ 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 * Enterprise Search Prettier override
* Lints unnecessary backticks - @see https://github.com/prettier/eslint-config-prettier/blob/main/README.md#forbid-unnecessary-backticks * 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', '@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-preset @elastic/kibana-operations
packages/kbn-babel-register @elastic/kibana-operations packages/kbn-babel-register @elastic/kibana-operations
packages/kbn-babel-transform @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-bazel-runner @elastic/kibana-operations
packages/kbn-cases-components @elastic/response-ops packages/kbn-cases-components @elastic/response-ops
packages/kbn-chart-icons @elastic/kibana-visualizations 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-health-gateway-server @elastic/kibana-core
packages/kbn-i18n @elastic/kibana-core packages/kbn-i18n @elastic/kibana-core
packages/kbn-i18n-react @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-import-resolver @elastic/kibana-operations
packages/kbn-interpreter @elastic/kibana-visualizations packages/kbn-interpreter @elastic/kibana-visualizations
packages/kbn-io-ts-utils @elastic/apm-ui packages/kbn-io-ts-utils @elastic/apm-ui
packages/kbn-jest-serializers @elastic/kibana-operations packages/kbn-jest-serializers @elastic/kibana-operations
packages/kbn-journeys @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-kibana-manifest-schema @elastic/kibana-operations
packages/kbn-language-documentation-popover @elastic/kibana-visualizations 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 @elastic/kibana-core
packages/kbn-logging-mocks @elastic/kibana-core packages/kbn-logging-mocks @elastic/kibana-core
packages/kbn-managed-vscode-config @elastic/kibana-operations 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 @elastic/kibana-operations
packages/kbn-optimizer-webpack-helpers @elastic/kibana-operations packages/kbn-optimizer-webpack-helpers @elastic/kibana-operations
packages/kbn-osquery-io-ts-types @elastic/security-asset-management 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 @elastic/kibana-operations
packages/kbn-peggy-loader @elastic/kibana-operations packages/kbn-peggy-loader @elastic/kibana-operations
packages/kbn-performance-testing-dataset-extractor @elastic/kibana-performance-testing 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-discovery @elastic/kibana-operations
packages/kbn-plugin-generator @elastic/kibana-operations packages/kbn-plugin-generator @elastic/kibana-operations
packages/kbn-plugin-helpers @elastic/kibana-operations packages/kbn-plugin-helpers @elastic/kibana-operations
packages/kbn-react-field @elastic/kibana-app-services 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-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-path @elastic/kibana-operations
packages/kbn-repo-source-classifier @elastic/kibana-operations packages/kbn-repo-source-classifier @elastic/kibana-operations
packages/kbn-repo-source-classifier-cli @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-securitysolution-utils @elastic/security-solution-platform
packages/kbn-server-http-tools @elastic/kibana-core packages/kbn-server-http-tools @elastic/kibana-core
packages/kbn-server-route-repository @elastic/apm-ui 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-svg @elastic/apm-ui
packages/kbn-shared-ux-utility @elastic/kibana-global-experience packages/kbn-shared-ux-utility @elastic/kibana-global-experience
packages/kbn-slo-schema @elastic/actionable-observability 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-timelion-grammar @elastic/kibana-visualizations
packages/kbn-tinymath @elastic/kibana-visualizations packages/kbn-tinymath @elastic/kibana-visualizations
packages/kbn-tooling-log @elastic/kibana-operations 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-projects @elastic/kibana-operations
packages/kbn-ts-type-check-cli @elastic/kibana-operations packages/kbn-ts-type-check-cli @elastic/kibana-operations
packages/kbn-typed-react-router-config @elastic/apm-ui packages/kbn-typed-react-router-config @elastic/apm-ui

3
.gitignore vendored
View file

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

View file

@ -14,9 +14,10 @@ import { haveNodeModulesBeenManuallyDeleted, removeYarnIntegrityFileIfExists } f
import { setupRemoteCache } from './setup_remote_cache.mjs'; import { setupRemoteCache } from './setup_remote_cache.mjs';
import { sortPackageJson } from './sort_package_json.mjs'; import { sortPackageJson } from './sort_package_json.mjs';
import { regeneratePackageMap } from './regenerate_package_map.mjs'; import { regeneratePackageMap } from './regenerate_package_map.mjs';
import { regenerateTsconfigPaths } from './regenerate_tsconfig_paths.mjs';
import { regenerateBaseTsconfig } from './regenerate_base_tsconfig.mjs'; import { regenerateBaseTsconfig } from './regenerate_base_tsconfig.mjs';
import { packageDiscovery, pluginDiscovery } from './discovery.mjs'; import { discovery } from './discovery.mjs';
import { validatePackageJson } from './validate_package_json.mjs'; import { updatePackageJson } from './update_package_json.mjs';
/** @type {import('../../lib/command').Command} */ /** @type {import('../../lib/command').Command} */
export const command = { export const command = {
@ -61,13 +62,33 @@ export const command = {
const forceInstall = const forceInstall =
args.getBooleanValue('force-install') ?? (await haveNodeModulesBeenManuallyDeleted()); 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 (async () => {
await Bazel.ensureInstalled(log); await Bazel.tryRemovingBazeliskFromYarnGlobal(log);
// Setup remote cache settings in .bazelrc.cache if needed // Install bazel machinery tools if needed
await setupRemoteCache(log); 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 // Bootstrap process for Bazel packages
// Bazel is now managing dependencies so yarn install // 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 time('pre-build webpack bundles for packages', async () => {
await Bazel.buildWebpackBundles(log, { offline, quiet }); await Bazel.buildWebpackBundles(log, { offline, quiet });
}); });
await time('regenerate tsconfig.base.json', async () => {
await regenerateBaseTsconfig();
});
await Promise.all([ await Promise.all([
time('sort package json', async () => { time('regenerate tsconfig.base.json', async () => {
await sortPackageJson(); await regenerateBaseTsconfig();
}), }),
time('validate package json', async () => { time('sort package json', async () => {
// now that deps are installed we can import `@kbn/yarn-lock-validator` await sortPackageJson(log);
const { kibanaPackageJson } = External['@kbn/repo-info']();
await validatePackageJson(kibanaPackageJson, log);
}), }),
validate validate
? time('validate dependencies', async () => { ? time('validate dependencies', async () => {

View file

@ -6,33 +6,84 @@
* Side Public License, v 1. * 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'; 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 export async function discovery() {
// 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() {
const { getPluginSearchPaths, simpleKibanaPlatformPluginDiscovery } = await import( const { getPluginSearchPaths, simpleKibanaPlatformPluginDiscovery } = await import(
// eslint-disable-next-line @kbn/imports/uniform_imports // eslint-disable-next-line @kbn/imports/uniform_imports
'../../../../packages/kbn-plugin-discovery/index.js' '../../../../packages/kbn-plugin-discovery/index.js'
); );
const searchPaths = getPluginSearchPaths({ const { Package } = await import(
rootDir: REPO_ROOT, // we need to run this before we install node modules, so it can't rely on @kbn/* imports
examples: true, // eslint-disable-next-line @kbn/imports/uniform_imports
oss: false, '../../../../packages/kbn-repo-packages/index.js'
testPlugins: true, );
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'; import External from '../../lib/external_packages.js';
export async function regenerateBaseTsconfig() { 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 tsconfigPath = Path.resolve(REPO_ROOT, 'tsconfig.base.json');
const lines = (await Fsp.readFile(tsconfigPath, 'utf-8')).split('\n'); 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/plugin-discovery').KibanaPlatformPlugin[]} plugins
* @param {import('@kbn/some-dev-log').SomeDevLog} log * @param {import('@kbn/some-dev-log').SomeDevLog} log
*/ */
export async function regeneratePackageMap(packages, plugins, log) { export async function regeneratePackageMap(packages, plugins, log) {
// clean up old version of package map package const path = Path.resolve(REPO_ROOT, 'packages/kbn-repo-packages/package-map.json');
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 existingContent = Fs.existsSync(path) ? await Fsp.readFile(path, 'utf8') : undefined; const existingContent = Fs.existsSync(path) ? await Fsp.readFile(path, 'utf8') : undefined;
/** @type {Array<[string, string]>} */ /** @type {Array<[string, string]>} */
@ -52,6 +46,6 @@ export async function regeneratePackageMap(packages, plugins, log) {
if (content !== existingContent) { if (content !== existingContent) {
await Fsp.writeFile(path, content); 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 Path from 'path';
import Fs from 'fs'; import Fsp from 'fs/promises';
import { REPO_ROOT } from '../../lib/paths.mjs'; import { REPO_ROOT } from '../../lib/paths.mjs';
import External from '../../lib/external_packages.js'; 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 { sortPackageJson } = External['@kbn/sort-package-json']();
const path = Path.resolve(REPO_ROOT, 'package.json'); const path = Path.resolve(REPO_ROOT, 'package.json');
const json = Fs.readFileSync(path, 'utf8'); const json = await Fsp.readFile(path, 'utf8');
Fs.writeFileSync(path, sortPackageJson(json)); 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 exclude = args.getStringValues('exclude') ?? [];
const include = args.getStringValues('include') ?? []; const include = args.getStringValues('include') ?? [];
const { discoverBazelPackages } = External['@kbn/bazel-packages'](); const { getPackages } = External['@kbn/repo-packages']();
const packages = await discoverBazelPackages(REPO_ROOT); const packages = getPackages(REPO_ROOT);
for (const { manifest, pkg, normalizedRepoRelativeDir } of packages) { for (const { manifest, pkg, normalizedRepoRelativeDir } of packages) {
if ( if (
exclude.includes(manifest.id) || exclude.includes(manifest.id) ||

View file

@ -6,9 +6,9 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
module.exports = { module.exports = {
['@kbn/bazel-packages']() { ['@kbn/repo-packages']() {
require('@kbn/babel-register').install(); require('@kbn/babel-register').install();
return require('@kbn/bazel-packages'); return require('@kbn/repo-packages');
}, },
['@kbn/ci-stats-reporter']() { ['@kbn/ci-stats-reporter']() {
@ -26,11 +26,6 @@ module.exports = {
return require('@kbn/sort-package-json'); return require('@kbn/sort-package-json');
}, },
['@kbn/package-map']() {
require('@kbn/babel-register').install();
return require('@kbn/package-map');
},
['@kbn/get-repo-files']() { ['@kbn/get-repo-files']() {
require('@kbn/babel-register').install(); require('@kbn/babel-register').install();
return require('@kbn/get-repo-files'); 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 * Attempt to load the package map, if bootstrap hasn't run successfully
* this might fail. * this might fail.
* @param {import('@kbn/some-dev-log').SomeDevLog} log * @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) { async function tryToGetPackageMap(log) {
try { try {
const { readPackageMap } = External['@kbn/package-map'](); const { readPackageMap } = External['@kbn/repo-packages']();
return readPackageMap(); return readPackageMap();
} catch (error) { } catch (error) {
log.warning('unable to load package map, unable to clean target directories in packages'); log.warning('unable to load package map, unable to clean target directories in packages');

View file

@ -15,14 +15,13 @@
], ],
"kbn_references": [ "kbn_references": [
"@kbn/babel-register", "@kbn/babel-register",
"@kbn/bazel-packages",
"@kbn/repo-info", "@kbn/repo-info",
"@kbn/yarn-lock-validator", "@kbn/yarn-lock-validator",
"@kbn/get-repo-files", "@kbn/get-repo-files",
"@kbn/sort-package-json", "@kbn/sort-package-json",
{ "path": "../src/dev/tsconfig.json" }, { "path": "../src/dev/tsconfig.json" },
"@kbn/ci-stats-reporter", "@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": "link:packages/kbn-config",
"@kbn/config-mocks": "link:packages/kbn-config-mocks", "@kbn/config-mocks": "link:packages/kbn-config-mocks",
"@kbn/config-schema": "link:packages/kbn-config-schema", "@kbn/config-schema": "link:packages/kbn-config-schema",
"@kbn/content-management-content-editor": "link:bazel-bin/packages/content-management/content_editor", "@kbn/content-management-content-editor": "link:packages/content-management/content_editor",
"@kbn/content-management-table-list": "link:bazel-bin/packages/content-management/table_list", "@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": "link:packages/core/analytics/core-analytics-browser",
"@kbn/core-analytics-browser-internal": "link:packages/core/analytics/core-analytics-browser-internal", "@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", "@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-internal": "link:packages/core/chrome/core-chrome-browser-internal",
"@kbn/core-chrome-browser-mocks": "link:packages/core/chrome/core-chrome-browser-mocks", "@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-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": "link:packages/core/deprecations/core-deprecations-browser",
"@kbn/core-deprecations-browser-internal": "link:packages/core/deprecations/core-deprecations-browser-internal", "@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", "@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": "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-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-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-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-client-server-mocks": "link:packages/core/elasticsearch/core-elasticsearch-client-server-mocks",
"@kbn/core-elasticsearch-server": "link:packages/core/elasticsearch/core-elasticsearch-server", "@kbn/core-elasticsearch-server": "link:packages/core/elasticsearch/core-elasticsearch-server",
@ -361,6 +359,7 @@
"@kbn/plugin-discovery": "link:packages/kbn-plugin-discovery", "@kbn/plugin-discovery": "link:packages/kbn-plugin-discovery",
"@kbn/react-field": "link:packages/kbn-react-field", "@kbn/react-field": "link:packages/kbn-react-field",
"@kbn/repo-info": "link:packages/kbn-repo-info", "@kbn/repo-info": "link:packages/kbn-repo-info",
"@kbn/repo-packages": "link:packages/kbn-repo-packages",
"@kbn/rison": "link:packages/kbn-rison", "@kbn/rison": "link:packages/kbn-rison",
"@kbn/rule-data-utils": "link:packages/kbn-rule-data-utils", "@kbn/rule-data-utils": "link:packages/kbn-rule-data-utils",
"@kbn/safer-lodash-set": "link:packages/kbn-safer-lodash-set", "@kbn/safer-lodash-set": "link:packages/kbn-safer-lodash-set",
@ -381,6 +380,7 @@
"@kbn/securitysolution-utils": "link:packages/kbn-securitysolution-utils", "@kbn/securitysolution-utils": "link:packages/kbn-securitysolution-utils",
"@kbn/server-http-tools": "link:packages/kbn-server-http-tools", "@kbn/server-http-tools": "link:packages/kbn-server-http-tools",
"@kbn/server-route-repository": "link:packages/kbn-server-route-repository", "@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-svg": "link:packages/kbn-shared-svg",
"@kbn/shared-ux-avatar-solution": "link:packages/shared-ux/avatar/solution", "@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", "@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-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-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-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-router-mocks": "link:packages/shared-ux/router/mocks",
"@kbn/shared-ux-services": "link:packages/kbn-shared-ux-services", "@kbn/shared-ux-router-types": "link:packages/shared-ux/router/types",
"@kbn/shared-ux-storybook": "link:packages/kbn-shared-ux-storybook", "@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-storybook-mock": "link:packages/shared-ux/storybook/mock",
"@kbn/shared-ux-utility": "link:packages/kbn-shared-ux-utility", "@kbn/shared-ux-utility": "link:packages/kbn-shared-ux-utility",
"@kbn/slo-schema": "link:packages/kbn-slo-schema", "@kbn/slo-schema": "link:packages/kbn-slo-schema",
@ -748,7 +749,6 @@
"@kbn/babel-preset": "link:packages/kbn-babel-preset", "@kbn/babel-preset": "link:packages/kbn-babel-preset",
"@kbn/babel-register": "link:packages/kbn-babel-register", "@kbn/babel-register": "link:packages/kbn-babel-register",
"@kbn/babel-transform": "link:packages/kbn-babel-transform", "@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/bazel-runner": "link:packages/kbn-bazel-runner",
"@kbn/ci-stats-core": "link:packages/kbn-ci-stats-core", "@kbn/ci-stats-core": "link:packages/kbn-ci-stats-core",
"@kbn/ci-stats-performance-metrics": "link:packages/kbn-ci-stats-performance-metrics", "@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/ftr-screenshot-filename": "link:packages/kbn-ftr-screenshot-filename",
"@kbn/generate": "link:packages/kbn-generate", "@kbn/generate": "link:packages/kbn-generate",
"@kbn/get-repo-files": "link:packages/kbn-get-repo-files", "@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/import-resolver": "link:packages/kbn-import-resolver",
"@kbn/jest-serializers": "link:packages/kbn-jest-serializers", "@kbn/jest-serializers": "link:packages/kbn-jest-serializers",
"@kbn/journeys": "link:packages/kbn-journeys", "@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/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": "link:packages/kbn-managed-vscode-config",
"@kbn/managed-vscode-config-cli": "link:packages/kbn-managed-vscode-config-cli", "@kbn/managed-vscode-config-cli": "link:packages/kbn-managed-vscode-config-cli",
"@kbn/optimizer": "link:packages/kbn-optimizer", "@kbn/optimizer": "link:packages/kbn-optimizer",
"@kbn/optimizer-webpack-helpers": "link:packages/kbn-optimizer-webpack-helpers", "@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": "link:packages/kbn-peggy",
"@kbn/peggy-loader": "link:packages/kbn-peggy-loader", "@kbn/peggy-loader": "link:packages/kbn-peggy-loader",
"@kbn/performance-testing-dataset-extractor": "link:packages/kbn-performance-testing-dataset-extractor", "@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-generator": "link:packages/kbn-plugin-generator",
"@kbn/plugin-helpers": "link:packages/kbn-plugin-helpers", "@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-path": "link:packages/kbn-repo-path",
"@kbn/repo-source-classifier": "link:packages/kbn-repo-source-classifier", "@kbn/repo-source-classifier": "link:packages/kbn-repo-source-classifier",
"@kbn/repo-source-classifier-cli": "link:packages/kbn-repo-source-classifier-cli", "@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-jest-helpers": "link:packages/kbn-test-jest-helpers",
"@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector", "@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector",
"@kbn/tooling-log": "link:packages/kbn-tooling-log", "@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-projects": "link:packages/kbn-ts-projects",
"@kbn/ts-type-check-cli": "link:packages/kbn-ts-type-check-cli", "@kbn/ts-type-check-cli": "link:packages/kbn-ts-type-check-cli",
"@kbn/web-worker-stub": "link:packages/kbn-web-worker-stub", "@kbn/web-worker-stub": "link:packages/kbn-web-worker-stub",
@ -931,6 +935,7 @@
"@types/pbf": "3.0.2", "@types/pbf": "3.0.2",
"@types/pdfmake": "^0.2.2", "@types/pdfmake": "^0.2.2",
"@types/pegjs": "^0.10.1", "@types/pegjs": "^0.10.1",
"@types/picomatch": "^2.3.0",
"@types/pidusage": "^2.0.2", "@types/pidusage": "^2.0.2",
"@types/pixelmatch": "^5.2.4", "@types/pixelmatch": "^5.2.4",
"@types/pngjs": "^3.4.0", "@types/pngjs": "^3.4.0",
@ -1116,6 +1121,7 @@
"openapi-types": "^10.0.0", "openapi-types": "^10.0.0",
"pbf": "3.2.1", "pbf": "3.2.1",
"peggy": "^1.2.0", "peggy": "^1.2.0",
"picomatch": "^2.3.1",
"pidusage": "^3.0.2", "pidusage": "^3.0.2",
"pirates": "^4.0.1", "pirates": "^4.0.1",
"piscina": "^3.2.0", "piscina": "^3.2.0",
@ -1172,4 +1178,4 @@
"xmlbuilder": "13.0.2", "xmlbuilder": "13.0.2",
"yargs": "^15.4.1" "yargs": "^15.4.1"
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,10 +11,10 @@ const Path = require('path');
const T = require('@babel/types'); const T = require('@babel/types');
const normalizePath = require('normalize-path'); const normalizePath = require('normalize-path');
const { declare } = require('@babel/helper-plugin-utils'); 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 { REPO_ROOT } = require('@kbn/repo-info');
const PKG_MAP = KbnSyntheticPackageMap.readPackageMap(); const PKG_MAP = readPackageMap();
/** /**
* @param {unknown} v * @param {unknown} v

View file

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

View file

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

View file

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

View file

@ -13,10 +13,10 @@
"**/*.ts", "**/*.ts",
], ],
"kbn_references": [ "kbn_references": [
"@kbn/package-map",
"@kbn/repo-info", "@kbn/repo-info",
"@kbn/babel-transform", "@kbn/babel-transform",
"@kbn/peggy", "@kbn/peggy",
"@kbn/repo-packages",
], ],
"exclude": [ "exclude": [
"target/**/*", "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({ const { watchPaths, ignorePaths } = getServerWatchPaths({
runExamples: cliArgs.runExamples,
pluginPaths: config.plugins.additionalPluginPaths, pluginPaths: config.plugins.additionalPluginPaths,
pluginScanDirs: config.plugins.pluginSearchPaths, pluginScanDirs: config.plugins.pluginSearchPaths,
}); });

View file

@ -10,13 +10,48 @@ import Path from 'path';
import { createAbsolutePathSerializer } from '@kbn/jest-serializers'; import { createAbsolutePathSerializer } from '@kbn/jest-serializers';
import { REPO_ROOT } from '@kbn/repo-info'; 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'; 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()); expect.addSnapshotSerializer(createAbsolutePathSerializer());
it('produces the right watch and ignore list', () => { 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({ const { watchPaths, ignorePaths } = getServerWatchPaths({
runExamples: false,
pluginPaths: [Path.resolve(REPO_ROOT, 'x-pack/test/plugin_functional/plugins/resolver_test')], pluginPaths: [Path.resolve(REPO_ROOT, 'x-pack/test/plugin_functional/plugins/resolver_test')],
pluginScanDirs: [ pluginScanDirs: [
Path.resolve(REPO_ROOT, 'src/plugins'), Path.resolve(REPO_ROOT, 'src/plugins'),
@ -33,14 +68,9 @@ it('produces the right watch and ignore list', () => {
<absolute path>/src/plugins, <absolute path>/src/plugins,
<absolute path>/test/plugin_functional/plugins, <absolute path>/test/plugin_functional/plugins,
<absolute path>/x-pack/plugins, <absolute path>/x-pack/plugins,
<absolute path>/packages, <absolute path>/packages/plugin-server,
<absolute path>/packages/shared-ux, <absolute path>/packages/shared-common,
<absolute path>/packages/analytics, <absolute path>/packages/shared-server,
<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,
] ]
`); `);
@ -89,4 +119,22 @@ it('produces the right watch and ignore list', () => {
<absolute path>/x-pack/plugins/observability/e2e, <absolute path>/x-pack/plugins/observability/e2e,
] ]
`); `);
expect(mockGetPluginPackagesFilter.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"examples": false,
"parentDirs": Array [
<absolute path>/src/plugins,
<absolute path>/test/plugin_functional/plugins,
<absolute path>/x-pack/plugins,
],
"paths": Array [
<absolute path>/x-pack/test/plugin_functional/plugins/resolver_test,
],
},
],
]
`);
}); });

View file

@ -7,24 +7,24 @@
*/ */
import Path from 'path'; import Path from 'path';
import Fs from 'fs';
import { REPO_ROOT } from '@kbn/repo-info'; import { REPO_ROOT } from '@kbn/repo-info';
import { BAZEL_PACKAGE_DIRS } from '@kbn/bazel-packages'; import { getPackages, getPluginPackagesFilter } from '@kbn/repo-packages';
interface Options { interface Options {
runExamples: boolean;
pluginPaths: string[]; pluginPaths: string[];
pluginScanDirs: string[]; pluginScanDirs: string[];
} }
export type WatchPaths = ReturnType<typeof getServerWatchPaths>; 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 fromRoot = (p: string) => Path.resolve(REPO_ROOT, p);
const pluginInternalDirsIgnore = pluginScanDirs const pluginInternalDirsIgnore = opts.pluginScanDirs
.map((scanDir) => Path.resolve(scanDir, '*')) .map((scanDir) => Path.resolve(scanDir, '*'))
.concat(pluginPaths) .concat(opts.pluginPaths)
.reduce( .reduce(
(acc: string[], path) => [ (acc: string[], path) => [
...acc, ...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( const watchPaths = Array.from(
new Set( new Set([
[ fromRoot('src/core'),
fromRoot('src/core'), fromRoot('config'),
fromRoot('src/legacy/server'), ...opts.pluginPaths.map((path) => Path.resolve(path)),
fromRoot('src/legacy/utils'), ...opts.pluginScanDirs.map((path) => Path.resolve(path)),
fromRoot('config'), ...getServerPkgDirs(),
...pluginPaths, ])
...pluginScanDirs, );
...BAZEL_PACKAGE_DIRS,
].map((path) => Path.resolve(path))
)
).filter((path) => Fs.existsSync(fromRoot(path)));
const ignorePaths = [ const ignorePaths = [
/[\\\/](\..*|node_modules|bower_components|target|public|__[a-z0-9_]+__|coverage)([\\\/]|$)/, /[\\\/](\..*|node_modules|bower_components|target|public|__[a-z0-9_]+__|coverage)([\\\/]|$)/,

View file

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

View file

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

View file

@ -13,9 +13,7 @@ import normalizePath from 'normalize-path';
import globby from 'globby'; import globby from 'globby';
import { ESLint } from 'eslint'; import { ESLint } from 'eslint';
import micromatch from 'micromatch';
import { REPO_ROOT } from '@kbn/repo-info'; 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 { createFailError, createFlagError, isFailError } from '@kbn/dev-cli-errors';
import { sortPackageJson } from '@kbn/sort-package-json'; 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 --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 --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. 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 --dir Specify where this package will be written.
directories selected by the BAZEL_PACKAGE_DIRS const in @kbn/bazel-packages.
Valid locations for packages:
${BAZEL_PACKAGE_DIRS.map((dir) => ` ./${dir}/*\n`).join('')}
defaults to [./packages/{kebab-case-version-of-name}] defaults to [./packages/{kebab-case-version-of-name}]
--force If the --dir already exists, delete it before generation --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 --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 const packageDir = flags.dir
? Path.resolve(`${flags.dir}`) ? Path.resolve(`${flags.dir}`)
: Path.resolve(ROOT_PKG_DIR, pkgId.slice(1).replace('/', '-')); : 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)); const normalizedRepoRelativeDir = normalizePath(Path.relative(REPO_ROOT, packageDir));
try { try {

View file

@ -11,12 +11,12 @@
"**/*.ts" "**/*.ts"
], ],
"kbn_references": [ "kbn_references": [
"@kbn/bazel-packages",
"@kbn/sort-package-json", "@kbn/sort-package-json",
"@kbn/dev-cli-runner", "@kbn/dev-cli-runner",
"@kbn/repo-info", "@kbn/repo-info",
"@kbn/dev-cli-errors", "@kbn/dev-cli-errors",
"@kbn/tooling-log", "@kbn/tooling-log",
"@kbn/repo-packages",
], ],
"exclude": [ "exclude": [
"target/**/*", "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 Fsp from 'fs/promises';
import Ts from 'typescript'; import Ts from 'typescript';
import { RepoPath } from '@kbn/repo-path';
import { stripSourceCode } from './strip_source_code'; import { stripSourceCode } from './strip_source_code';
const EMPTY = new Set<string>(); const EMPTY = new Set<string>();
export class TypeScriptImportLocator { export class ImportLocator {
private readonly scanner: Ts.Scanner; private readonly scanner: Ts.Scanner;
constructor() { constructor() {
this.scanner = Ts.createScanner(Ts.ScriptTarget.Latest, false, Ts.LanguageVariant.JSX); this.scanner = Ts.createScanner(Ts.ScriptTarget.Latest, false, Ts.LanguageVariant.JSX);
} }
async get(path: RepoPath): Promise<Set<string>> { get(path: string, content: string): Set<string> {
const content = await Fsp.readFile(path.abs, 'utf8');
const strippedContent = stripSourceCode(this.scanner, content); const strippedContent = stripSourceCode(this.scanner, content);
if (strippedContent === '') { if (strippedContent === '') {
return EMPTY; return EMPTY;
@ -33,7 +31,7 @@ export class TypeScriptImportLocator {
const imports = new Set<string>(); const imports = new Set<string>();
const queue: Ts.Node[] = [ 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) => { const addNodeToQueue = (n: Ts.Node) => {
queue.push(n); queue.push(n);
@ -76,4 +74,9 @@ export class TypeScriptImportLocator {
return imports; 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 = { module.exports = {
preset: '@kbn/test/jest_node', preset: '@kbn/test/jest_node',
rootDir: '../..', rootDir: '../..',
roots: ['<rootDir>/packages/kbn-bazel-packages'], roots: ['<rootDir>/packages/kbn-import-locator'],
}; };

View file

@ -1,6 +1,6 @@
{ {
"type": "shared-common", "type": "shared-common",
"id": "@kbn/ts-project-linter", "id": "@kbn/import-locator",
"owner": "@elastic/kibana-operations", "owner": "@elastic/kibana-operations",
"devOnly": true "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": { "compilerOptions": {
"outDir": "target/types", "outDir": "target/types",
"types": [ "types": [
"jest",
"node" "node"
] ]
}, },
"include": [ "include": [
"index.d.ts" "**/*.ts",
], ],
"exclude": [ "exclude": [
"target/**/*", "target/**/*"
] ],
"kbn_references": []
} }

View file

@ -10,9 +10,9 @@ import Path from 'path';
import Fs from 'fs'; import Fs from 'fs';
import Resolve from 'resolve'; 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 { 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 { safeStat, readFileSync } from './helpers/fs';
import { ResolveResult } from './resolve_result'; import { ResolveResult } from './resolve_result';

View file

@ -11,10 +11,9 @@
"**/*.ts" "**/*.ts"
], ],
"kbn_references": [ "kbn_references": [
"@kbn/bazel-packages",
"@kbn/package-map",
"@kbn/repo-info", "@kbn/repo-info",
"@kbn/jest-serializers", "@kbn/jest-serializers",
"@kbn/repo-packages",
], ],
"exclude": [ "exclude": [
"target/**/*", "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. * Side Public License, v 1.
*/ */
export { removeCompilerOption, setCompilerOption } from './compiler_options'; export { removeCompilerOption, setCompilerOption } from './src/compiler_options';
export { setExclude } from './exclude'; export {
export { addReferences, removeReferences, replaceReferences } from './references'; 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 = { module.exports = {
preset: '@kbn/test/jest_node', preset: '@kbn/test/jest_node',
rootDir: '../..', rootDir: '../..',
roots: ['<rootDir>/packages/kbn-ts-project-linter'], roots: ['<rootDir>/packages/kbn-json-ast'],
}; };

View file

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

View file

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

View file

@ -273,7 +273,6 @@ describe('setCompilerOptions()', () => {
`, `,
'skipLibCheck' 'skipLibCheck'
), ),
'outDir', 'outDir',
'foo/bar' '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. * Side Public License, v 1.
*/ */
import { Jsonc } from '@kbn/bazel-packages'; import { Jsonc } from '@kbn/repo-packages';
import { T } from './babel'; import { T } from './babel';
import { getAst } from './ast'; import { getAst } from './ast';
import { getEnds, getExpandedEnds } from './ends'; import { getEnds, getExpandedEnds } from './ends';
import { getProp, getEndOfLastProp } from './props'; import { getProp, getEndOfLastProp } from './props';
import { snip } from './snip';
import { redentJson, stringify } from './json';
export function getCompilerOptions(source: string) { export function getCompilerOptions(ast: T.ObjectExpression) {
const compilerOptions = getProp(getAst(source), 'compilerOptions'); const compilerOptions = getProp(ast, 'compilerOptions');
if (!compilerOptions) { if (!compilerOptions) {
throw new Error('unable to find compilerOptions property'); 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) { 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); const existing = getProp(compilerOptions, name);
if (existing) { if (existing) {
@ -41,21 +65,7 @@ export function setCompilerOption(source: string, name: string, value: any) {
// convert to multiline // convert to multiline
const orig = (Jsonc.parse(source) as any).compilerOptions; const orig = (Jsonc.parse(source) as any).compilerOptions;
const [start, end] = getEnds(compilerOptions); const [start, end] = getEnds(compilerOptions);
return ( return source.slice(0, start) + redentJson({ ...orig, [name]: value }) + source.slice(end);
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)
);
} }
const endOfLastProp = getEndOfLastProp(compilerOptions); const endOfLastProp = getEndOfLastProp(compilerOptions);
@ -64,11 +74,12 @@ export function setCompilerOption(source: string, name: string, value: any) {
left = left.slice(0, -1); left = left.slice(0, -1);
} }
const right = source.slice(endOfLastProp); 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) { export function removeCompilerOption(source: string, name: string) {
const compilerOptions = getCompilerOptions(source); const ast = getAst(source);
const compilerOptions = getCompilerOptions(ast);
const culprit = getProp(compilerOptions, name); const culprit = getProp(compilerOptions, name);
if (!culprit) { if (!culprit) {

View file

@ -8,66 +8,61 @@
import dedent from 'dedent'; import dedent from 'dedent';
import { setExclude } from './exclude'; import { setExtends } from './extends';
describe('setExclude()', () => { describe('setExtends()', () => {
it('overwrites previous formatting', () => { it('overrides the value of the extends key', () => {
expect( expect(
setExclude( setExtends(
dedent` dedent`
{ {
"exclude": [1, 2, "foo": "bar",
"foo" "extends": "foo",
] "x": 1
} }
`, `,
['1', 'bar'] 'new value'
) )
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"{ "{
\\"exclude\\": [ \\"foo\\": \\"bar\\",
\\"1\\", \\"extends\\": \\"new value\\",
\\"bar\\", \\"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( expect(
setExclude( setExtends(
dedent` dedent`
{ {
"foo": 1 "foo": "bar",
"x": 1
} }
`, `,
['1', 'bar'] 'new value'
) )
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"{ "{
\\"foo\\": 1, \\"extends\\": \\"new value\\",
\\"exclude\\": [ \\"foo\\": \\"bar\\",
\\"1\\", \\"x\\": 1
\\"bar\\",
]
}" }"
`); `);
});
it('supports setting on an empty object', () => {
expect( expect(
setExclude( setExtends(
dedent` dedent`
{ {}
"foo": 1,
}
`, `,
['1', 'bar'] 'new value'
) )
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"{ "{
\\"foo\\": 1, \\"extends\\": \\"new value\\"
\\"exclude\\": [
\\"1\\",
\\"bar\\",
],
}" }"
`); `);
}); });

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. * 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');
}
/** export function stringify(obj: any) {
* Read the package map from disk return JSON.stringify(obj, null, 2);
*/ }
export function readPackageMap(): PackageMap;
/** export function redentJson(obj: any, spaces: string = ' ') {
* Read the package map and calculate a cache key/hash of the package map return redent(stringify(obj), spaces);
*/ }
export function readHashOfPackageMap(): string;

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); 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[]) { export function removeReferences(source: string, refs: string[]) {
const ast = getAst(source); const ast = getAst(source);

View file

@ -6,7 +6,11 @@
* Side Public License, v 1. * 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[]) { export function snip(source: string, snips: Snip[]) {
const queue = snips const queue = snips
.map((s): Snip => { .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 = { module.exports = {
preset: '@kbn/test/jest_node', preset: '@kbn/test/jest_node',
rootDir: '../..', 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/**/*" "target/**/*"
], ],
"kbn_references": [ "kbn_references": [
"@kbn/repo-linter",
"@kbn/dev-cli-runner", "@kbn/dev-cli-runner",
"@kbn/dev-cli-errors", "@kbn/dev-cli-errors",
"@kbn/repo-path",
"@kbn/get-repo-files", "@kbn/get-repo-files",
"@kbn/repo-packages",
"@kbn/repo-info",
"@kbn/ts-projects", "@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, "private": true,
"version": "1.0.0", "version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0", "license": "SSPL-1.0 OR Elastic License 2.0",

View file

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

View file

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

View file

@ -9,13 +9,13 @@
import Path from 'path'; import Path from 'path';
import { REPO_ROOT } from '@kbn/repo-info'; 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 { TsProjectRule } from '@kbn/repo-linter';
import { replaceReferences } from '../ast';
export const refPkgsIds = Rule.create('refPkgIds', { export const refPkgsIds = TsProjectRule.create('refPkgIds', {
check(proj) { check({ tsProject }) {
const dirsToPkgIds = this.getCache(() => { const dirsToPkgIds = this.getCache(() => {
const pkgMap = readPackageMap(); const pkgMap = readPackageMap();
return new Map(Array.from(pkgMap).map(([k, v]) => [v, k])); 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]> = []; 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) { if (typeof ref === 'string' || ref.force === true) {
continue; continue;
} }
const refPath = Path.resolve(proj.directory, ref.path); const refPath = this.resolve(ref.path);
const pkgIdJson = getPkgId(refPath); const pkgIdJson = getPkgId(refPath);
if (pkgIdJson) { if (pkgIdJson) {
replaceWithPkgId.push([ref.path, pkgIdJson]); replaceWithPkgId.push([ref.path, pkgIdJson]);
@ -48,8 +48,10 @@ export const refPkgsIds = Rule.create('refPkgIds', {
return { return {
msg: `kbn_references must use pkgIds to refer to other packages (use --fix to autofix, or add "force": true to ignore):\n${list}`, 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) { fixes: {
return replaceReferences(source, replaceWithPkgId); '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. * Side Public License, v 1.
*/ */
import { Rule } from '../lib/rule'; import { setCompilerOption } from '@kbn/json-ast';
import { setCompilerOption } from '../ast';
import { TsProjectRule } from '@kbn/repo-linter';
const REQUIRED: Array<[string, string]> = [['outDir', 'target/types']]; const REQUIRED: Array<[string, string]> = [['outDir', 'target/types']];
export const requiredCompilerOptions = Rule.create('requiredCompilerOptions', { export const requiredCompilerOptions = TsProjectRule.create('requiredCompilerOptions', {
check({ config }) { check({ tsProject }) {
for (const [key, value] of REQUIRED) { for (const [key, value] of REQUIRED) {
if (config.compilerOptions?.[key] !== value) { if (tsProject.config.compilerOptions?.[key] !== value) {
this.err(`the value of the compiler option "${key}" must be set to "${value}"`, (source) => this.err(`the value of the compiler option "${key}" must be set to "${value}"`, {
setCompilerOption(source, key, 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. * Side Public License, v 1.
*/ */
import { Rule } from '../lib/rule'; import { TsProjectRule } from '@kbn/repo-linter';
export const requiredFileSelectors = Rule.create('requiredFileSelectors', { export const requiredFileSelectors = TsProjectRule.create('requiredFileSelectors', {
check(project) { check({ tsProject }) {
if (!project.config.include || project.config.files) { if (tsProject.config.files || !tsProject.config.include) {
return { return {
msg: 'every ts project must use the "include" key (and not the "files" key) to select the files for that project', 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 Path from 'path';
import { REPO_ROOT } from '@kbn/repo-info'; 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(); const base = proj.getBase();
if (!base) { if (!base) {
return []; return [];
@ -21,15 +22,27 @@ function getBaseConfigRels(proj: Project): string[] {
return [base.repoRel, ...getBaseConfigRels(base)]; return [base.repoRel, ...getBaseConfigRels(base)];
} }
export const validBaseConfig = Rule.create('validBaseConfig', { export const validBaseTsconfig = TsProjectRule.create('validBaseTsconfig', {
check(proj) { check({ tsProject }) {
const baseConfigRels = getBaseConfigRels(proj); 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') { 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.`; 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')) { 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.`; 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 { getOptimizerCacheKey } from './optimizer_cache_key';
import { OptimizerConfig } from './optimizer_config'; import { OptimizerConfig } from './optimizer_config';
jest.mock('@kbn/package-map', () => { jest.mock('@kbn/repo-packages', () => {
return { return {
readHashOfPackageMap() { readHashOfPackageMap() {
return '<hash of package map>'; return '<hash of package map>';

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