[plugins] use module ids to import across plugins

This commit is contained in:
spalger 2022-04-03 00:23:30 -06:00
parent e47bf4b205
commit bd8171c13e
120 changed files with 10263 additions and 8032 deletions

View file

@ -3,18 +3,4 @@
set -euo pipefail
echo "--- Build Platform Plugins"
node scripts/build_kibana_platform_plugins \
--scan-dir "$KIBANA_DIR/test/analytics/__fixtures__/plugins" \
--scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \
--scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \
--scan-dir "$KIBANA_DIR/test/common/fixtures/plugins" \
--scan-dir "$KIBANA_DIR/examples" \
--scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \
--scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \
--scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \
--scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \
--scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \
--scan-dir "$XPACK_DIR/test/licensing_plugin/plugins" \
--scan-dir "$XPACK_DIR/test/usage_collection/plugins" \
--scan-dir "$XPACK_DIR/test/security_functional/fixtures/common" \
--scan-dir "$XPACK_DIR/examples"
node scripts/build_kibana_platform_plugins --examples --test-plugins

2
.gitignore vendored
View file

@ -10,6 +10,7 @@
node_modules
!/src/dev/npm/integration_tests/__fixtures__/fixture1/node_modules
!/src/dev/notice/__fixtures__/node_modules
!/packages/kbn-import-resolver/src/__fixtures__/node_modules
trash
/optimize
/built_assets
@ -98,4 +99,5 @@ elastic-agent-*
fleet-server-*
elastic-agent.yml
fleet-server.yml
/packages/kbn-synthetic-package-map/synthetic-packages.json

View file

@ -57,7 +57,6 @@ yarn kbn watch
- @kbn/analytics
- @kbn/apm-config-loader
- @kbn/apm-utils
- @kbn/babel-code-parser
- @kbn/babel-preset
- @kbn/cli-dev-mode
- @kbn/config

View file

@ -445,6 +445,7 @@
"@babel/eslint-parser": "^7.17.0",
"@babel/eslint-plugin": "^7.17.7",
"@babel/generator": "^7.17.7",
"@babel/helper-plugin-utils": "^7.16.7",
"@babel/parser": "^7.17.8",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-export-namespace-from": "^7.16.7",
@ -476,7 +477,7 @@
"@jest/console": "^26.6.2",
"@jest/reporters": "^26.6.2",
"@kbn/axe-config": "link:bazel-bin/packages/kbn-axe-config",
"@kbn/babel-code-parser": "link:bazel-bin/packages/kbn-babel-code-parser",
"@kbn/babel-plugin-synthetic-packages": "link:bazel-bin/packages/kbn-babel-plugin-synthetic-packages",
"@kbn/babel-preset": "link:bazel-bin/packages/kbn-babel-preset",
"@kbn/bazel-packages": "link:bazel-bin/packages/kbn-bazel-packages",
"@kbn/cli-dev-mode": "link:bazel-bin/packages/kbn-cli-dev-mode",
@ -486,13 +487,16 @@
"@kbn/es-archiver": "link:bazel-bin/packages/kbn-es-archiver",
"@kbn/eslint-plugin-eslint": "link:bazel-bin/packages/kbn-eslint-plugin-eslint",
"@kbn/expect": "link:bazel-bin/packages/kbn-expect",
"@kbn/find-used-node-modules": "link:bazel-bin/packages/kbn-find-used-node-modules",
"@kbn/generate": "link:bazel-bin/packages/kbn-generate",
"@kbn/import-resolver": "link:bazel-bin/packages/kbn-import-resolver",
"@kbn/optimizer": "link:bazel-bin/packages/kbn-optimizer",
"@kbn/plugin-generator": "link:bazel-bin/packages/kbn-plugin-generator",
"@kbn/plugin-helpers": "link:bazel-bin/packages/kbn-plugin-helpers",
"@kbn/pm": "link:packages/kbn-pm",
"@kbn/spec-to-console": "link:bazel-bin/packages/kbn-spec-to-console",
"@kbn/storybook": "link:bazel-bin/packages/kbn-storybook",
"@kbn/synthetic-package-map": "link:bazel-bin/packages/kbn-synthetic-package-map",
"@kbn/telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools",
"@kbn/test": "link:bazel-bin/packages/kbn-test",
"@kbn/test-jest-helpers": "link:bazel-bin/packages/kbn-test-jest-helpers",
@ -528,6 +532,7 @@
"@types/apidoc": "^0.22.3",
"@types/archiver": "^5.1.0",
"@types/babel__core": "^7.1.19",
"@types/babel__helper-plugin-utils": "^7.10.0",
"@types/base64-js": "^1.2.5",
"@types/chance": "^1.0.0",
"@types/chroma-js": "^1.4.2",
@ -607,9 +612,11 @@
"@types/kbn__es-query": "link:bazel-bin/packages/kbn-es-query/npm_module_types",
"@types/kbn__eslint-plugin-imports": "link:bazel-bin/packages/kbn-eslint-plugin-imports/npm_module_types",
"@types/kbn__field-types": "link:bazel-bin/packages/kbn-field-types/npm_module_types",
"@types/kbn__find-used-node-modules": "link:bazel-bin/packages/kbn-find-used-node-modules/npm_module_types",
"@types/kbn__generate": "link:bazel-bin/packages/kbn-generate/npm_module_types",
"@types/kbn__i18n": "link:bazel-bin/packages/kbn-i18n/npm_module_types",
"@types/kbn__i18n-react": "link:bazel-bin/packages/kbn-i18n-react/npm_module_types",
"@types/kbn__import-resolver": "link:bazel-bin/packages/kbn-import-resolver/npm_module_types",
"@types/kbn__interpreter": "link:bazel-bin/packages/kbn-interpreter/npm_module_types",
"@types/kbn__io-ts-utils": "link:bazel-bin/packages/kbn-io-ts-utils/npm_module_types",
"@types/kbn__logging": "link:bazel-bin/packages/kbn-logging/npm_module_types",

View file

@ -21,7 +21,7 @@ filegroup(
"//packages/kbn-apm-config-loader:build",
"//packages/kbn-apm-utils:build",
"//packages/kbn-axe-config:build",
"//packages/kbn-babel-code-parser:build",
"//packages/kbn-babel-plugin-synthetic-packages:build",
"//packages/kbn-babel-preset:build",
"//packages/kbn-bazel-packages:build",
"//packages/kbn-cli-dev-mode:build",
@ -40,10 +40,12 @@ filegroup(
"//packages/kbn-eslint-plugin-imports:build",
"//packages/kbn-expect:build",
"//packages/kbn-field-types:build",
"//packages/kbn-find-used-node-modules:build",
"//packages/kbn-flot-charts:build",
"//packages/kbn-generate:build",
"//packages/kbn-i18n-react:build",
"//packages/kbn-i18n:build",
"//packages/kbn-import-resolver:build",
"//packages/kbn-interpreter:build",
"//packages/kbn-io-ts-utils:build",
"//packages/kbn-logging-mocks:build",
@ -79,6 +81,7 @@ filegroup(
"//packages/kbn-spec-to-console:build",
"//packages/kbn-std:build",
"//packages/kbn-storybook:build",
"//packages/kbn-synthetic-package-map:build",
"//packages/kbn-telemetry-tools:build",
"//packages/kbn-test-jest-helpers:build",
"//packages/kbn-test-subj-selector:build",
@ -124,9 +127,11 @@ filegroup(
"//packages/kbn-es-query:build_types",
"//packages/kbn-eslint-plugin-imports:build_types",
"//packages/kbn-field-types:build_types",
"//packages/kbn-find-used-node-modules:build_types",
"//packages/kbn-generate:build_types",
"//packages/kbn-i18n-react:build_types",
"//packages/kbn-i18n:build_types",
"//packages/kbn-import-resolver:build_types",
"//packages/kbn-interpreter:build_types",
"//packages/kbn-io-ts-utils:build_types",
"//packages/kbn-logging-mocks:build_types",

View file

@ -110,6 +110,7 @@ module.exports = {
'@kbn/eslint/no_trailing_import_slash': 'error',
'@kbn/eslint/no_constructor_args_in_property_initializers': 'error',
'@kbn/eslint/no_this_in_property_initializers': 'error',
'@kbn/imports/no_unresolved_imports': 'error',
'@kbn/imports/no_unresolvable_imports': 'error',
'@kbn/imports/uniform_imports': 'error',
},
};

View file

@ -1,61 +0,0 @@
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm")
PKG_BASE_NAME = "kbn-babel-code-parser"
PKG_REQUIRE_NAME = "@kbn/babel-code-parser"
SOURCE_FILES = glob(
[
"src/**/*",
],
exclude = [
"**/*.test.*"
],
)
SRCS = SOURCE_FILES
filegroup(
name = "srcs",
srcs = SRCS,
)
NPM_MODULE_EXTRA_FILES = [
"package.json",
"README.md",
]
RUNTIME_DEPS = [
"@npm//@babel/parser",
"@npm//@babel/traverse",
"@npm//lodash",
]
jsts_transpiler(
name = "target_node",
srcs = SRCS,
build_pkg_name = package_name(),
)
js_library(
name = PKG_BASE_NAME,
srcs = NPM_MODULE_EXTRA_FILES,
deps = RUNTIME_DEPS + [":target_node"],
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)
pkg_npm(
name = "npm_module",
deps = [
":%s" % PKG_BASE_NAME,
]
)
filegroup(
name = "build",
srcs = [
":npm_module",
],
visibility = ["//visibility:public"],
)

View file

@ -1,19 +0,0 @@
# @kbn/babel-code-parser
Simple abstraction over the `@babel/parser` and the `@babel/traverse` in order
to build a code parser on top.
We have two main functions `parseSingleFile` (sync and sync version) and the
`parseEntries` (only async version). The first one just parse one entry file
and the second one parses recursively all the files from a list of
start entry points.
Then we have `visitors` and `strategies`. The first ones are basically the
`visitors` to use into the ast from the `@babel/traverse`. They are the only
way to collect info when using the `parseSingleFile`. The `strategies` are
meant to be used with the `parseEntries` and configures the info we want
to collect from our parsed code. After each loop, one per entry file, the
`parseEntries` method will call the given `strategy` expecting that
`strategy` would call the desired visitors, assemble the important
information to collect and adds them to the final results.

View file

@ -1,15 +0,0 @@
{
"name": "@kbn/babel-code-parser",
"description": "babel code parser for Kibana",
"private": true,
"version": "1.0.0",
"main": "./target_node/index.js",
"license": "SSPL-1.0 OR Elastic License 2.0",
"repository": {
"type": "git",
"url": "https://github.com/elastic/kibana/tree/main/packages/kbn-babel-code-parser"
},
"kibana": {
"devOnly": true
}
}

View file

@ -1,23 +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.
*/
export function canRequire(cwd, entry) {
try {
// We will try to test if we can resolve
// this entry through the require.resolve
// setting as the start looking path the
// given cwd. Require.resolve will keep
// looking recursively as normal starting
// from that location.
return require.resolve(entry, {
paths: [cwd],
});
} catch (e) {
return false;
}
}

View file

@ -1,94 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { canRequire } from './can_require';
import { readFile, readFileSync } from 'fs';
import { extname } from 'path';
import { promisify } from 'util';
import * as parser from '@babel/parser';
import traverse from '@babel/traverse';
import * as babelParserOptions from '@kbn/babel-preset/common_babel_parser_options';
const read = promisify(readFile);
function _cannotParseFile(filePath) {
return extname(filePath) !== '.js';
}
function _parseAndTraverseFileContent(fileContent, visitorsGenerator) {
const results = [];
// Parse and get the code AST
// All the babel parser plugins
// were enabled
const ast = parser.parse(fileContent, babelParserOptions);
// Loop through the code AST with
// the defined visitors
traverse(ast, visitorsGenerator(results));
return results;
}
export async function parseSingleFile(filePath, visitorsGenerator) {
// Don't parse any other files than .js ones
if (_cannotParseFile(filePath)) {
return [];
}
// Read the file
const content = await read(filePath, { encoding: 'utf8' });
// return the results found on parse and traverse
// the file content with the given visitors
return _parseAndTraverseFileContent(content, visitorsGenerator);
}
export function parseSingleFileSync(filePath, visitorsGenerator) {
// Don't parse any other files than .js ones
if (_cannotParseFile(filePath)) {
return [];
}
// Read the file
const content = readFileSync(filePath, { encoding: 'utf8' });
// return the results found on parse and traverse
// the file content with the given visitors
return _parseAndTraverseFileContent(content, visitorsGenerator);
}
export async function parseEntries(cwd, entries, strategy, results, wasParsed = {}) {
// Assure that we always have a cwd
const sanitizedCwd = cwd || process.cwd();
// Test each entry against canRequire function
const entriesQueue = entries.map((entry) => canRequire(sanitizedCwd, entry));
while (entriesQueue.length) {
// Get the first element in the queue as
// select it as our current entry to parse
const mainEntry = entriesQueue.shift();
// Avoid parse the current entry if it is not valid
// or it was already parsed
if (typeof mainEntry !== 'string' || wasParsed[mainEntry]) {
continue;
}
// Find new entries and adds them to the end of the queue
entriesQueue.push(
...(await strategy(sanitizedCwd, parseSingleFile, mainEntry, wasParsed, results))
);
// Mark the current main entry as already parsed
wasParsed[mainEntry] = true;
}
return results;
}

View file

@ -1,90 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { canRequire } from './can_require';
import { dependenciesVisitorsGenerator } from './visitors';
import { dirname, isAbsolute, resolve } from 'path';
export function _calculateTopLevelDependency(inputDep, outputDep = '') {
// The path separator will be always the forward slash
// as at this point we only have the found entries into
// the provided source code entries where we just use it
const pathSeparator = '/';
const depSplitPaths = inputDep.split(pathSeparator);
const firstPart = depSplitPaths.shift();
const outputDepFirstArgAppend = outputDep ? pathSeparator : '';
outputDep += `${outputDepFirstArgAppend}${firstPart}`;
// In case our dependency isn't started by @
// we are already done and we can return the
// dependency value we already have
if (firstPart.charAt(0) !== '@') {
return outputDep;
}
// Otherwise we need to keep constructing the dependency
// value because dependencies starting with @ points to
// folders of dependencies. For example, in case we found
// dependencies values with '@the-deps/a' and '@the-deps/a/b'
// we don't want to map it to '@the-deps' but also to @'the-deps/a'
// because inside '@the-deps' we can also have '@the-dep/b'
return _calculateTopLevelDependency(depSplitPaths.join(pathSeparator), outputDep);
}
export async function dependenciesParseStrategy(
cwd,
parseSingleFile,
mainEntry,
wasParsed,
results
) {
// Retrieve native nodeJS modules
const natives = process.binding('natives');
// Get dependencies from a single file and filter
// out node native modules from the result
const dependencies = (await parseSingleFile(mainEntry, dependenciesVisitorsGenerator)).filter(
(dep) => !natives[dep]
);
// Return the list of all the new entries found into
// the current mainEntry that we could use to look for
// new dependencies
return dependencies.reduce((filteredEntries, entry) => {
const absEntryPath = resolve(cwd, dirname(mainEntry), entry);
const requiredPath = canRequire(cwd, absEntryPath);
const requiredRelativePath = canRequire(cwd, entry);
const isRelativeFile = !isAbsolute(entry);
const isNodeModuleDep = isRelativeFile && !requiredPath && requiredRelativePath;
const isNewEntry = isRelativeFile && requiredPath;
// If it is a node_module add it to the results and also
// add the resolved path for the node_module main file
// as an entry point to look for dependencies it was
// not already parsed
if (isNodeModuleDep) {
// Save the result as the top level dependency
results[_calculateTopLevelDependency(entry)] = true;
if (!wasParsed[requiredRelativePath]) {
filteredEntries.push(requiredRelativePath);
}
}
// If a new, not yet parsed, relative entry were found
// add it to the list of entries to be parsed
if (isNewEntry && !wasParsed[requiredPath]) {
if (!wasParsed[requiredPath]) {
filteredEntries.push(requiredPath);
}
}
return filteredEntries;
}, []);
}

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.
*/
import { readFile } from 'fs';
import { canRequire } from './can_require';
import { parseSingleFile } from './code_parser';
import { _calculateTopLevelDependency, dependenciesParseStrategy } from './strategies';
jest.mock('./can_require', () => ({
canRequire: jest.fn(),
}));
jest.mock('fs', () => ({
readFile: jest.fn(),
}));
const mockCwd = '/tmp/project/dir/';
describe('Code Parser Strategies', () => {
it('should calculate the top level dependencies correctly', () => {
const plainDep = 'dep1/file';
const foldedDep = '@kbn/es/file';
const otherFoldedDep = '@kbn/es';
expect(_calculateTopLevelDependency(plainDep)).toEqual('dep1');
expect(_calculateTopLevelDependency(foldedDep)).toEqual('@kbn/es');
expect(_calculateTopLevelDependency(otherFoldedDep)).toEqual('@kbn/es');
});
it('should exclude native modules', async () => {
readFile.mockImplementationOnce((path, options, cb) => {
cb(null, `require('fs')`);
});
const results = [];
await dependenciesParseStrategy(mockCwd, parseSingleFile, 'dep1/file.js', {}, results);
expect(results.length).toBe(0);
});
it('should return a dep from_modules', async () => {
readFile.mockImplementationOnce((path, options, cb) => {
cb(null, `require('dep_from_node_modules')`);
});
canRequire.mockImplementation((mockCwd, entry) => {
if (entry === `${mockCwd}dep1/dep_from_node_modules`) {
return false;
}
if (entry === 'dep_from_node_modules') {
return `${mockCwd}node_modules/dep_from_node_modules/index.js`;
}
});
const results = await dependenciesParseStrategy(
mockCwd,
parseSingleFile,
'dep1/file.js',
{},
{}
);
expect(results[0]).toBe(`${mockCwd}node_modules/dep_from_node_modules/index.js`);
});
it('should return a relative dep file', async () => {
readFile.mockImplementationOnce((path, options, cb) => {
cb(null, `require('./relative_dep')`);
});
canRequire.mockImplementation((mockCwd, entry) => {
if (entry === `${mockCwd}dep1/relative_dep`) {
return `${entry}/index.js`;
}
return false;
});
const results = await dependenciesParseStrategy(
mockCwd,
parseSingleFile,
'dep1/file.js',
{},
{}
);
expect(results[0]).toBe(`${mockCwd}dep1/relative_dep/index.js`);
});
afterAll(() => {
jest.clearAllMocks();
});
});

View file

@ -1,131 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { matches } from 'lodash';
/**
* @notice
*
* This product has relied on ASTExplorer that is licensed under MIT.
*/
export function dependenciesVisitorsGenerator(dependenciesAcc) {
return (() => {
// This was built with help on an ast explorer and some ESTree docs
// like the babel parser ast spec and the main docs for the Esprima
// which is a complete and useful docs for the ESTree spec.
//
// https://astexplorer.net
// https://github.com/babel/babel/blob/master/packages/babel-parser/ast/spec.md
// https://esprima.readthedocs.io/en/latest/syntax-tree-format.html
// https://github.com/estree/estree
return {
// Visitors to traverse and found dependencies
// raw values on require + require.resolve
CallExpression: ({ node }) => {
// AST check for require expressions
const isRequire = (node) => {
return matches({
callee: {
type: 'Identifier',
name: 'require',
},
})(node);
};
// AST check for require.resolve expressions
const isRequireResolve = (node) => {
return matches({
callee: {
type: 'MemberExpression',
object: {
type: 'Identifier',
name: 'require',
},
property: {
type: 'Identifier',
name: 'resolve',
},
},
})(node);
};
// Get string values inside the expressions
// whether they are require or require.resolve
if (isRequire(node) || isRequireResolve(node)) {
const nodeArguments = node.arguments;
const reqArg = Array.isArray(nodeArguments) ? nodeArguments.shift() : null;
if (!reqArg) {
return;
}
if (reqArg.type === 'StringLiteral') {
dependenciesAcc.push(reqArg.value);
}
}
},
// Visitors to traverse and found dependencies
// raw values on import
ImportDeclaration: ({ node }) => {
// AST check for supported import expressions
const isImport = (node) => {
return matches({
type: 'ImportDeclaration',
source: {
type: 'StringLiteral',
},
})(node);
};
// Get string values from import expressions
if (isImport(node)) {
const importSource = node.source;
dependenciesAcc.push(importSource.value);
}
},
// Visitors to traverse and found dependencies
// raw values on export from
ExportNamedDeclaration: ({ node }) => {
// AST check for supported export from expressions
const isExportFrom = (node) => {
return matches({
type: 'ExportNamedDeclaration',
source: {
type: 'StringLiteral',
},
})(node);
};
// Get string values from export from expressions
if (isExportFrom(node)) {
const exportFromSource = node.source;
dependenciesAcc.push(exportFromSource.value);
}
},
// Visitors to traverse and found dependencies
// raw values on export * from
ExportAllDeclaration: ({ node }) => {
// AST check for supported export * from expressions
const isExportAllFrom = (node) => {
return matches({
type: 'ExportAllDeclaration',
source: {
type: 'StringLiteral',
},
})(node);
};
// Get string values from export * from expressions
if (isExportAllFrom(node)) {
const exportAllFromSource = node.source;
dependenciesAcc.push(exportAllFromSource.value);
}
},
};
})();
}

View file

@ -1,57 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as parser from '@babel/parser';
import traverse from '@babel/traverse';
import { dependenciesVisitorsGenerator } from './visitors';
const visitorsApplier = (code) => {
const result = [];
traverse(
parser.parse(code, {
sourceType: 'unambiguous',
plugins: ['exportDefaultFrom'],
}),
dependenciesVisitorsGenerator(result)
);
return result;
};
describe('Code Parser Visitors', () => {
it('should get values from require', () => {
const rawCode = `/*foo*/require('dep1'); const bar = 1;`;
const foundDeps = visitorsApplier(rawCode);
expect(foundDeps[0] === 'dep1');
});
it('should get values from require.resolve', () => {
const rawCode = `/*foo*/require.resolve('dep2'); const bar = 1;`;
const foundDeps = visitorsApplier(rawCode);
expect(foundDeps[0] === 'dep2');
});
it('should get values from import', () => {
const rawCode = `/*foo*/import dep1 from 'dep1'; import dep2 from 'dep2';const bar = 1;`;
const foundDeps = visitorsApplier(rawCode);
expect(foundDeps[0] === 'dep1');
expect(foundDeps[1] === 'dep2');
});
it('should get values from export from', () => {
const rawCode = `/*foo*/export dep1 from 'dep1'; import dep2 from 'dep2';const bar = 1;`;
const foundDeps = visitorsApplier(rawCode);
expect(foundDeps[0] === 'dep1');
});
it('should get values from export * from', () => {
const rawCode = `/*foo*/export * from 'dep1'; export dep2 from 'dep2';const bar = 1;`;
const foundDeps = visitorsApplier(rawCode);
expect(foundDeps[0] === 'dep1');
expect(foundDeps[1] === 'dep2');
});
});

View file

@ -0,0 +1,64 @@
load("@npm//@bazel/typescript:index.bzl", "ts_config")
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
PKG_DIRNAME = "kbn-babel-plugin-synthetic-packages"
PKG_REQUIRE_NAME = "@kbn/babel-plugin-synthetic-packages"
filegroup(
name = "srcs",
srcs = [
"babel_plugin_synthetic_packages.js"
],
)
NPM_MODULE_EXTRA_FILES = [
"package.json",
]
# In this array place runtime dependencies, including other packages and NPM packages
# which must be available for this code to run.
#
# To reference other packages use:
# "//repo/relative/path/to/package"
# eg. "//packages/kbn-utils"
#
# To reference a NPM package use:
# "@npm//name-of-package"
# eg. "@npm//lodash"
RUNTIME_DEPS = [
"@npm//@babel/helper-plugin-utils",
"@npm//normalize-path",
"//packages/kbn-synthetic-package-map",
]
js_library(
name = PKG_DIRNAME,
srcs = NPM_MODULE_EXTRA_FILES + [
":srcs",
],
deps = RUNTIME_DEPS,
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)
pkg_npm(
name = "npm_module",
deps = [
":%s" % PKG_DIRNAME,
]
)
filegroup(
name = "build",
srcs = [
":npm_module",
],
visibility = ["//visibility:public"],
)
alias(
name = "npm_module_types",
actual = ":" + PKG_DIRNAME,
visibility = ["//visibility:public"],
)

View file

@ -0,0 +1,183 @@
/*
* 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('@babel/core').PluginObj} PluginObj */
const Path = require('path');
const Fs = require('fs');
const T = require('@babel/types');
const normalizePath = require('normalize-path');
const { declare } = require('@babel/helper-plugin-utils');
const KbnSyntheticPackageMap = require('@kbn/synthetic-package-map');
const PKG_MAP = KbnSyntheticPackageMap.readPackageMap();
const PKG_MAP_HASH = KbnSyntheticPackageMap.readHashOfPackageMap();
function getFilename(state) {
if (typeof state !== 'object' || !state || !state.filename || !Path.isAbsolute(state.filename)) {
throw new Error(
`@kbn/babel-plugin-synthetic-packages is only compatible when building files with absolute filename state`
);
}
return state.filename;
}
let foundKibanaRoot;
function getKibanaRoot(someSourceFilename) {
if (foundKibanaRoot) {
return foundKibanaRoot;
}
// try to find the Kibana package.json file in a parent directory of the sourceFile
let cursorDir = Path.dirname(someSourceFilename);
while (true) {
const packageJsonPath = Path.resolve(cursorDir, 'package.json');
try {
const pkg = JSON.parse(Fs.readFileSync(packageJsonPath, 'utf8'));
if (pkg && pkg.name === 'kibana') {
foundKibanaRoot = cursorDir;
return foundKibanaRoot;
}
} catch {
// this directory is not the Kibana dir
}
const nextCursor = Path.dirname(cursorDir);
if (!nextCursor || nextCursor === cursorDir) {
// stop iterating when we get to the root of the root of the filesystem
break;
}
cursorDir = nextCursor;
}
throw new Error(
'@kbn/*-plugin and @kbn/core imports can only be used by source files which have not been converted to packages, building packages which rely on these imports requires converting the thing you want into a package.'
);
}
function fixImportRequest(req, filename, kibanaRoot) {
if (!req.startsWith('@kbn/')) {
return;
}
const parts = req.split('/');
const dir = PKG_MAP.get(`@kbn/${parts[1]}`);
if (!dir) {
return;
}
return normalizePath(
Path.relative(
Path.dirname(filename),
Path.resolve(kibanaRoot ?? getKibanaRoot(filename), dir, ...parts.slice(2))
)
);
}
/**
* @param {T.CallExpression} node
*/
function isDynamicImport(node) {
return !!(
T.isImport(node.callee) &&
node.arguments.length === 1 &&
T.isStringLiteral(node.arguments[0])
);
}
/**
* @param {T.CallExpression} node
*/
function isRequire(node) {
return !!(
T.isIdentifier(node.callee) &&
node.callee.name === 'require' &&
node.arguments.length >= 1 &&
T.isStringLiteral(node.arguments[0])
);
}
/**
* @param {T.CallExpression} node
*/
function isRequireResolve(node) {
return !!(
T.isMemberExpression(node.callee) &&
T.isIdentifier(node.callee.object) &&
node.callee.object.name === 'require' &&
T.isIdentifier(node.callee.property) &&
node.callee.property.name === 'resolve' &&
node.arguments.length >= 1 &&
T.isStringLiteral(node.arguments[0])
);
}
/**
* @param {T.CallExpression} node
*/
function isJestMockCall(node) {
return !!(
T.isMemberExpression(node.callee) &&
T.isIdentifier(node.callee.object) &&
node.callee.object.name === 'jest' &&
node.arguments.length >= 1 &&
T.isStringLiteral(node.arguments[0])
);
}
module.exports = declare((api, options) => {
const kibanaRoot = options['kibana/rootDir'];
api.assertVersion(7);
api.cache.using(() => `${PKG_MAP_HASH}:${kibanaRoot}`);
/** @type {PluginObj} */
const plugin = {
name: 'synthetic-packages',
visitor: {
'ImportDeclaration|ExportNamedDeclaration|ExportAllDeclaration'(path) {
const filename = getFilename(this);
const source = path.node.source;
if (!T.isStringLiteral(source)) {
return;
}
const req = source.value;
const newReq = fixImportRequest(req, filename, kibanaRoot);
if (newReq) {
path.get('source').replaceWith(T.stringLiteral(newReq));
}
},
CallExpression(path) {
const filename = getFilename(this);
const { node } = path;
if (
!isDynamicImport(node) &&
!isRequire(node) &&
!isRequireResolve(node) &&
!isJestMockCall(node)
) {
return;
}
const req = node.arguments[0].value;
const newReq = fixImportRequest(req, filename, kibanaRoot);
if (newReq) {
path.get('arguments.0').replaceWith(T.stringLiteral(newReq));
}
},
},
};
return plugin;
});

View file

@ -0,0 +1,10 @@
{
"name": "@kbn/babel-plugin-synthetic-packages",
"private": true,
"version": "1.0.0",
"main": "./babel_plugin_synthetic_packages.js",
"license": "SSPL-1.0 OR Elastic License 2.0",
"kibana": {
"devOnly": true
}
}

View file

@ -40,6 +40,7 @@ RUNTIME_DEPS = [
"@npm//babel-plugin-add-module-exports",
"@npm//babel-plugin-styled-components",
"@npm//babel-plugin-transform-react-remove-prop-types",
"//packages/kbn-babel-plugin-synthetic-packages",
]
js_library(

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
module.exports = {
module.exports = (_, options = {}) => ({
presets: [
// plugins always run before presets, but in this case we need the
// @babel/preset-typescript preset to run first so we have to move
@ -46,6 +46,8 @@ module.exports = {
version: '^7.12.5',
},
],
[require.resolve('@kbn/babel-plugin-synthetic-packages'), options],
],
},
@ -59,4 +61,4 @@ module.exports = {
},
],
],
};
});

View file

@ -37,7 +37,7 @@ module.exports = (_, options = {}) => {
...(options['@babel/preset-env'] || {}),
},
],
require('./common_preset'),
[require('./common_preset'), options],
],
};
};

View file

@ -8,7 +8,7 @@
const { USES_STYLED_COMPONENTS } = require('./styled_components_files');
module.exports = () => {
module.exports = (_, options = {}) => {
return {
presets: [
[
@ -22,7 +22,7 @@ module.exports = () => {
bugfixes: true,
},
],
require('./common_preset'),
[require('./common_preset'), options],
],
env: {
production: {

View file

@ -39,6 +39,7 @@ NPM_MODULE_EXTRA_FILES = [
RUNTIME_DEPS = [
"//packages/kbn-utils",
"//packages/kbn-std",
"//packages/kbn-synthetic-package-map",
"@npm//globby",
"@npm//normalize-path",
]
@ -56,6 +57,7 @@ RUNTIME_DEPS = [
TYPES_DEPS = [
"//packages/kbn-utils:npm_module_types",
"//packages/kbn-std:npm_module_types",
"//packages/kbn-synthetic-package-map:npm_module_types",
"@npm//@types/normalize-path",
"@npm//globby",
"@npm//normalize-path",

View file

@ -13,51 +13,21 @@ import Fsp from 'fs/promises';
import normalizePath from 'normalize-path';
import { REPO_ROOT } from '@kbn/utils';
import { readPackageJson, ParsedPackageJson } from './parse_package_json';
const BUILD_RULE_NAME = /(^|\s)name\s*=\s*"build"/;
const BUILD_TYPES_RULE_NAME = /(^|\s)name\s*=\s*"build_types"/;
/**
* Simple parsed representation of a package.json file, validated
* by `assertParsedPackageJson()` and extensible as needed in the future
*/
export interface ParsedPackageJson {
/**
* The name of the package, usually `@kbn/`+something
*/
name: string;
/**
* All other fields in the package.json are typed as unknown as all we need at this time is "name"
*/
[key: string]: unknown;
}
function isObj(v: unknown): v is Record<string, unknown> {
return !!(typeof v === 'object' && v);
}
function assertParsedPackageJson(v: unknown): asserts v is ParsedPackageJson {
if (!isObj(v) || typeof v.name !== 'string') {
throw new Error('Expected parsed package.json to be an object with at least a "name" property');
}
}
/**
* Representation of a Bazel Package in the Kibana repository
*/
export class BazelPackage {
/**
* Create a BazelPackage object from a package directory. Reads some files from the package and returns
* a Promise for a BazelPackage instance
* a Promise for a BazelPackage instance.
*/
static async fromDir(dir: string) {
let pkg;
try {
pkg = JSON.parse(await Fsp.readFile(Path.resolve(dir, 'package.json'), 'utf8'));
} catch (error) {
throw new Error(`unable to parse package.json in [${dir}]: ${error.message}`);
}
assertParsedPackageJson(pkg);
const pkg = readPackageJson(Path.resolve(dir, 'package.json'));
let buildBazelContent;
if (pkg.name !== '@kbn/pm') {

View file

@ -15,22 +15,23 @@ import { asyncMapWithLimit } from '@kbn/std';
import { BazelPackage } from './bazel_package';
import { BAZEL_PACKAGE_DIRS } from './bazel_package_dirs';
/**
* Search the local Kibana repo for bazel packages and return an array of BazelPackage objects
* representing each package found.
*/
export async function discoverBazelPackages() {
const packageJsons = globby.sync(
BAZEL_PACKAGE_DIRS.map((dir) => `${dir}/*/package.json`),
{
cwd: REPO_ROOT,
absolute: true,
}
);
export function discoverBazelPackageLocations(repoRoot: string) {
return globby
.sync(
BAZEL_PACKAGE_DIRS.map((dir) => `${dir}/*/package.json`),
{
cwd: repoRoot,
absolute: true,
}
)
.sort((a, b) => a.localeCompare(b))
.map((path) => Path.dirname(path));
}
export async function discoverBazelPackages(repoRoot: string = REPO_ROOT) {
return await asyncMapWithLimit(
packageJsons.sort((a, b) => a.localeCompare(b)),
discoverBazelPackageLocations(repoRoot),
100,
async (path) => await BazelPackage.fromDir(Path.dirname(path))
async (dir) => await BazelPackage.fromDir(dir)
);
}

View file

@ -0,0 +1,63 @@
/*
* 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 Fs from 'fs';
/**
* Simple parsed representation of a package.json file, validated
* by `assertParsedPackageJson()` and extensible as needed in the future
*/
export interface ParsedPackageJson {
/**
* The name of the package, usually `@kbn/`+something
*/
name: string;
/** "dependenices" property from package.json */
dependencies?: Record<string, string>;
/** "devDependenices" property from package.json */
devDependencies?: Record<string, string>;
/**
* All other fields in the package.json are typed as unknown as we don't care what they are
*/
[key: string]: unknown;
}
function isObj(v: unknown): v is Record<string, unknown> {
return !!(typeof v === 'object' && v);
}
/**
* Asserts that given value looks like a parsed package.json file
*/
export function assertParsedPackageJson(v: unknown): asserts v is ParsedPackageJson {
if (!isObj(v) || typeof v.name !== 'string') {
throw new Error('Expected at least a "name" property');
}
if (v.dependencies && !isObj(v.dependencies)) {
throw new Error('Expected "dependencies" to be an object');
}
if (v.devDependencies && !isObj(v.devDependencies)) {
throw new Error('Expected "dependencies" to be an object');
}
}
/**
* Reads a given package.json file from disk and parses it
*/
export function readPackageJson(path: string): ParsedPackageJson {
let pkg;
try {
pkg = JSON.parse(Fs.readFileSync(path, 'utf8'));
assertParsedPackageJson(pkg);
} catch (error) {
throw new Error(`unable to parse package.json at [${path}]: ${error.message}`);
}
return pkg;
}

View file

@ -37,6 +37,7 @@ RUNTIME_DEPS = [
"//packages/kbn-std",
"//packages/kbn-utility-types",
"//packages/kbn-i18n",
"//packages/kbn-plugin-discovery",
"@npm//js-yaml",
"@npm//load-json-file",
"@npm//lodash",
@ -52,6 +53,7 @@ TYPES_DEPS = [
"//packages/kbn-std:npm_module_types",
"//packages/kbn-utility-types:npm_module_types",
"//packages/kbn-i18n:npm_module_types",
"//packages/kbn-plugin-discovery:npm_module_types",
"@npm//load-json-file",
"@npm//rxjs",
"@npm//@types/jest",

View file

@ -8,7 +8,7 @@
import { resolve, join } from 'path';
import loadJsonFile from 'load-json-file';
import { getPluginSearchPaths } from './plugins';
import { getPluginSearchPaths } from '@kbn/plugin-discovery';
import { PackageInfo, EnvironmentMode } from './types';
/** @internal */

View file

@ -30,4 +30,3 @@ export { ObjectToConfigAdapter } from './object_to_config_adapter';
export type { CliArgs, RawPackageInfo } from './env';
export { Env } from './env';
export type { EnvironmentMode, PackageInfo } from './types';
export { getPluginSearchPaths } from './plugins';

View file

@ -1,26 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { resolve } from 'path';
interface SearchOptions {
rootDir: string;
oss: boolean;
examples: boolean;
}
export function getPluginSearchPaths({ rootDir, oss, examples }: SearchOptions) {
return [
resolve(rootDir, 'src', 'plugins'),
...(oss ? [] : [resolve(rootDir, 'x-pack', 'plugins')]),
resolve(rootDir, 'plugins'),
...(examples ? [resolve(rootDir, 'examples')] : []),
...(examples && !oss ? [resolve(rootDir, 'x-pack', 'examples')] : []),
resolve(rootDir, '..', 'kibana-extra'),
];
}

View file

@ -28,7 +28,7 @@ NPM_MODULE_EXTRA_FILES = [
]
RUNTIME_DEPS = [
"//packages/kbn-config",
"//packages/kbn-plugin-discovery",
"//packages/kbn-dev-utils",
"//packages/kbn-utils",
"@npm//dedent",
@ -36,7 +36,7 @@ RUNTIME_DEPS = [
]
TYPES_DEPS = [
"//packages/kbn-config:npm_module_types",
"//packages/kbn-plugin-discovery:npm_module_types",
"//packages/kbn-dev-utils:npm_module_types",
"//packages/kbn-utils:npm_module_types",
"@npm//ts-morph",

View file

@ -11,8 +11,7 @@ import globby from 'globby';
import loadJsonFile from 'load-json-file';
import { getPluginSearchPaths } from '@kbn/config';
import { simpleKibanaPlatformPluginDiscovery } from '@kbn/plugin-discovery';
import { getPluginSearchPaths, simpleKibanaPlatformPluginDiscovery } from '@kbn/plugin-discovery';
import { REPO_ROOT } from '@kbn/utils';
import { ApiScope, PluginOrPackage } from './types';

View file

@ -30,6 +30,7 @@ NPM_MODULE_EXTRA_FILES = [
]
RUNTIME_DEPS = [
"//packages/kbn-utils",
"@npm//@babel/eslint-parser",
"@npm//dedent",
"@npm//eslint",

View file

@ -28,7 +28,7 @@
*/
const path = require('path');
const mm = require('micromatch');
const { resolveKibanaImport } = require('@kbn/eslint-plugin-imports');
const { getImportResolver } = require('@kbn/eslint-plugin-imports');
function isStaticRequire(node) {
return (
@ -90,6 +90,8 @@ module.exports = {
},
create(context) {
const resolver = getImportResolver(context);
const sourcePath = context.getPhysicalFilename
? context.getPhysicalFilename()
: context.getFilename();
@ -103,7 +105,7 @@ module.exports = {
}
function checkForRestrictedImportPath(importPath, node) {
const resolveReport = resolveKibanaImport(importPath, sourceDirname);
const resolveReport = resolver.resolve(importPath, sourceDirname);
if (resolveReport?.type !== 'file' || resolveReport.nodeModule) {
return;

View file

@ -37,6 +37,7 @@ NPM_MODULE_EXTRA_FILES = [
# eg. "@npm//lodash"
RUNTIME_DEPS = [
"//packages/kbn-utils",
"//packages/kbn-import-resolver",
"@npm//resolve",
"@npm//@typescript-eslint/typescript-estree",
"@npm//eslint",
@ -55,6 +56,7 @@ RUNTIME_DEPS = [
TYPES_DEPS = [
"//packages/kbn-utils:npm_module_types",
"//packages/kbn-dev-utils:npm_module_types", # only required for the tests, which are excluded except on windows
"//packages/kbn-import-resolver:npm_module_types",
"@npm//@types/eslint",
"@npm//@types/jest",
"@npm//@types/node",

View file

@ -0,0 +1,30 @@
/*
* 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 { ImportResolver } from '@kbn/import-resolver';
import { REPO_ROOT } from '@kbn/utils';
import { Rule } from 'eslint';
import { RUNNING_IN_EDITOR } from './helpers/running_in_editor';
let importResolverCache: ImportResolver | undefined;
/**
* Create a request resolver for ESLint, requires a PluginPackageResolver from @kbn/bazel-packages which will
* be created and cached on contextServices automatically.
*
* All import requests in the repository should return a result, if they don't it's a bug
* which should be caught by the `@kbn/import/no_unresolved` rule, which should never be disabled. If you need help
* adding support for an import style please reach out to operations.
*/
export function getImportResolver(context: Rule.RuleContext): ImportResolver {
if (RUNNING_IN_EDITOR) {
return (context.parserServices.kibanaImportResolver ||= ImportResolver.create(REPO_ROOT));
}
return (importResolverCache ||= ImportResolver.create(REPO_ROOT));
}

View file

@ -0,0 +1,31 @@
/*
* 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 Eslint from 'eslint';
import { SomeNode } from './visit_all_import_statements';
interface ReportOptions {
node: SomeNode;
message: string;
correctImport?: string;
}
/**
* Simple wrapper around context.report so that the types work better with typescript-estree
*/
export function report(context: Eslint.Rule.RuleContext, options: ReportOptions) {
context.report({
node: options.node as any,
message: options.message,
fix: options.correctImport
? (fixer) => {
return fixer.replaceText(options.node as any, `'${options.correctImport}'`);
}
: null,
});
}

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.
*/
export const RUNNING_IN_EDITOR =
// vscode sets this in the env for all workers
!!process.env.VSCODE_CWD ||
// MacOS sets this for intellij processes, not sure if it works in webstorm but we could expand this check later
!!process.env.__CFBundleIdentifier?.startsWith('com.jetbrains.intellij');

View file

@ -6,73 +6,113 @@
* Side Public License, v 1.
*/
import { Rule } from 'eslint';
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree';
// @ts-expect-error no types for this module
import moduleVisitor from 'eslint-module-utils/moduleVisitor';
import * as T from '@babel/types';
import { ImportType } from '@kbn/import-resolver';
type Importers =
| TSESTree.ImportDeclaration
| TSESTree.ExportNamedDeclaration
| TSESTree.ExportAllDeclaration
| TSESTree.CallExpression
| TSESTree.ImportExpression
| TSESTree.CallExpression;
const JEST_MODULE_METHODS = [
'jest.createMockFromModule',
'jest.mock',
'jest.unmock',
'jest.doMock',
'jest.dontMock',
'jest.setMock',
'jest.requireActual',
'jest.requireMock',
];
type ImportVisitor = (req: string, node: Importers) => void;
export type SomeNode = TSESTree.Node | T.Node;
type Visitor = (req: string | null, node: SomeNode, type: ImportType) => void;
const isIdent = (node: SomeNode): node is TSESTree.Identifier | T.Identifier =>
T.isIdentifier(node) || node.type === AST_NODE_TYPES.Identifier;
const isStringLiteral = (node: SomeNode): node is TSESTree.StringLiteral | T.StringLiteral =>
T.isStringLiteral(node) ||
(node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string');
const isTemplateLiteral = (node: SomeNode): node is TSESTree.TemplateLiteral | T.TemplateLiteral =>
T.isTemplateLiteral(node) || node.type === AST_NODE_TYPES.TemplateLiteral;
function passSourceAsString(source: SomeNode | null | undefined, type: ImportType, fn: Visitor) {
if (!source) {
return;
}
if (isStringLiteral(source)) {
return fn(source.value, source, type);
}
if (isTemplateLiteral(source)) {
if (source.expressions.length) {
return null;
}
return fn(
[...source.quasis].reduce((acc, q) => acc + q.value.raw, ''),
source,
type
);
}
return fn(null, source, type);
}
/**
* Create an ESLint rule visitor that calls visitor() for every import string, including
* 'export from' statements, require() calls, jest.mock() calls, and more.
* Create an ESLint rule visitor that calls fn() for every import string, including
* 'export from' statements, require() calls, require.resolve(), jest.mock() calls, and more.
* Works with both babel eslint and typescript-eslint parsers
*/
export function visitAllImportStatements(visitor: ImportVisitor) {
const baseWrapper = moduleVisitor(
(reqNode: TSESTree.Literal, importer: Importers) => {
const req = reqNode.value;
if (typeof req !== 'string') {
throw new Error('unable to read value of import request');
}
visitor(req, importer);
export function visitAllImportStatements(fn: Visitor) {
const visitor = {
ImportDeclaration(node: TSESTree.ImportDeclaration | T.ImportDeclaration) {
passSourceAsString(node.source, 'esm', fn);
},
{
esmodules: true,
commonjs: true,
}
);
const baseCallExpressionVisitor = baseWrapper.CallExpression;
/**
* wrapper around the base wrapper which also picks up calls to jest.<any>('../<any>' or '@kbn/<any>', ...) as "import statements"
* @param {CallExpression} node
*/
baseWrapper.CallExpression = (node: TSESTree.CallExpression) => {
const { callee } = node;
// is this call expression a represenation of an obj.method() call?
if (callee.type === AST_NODE_TYPES.MemberExpression) {
const { object } = callee;
// is the object being called named "jest"?
if (object.type === AST_NODE_TYPES.Identifier && object.name === 'jest') {
const [path] = node.arguments;
// is the first argument to the method a string which starts with '../' or '@kbn/'?
if (
path &&
path.type === AST_NODE_TYPES.Literal &&
typeof path.value === 'string' &&
(path.value.startsWith('../') || path.value.startsWith('@kbn/'))
) {
// call our visitor and assume this node represents a call to a jest mocking function and validate the relative path
visitor(path.value, node);
}
ExportNamedDeclaration(node: TSESTree.ExportNamedDeclaration | T.ExportNamedDeclaration) {
passSourceAsString(node.source, 'esm', fn);
},
ExportAllDeclaration(node: TSESTree.ExportAllDeclaration | T.ExportAllDeclaration) {
passSourceAsString(node.source, 'esm', fn);
},
ImportExpression(node: TSESTree.ImportExpression) {
passSourceAsString(node.source, 'esm', fn);
},
CallExpression({ callee, arguments: args }: TSESTree.CallExpression | T.CallExpression) {
// babel parser used for .js files treats import() calls as CallExpressions with callees of type "Import"
if (T.isImport(callee)) {
passSourceAsString(args[0], 'esm', fn);
return;
}
}
return baseCallExpressionVisitor(node);
// is this a `require()` call?
if (isIdent(callee) && callee.name === 'require') {
passSourceAsString(args[0], 'require', fn);
return;
}
// is this an `obj.method()` call?
if (
callee.type === AST_NODE_TYPES.MemberExpression &&
isIdent(callee.object) &&
isIdent(callee.property)
) {
const { object: left, property: right } = callee;
const name = `${left.name}.${right.name}`;
// is it "require.resolve()"?
if (name === 'require.resolve') {
passSourceAsString(args[0], 'require-resolve', fn);
}
// is it one of jest's mock methods?
if (left.name === 'jest' && JEST_MODULE_METHODS.includes(name)) {
passSourceAsString(args[0], 'jest', fn);
}
}
},
};
return baseWrapper;
return visitor as Rule.RuleListener;
}

View file

@ -6,15 +6,15 @@
* Side Public License, v 1.
*/
export * from './resolve_kibana_import';
export * from './resolve_result';
import { NoUnresolvedImportsRule } from './rules/no_unresolved_imports';
export * from './get_import_resolver';
import { NoUnresolvableImportsRule } from './rules/no_unresolvable_imports';
import { UniformImportsRule } from './rules/uniform_imports';
/**
* Custom ESLint rules, add `'@kbn/eslint-plugin-imports'` to your eslint config to use them
* @internal
*/
export const rules = {
no_unresolved_imports: NoUnresolvedImportsRule,
no_unresolvable_imports: NoUnresolvableImportsRule,
uniform_imports: UniformImportsRule,
};

View file

@ -1,149 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import Path from 'path';
import { REPO_ROOT } from '@kbn/utils';
import { createAbsolutePathSerializer } from '@kbn/dev-utils';
import { resolveKibanaImport } from '../resolve_kibana_import';
expect.addSnapshotSerializer(createAbsolutePathSerializer());
const plugin = (subPath: string) => Path.resolve(REPO_ROOT, 'src/plugins', subPath);
const xPlugin = (subPath: string) => Path.resolve(REPO_ROOT, 'x-pack/plugins', subPath);
const pkg = (subPath: string) => Path.resolve(REPO_ROOT, 'packages', subPath);
describe('standard import formats', () => {
it('resolves requests to src/', () => {
expect(resolveKibanaImport('src/core/public', plugin('discovery/public/components/foo')))
.toMatchInlineSnapshot(`
Object {
"absolute": <absolute path>/src/core/public/index.ts,
"type": "file",
}
`);
expect(resolveKibanaImport('src/core/server', xPlugin('spaces/server/routes')))
.toMatchInlineSnapshot(`
Object {
"absolute": <absolute path>/src/core/server/index.ts,
"type": "file",
}
`);
expect(resolveKibanaImport('src/core/utils', pkg('kbn-dev-utils/lib'))).toMatchInlineSnapshot(`
Object {
"absolute": <absolute path>/src/core/utils/index.ts,
"type": "file",
}
`);
});
it('resolves relative paths too', () => {
expect(resolveKibanaImport('../../../../core/public', plugin('foo/bar/baz')))
.toMatchInlineSnapshot(`
Object {
"absolute": <absolute path>/src/core/public/index.ts,
"type": "file",
}
`);
});
it('resolves @kbn/ imports', () => {
expect(resolveKibanaImport('@kbn/std', pkg('kbn-dev-utils/src'))).toMatchInlineSnapshot(`
Object {
"absolute": <absolute path>/node_modules/@kbn/std/target_node/index.js,
"nodeModule": "@kbn/std",
"type": "file",
}
`);
});
it('resolves @elastic/ imports', () => {
expect(resolveKibanaImport('@elastic/eui', pkg('kbn-dev-utils/src'))).toMatchInlineSnapshot(`
Object {
"absolute": <absolute path>/node_modules/@elastic/eui/lib/index.js,
"nodeModule": "@elastic/eui",
"type": "file",
}
`);
});
it('resolves normal node module imports', () => {
expect(resolveKibanaImport('lodash', pkg('kbn-dev-utils/src'))).toMatchInlineSnapshot(`
Object {
"absolute": <absolute path>/node_modules/lodash/lodash.js,
"nodeModule": "lodash",
"type": "file",
}
`);
expect(resolveKibanaImport('globby', pkg('kbn-dev-utils/src'))).toMatchInlineSnapshot(`
Object {
"absolute": <absolute path>/node_modules/globby/index.js,
"nodeModule": "globby",
"type": "file",
}
`);
});
it('returns null when the import cannot be resolved', () => {
expect(resolveKibanaImport('../../../../invalid', plugin('foo/bar'))).toMatchInlineSnapshot(
`null`
);
expect(resolveKibanaImport('src/invalid', plugin('foo/bar'))).toMatchInlineSnapshot(`null`);
expect(resolveKibanaImport('kibana/invalid', plugin('foo/bar'))).toMatchInlineSnapshot(`null`);
expect(resolveKibanaImport('@kbn/invalid', plugin('foo/bar'))).toMatchInlineSnapshot(`null`);
});
it('returns ignore results for known unresolvable but okay import statements', () => {
expect(resolveKibanaImport('../../grammar/built_grammar.js', plugin('foo/bar')))
.toMatchInlineSnapshot(`
Object {
"type": "ignore",
}
`);
expect(resolveKibanaImport('kibana-buildkite-library', pkg('kbn-foo/src')))
.toMatchInlineSnapshot(`
Object {
"type": "ignore",
}
`);
expect(resolveKibanaImport('core_styles', pkg('kbn-foo/src'))).toMatchInlineSnapshot(`
Object {
"type": "ignore",
}
`);
expect(resolveKibanaImport('core_app_image_assets', pkg('kbn-foo/src'))).toMatchInlineSnapshot(`
Object {
"type": "ignore",
}
`);
expect(resolveKibanaImport('ace/lib/dom', pkg('kbn-foo/src'))).toMatchInlineSnapshot(`
Object {
"type": "ignore",
}
`);
expect(resolveKibanaImport('@elastic/eui/src/components/', pkg('kbn-foo/src')))
.toMatchInlineSnapshot(`
Object {
"type": "ignore",
}
`);
expect(resolveKibanaImport('@elastic/eui/src/services/', pkg('kbn-foo/src')))
.toMatchInlineSnapshot(`
Object {
"type": "ignore",
}
`);
});
});

View file

@ -1,161 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import Fs from 'fs';
import Path from 'path';
import Resolve from 'resolve';
import { REPO_ROOT } from '@kbn/utils';
import { isDirectory, isFile } from './helpers/fs';
import { ResolveResult } from './resolve_result';
const NODE_MODULE_SEG = Path.sep + 'node_modules' + Path.sep;
function packageFilter(pkg: Record<string, unknown>) {
if (!pkg.main && pkg.types) {
// for the purpose of resolving files, a "types" file is adequate
return {
...pkg,
main: pkg.types,
};
}
return pkg;
}
function adaptReq(req: string, dirname: string): string | undefined {
// transform webpack loader requests and focus on the actual file selected
if (req.startsWith('!!')) {
return req.split('!').pop()?.split('?').shift();
}
// handle typescript aliases
if (req === 'kibana/public') {
return adaptReq('src/core/public', dirname);
}
if (req === 'kibana/server') {
return adaptReq('src/core/server', dirname);
}
// turn root-relative paths into relative paths
if (
req.startsWith('src/') ||
req.startsWith('x-pack/') ||
req.startsWith('examples/') ||
req.startsWith('test/')
) {
const absolute = Path.resolve(REPO_ROOT, req);
return `./${Path.relative(dirname, absolute)}`;
}
}
function shouldResolve(req: string) {
// this library is only installed on CI and never resolvable
if (req === 'kibana-buildkite-library') {
return;
}
// these are special webpack-aliases only used in storybooks, ignore them
if (req === 'core_styles' || req === 'core_app_image_assets') {
return;
}
// ignore amd require done by ace syntax plugin
if (req === 'ace/lib/dom') {
return;
}
// ignore requests to grammar/built_grammar.js files or built kbn-monaco workers, these are built by bazel and never resolvable
if (
req.endsWith('grammar/built_grammar.js') ||
(req.includes('/target_workers/') && req.endsWith('.editor.worker.js'))
) {
return;
}
// typescript validates these imports fine and they're purely virtual thanks to ambient type definitions in @elastic/eui so /shrug
if (
req.startsWith('@elastic/eui/src/components/') ||
req.startsWith('@elastic/eui/src/services/')
) {
return;
}
return true;
}
function tryNodeResolve(req: string, dirname: string): ResolveResult | null {
try {
const path = Resolve.sync(req, {
basedir: dirname,
extensions: ['.js', '.json', '.ts', '.tsx', '.d.ts'],
isDirectory,
isFile,
packageFilter,
});
if (path.includes(NODE_MODULE_SEG)) {
const modulePath = path.split(NODE_MODULE_SEG).pop()!.split(Path.sep);
const moduleId = modulePath[0].startsWith('@')
? `${modulePath[0]}/${modulePath[1]}`
: modulePath[0];
return {
type: 'file',
absolute: path,
nodeModule: moduleId,
};
}
return {
type: 'file',
absolute: path.includes('node_modules') ? Fs.readlinkSync(path) : path,
};
} catch (error) {
if (error && error.code === 'MODULE_NOT_FOUND') {
return null;
}
throw error;
}
}
function tryTypesResolve(req: string, dirname: string): ResolveResult | null {
const parts = req.split('/');
const nmParts = parts[0].startsWith('@') ? [parts[0].slice(1), parts[1]] : [parts[0]];
const typesReq = `@types/${nmParts.join('__')}`;
const result = tryNodeResolve(typesReq, dirname);
if (result) {
return {
type: '@types',
module: typesReq,
};
}
return null;
}
/**
* Resolve an import request. All import requests in the repository should return a result, if they don't it's a bug
* which should be caught by the `@kbn/import/no_unresolved` rule, which should never be disabled. If you need help
* adding support for an import style please reach out to operations.
*
* @param req Text from an import/require, like `../../src/core/public` or `@kbn/std`
* @param dirname The directory of the file where the req was found
*/
export function resolveKibanaImport(req: string, dirname: string): ResolveResult | null {
req = adaptReq(req, dirname) ?? req;
if (!shouldResolve(req)) {
return { type: 'ignore' };
}
return tryNodeResolve(req, dirname) ?? tryTypesResolve(req, dirname);
}

View file

@ -9,11 +9,14 @@
import Path from 'path';
import { Rule } from 'eslint';
import { resolveKibanaImport } from '../resolve_kibana_import';
import { report } from '../helpers/report';
import { getImportResolver } from '../get_import_resolver';
import { visitAllImportStatements } from '../helpers/visit_all_import_statements';
export const NoUnresolvedImportsRule: Rule.RuleModule = {
export const NoUnresolvableImportsRule: Rule.RuleModule = {
create(context) {
const resolver = getImportResolver(context);
const sourceFilename = context.getPhysicalFilename
? context.getPhysicalFilename()
: context.getFilename();
@ -23,9 +26,9 @@ export const NoUnresolvedImportsRule: Rule.RuleModule = {
}
return visitAllImportStatements((req, importer) => {
if (!resolveKibanaImport(req, Path.dirname(sourceFilename))) {
context.report({
node: importer as any,
if (req !== null && !resolver.resolve(req, Path.dirname(sourceFilename))) {
report(context, {
node: importer,
message: `Unable to resolve import [${req}]`,
});
}

View file

@ -0,0 +1,120 @@
/*
* 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 Eslint from 'eslint';
import { REPO_ROOT } from '@kbn/utils';
import { getRelativeImportReq, getPackageRelativeImportReq } from '@kbn/import-resolver';
import { report } from '../helpers/report';
import { visitAllImportStatements } from '../helpers/visit_all_import_statements';
import { getImportResolver } from '../get_import_resolver';
// TODO: get rid of all the special cases in here by moving more things to packages
const SETUP_NODE_ENV_DIR = Path.resolve(REPO_ROOT, 'src/setup_node_env');
const PKGJSON_PATH = Path.resolve(REPO_ROOT, 'package.json');
const XPACK_PKGJSON_PATH = Path.resolve(REPO_ROOT, 'x-pack/package.json');
const KBN_PM_SCRIPT = Path.resolve(REPO_ROOT, 'packages/kbn-pm/dist/index.js');
export const UniformImportsRule: Eslint.Rule.RuleModule = {
meta: {
fixable: 'code',
},
create(context) {
const resolver = getImportResolver(context);
const sourceFilename = context.getPhysicalFilename
? context.getPhysicalFilename()
: context.getFilename();
const sourceDirname = Path.dirname(sourceFilename);
const ownPackageId = resolver.getPackageIdForPath(sourceFilename);
return visitAllImportStatements((req, importer, type) => {
if (!req) {
return;
}
const result = resolver.resolve(req, sourceDirname);
if (result?.type !== 'file' || result.nodeModule) {
return;
}
const { absolute } = result;
// don't mess with imports to the kbn/pm script for now
if (absolute === KBN_PM_SCRIPT) {
return;
}
const packageId = resolver.getPackageIdForPath(absolute);
if (ownPackageId && !packageId) {
// special cases, files that aren't in packages but packages are allowed to import them
if (
absolute === PKGJSON_PATH ||
absolute === XPACK_PKGJSON_PATH ||
absolute.startsWith(SETUP_NODE_ENV_DIR)
) {
return;
}
if (resolver.isBazelPackage(ownPackageId)) {
report(context, {
node: importer,
message: `Package [${ownPackageId}] can only import other packages`,
});
return;
}
}
if (packageId === ownPackageId || !packageId) {
const correct = getRelativeImportReq({
...result,
original: req,
dirname: sourceDirname,
type,
});
if (req !== correct) {
report(context, {
node: importer,
message: `Use import request [${correct}]`,
correctImport: correct,
});
}
return;
}
const packageDir = resolver.getAbsolutePackageDir(packageId);
if (!packageDir) {
report(context, {
node: importer,
message: `Unable to determine location of package [${packageId}]`,
});
return;
}
const correct = getPackageRelativeImportReq({
...result,
packageDir,
packageId,
type,
});
if (req !== correct) {
report(context, {
node: importer,
message: `Use import request [${correct}]`,
correctImport: correct,
});
return;
}
});
},
};

View file

@ -0,0 +1,122 @@
load("@npm//@bazel/typescript:index.bzl", "ts_config")
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
PKG_DIRNAME = "kbn-find-used-node-modules"
PKG_REQUIRE_NAME = "@kbn/find-used-node-modules"
SOURCE_FILES = glob(
[
"src/**/*.ts",
],
exclude = [
"**/*.test.*",
],
)
SRCS = SOURCE_FILES
filegroup(
name = "srcs",
srcs = SRCS,
)
NPM_MODULE_EXTRA_FILES = [
"package.json",
]
# In this array place runtime dependencies, including other packages and NPM packages
# which must be available for this code to run.
#
# To reference other packages use:
# "//repo/relative/path/to/package"
# eg. "//packages/kbn-utils"
#
# To reference a NPM package use:
# "@npm//name-of-package"
# eg. "@npm//lodash"
RUNTIME_DEPS = [
"//packages/kbn-babel-preset",
"@npm//@babel/core",
"@npm//@babel/types",
"@npm//@babel/traverse",
]
# In this array place dependencies necessary to build the types, which will include the
# :npm_module_types target of other packages and packages from NPM, including @types/*
# packages.
#
# To reference the types for another package use:
# "//repo/relative/path/to/package:npm_module_types"
# eg. "//packages/kbn-utils:npm_module_types"
#
# References to NPM packages work the same as RUNTIME_DEPS
TYPES_DEPS = [
"//packages/kbn-import-resolver:npm_module_types",
"@npm//@types/node",
"@npm//@types/jest",
"@npm//@types/babel__core",
"@npm//@babel/traverse",
"@npm//@babel/types",
]
jsts_transpiler(
name = "target_node",
srcs = SRCS,
build_pkg_name = package_name(),
)
ts_config(
name = "tsconfig",
src = "tsconfig.json",
deps = [
"//:tsconfig.base.json",
"//:tsconfig.bazel.json",
],
)
ts_project(
name = "tsc_types",
args = ['--pretty'],
srcs = SRCS,
deps = TYPES_DEPS,
declaration = True,
emit_declaration_only = True,
out_dir = "target_types",
root_dir = "src",
tsconfig = ":tsconfig",
)
js_library(
name = PKG_DIRNAME,
srcs = NPM_MODULE_EXTRA_FILES,
deps = RUNTIME_DEPS + [":target_node"],
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)
pkg_npm(
name = "npm_module",
deps = [":" + PKG_DIRNAME],
)
filegroup(
name = "build",
srcs = [":npm_module"],
visibility = ["//visibility:public"],
)
pkg_npm_types(
name = "npm_module_types",
srcs = SRCS,
deps = [":tsc_types"],
package_name = PKG_REQUIRE_NAME,
tsconfig = ":tsconfig",
visibility = ["//visibility:public"],
)
filegroup(
name = "build_types",
srcs = [":npm_module_types"],
visibility = ["//visibility:public"],
)

View file

@ -0,0 +1,7 @@
# @kbn/find-used-node-modules
Simple abstraction over the `@babel/parser` and the `@babel/traverse` to find the node_modules used by a list of files.
## `findUsedNodeModules(resolver, entryPaths): string[]`
Pass an `ImportResolver` instance from `@kbn/import-resolver` and a list of absolute paths to JS files to get the list of `node_modules` used by those files and the files the import.

View file

@ -7,7 +7,7 @@
*/
module.exports = {
preset: '@kbn/test',
preset: '@kbn/test/jest_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-babel-code-parser'],
roots: ['<rootDir>/packages/kbn-find-used-node-modules'],
};

View file

@ -0,0 +1,10 @@
{
"name": "@kbn/find-used-node-modules",
"private": true,
"version": "1.0.0",
"main": "./target_node/index.js",
"license": "SSPL-1.0 OR Elastic License 2.0",
"kibana": {
"devOnly": true
}
}

View file

@ -0,0 +1,186 @@
/*
* 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 { findUsedNodeModules } from './find_used_node_modules';
import { ImportResolver } from '@kbn/import-resolver';
jest.mock('./fs');
const FILES: Record<string, string> = {
'/foo.js': `
require('./bar.js')
`,
'/bar.js': `
require('./box')
`,
'/box.js': `
require('foo')
`,
};
class MockResolver extends ImportResolver {
constructor() {
super('/', new Map(), new Map());
}
isBazelPackage = jest.fn();
resolve = jest.fn();
}
const RESOLVER = new MockResolver();
beforeEach(() => {
jest.resetAllMocks();
jest.requireMock('./fs').readFile.mockImplementation((path: string) => {
if (Object.hasOwn(FILES, path)) {
return FILES[path];
}
const error: any = new Error(`ENOENT, missing file [${path}]`);
error.code = 'ENOENT';
throw error;
});
});
describe('findUsedNodeModules()', () => {
it('excludes built-in modules', async () => {
RESOLVER.resolve.mockImplementation(() => ({
type: 'built-in',
}));
const results = await findUsedNodeModules({
entryPaths: ['/foo.js'],
resolver: RESOLVER,
findUsedPeers: false,
});
expect(RESOLVER.resolve).toHaveBeenCalledTimes(1);
expect(results).toEqual([]);
});
it('returns node_modules found in the source file', async () => {
RESOLVER.resolve.mockImplementation((req) => {
if (req === './bar.js') {
return {
type: 'file',
nodeModule: '@foo/bar',
absolute: '/bar.js',
};
}
throw new Error('unexpected request');
});
const results = await findUsedNodeModules({
entryPaths: ['/foo.js'],
resolver: RESOLVER,
findUsedPeers: false,
});
expect(RESOLVER.resolve).toHaveBeenCalledTimes(1);
expect(results).toEqual(['@foo/bar']);
});
it('returns node_modules found in referenced files', async () => {
RESOLVER.resolve.mockImplementation((req) => {
if (req === './bar.js') {
return {
type: 'file',
absolute: '/bar.js',
};
}
if (req === './box') {
return {
type: 'file',
nodeModule: '@foo/box',
absolute: '/box.js',
};
}
throw new Error('unexpected request');
});
const results = await findUsedNodeModules({
entryPaths: ['/foo.js'],
resolver: RESOLVER,
findUsedPeers: false,
});
expect(RESOLVER.resolve).toHaveBeenCalledTimes(2);
expect(results).toEqual(['@foo/box']);
});
it('does not traverse node_modules', async () => {
RESOLVER.resolve.mockImplementation((req) => {
if (req === './bar.js') {
return {
type: 'file',
absolute: '/bar.js',
};
}
if (req === './box') {
return {
type: 'file',
nodeModule: '@foo/box',
absolute: '/box.js',
};
}
throw new Error('unexpected request');
});
const results = await findUsedNodeModules({
entryPaths: ['/foo.js'],
resolver: RESOLVER,
findUsedPeers: false,
});
expect(RESOLVER.resolve).toHaveBeenCalledTimes(2);
expect(results).toEqual(['@foo/box']);
});
it('does traverse node_modules which are also bazel packages', async () => {
RESOLVER.resolve.mockImplementation((req) => {
if (req === './bar.js') {
return {
type: 'file',
absolute: '/bar.js',
};
}
if (req === './box') {
return {
type: 'file',
nodeModule: '@foo/box',
absolute: '/box.js',
};
}
if (req === 'foo') {
return {
type: 'file',
nodeModule: '@foo/core',
absolute: '/non-existant',
};
}
throw new Error('unexpected request');
});
RESOLVER.isBazelPackage.mockImplementation((pkgId) => {
return pkgId === '@foo/box';
});
const results = await findUsedNodeModules({
entryPaths: ['/foo.js'],
resolver: RESOLVER,
findUsedPeers: false,
});
expect(RESOLVER.resolve).toHaveBeenCalledTimes(3);
expect(results).toEqual(['@foo/box', '@foo/core']);
});
});

View file

@ -0,0 +1,138 @@
/*
* 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 { asyncForEachWithLimit } from '@kbn/std';
import type { ImportResolver } from '@kbn/import-resolver';
import { readFile, readFileSync } from './fs';
import { getImportRequests } from './get_import_requests';
function isObj(v: any): v is Record<string, unknown> {
return typeof v === 'object' && v !== null;
}
function getPeerDeps(thisNodeModule: string) {
const pkgPath = require.resolve(`${thisNodeModule}/package.json`);
const pkg = JSON.parse(readFileSync(pkgPath));
if (isObj(pkg) && isObj(pkg.peerDependencies)) {
return Object.keys(pkg.peerDependencies);
} else {
return [];
}
}
interface Options {
resolver: ImportResolver;
entryPaths: string[];
findUsedPeers: boolean;
// if we are finding used modules in a node_module, this must be the name of the node_module
// we should treat as "this module" rather than "another node module"
thisNodeModule?: string;
}
/**
* Parse a list of entry paths and find the node_modules which are required by them. If the
* entry path requires/imports a non-node_module then that file is scanned too, deeply, until
* all referenced files are scanned.
*
* Optionally, we can find the used peers of the used node_modules. This will keep track of all
* the paths we use to enter a node_module and then traverse from those points, finding the
* used modules and comparing those to the `peerDependencies` listed in the node_module's package.json
* file. If a used dependeny is in the `peerDependencies` and is used by the node_module it will
* be included in the results.
*
* This was implemented mostly for `@emotion/react` which is used by @elastic/eui but only listed
* as a peerDependency. If we didn't keep it in the Kibana package.json then the package would not
* be installed and cause an error on startup because `@emotion/react` can't be found. We used to
* solve this by scanning the node_modules directory for all the packages which are used but that
* was much slower and lead to extra entries in package.json.
*/
export async function findUsedNodeModules(options: Options) {
const queue = new Set<string>(options.entryPaths);
const results = new Set<string>();
const entryPathsIntoNodeModules = new Map<string, Set<string>>();
for (const path of queue) {
if (Path.extname(path) !== '.js') {
continue;
}
const dirname = Path.dirname(path);
const code = await readFile(path);
const reqs = getImportRequests(code);
for (const req of reqs) {
// resolve the request to it's actual file on dist
const result = options.resolver.resolve(req, dirname);
// ignore non-file resolution results, these represent files which aren't on
// the file-system yet (like during the build) built-ins, explicitily ignored
// files, and @types only imports
if (result?.type !== 'file') {
continue;
}
// if the result points to a node_module (or another node_module)...
if (result.nodeModule && result.nodeModule !== options.thisNodeModule) {
// add it to the results
results.add(result.nodeModule);
// record this absolute path as an entry path into the node module from our entries, if we
// need to scan this node_module for used deps we need to know how we access it.
const nmEntries = entryPathsIntoNodeModules.get(result.nodeModule);
if (!nmEntries) {
entryPathsIntoNodeModules.set(result.nodeModule, new Set([result.absolute]));
} else {
nmEntries.add(result.absolute);
}
}
// no need to scan node_modules unless they're bazel packages
if (
!result.nodeModule ||
result.nodeModule === options.thisNodeModule ||
options.resolver.isBazelPackage(result.nodeModule)
) {
queue.add(result.absolute);
}
}
}
if (options.findUsedPeers) {
await asyncForEachWithLimit(results, 10, async (dep) => {
const entryPaths = entryPathsIntoNodeModules.get(dep);
if (!entryPaths?.size) {
return;
}
const peerDeps = getPeerDeps(dep);
if (!peerDeps.length) {
return;
}
const usedInside = await findUsedNodeModules({
resolver: options.resolver,
entryPaths: Array.from(entryPaths),
findUsedPeers: false,
thisNodeModule: dep,
});
for (const peer of peerDeps) {
if (usedInside.includes(peer)) {
results.add(peer);
}
}
});
}
return Array.from(results).sort((a, b) => a.localeCompare(b));
}

View file

@ -6,11 +6,13 @@
* Side Public License, v 1.
*/
import Fs from 'fs/promises';
import { sortPackageJson as sort } from '@kbn/dev-utils/sort_package_json';
import { Kibana } from './kibana';
import Fs from 'fs';
import Fsp from 'fs/promises';
export async function sortPackageJson(kbn: Kibana) {
const packageJsonPath = kbn.getAbsolute('package.json');
await Fs.writeFile(packageJsonPath, sort(await Fs.readFile(packageJsonPath, 'utf-8')));
export function readFileSync(path: string) {
return Fs.readFileSync(path, 'utf8');
}
export function readFile(path: string) {
return Fsp.readFile(path, 'utf8');
}

View file

@ -0,0 +1,64 @@
/*
* 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 { getImportRequests } from './get_import_requests';
describe('getImportRequests()', () => {
it('should get requests from `require`', () => {
const rawCode = `/*foo*/require('dep1'); const bar = 1;`;
const foundDeps = getImportRequests(rawCode);
expect(foundDeps).toMatchInlineSnapshot(`
Array [
"dep1",
]
`);
});
it('should get requests from `require.resolve`', () => {
const rawCode = `/*foo*/require.resolve('dep2'); const bar = 1;`;
const foundDeps = getImportRequests(rawCode);
expect(foundDeps).toMatchInlineSnapshot(`
Array [
"dep2",
]
`);
});
it('should get requests from `import`', () => {
const rawCode = `/*foo*/import dep1 from 'dep1'; import dep2 from 'dep2';const bar = 1;`;
const foundDeps = getImportRequests(rawCode);
expect(foundDeps).toMatchInlineSnapshot(`
Array [
"dep1",
"dep2",
]
`);
});
it('should get requests from `export from`', () => {
const rawCode = `/*foo*/export dep1 from 'dep1'; import dep2 from 'dep2';const bar = 1;`;
const foundDeps = getImportRequests(rawCode);
expect(foundDeps).toMatchInlineSnapshot(`
Array [
"dep1",
"dep2",
]
`);
});
it('should get requests from `export * from`', () => {
const rawCode = `/*foo*/export * from 'dep1'; export dep2 from 'dep2';const bar = 1;`;
const foundDeps = getImportRequests(rawCode);
expect(foundDeps).toMatchInlineSnapshot(`
Array [
"dep1",
"dep2",
]
`);
});
});

View file

@ -0,0 +1,30 @@
/*
* 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 * as parser from '@babel/parser';
import traverse from '@babel/traverse';
// @ts-expect-error Not available with types
import babelParserOptions from '@kbn/babel-preset/common_babel_parser_options';
import { importVisitor } from './import_visitor';
/**
* Parse the code and return an array of all the import requests in that file
*/
export function getImportRequests(code: string) {
const importRequests: string[] = [];
// Parse and get the code AST
const ast = parser.parse(code, babelParserOptions);
// Loop through the code AST with
// the defined visitors
traverse(ast, importVisitor(importRequests));
return importRequests;
}

View file

@ -0,0 +1,82 @@
/*
* 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 * as T from '@babel/types';
import type { Visitor } from '@babel/core';
/**
* @notice
*
* This product has relied on ASTExplorer that is licensed under MIT.
*/
// AST check for require expressions
const isRequire = ({ callee }: T.CallExpression) =>
T.isIdentifier(callee) && callee.name === 'require';
// AST check for require.resolve expressions
const isRequireResolve = ({ callee }: T.CallExpression) =>
T.isMemberExpression(callee) &&
T.isIdentifier(callee.object) &&
callee.object.name === 'require' &&
T.isIdentifier(callee.property) &&
callee.property.name === 'resolve';
/**
* Create a Babel AST visitor that will write import requests into the passed array
*/
export function importVisitor(importRequests: string[]): Visitor {
// This was built with help on an ast explorer and some ESTree docs
// like the babel parser ast spec and the main docs for the Esprima
// which is a complete and useful docs for the ESTree spec.
//
// https://astexplorer.net
// https://github.com/babel/babel/blob/master/packages/babel-parser/ast/spec.md
// https://esprima.readthedocs.io/en/latest/syntax-tree-format.html
// https://github.com/estree/estree
// Visitors to traverse and find dependencies
return {
// raw values on require + require.resolve
CallExpression: ({ node }) => {
if (isRequire(node) || isRequireResolve(node)) {
const nodeArguments = node.arguments;
const reqArg = Array.isArray(nodeArguments) ? nodeArguments.shift() : null;
if (!reqArg) {
return;
}
if (reqArg.type === 'StringLiteral') {
importRequests.push(reqArg.value);
}
}
},
// raw values on import
ImportDeclaration: ({ node }) => {
// Get string values from import expressions
const importSource = node.source;
importRequests.push(importSource.value);
},
// raw values on export from
ExportNamedDeclaration: ({ node }) => {
// Get string values from export from expressions
if (node.source) {
importRequests.push(node.source.value);
}
},
// raw values on export * from
ExportAllDeclaration: ({ node }) => {
const exportAllFromSource = node.source;
importRequests.push(exportAllFromSource.value);
},
};
}

View file

@ -6,6 +6,4 @@
* Side Public License, v 1.
*/
export { dependenciesParseStrategy } from './strategies';
export { dependenciesVisitorsGenerator } from './visitors';
export { parseSingleFile, parseSingleFileSync, parseEntries } from './code_parser';
export { findUsedNodeModules } from './find_used_node_modules';

View file

@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.bazel.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "target_types",
"rootDir": "src",
"stripInternal": false,
"types": [
"jest",
"node"
]
},
"include": [
"src/**/*"
]
}

View file

@ -0,0 +1,124 @@
load("@npm//@bazel/typescript:index.bzl", "ts_config")
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
PKG_DIRNAME = "kbn-import-resolver"
PKG_REQUIRE_NAME = "@kbn/import-resolver"
SOURCE_FILES = glob(
[
"src/**/*.ts",
],
exclude = [
"**/*.test.*",
],
)
SRCS = SOURCE_FILES
filegroup(
name = "srcs",
srcs = SRCS,
)
NPM_MODULE_EXTRA_FILES = [
"package.json",
]
# In this array place runtime dependencies, including other packages and NPM packages
# which must be available for this code to run.
#
# To reference other packages use:
# "//repo/relative/path/to/package"
# eg. "//packages/kbn-utils"
#
# To reference a NPM package use:
# "@npm//name-of-package"
# eg. "@npm//lodash"
RUNTIME_DEPS = [
"//packages/kbn-bazel-packages",
"//packages/kbn-utils",
"//packages/kbn-synthetic-package-map",
"@npm//resolve",
"@npm//normalize-path",
]
# In this array place dependencies necessary to build the types, which will include the
# :npm_module_types target of other packages and packages from NPM, including @types/*
# packages.
#
# To reference the types for another package use:
# "//repo/relative/path/to/package:npm_module_types"
# eg. "//packages/kbn-utils:npm_module_types"
#
# References to NPM packages work the same as RUNTIME_DEPS
TYPES_DEPS = [
"//packages/kbn-bazel-packages:npm_module_types",
"//packages/kbn-utils:npm_module_types",
"//packages/kbn-synthetic-package-map:npm_module_types",
"@npm//@types/node",
"@npm//@types/jest",
"@npm//@types/resolve",
"@npm//@types/normalize-path",
]
jsts_transpiler(
name = "target_node",
srcs = SRCS,
build_pkg_name = package_name(),
)
ts_config(
name = "tsconfig",
src = "tsconfig.json",
deps = [
"//:tsconfig.base.json",
"//:tsconfig.bazel.json",
],
)
ts_project(
name = "tsc_types",
args = ['--pretty'],
srcs = SRCS,
deps = TYPES_DEPS,
declaration = True,
emit_declaration_only = True,
out_dir = "target_types",
root_dir = "src",
tsconfig = ":tsconfig",
)
js_library(
name = PKG_DIRNAME,
srcs = NPM_MODULE_EXTRA_FILES,
deps = RUNTIME_DEPS + [":target_node"],
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)
pkg_npm(
name = "npm_module",
deps = [":" + PKG_DIRNAME],
)
filegroup(
name = "build",
srcs = [":npm_module"],
visibility = ["//visibility:public"],
)
pkg_npm_types(
name = "npm_module_types",
srcs = SRCS,
deps = [":tsc_types"],
package_name = PKG_REQUIRE_NAME,
tsconfig = ":tsconfig",
visibility = ["//visibility:public"],
)
filegroup(
name = "build_types",
srcs = [":npm_module_types"],
visibility = ["//visibility:public"],
)

View file

@ -0,0 +1,3 @@
# @kbn/import-resolver
Empty package generated by @kbn/generate

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_integration_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-import-resolver'],
};

View file

@ -0,0 +1,10 @@
{
"name": "@kbn/import-resolver",
"private": true,
"version": "1.0.0",
"main": "./target_node/index.js",
"license": "SSPL-1.0 OR Elastic License 2.0",
"kibana": {
"devOnly": true
}
}

View file

@ -5,5 +5,3 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
require('./dist').run(process.argv.slice(2));

View file

@ -5,5 +5,3 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { getPluginSearchPaths } from './plugin_search_paths';

View file

@ -0,0 +1,7 @@
/*
* 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.
*/

View file

@ -0,0 +1,7 @@
/*
* 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.
*/

View file

@ -8,11 +8,7 @@
import Fs from 'fs';
import { memoize } from './memoize';
const runningInEditor = !!process.env.VSCODE_CWD;
const safeStat = (path: string) => {
export function safeStat(path: string) {
try {
return Fs.statSync(path);
} catch (error) {
@ -22,15 +18,8 @@ const safeStat = (path: string) => {
throw error;
}
};
function _isDirectory(path: string) {
return !!safeStat(path)?.isDirectory();
}
function _isFile(path: string) {
return !!safeStat(path)?.isFile();
export function readFileSync(path: string) {
return Fs.readFileSync(path, 'utf8');
}
export const isDirectory = runningInEditor ? _isDirectory : memoize(_isDirectory);
export const isFile = runningInEditor ? _isFile : memoize(_isFile);

View file

@ -0,0 +1,100 @@
/*
* 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 normalizePath from 'normalize-path';
export type ImportType = 'esm' | 'require' | 'require-resolve' | 'jest';
interface WrapOptions {
prefix?: string;
postfix?: string;
}
function wrap(req: string, options: WrapOptions) {
return `${options.prefix ?? ''}${req}${options.postfix ?? ''}`;
}
const EXT_RE = /\.(jsx?|(d\.)?tsx?)$/;
const INDEX_IN_INDEX_RE = /\/index\/index(\.jsx?|\.d\.tsx?|\.tsx?)$/;
const INCLUDES_FILENAME_RE = /\/.*\..{2,4}$/;
export function reduceImportRequest(req: string, type: ImportType, original?: string) {
let reduced = req;
if (type === 'require-resolve' && original && original.match(INCLUDES_FILENAME_RE)) {
// require.resolve() can be a complicated, it's often used in config files and
// sometimes we don't have babel to help resolve .ts to .js, so we try to rely
// on the original request and keep the filename listed if it's in the original
return req;
}
const indexInIndexMatch = req.match(INDEX_IN_INDEX_RE);
if (indexInIndexMatch) {
if (indexInIndexMatch[1] !== '.ts' && indexInIndexMatch[1] !== '.tsx') {
// this is a very ambiguous request, leave the whole import statement to make it less so
return req;
}
// this is also a very ambiguous request, but TS complains about leaving .ts or .tsx on a request so strip it
return req.slice(0, -indexInIndexMatch[1].length);
}
const extMatch = req.match(EXT_RE);
if (extMatch) {
reduced = reduced.slice(0, -extMatch[0].length);
}
if (reduced === 'index') {
return '';
}
if (reduced.endsWith('/index')) {
reduced = reduced.slice(0, -6);
}
return reduced;
}
interface RelativeImportReqOptions extends WrapOptions {
dirname: string;
absolute: string;
type: ImportType;
original?: string;
}
export function getRelativeImportReq(options: RelativeImportReqOptions) {
const relative = normalizePath(Path.relative(options.dirname, options.absolute));
return wrap(
reduceImportRequest(
relative.startsWith('.') ? relative : `./${relative}`,
options.type,
options.original
),
options
);
}
interface PackageRelativeImportReqOptions extends WrapOptions {
packageDir: string;
packageId: string;
absolute: string;
type: ImportType;
}
export function getPackageRelativeImportReq(options: PackageRelativeImportReqOptions) {
const relative = normalizePath(Path.relative(options.packageDir, options.absolute));
if (!relative) {
return wrap(options.packageId, options);
}
const subPath = reduceImportRequest(relative, options.type);
return wrap(subPath ? `${options.packageId}/${subPath}` : options.packageId, options);
}

View file

@ -0,0 +1,281 @@
/*
* 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 Resolve from 'resolve';
import { REPO_ROOT } from '@kbn/utils';
import normalizePath from 'normalize-path';
import { discoverBazelPackageLocations } from '@kbn/bazel-packages';
import { readPackageMap, PackageMap } from '@kbn/synthetic-package-map';
import { safeStat, readFileSync } from './helpers/fs';
import { ResolveResult } from './resolve_result';
import { getRelativeImportReq } from './helpers/import_req';
import { memoize } from './helpers/memoize';
const NODE_MODULE_SEG = Path.sep + 'node_modules' + Path.sep;
export class ImportResolver {
static create(repoRoot: string) {
const pkgMap = new Map();
for (const dir of discoverBazelPackageLocations(repoRoot)) {
const pkg = JSON.parse(Fs.readFileSync(Path.resolve(dir, 'package.json'), 'utf8'));
pkgMap.set(pkg.name, normalizePath(Path.relative(repoRoot, dir)));
}
return new ImportResolver(repoRoot, pkgMap, readPackageMap());
}
private safeStat = memoize(safeStat);
private baseResolveOpts = {
extensions: ['.js', '.json', '.ts', '.tsx', '.d.ts'],
isFile: (path: string) => !!this.safeStat(path)?.isFile(),
isDirectory: (path: string) => !!this.safeStat(path)?.isDirectory(),
readFileSync: memoize(readFileSync),
packageFilter(pkg: Record<string, unknown>) {
if (!pkg.main && pkg.types) {
// for the purpose of resolving files, a "types" file is adequate
return {
...pkg,
main: pkg.types,
};
}
return pkg;
},
};
constructor(
/**
* Root directory that all source files for packages are expected to be
* in, also the directory that package maps are resolved against.
*/
private readonly cwd: string,
/**
* Map of actual package names to normalized root-relative directories
* for each package
*/
private readonly pkgMap: PackageMap,
/**
* Map of synthetic package names to normalized root-relative directories
* for each simulated package
*/
private readonly synthPkgMap: PackageMap
) {}
getPackageIdForPath(path: string) {
const relative = normalizePath(Path.relative(this.cwd, path));
if (relative.startsWith('..')) {
throw new Error(`path is outside of cwd [${this.cwd}]`);
}
for (const [synthPkgId, dir] of this.synthPkgMap) {
if (relative === dir || relative.startsWith(dir + '/')) {
return synthPkgId;
}
}
for (const [pkgId, dir] of this.pkgMap) {
if (relative === dir || relative.startsWith(dir + '/')) {
return pkgId;
}
}
return null;
}
getAbsolutePackageDir(pkgId: string) {
const dir = this.synthPkgMap.get(pkgId) ?? this.pkgMap.get(pkgId);
if (!dir) {
return null;
}
return Path.resolve(this.cwd, dir);
}
isBazelPackage(pkgId: string) {
return this.pkgMap.has(pkgId);
}
isSyntheticPackage(pkgId: string) {
return this.synthPkgMap.has(pkgId);
}
private shouldIgnore(req: string): boolean {
// this library is only installed on CI and never resolvable
if (req === 'kibana-buildkite-library') {
return true;
}
// these are special webpack-aliases only used in storybooks, ignore them
if (req === 'core_styles' || req === 'core_app_image_assets') {
return true;
}
// ignore amd require done by ace syntax plugin
if (req === 'ace/lib/dom') {
return true;
}
// ignore requests to grammar/built_grammar.js files or bazel target dirs, these files are only
// available in the build output and will never resolve in dev. We will validate that people don't
// import these files from outside the package in another rule
if (
req.endsWith('grammar/built_grammar.js') ||
req.includes('/target_workers/') ||
req.includes('/target_node/')
) {
return true;
}
// typescript validates these imports fine and they're purely virtual thanks to ambient type definitions in @elastic/eui so /shrug
if (
req.startsWith('@elastic/eui/src/components/') ||
req.startsWith('@elastic/eui/src/services/')
) {
return true;
}
return false;
}
private adaptReq(req: string, dirname: string): string | undefined {
// handle typescript aliases
if (req === 'kibana/public') {
return this.adaptReq('src/core/public', dirname);
}
if (req === 'kibana/server') {
return this.adaptReq('src/core/server', dirname);
}
// turn root-relative paths into relative paths
if (
req.startsWith('src/') ||
req.startsWith('x-pack/') ||
req.startsWith('examples/') ||
req.startsWith('test/')
) {
return getRelativeImportReq({
dirname,
absolute: Path.resolve(REPO_ROOT, req),
type: 'esm',
});
}
}
private tryNodeResolve(req: string, dirname: string): ResolveResult | null {
try {
const path = Resolve.sync(req, {
basedir: dirname,
...this.baseResolveOpts,
});
if (!Path.isAbsolute(path)) {
return {
type: 'built-in',
};
}
const lastNmSeg = path.lastIndexOf(NODE_MODULE_SEG);
if (lastNmSeg !== -1) {
const segs = path.slice(lastNmSeg + NODE_MODULE_SEG.length).split(Path.sep);
const moduleId = segs[0].startsWith('@') ? `${segs[0]}/${segs[1]}` : segs[0];
return {
type: 'file',
absolute: path,
nodeModule: moduleId,
};
}
return {
type: 'file',
absolute: path,
};
} catch (error) {
if (error && error.code === 'MODULE_NOT_FOUND') {
if (req === 'fsevents') {
return {
type: 'optional-and-missing',
};
}
return null;
}
throw error;
}
}
private tryTypesResolve(req: string, dirname: string): ResolveResult | null {
const parts = req.split('/');
const nmParts = parts[0].startsWith('@') ? [parts[0].slice(1), parts[1]] : [parts[0]];
const typesReq = `@types/${nmParts.join('__')}`;
const result = this.tryNodeResolve(typesReq, dirname);
if (result) {
return {
type: '@types',
module: typesReq,
};
}
return null;
}
/**
* Resolve an import request from a file in the given dirname
*/
resolve(req: string, dirname: string): ResolveResult | null {
// transform webpack loader requests and focus on the actual file selected
const lastExI = req.lastIndexOf('!');
if (lastExI > -1) {
const quesI = req.lastIndexOf('?');
const prefix = req.slice(0, lastExI + 1);
const postfix = quesI > -1 ? req.slice(quesI) : '';
const result = this.resolve(req.slice(lastExI + 1, quesI > -1 ? quesI : undefined), dirname);
if (result?.type !== 'file') {
return result;
}
return {
...result,
prefix,
postfix,
};
}
if (req[0] !== '.') {
const parts = req.split('/');
const pkgId = parts[0].startsWith('@') ? `${parts[0]}/${parts[1]}` : `${parts[0]}`;
if (this.synthPkgMap.has(pkgId)) {
const pkgDir = this.getAbsolutePackageDir(pkgId);
if (pkgDir) {
return this.resolve(
getRelativeImportReq({
absolute: parts.length > 2 ? Path.resolve(pkgDir, ...parts.slice(2)) : pkgDir,
dirname,
type: 'esm',
}),
dirname
);
}
}
}
req = this.adaptReq(req, dirname) ?? req;
if (this.shouldIgnore(req)) {
return { type: 'ignore' };
}
return this.tryNodeResolve(req, dirname) ?? this.tryTypesResolve(req, dirname);
}
}

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './import_resolver';
export * from './helpers/import_req';
export * from './resolve_result';

View file

@ -0,0 +1,192 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import Path from 'path';
import { createAbsolutePathSerializer } from '@kbn/dev-utils';
import { ImportResolver } from '../import_resolver';
const FIXTURES_DIR = Path.resolve(__dirname, '../__fixtures__');
expect.addSnapshotSerializer(createAbsolutePathSerializer());
const resolver = new ImportResolver(
FIXTURES_DIR,
new Map([['@pkg/box', 'packages/box']]),
new Map([['@synth/bar', 'src/bar']])
);
describe('#resolve()', () => {
it('resolves imports to synth packages', () => {
expect(resolver.resolve('@synth/bar', FIXTURES_DIR)).toMatchInlineSnapshot(`
Object {
"absolute": <absolute path>/packages/kbn-import-resolver/src/__fixtures__/src/bar/index.js,
"type": "file",
}
`);
});
it('resolves imports to bazel packages that are also found in node_modules', () => {
expect(resolver.resolve('@pkg/box', FIXTURES_DIR)).toMatchInlineSnapshot(`
Object {
"absolute": <absolute path>/packages/kbn-import-resolver/src/__fixtures__/node_modules/@pkg/box/index.js,
"nodeModule": "@pkg/box",
"type": "file",
}
`);
});
it('resolves node_module imports', () => {
expect(resolver.resolve('foo', FIXTURES_DIR)).toMatchInlineSnapshot(`
Object {
"absolute": <absolute path>/packages/kbn-import-resolver/src/__fixtures__/node_modules/foo/index.js,
"nodeModule": "foo",
"type": "file",
}
`);
});
it('resolves requests to src/', () => {
expect(resolver.resolve('src/core/public', FIXTURES_DIR)).toMatchInlineSnapshot(`
Object {
"absolute": <absolute path>/src/core/public/index.ts,
"type": "file",
}
`);
});
it('resolves relative paths', () => {
expect(resolver.resolve('./bar', Path.resolve(FIXTURES_DIR, 'src/bar'))).toMatchInlineSnapshot(`
Object {
"absolute": <absolute path>/packages/kbn-import-resolver/src/__fixtures__/src/bar/bar.js,
"type": "file",
}
`);
});
it('returns null when the import cannot be resolved', () => {
expect(resolver.resolve('../../../../invalid', FIXTURES_DIR)).toMatchInlineSnapshot(`null`);
expect(resolver.resolve('src/invalid', FIXTURES_DIR)).toMatchInlineSnapshot(`null`);
expect(resolver.resolve('kibana/invalid', FIXTURES_DIR)).toMatchInlineSnapshot(`null`);
expect(resolver.resolve('@kbn/invalid', FIXTURES_DIR)).toMatchInlineSnapshot(`null`);
});
it('returns ignore results for known unresolvable but okay import statements', () => {
expect(resolver.resolve('../../grammar/built_grammar.js', FIXTURES_DIR)).toMatchInlineSnapshot(`
Object {
"type": "ignore",
}
`);
expect(resolver.resolve('kibana-buildkite-library', FIXTURES_DIR)).toMatchInlineSnapshot(`
Object {
"type": "ignore",
}
`);
expect(resolver.resolve('core_styles', FIXTURES_DIR)).toMatchInlineSnapshot(`
Object {
"type": "ignore",
}
`);
expect(resolver.resolve('core_app_image_assets', FIXTURES_DIR)).toMatchInlineSnapshot(`
Object {
"type": "ignore",
}
`);
expect(resolver.resolve('ace/lib/dom', FIXTURES_DIR)).toMatchInlineSnapshot(`
Object {
"type": "ignore",
}
`);
expect(resolver.resolve('@elastic/eui/src/components/foo', FIXTURES_DIR))
.toMatchInlineSnapshot(`
Object {
"type": "ignore",
}
`);
expect(resolver.resolve('@elastic/eui/src/services/foo', FIXTURES_DIR)).toMatchInlineSnapshot(`
Object {
"type": "ignore",
}
`);
});
});
describe('#getPackageIdForPath()', () => {
it('returns package id for bazel package', () => {
expect(
resolver.getPackageIdForPath(Path.resolve(FIXTURES_DIR, 'packages/box/index.js'))
).toMatchInlineSnapshot(`"@pkg/box"`);
});
it('returns package id for synth package', () => {
expect(
resolver.getPackageIdForPath(Path.resolve(FIXTURES_DIR, 'src/bar/index.js'))
).toMatchInlineSnapshot(`"@synth/bar"`);
});
it('returns null for files outside of a package', () => {
expect(
resolver.getPackageIdForPath(Path.resolve(FIXTURES_DIR, 'src/index.js'))
).toMatchInlineSnapshot(`null`);
});
});
describe('#getAbsolutePackageDir()', () => {
it('returns path for bazel package', () => {
expect(resolver.getAbsolutePackageDir('@pkg/box')).toMatchInlineSnapshot(
`<absolute path>/packages/kbn-import-resolver/src/__fixtures__/packages/box`
);
});
it('returns path for synth package', () => {
expect(resolver.getAbsolutePackageDir('@synth/bar')).toMatchInlineSnapshot(
`<absolute path>/packages/kbn-import-resolver/src/__fixtures__/src/bar`
);
});
it('returns null for node_modules', () => {
expect(resolver.getAbsolutePackageDir('foo')).toMatchInlineSnapshot(`null`);
});
it('returns null for unknown packages', () => {
expect(resolver.getAbsolutePackageDir('@kbn/invalid')).toMatchInlineSnapshot(`null`);
});
});
describe('#isBazelPackage()', () => {
it('returns true for bazel packages', () => {
expect(resolver.isBazelPackage('@pkg/box')).toBe(true);
});
it('returns false for synth packages', () => {
expect(resolver.isBazelPackage('@synth/bar')).toBe(false);
});
it('returns false for node_modules packages', () => {
expect(resolver.isBazelPackage('foo')).toBe(false);
});
it('returns false for unknown packages', () => {
expect(resolver.isBazelPackage('@kbn/invalid')).toBe(false);
});
});
describe('#isSyntheticPackage()', () => {
it('returns true for synth packages', () => {
expect(resolver.isSyntheticPackage('@synth/bar')).toBe(true);
});
it('returns false for bazel packages', () => {
expect(resolver.isSyntheticPackage('@pkg/box')).toBe(false);
});
it('returns false for node_modules packages', () => {
expect(resolver.isSyntheticPackage('foo')).toBe(false);
});
it('returns false for unknown packages', () => {
expect(resolver.isSyntheticPackage('@kbn/invalid')).toBe(false);
});
});

View file

@ -6,6 +6,21 @@
* Side Public License, v 1.
*/
/**
* Resolution result indicating that the import request resolves to a built-in node library
*/
export interface BuiltInResult {
type: 'built-in';
}
/**
* Resolution result indicating that the import request resolves to an npm dep which isn't
* currently installed so assumed to be optional
*/
export interface OptionalDepResult {
type: 'optional-and-missing';
}
/**
* Resolution result indicating that the import request can't be resolved, but it shouldn't need to be
* because the file that is imported can't be resolved from the source alone, usually because it is explicitly
@ -35,9 +50,16 @@ export interface FileResult {
type: 'file';
absolute: string;
nodeModule?: string;
prefix?: string;
postfix?: string;
}
/**
* Possible resolve result types
*/
export type ResolveResult = IgnoreResult | TypesResult | FileResult;
export type ResolveResult =
| BuiltInResult
| IgnoreResult
| TypesResult
| FileResult
| OptionalDepResult;

View file

@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.bazel.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "target_types",
"rootDir": "src",
"stripInternal": false,
"types": [
"jest",
"node"
]
},
"include": [
"src/**/*"
]
}

View file

@ -38,6 +38,7 @@ RUNTIME_DEPS = [
"//packages/kbn-ui-shared-deps-npm",
"//packages/kbn-ui-shared-deps-src",
"//packages/kbn-utils",
"//packages/kbn-synthetic-package-map",
"@npm//@babel/core",
"@npm//chalk",
"@npm//clean-webpack-plugin",
@ -70,6 +71,7 @@ TYPES_DEPS = [
"//packages/kbn-ui-shared-deps-npm:npm_module_types",
"//packages/kbn-ui-shared-deps-src:npm_module_types",
"//packages/kbn-utils:npm_module_types",
"//packages/kbn-synthetic-package-map:npm_module_types",
"@npm//chalk",
"@npm//clean-webpack-plugin",
"@npm//cpy",

View file

@ -68,6 +68,11 @@ export function runKbnOptimizerCli(options: { defaultLimitsPath: string }) {
throw createFlagError('expected --no-examples to have no value');
}
const testPlugins = flags['test-plugins'] ?? false;
if (typeof testPlugins !== 'boolean') {
throw createFlagError('expected --test-plugins to have no value');
}
const profileWebpack = flags.profile ?? false;
if (typeof profileWebpack !== 'boolean') {
throw createFlagError('expected --profile to have no value');
@ -133,6 +138,7 @@ export function runKbnOptimizerCli(options: { defaultLimitsPath: string }) {
dist: dist || updateLimits,
cache,
examples: examples && !(validateLimits || updateLimits),
testPlugins: testPlugins && !(validateLimits || updateLimits),
profileWebpack,
extraPluginScanDirs,
inspectWorkers,
@ -173,6 +179,7 @@ export function runKbnOptimizerCli(options: { defaultLimitsPath: string }) {
'watch',
'oss',
'examples',
'test-plugins',
'dist',
'cache',
'profile',
@ -202,6 +209,7 @@ export function runKbnOptimizerCli(options: { defaultLimitsPath: string }) {
--focus just like --filter, except dependencies are automatically included, --filter applies to result
--filter comma-separated list of bundle id filters, results from multiple flags are merged, * and ! are supported
--no-examples don't build the example plugins
--test-plugins build test plugins too
--dist create bundles that are suitable for inclusion in the Kibana distributable, enabled when running with --update-limits
--scan-dir add a directory to the list of directories scanned for plugins (specify as many times as necessary)
--no-inspect-workers when inspecting the parent process, don't inspect the workers

View file

@ -12,6 +12,14 @@ import { createAbsolutePathSerializer } from '@kbn/dev-utils';
import { getOptimizerCacheKey } from './optimizer_cache_key';
import { OptimizerConfig } from './optimizer_config';
jest.mock('@kbn/synthetic-package-map', () => {
return {
readHashOfPackageMap() {
return '<hash of package map>';
},
};
});
jest.mock('../common/hashes', () => {
return {
Hashes: class MockHashes {
@ -68,6 +76,7 @@ describe('getOptimizerCacheKey()', () => {
"checksums": Object {
"foo": "bar",
},
"synthPackages": "<hash of package map>",
"workerConfig": Object {
"browserslistEnv": "dev",
"dist": false,

View file

@ -6,6 +6,8 @@
* Side Public License, v 1.
*/
import { readHashOfPackageMap } from '@kbn/synthetic-package-map';
import { CacheableWorkerConfig, Hashes } from '../common';
import { OptimizerConfig } from './optimizer_config';
import { getOptimizerBuiltPaths } from './optimizer_built_paths';
@ -13,6 +15,7 @@ import { getOptimizerBuiltPaths } from './optimizer_built_paths';
export interface OptimizerCacheKey {
readonly workerConfig: CacheableWorkerConfig;
readonly checksums: Record<string, string>;
readonly synthPackages: string;
}
/**
@ -25,5 +28,6 @@ export async function getOptimizerCacheKey(config: OptimizerConfig): Promise<Opt
return {
checksums: hashes.cacheToJson(),
workerConfig: config.getCacheableWorkerConfig(),
synthPackages: readHashOfPackageMap(),
};
}

View file

@ -8,7 +8,7 @@
import Path from 'path';
import Os from 'os';
import { getPluginSearchPaths } from '@kbn/config';
import { getPluginSearchPaths } from '@kbn/plugin-discovery';
import {
Bundle,
@ -109,6 +109,9 @@ interface Options {
/** path to a limits.yml file that should be used to inform ci-stats of metric limits */
limitsPath?: string;
/** discover and build test plugins along with the standard plugins */
testPlugins?: boolean;
}
export interface ParsedOptions {
@ -136,6 +139,7 @@ export class OptimizerConfig {
const examples = !!options.examples;
const profileWebpack = !!options.profileWebpack;
const inspectWorkers = !!options.inspectWorkers;
const testPlugins = !!options.testPlugins;
const cache = options.cache !== false && !process.env.KBN_OPTIMIZER_NO_CACHE;
const includeCoreBundle = !!options.includeCoreBundle;
const filters = options.filter || [];
@ -157,6 +161,7 @@ export class OptimizerConfig {
rootDir: repoRoot,
oss,
examples,
testPlugins,
});
if (!pluginScanDirs.every((p) => Path.isAbsolute(p))) {

View file

@ -12,9 +12,10 @@ export interface SearchOptions {
rootDir: string;
oss: boolean;
examples: boolean;
testPlugins?: boolean;
}
export function getPluginSearchPaths({ rootDir, oss, examples }: SearchOptions) {
export function getPluginSearchPaths({ rootDir, oss, examples, testPlugins }: SearchOptions) {
return [
resolve(rootDir, 'src', 'plugins'),
...(oss ? [] : [resolve(rootDir, 'x-pack', 'plugins')]),
@ -22,5 +23,25 @@ export function getPluginSearchPaths({ rootDir, oss, examples }: SearchOptions)
...(examples ? [resolve(rootDir, 'examples')] : []),
...(examples && !oss ? [resolve(rootDir, 'x-pack', 'examples')] : []),
resolve(rootDir, '..', 'kibana-extra'),
...(testPlugins
? [
resolve(rootDir, 'test/analytics/__fixtures__/plugins'),
resolve(rootDir, 'test/plugin_functional/plugins'),
resolve(rootDir, 'test/interpreter_functional/plugins'),
resolve(rootDir, 'test/common/fixtures/plugins'),
]
: []),
...(testPlugins && !oss
? [
resolve(rootDir, 'x-pack/test/plugin_functional/plugins'),
resolve(rootDir, 'x-pack/test/functional_with_es_ssl/fixtures/plugins'),
resolve(rootDir, 'x-pack/test/alerting_api_integration/plugins'),
resolve(rootDir, 'x-pack/test/plugin_api_integration/plugins'),
resolve(rootDir, 'x-pack/test/plugin_api_perf/plugins'),
resolve(rootDir, 'x-pack/test/licensing_plugin/plugins'),
resolve(rootDir, 'x-pack/test/usage_collection/plugins'),
resolve(rootDir, 'x-pack/test/security_functional/fixtures/common'),
]
: []),
];
}

File diff suppressed because one or more lines are too long

View file

@ -13,9 +13,8 @@ import { log } from '../utils/log';
import { spawnStreaming } from '../utils/child_process';
import { linkProjectExecutables } from '../utils/link_project_executables';
import { getNonBazelProjectsOnly, topologicallyBatchProjects } from '../utils/projects';
import { ICommand } from './';
import { ICommand } from '.';
import { readYarnLock } from '../utils/yarn_lock';
import { sortPackageJson } from '../utils/sort_package_json';
import { validateDependencies } from '../utils/validate_dependencies';
import { installBazelTools, removeYarnIntegrityFileIfExists, runBazel } from '../utils/bazel';
import { setupRemoteCache } from '../utils/bazel/setup_remote_cache';
@ -114,10 +113,6 @@ export const BootstrapCommand: ICommand = {
}
}
await time('sort package json', async () => {
await sortPackageJson(kbn);
});
const yarnLock = await time('read yarn.lock', async () => await readYarnLock(kbn));
if (options.validate) {

View file

@ -15,7 +15,11 @@ import { log } from './utils/log';
log.setLogLevel('silent');
const rootPath = resolve(`${__dirname}/utils/__fixtures__/kibana`);
const rootPath = resolve(__dirname, 'utils/__fixtures__/kibana');
jest.mock('./utils/regenerate_package_json');
jest.mock('./utils/regenerate_synthetic_package_map');
jest.mock('./utils/regenerate_base_tsconfig');
function getExpectedProjectsAndGraph(runMock: any) {
const [fullProjects, fullProjectGraph] = (runMock as jest.Mock<any>).mock.calls[0];

View file

@ -6,25 +6,98 @@
* Side Public License, v 1.
*/
import { CiStatsReporter } from '@kbn/dev-utils/ci_stats_reporter';
import { CiStatsReporter, CiStatsTiming } from '@kbn/dev-utils/ci_stats_reporter';
import { simpleKibanaPlatformPluginDiscovery, getPluginSearchPaths } from '@kbn/plugin-discovery';
import { ICommand, ICommandConfig } from './commands';
import { CliError } from './utils/errors';
import { log } from './utils/log';
import { buildProjectGraph } from './utils/projects';
import { renderProjectsTree } from './utils/projects_tree';
import { regeneratePackageJson } from './utils/regenerate_package_json';
import { regenerateSyntheticPackageMap } from './utils/regenerate_synthetic_package_map';
import { regenerateBaseTsconfig } from './utils/regenerate_base_tsconfig';
import { Kibana } from './utils/kibana';
process.env.CI_STATS_NESTED_TIMING = 'true';
export async function runCommand(command: ICommand, config: Omit<ICommandConfig, 'kbn'>) {
const runStartTime = Date.now();
let kbn;
let kbn: undefined | Kibana;
const timings: Array<Omit<CiStatsTiming, 'group'>> = [];
async function time<T>(id: string, block: () => Promise<T>): Promise<T> {
const start = Date.now();
let success = true;
try {
return await block();
} catch (error) {
success = false;
throw error;
} finally {
timings.push({
id,
ms: Date.now() - start,
meta: {
success,
},
});
}
}
async function reportTimes(timingConfig: { group: string; id: string }, error?: Error) {
if (!kbn) {
// things are too broken to report remotely
return;
}
const reporter = CiStatsReporter.fromEnv(log);
try {
await reporter.timings({
upstreamBranch: kbn.kibanaProject.json.branch,
// prevent loading @kbn/utils by passing null
kibanaUuid: kbn.getUuid() || null,
timings: [
...timings.map((t) => ({ ...timingConfig, ...t })),
{
group: timingConfig.group,
id: timingConfig.id,
ms: Date.now() - runStartTime,
meta: {
success: !error,
},
},
],
});
} catch (e) {
// prevent hiding bootstrap errors
log.error('failed to report timings:');
log.error(e);
}
}
try {
log.debug(`Running [${command.name}] command from [${config.rootPath}]`);
kbn = await Kibana.loadFrom(config.rootPath);
await time('regenerate package.json, synthetic-package map and tsconfig', async () => {
const plugins = simpleKibanaPlatformPluginDiscovery(
getPluginSearchPaths({
rootDir: config.rootPath,
oss: false,
examples: true,
testPlugins: true,
}),
[]
);
await Promise.all([
regeneratePackageJson(config.rootPath),
regenerateSyntheticPackageMap(plugins, config.rootPath),
regenerateBaseTsconfig(plugins, config.rootPath),
]);
});
kbn = await time('load Kibana project', async () => await Kibana.loadFrom(config.rootPath));
const projects = kbn.getFilteredProjects({
skipKibanaPlugins: Boolean(config.options['skip-kibana-plugins']),
ossOnly: Boolean(config.options.oss),
@ -50,50 +123,11 @@ export async function runCommand(command: ICommand, config: Omit<ICommandConfig,
});
if (command.reportTiming) {
const reporter = CiStatsReporter.fromEnv(log);
await reporter.timings({
upstreamBranch: kbn.kibanaProject.json.branch,
// prevent loading @kbn/utils by passing null
kibanaUuid: kbn.getUuid() || null,
timings: [
{
group: command.reportTiming.group,
id: command.reportTiming.id,
ms: Date.now() - runStartTime,
meta: {
success: true,
},
},
],
});
await reportTimes(command.reportTiming);
}
} catch (error) {
if (command.reportTiming) {
// if we don't have a kbn object then things are too broken to report on
if (kbn) {
try {
const reporter = CiStatsReporter.fromEnv(log);
await reporter.timings({
upstreamBranch: kbn.kibanaProject.json.branch,
// prevent loading @kbn/utils by passing null
kibanaUuid: kbn.getUuid() || null,
timings: [
{
group: command.reportTiming.group,
id: command.reportTiming.id,
ms: Date.now() - runStartTime,
meta: {
success: false,
},
},
],
});
} catch (e) {
// prevent hiding bootstrap errors
log.error('failed to report timings:');
log.error(e);
}
}
await reportTimes(command.reportTiming, error);
}
log.error(`[${command.name}] failed:`);

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export function convertPluginIdToPackageId(pluginId: string) {
if (pluginId === 'core') {
// core is the only non-plugin
return `@kbn/core`;
}
return `@kbn/${pluginId
.split('')
.flatMap((c) => (c.toUpperCase() === c ? `-${c.toLowerCase()}` : c))
.join('')}-plugin`
.replace(/-\w(-\w)+-/g, (match) => `-${match.split('-').join('')}-`)
.replace(/-plugin-plugin$/, '-plugin');
}

View file

@ -0,0 +1,35 @@
/*
* 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 Fs from 'fs/promises';
import Path from 'path';
import { KibanaPlatformPlugin } from '@kbn/plugin-discovery';
import { convertPluginIdToPackageId } from './convert_plugin_id_to_package_id';
export async function regenerateBaseTsconfig(plugins: KibanaPlatformPlugin[], repoRoot: string) {
const tsconfigPath = Path.resolve(repoRoot, 'tsconfig.base.json');
const lines = (await Fs.readFile(tsconfigPath, 'utf-8')).split('\n');
const packageMap = plugins
.slice()
.sort((a, b) => a.manifestPath.localeCompare(b.manifestPath))
.flatMap((p) => {
const id = convertPluginIdToPackageId(p.manifest.id);
const path = Path.relative(repoRoot, p.directory);
return [` "${id}": ["${path}"],`, ` "${id}/*": ["${path}/*"],`];
});
const start = lines.findIndex((l) => l.trim() === '// START AUTOMATED PACKAGE LISTING');
const end = lines.findIndex((l) => l.trim() === '// END AUTOMATED PACKAGE LISTING');
await Fs.writeFile(
tsconfigPath,
[...lines.slice(0, start + 1), ...packageMap, ...lines.slice(end)].join('\n')
);
}

View file

@ -0,0 +1,18 @@
/*
* 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 { sortPackageJson } from '@kbn/dev-utils/sort_package_json';
export async function regeneratePackageJson(rootPath: string) {
const path = Path.resolve(rootPath, 'package.json');
const json = await Fsp.readFile(path, 'utf8');
await Fsp.writeFile(path, sortPackageJson(json));
}

View file

@ -0,0 +1,33 @@
/*
* 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 Fs from 'fs/promises';
import Path from 'path';
import normalizePath from 'normalize-path';
import { KibanaPlatformPlugin } from '@kbn/plugin-discovery';
import { convertPluginIdToPackageId } from './convert_plugin_id_to_package_id';
export async function regenerateSyntheticPackageMap(
plugins: KibanaPlatformPlugin[],
repoRoot: string
) {
const entries: Array<[string, string]> = [['@kbn/core', 'src/core']];
for (const plugin of plugins) {
entries.push([
convertPluginIdToPackageId(plugin.manifest.id),
normalizePath(Path.relative(repoRoot, plugin.directory)),
]);
}
await Fs.writeFile(
Path.resolve(repoRoot, 'packages/kbn-synthetic-package-map/synthetic-packages.json'),
JSON.stringify(entries, null, 2)
);
}

View file

@ -1,65 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EventEmitter } from 'events';
import { waitUntilWatchIsReady } from './watch';
describe('#waitUntilWatchIsReady', () => {
let buildOutputStream: EventEmitter;
let completionHintPromise: Promise<string>;
beforeEach(() => {
jest.useFakeTimers();
buildOutputStream = new EventEmitter();
completionHintPromise = waitUntilWatchIsReady(buildOutputStream, {
handlerDelay: 100,
handlerReadinessTimeout: 50,
});
});
test('`waitUntilWatchIsReady` correctly handles `webpack` output', async () => {
buildOutputStream.emit('data', Buffer.from('$ webpack'));
buildOutputStream.emit('data', Buffer.from('Chunk Names'));
jest.runAllTimers();
expect(await completionHintPromise).toBe('webpack');
});
test('`waitUntilWatchIsReady` correctly handles `tsc` output', async () => {
buildOutputStream.emit('data', Buffer.from('$ tsc'));
buildOutputStream.emit('data', Buffer.from('Compilation complete.'));
jest.runAllTimers();
expect(await completionHintPromise).toBe('tsc');
});
test('`waitUntilWatchIsReady` fallbacks to default output handler if output is not recognizable', async () => {
buildOutputStream.emit('data', Buffer.from('$ some-cli'));
buildOutputStream.emit('data', Buffer.from('Compilation complete.'));
buildOutputStream.emit('data', Buffer.from('Chunk Names.'));
jest.runAllTimers();
expect(await completionHintPromise).toBe('timeout');
});
test('`waitUntilWatchIsReady` fallbacks to default output handler if none output is detected', async () => {
jest.runAllTimers();
expect(await completionHintPromise).toBe('timeout');
});
test('`waitUntilWatchIsReady` fails if output stream receives error', async () => {
buildOutputStream.emit('error', new Error('Uh, oh!'));
jest.runAllTimers();
await expect(completionHintPromise).rejects.toThrow(/Uh, oh!/);
});
});

View file

@ -140,13 +140,13 @@ export default ({ config: storybookConfig }: { config: Configuration }) => {
const options = (loader.options = { ...(loader.options as Record<string, any>) });
// capture the plugins defined at the root level
const plugins: string[] = options.plugins;
const plugins: string[] = options.plugins ?? [];
options.plugins = [];
// move the plugins to the top of the preset array so they will run after the typescript preset
options.presets = [
{
plugins,
plugins: [...plugins, require.resolve('@kbn/babel-plugin-synthetic-packages')],
},
...(options.presets as Preset[]).filter(isDesiredPreset).map((preset) => {
const tsPreset = getTsPreset(preset);

View file

@ -0,0 +1,51 @@
load("@npm//@bazel/typescript:index.bzl", "ts_config")
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
PKG_DIRNAME = "kbn-synthetic-package-map"
PKG_REQUIRE_NAME = "@kbn/synthetic-package-map"
NPM_MODULE_EXTRA_FILES = [
"package.json",
"index.js",
"index.d.ts",
"synthetic-packages.json",
]
# In this array place runtime dependencies, including other packages and NPM packages
# which must be available for this code to run.
#
# To reference other packages use:
# "//repo/relative/path/to/package"
# eg. "//packages/kbn-utils"
#
# To reference a NPM package use:
# "@npm//name-of-package"
# eg. "@npm//lodash"
RUNTIME_DEPS = [
]
js_library(
name = PKG_DIRNAME,
srcs = NPM_MODULE_EXTRA_FILES,
deps = RUNTIME_DEPS,
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)
pkg_npm(
name = "npm_module",
deps = [":" + PKG_DIRNAME],
)
filegroup(
name = "build",
srcs = [":npm_module"],
visibility = ["//visibility:public"],
)
alias(
name = "npm_module_types",
actual = ":" + PKG_DIRNAME,
visibility = ["//visibility:public"],
)

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type PackageMap = Map<string, string>;
/**
* Read the package map from disk
*/
export function readPackageMap(): PackageMap;
/**
* Read the package map and calculate a cache key/hash of the package map
*/
export function readHashOfPackageMap(): string;

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.
*/
const Fs = require('fs');
const Path = require('path');
const Crypto = require('crypto');
const PACKAGE_MAP_PATH = Path.resolve(__dirname, 'synthetic-packages.json');
function readPackageMap() {
return new Map(JSON.parse(Fs.readFileSync(PACKAGE_MAP_PATH, 'utf8')));
}
function readHashOfPackageMap() {
return Crypto.createHash('sha256').update(Fs.readFileSync(PACKAGE_MAP_PATH)).digest('hex');
}
module.exports = {
readPackageMap,
readHashOfPackageMap,
};

View file

@ -0,0 +1,10 @@
{
"name": "@kbn/synthetic-package-map",
"private": true,
"version": "1.0.0",
"main": "./index.js",
"license": "SSPL-1.0 OR Elastic License 2.0",
"kibana": {
"devOnly": true
}
}

View file

@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.bazel.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "target_types",
"stripInternal": false,
"types": [
"node"
]
},
"include": [
"index.d.ts"
]
}

View file

@ -31,7 +31,9 @@ NPM_MODULE_EXTRA_FILES = [
RUNTIME_DEPS = [
"//packages/kbn-dev-utils",
"//packages/kbn-eslint-plugin-imports",
"//packages/kbn-utility-types",
"//packages/kbn-utils",
"@npm//glob",
"@npm//listr",
"@npm//normalize-path",
@ -39,7 +41,9 @@ RUNTIME_DEPS = [
TYPES_DEPS = [
"//packages/kbn-dev-utils:npm_module_types",
"//packages/kbn-eslint-plugin-imports:npm_module_types",
"//packages/kbn-utility-types:npm_module_types",
"//packages/kbn-utils:npm_module_types",
"@npm//tslib",
"@npm//@types/glob",
"@npm//@types/jest",

View file

@ -0,0 +1,59 @@
/*
* 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 ts from 'typescript';
import { REPO_ROOT } from '@kbn/utils';
import { ImportResolver } from '@kbn/import-resolver';
function readTsConfigFile(path: string) {
const json = ts.readConfigFile(path, ts.sys.readFile);
if (json.error) {
throw new Error(`Unable to load tsconfig file: ${json.error.messageText}`);
}
return json.config;
}
function loadTsConfigFile(path: string) {
return ts.parseJsonConfigFileContent(readTsConfigFile(path) ?? {}, ts.sys, Path.dirname(path));
}
const baseTsConfig = loadTsConfigFile(Path.resolve(REPO_ROOT, 'tsconfig.base.json'));
const resolver = ImportResolver.create(REPO_ROOT);
function isTsCompatible(path: string) {
const extname = Path.extname(path);
return extname === '.ts' || extname === '.tsx' || extname === '.js';
}
export const compilerHost: ts.CompilerHost = {
...ts.createCompilerHost(baseTsConfig.options),
resolveModuleNames(moduleNames, sourceFilePath) {
const dirname = Path.dirname(sourceFilePath);
const results: Array<ts.ResolvedModule | undefined> = [];
for (const req of moduleNames) {
const result = resolver.resolve(req, dirname);
if (result?.type !== 'file' || !isTsCompatible(result.absolute)) {
results.push(undefined);
} else {
results.push({
resolvedFileName: result.absolute,
isExternalLibraryImport: !!result.nodeModule,
});
}
}
return results;
},
};

View file

@ -11,6 +11,7 @@ import * as path from 'path';
import { parseUsageCollection } from './ts_parser';
import { globAsync } from './utils';
import { TelemetryRC } from './config';
import { compilerHost } from './compiler_host';
export async function getProgramPaths({
root,
@ -48,7 +49,7 @@ export async function getProgramPaths({
}
export function* extractCollectors(fullPaths: string[], tsConfig: any) {
const program = ts.createProgram(fullPaths, tsConfig);
const program = ts.createProgram(fullPaths, tsConfig, compilerHost);
program.getTypeChecker();
const sourceFiles = fullPaths.map((fullPath) => {
const sourceFile = program.getSourceFile(fullPath);

View file

@ -10,6 +10,7 @@ import * as ts from 'typescript';
import * as path from 'path';
import { getDescriptor, TelemetryKinds } from './serializer';
import { traverseNodes } from './ts_parser';
import { compilerHost } from './compiler_host';
export function loadFixtureProgram(fixtureName: string) {
const fixturePath = path.resolve(
@ -23,7 +24,7 @@ export function loadFixtureProgram(fixtureName: string) {
if (!tsConfig) {
throw new Error('Could not find a valid tsconfig.json.');
}
const program = ts.createProgram([fixturePath], tsConfig as any);
const program = ts.createProgram([fixturePath], tsConfig as any, compilerHost);
const checker = program.getTypeChecker();
const sourceFile = program.getSourceFile(fixturePath);
if (!sourceFile) {

View file

@ -9,6 +9,7 @@
import { parseUsageCollection } from './ts_parser';
import * as ts from 'typescript';
import * as path from 'path';
import { compilerHost } from './compiler_host';
import { parsedWorkingCollector } from './__fixture__/parsed_working_collector';
import { parsedNestedCollector } from './__fixture__/parsed_nested_collector';
import { parsedExternallyDefinedCollector } from './__fixture__/parsed_externally_defined_collector';
@ -30,7 +31,7 @@ export function loadFixtureProgram(fixtureName: string) {
if (!tsConfig) {
throw new Error('Could not find a valid tsconfig.json.');
}
const program = ts.createProgram([fixturePath], tsConfig as any);
const program = ts.createProgram([fixturePath], tsConfig as any, compilerHost);
const checker = program.getTypeChecker();
const sourceFile = program.getSourceFile(fixturePath);
if (!sourceFile) {

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