mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[plugins] use module ids to import across plugins
This commit is contained in:
parent
e47bf4b205
commit
bd8171c13e
120 changed files with 10263 additions and 8032 deletions
|
@ -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
2
.gitignore
vendored
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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"],
|
||||
)
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}, []);
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
})();
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
64
packages/kbn-babel-plugin-synthetic-packages/BUILD.bazel
Normal file
64
packages/kbn-babel-plugin-synthetic-packages/BUILD.bazel
Normal 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"],
|
||||
)
|
|
@ -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;
|
||||
});
|
10
packages/kbn-babel-plugin-synthetic-packages/package.json
Normal file
10
packages/kbn-babel-plugin-synthetic-packages/package.json
Normal 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
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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 = {
|
|||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
});
|
||||
|
|
|
@ -37,7 +37,7 @@ module.exports = (_, options = {}) => {
|
|||
...(options['@babel/preset-env'] || {}),
|
||||
},
|
||||
],
|
||||
require('./common_preset'),
|
||||
[require('./common_preset'), options],
|
||||
],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
|
63
packages/kbn-bazel-packages/src/parse_package_json.ts
Normal file
63
packages/kbn-bazel-packages/src/parse_package_json.ts
Normal 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;
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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'),
|
||||
];
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ NPM_MODULE_EXTRA_FILES = [
|
|||
]
|
||||
|
||||
RUNTIME_DEPS = [
|
||||
"//packages/kbn-utils",
|
||||
"@npm//@babel/eslint-parser",
|
||||
"@npm//dedent",
|
||||
"@npm//eslint",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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));
|
||||
}
|
31
packages/kbn-eslint-plugin-imports/src/helpers/report.ts
Normal file
31
packages/kbn-eslint-plugin-imports/src/helpers/report.ts
Normal 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,
|
||||
});
|
||||
}
|
|
@ -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');
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
|
@ -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}]`,
|
||||
});
|
||||
}
|
120
packages/kbn-eslint-plugin-imports/src/rules/uniform_imports.ts
Normal file
120
packages/kbn-eslint-plugin-imports/src/rules/uniform_imports.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
122
packages/kbn-find-used-node-modules/BUILD.bazel
Normal file
122
packages/kbn-find-used-node-modules/BUILD.bazel
Normal 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"],
|
||||
)
|
7
packages/kbn-find-used-node-modules/README.md
Normal file
7
packages/kbn-find-used-node-modules/README.md
Normal 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.
|
|
@ -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'],
|
||||
};
|
10
packages/kbn-find-used-node-modules/package.json
Normal file
10
packages/kbn-find-used-node-modules/package.json
Normal 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
|
||||
}
|
||||
}
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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));
|
||||
}
|
|
@ -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');
|
||||
}
|
|
@ -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",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
82
packages/kbn-find-used-node-modules/src/import_visitor.ts
Normal file
82
packages/kbn-find-used-node-modules/src/import_visitor.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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';
|
17
packages/kbn-find-used-node-modules/tsconfig.json
Normal file
17
packages/kbn-find-used-node-modules/tsconfig.json
Normal 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/**/*"
|
||||
]
|
||||
}
|
124
packages/kbn-import-resolver/BUILD.bazel
Normal file
124
packages/kbn-import-resolver/BUILD.bazel
Normal 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"],
|
||||
)
|
3
packages/kbn-import-resolver/README.md
Normal file
3
packages/kbn-import-resolver/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/import-resolver
|
||||
|
||||
Empty package generated by @kbn/generate
|
13
packages/kbn-import-resolver/jest.integration.config.js
Normal file
13
packages/kbn-import-resolver/jest.integration.config.js
Normal 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'],
|
||||
};
|
10
packages/kbn-import-resolver/package.json
Normal file
10
packages/kbn-import-resolver/package.json
Normal 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
|
||||
}
|
||||
}
|
0
packages/kbn-import-resolver/src/__fixtures__/node_modules/@pkg/box/index.js
generated
vendored
Normal file
0
packages/kbn-import-resolver/src/__fixtures__/node_modules/@pkg/box/index.js
generated
vendored
Normal file
0
packages/kbn-import-resolver/src/__fixtures__/node_modules/foo/index.js
generated
vendored
Normal file
0
packages/kbn-import-resolver/src/__fixtures__/node_modules/foo/index.js
generated
vendored
Normal 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));
|
|
@ -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';
|
|
@ -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.
|
||||
*/
|
|
@ -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.
|
||||
*/
|
|
@ -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);
|
100
packages/kbn-import-resolver/src/helpers/import_req.ts
Normal file
100
packages/kbn-import-resolver/src/helpers/import_req.ts
Normal 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);
|
||||
}
|
281
packages/kbn-import-resolver/src/import_resolver.ts
Normal file
281
packages/kbn-import-resolver/src/import_resolver.ts
Normal 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);
|
||||
}
|
||||
}
|
11
packages/kbn-import-resolver/src/index.ts
Normal file
11
packages/kbn-import-resolver/src/index.ts
Normal 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';
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
17
packages/kbn-import-resolver/tsconfig.json
Normal file
17
packages/kbn-import-resolver/tsconfig.json
Normal 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/**/*"
|
||||
]
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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))) {
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
|
13810
packages/kbn-pm/dist/index.js
vendored
13810
packages/kbn-pm/dist/index.js
vendored
File diff suppressed because one or more lines are too long
|
@ -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) {
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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:`);
|
||||
|
|
21
packages/kbn-pm/src/utils/convert_plugin_id_to_package_id.ts
Normal file
21
packages/kbn-pm/src/utils/convert_plugin_id_to_package_id.ts
Normal 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');
|
||||
}
|
35
packages/kbn-pm/src/utils/regenerate_base_tsconfig.ts
Normal file
35
packages/kbn-pm/src/utils/regenerate_base_tsconfig.ts
Normal 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')
|
||||
);
|
||||
}
|
18
packages/kbn-pm/src/utils/regenerate_package_json.ts
Normal file
18
packages/kbn-pm/src/utils/regenerate_package_json.ts
Normal 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));
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
}
|
|
@ -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!/);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
|
51
packages/kbn-synthetic-package-map/BUILD.bazel
Normal file
51
packages/kbn-synthetic-package-map/BUILD.bazel
Normal 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"],
|
||||
)
|
18
packages/kbn-synthetic-package-map/index.d.ts
vendored
Normal file
18
packages/kbn-synthetic-package-map/index.d.ts
vendored
Normal 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;
|
26
packages/kbn-synthetic-package-map/index.js
Normal file
26
packages/kbn-synthetic-package-map/index.js
Normal 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,
|
||||
};
|
10
packages/kbn-synthetic-package-map/package.json
Normal file
10
packages/kbn-synthetic-package-map/package.json
Normal 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
|
||||
}
|
||||
}
|
15
packages/kbn-synthetic-package-map/tsconfig.json
Normal file
15
packages/kbn-synthetic-package-map/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
|
@ -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",
|
||||
|
|
59
packages/kbn-telemetry-tools/src/tools/compiler_host.ts
Normal file
59
packages/kbn-telemetry-tools/src/tools/compiler_host.ts
Normal 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;
|
||||
},
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue