Skip to content

Commit

Permalink
[kbn/bazel-packages] convert to js (#139235)
Browse files Browse the repository at this point in the history
  • Loading branch information
Spencer authored Aug 23, 2022
1 parent fe646b2 commit 6b0bd5b
Show file tree
Hide file tree
Showing 18 changed files with 509 additions and 211 deletions.
142 changes: 71 additions & 71 deletions packages/BUILD.bazel

Large diffs are not rendered by default.

14 changes: 3 additions & 11 deletions packages/kbn-bazel-packages/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ PKG_REQUIRE_NAME = "@kbn/bazel-packages"

SOURCE_FILES = glob(
[
"src/**/*.js",
"src/**/*.ts",
],
exclude = [
Expand Down Expand Up @@ -37,11 +38,6 @@ NPM_MODULE_EXTRA_FILES = [
# "@npm//name-of-package"
# eg. "@npm//lodash"
RUNTIME_DEPS = [
"//packages/kbn-utils",
"//packages/kbn-std",
"//packages/kbn-synthetic-package-map",
"@npm//globby",
"@npm//normalize-path",
]

# In this array place dependencies necessary to build the types, which will include the
Expand All @@ -55,13 +51,8 @@ RUNTIME_DEPS = [
# References to NPM packages work the same as RUNTIME_DEPS:
# eg. "@npm//@types/babel__core"
TYPES_DEPS = [
"//packages/kbn-utils:npm_module_types",
"//packages/kbn-std:npm_module_types",
"//packages/kbn-synthetic-package-map:npm_module_types",
"@npm//@types/node",
"@npm//@types/normalize-path",
"@npm//globby",
"@npm//normalize-path",
"@npm//@types/jest",
]

jsts_transpiler(
Expand All @@ -87,6 +78,7 @@ ts_project(
deps = TYPES_DEPS,
declaration = True,
declaration_map = True,
allow_js = True,
emit_declaration_only = True,
out_dir = "target_types",
root_dir = "src",
Expand Down
107 changes: 107 additions & 0 deletions packages/kbn-bazel-packages/src/async.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

/**
*
* @template T
* @template T2
* @param {(v: T) => Promise<T2>} fn
* @param {T} item
* @returns {Promise<PromiseSettledResult<T2>>}
*/
const settle = async (fn, item) => {
const [result] = await Promise.allSettled([(async () => fn(item))()]);
return result;
};

/**
* @template T
* @template T2
* @param {Array<T>} source
* @param {number} limit
* @param {(v: T) => Promise<T2>} mapFn
* @returns {Promise<T2[]>}
*/
function asyncMapWithLimit(source, limit, mapFn) {
return new Promise((resolve, reject) => {
if (limit < 1) {
reject(new Error('invalid limit, must be greater than 0'));
return;
}

let failed = false;
let inProgress = 0;
const queue = [...source.entries()];

/** @type {T2[]} */
const results = new Array(source.length);

/**
* this is run for each item, manages the inProgress state,
* calls the mapFn with that item, writes the map result to
* the result array, and calls runMore() after each item
* completes to either start another item or resolve the
* returned promise.
*
* @param {number} index
* @param {T} item
*/
function run(index, item) {
inProgress += 1;
settle(mapFn, item).then((result) => {
inProgress -= 1;

if (failed) {
return;
}

if (result.status === 'fulfilled') {
results[index] = result.value;
runMore();
return;
}

// when an error occurs we update the state to prevent
// holding onto old results and ignore future results
// from in-progress promises
failed = true;
results.length = 0;
reject(result.reason);
});
}

/**
* If there is work in the queue, schedule it, if there isn't
* any work to be scheduled and there isn't anything in progress
* then we're done. This function is called every time a mapFn
* promise resolves and once after initialization
*/
function runMore() {
if (!queue.length) {
if (inProgress === 0) {
resolve(results);
}

return;
}

while (inProgress < limit) {
const entry = queue.shift();
if (!entry) {
break;
}

run(...entry);
}
}

runMore();
});
}

module.exports = { asyncMapWithLimit };
61 changes: 61 additions & 0 deletions packages/kbn-bazel-packages/src/async.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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 { setTimeout } from 'timers/promises';
import { asyncMapWithLimit } from './async';

const NUMS = [1, 2, 3, 4];
const ident = jest.fn(async function ident<T>(x: T) {
return await x;
});
const double = jest.fn(async function double(x: number) {
return x * 2;
});

beforeEach(() => {
jest.clearAllMocks();
});

it('resolves with an empty array', async () => {
const result = await asyncMapWithLimit([], 10, ident);
expect(ident).not.toHaveBeenCalled();
expect(result).toEqual([]);
});

it('resolves with a mapped array', async () => {
const result = await asyncMapWithLimit(NUMS, 10, double);
expect(double).toHaveBeenCalledTimes(NUMS.length);
expect(result.join(',')).toMatchInlineSnapshot(`"2,4,6,8"`);
});

it('rejects when limit it not >= 1', async () => {
await expect(() => asyncMapWithLimit([], -1, ident)).rejects.toMatchInlineSnapshot(
`[Error: invalid limit, must be greater than 0]`
);
await expect(() => asyncMapWithLimit([], 0, ident)).rejects.toMatchInlineSnapshot(
`[Error: invalid limit, must be greater than 0]`
);
await expect(() => asyncMapWithLimit([], -Infinity, ident)).rejects.toMatchInlineSnapshot(
`[Error: invalid limit, must be greater than 0]`
);
});

it('rejects with the first error produced and stops calling mapFn', async () => {
const map = jest.fn(async (num) => {
if (num % 2 === 0) {
throw new Error('even numbers are not supported');
}
return num * 2;
});

await expect(() => asyncMapWithLimit(NUMS, 1, map)).rejects.toMatchInlineSnapshot(
`[Error: even numbers are not supported]`
);
await setTimeout(10);
expect(map).toHaveBeenCalledTimes(2);
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,31 @@
* Side Public License, v 1.
*/

import { inspect } from 'util';
import Path from 'path';
import Fsp from 'fs/promises';
const { inspect } = require('util');
const Path = require('path');
const Fsp = require('fs/promises');

import normalizePath from 'normalize-path';
import { REPO_ROOT } from '@kbn/utils';

import { readPackageJson, ParsedPackageJson } from './parse_package_json';
/** @typedef {import('./types').ParsedPackageJson} ParsedPackageJson */
const { readPackageJson } = require('./parse_package_json');

const BUILD_RULE_NAME = /(^|\s)name\s*=\s*"build"/;
const BUILD_TYPES_RULE_NAME = /(^|\s)name\s*=\s*"build_types"/;

/**
* Representation of a Bazel Package in the Kibana repository
* @class
* @property {string} normalizedRepoRelativeDir
* @property {import('./types').ParsedPackageJson} pkg
* @property {string | undefined} buildBazelContent
*/
export class BazelPackage {
class BazelPackage {
/**
* Create a BazelPackage object from a package directory. Reads some files from the package and returns
* a Promise for a BazelPackage instance.
* @param {string} repoRoot
* @param {string} dir
*/
static async fromDir(dir: string) {
static async fromDir(repoRoot, dir) {
const pkg = readPackageJson(Path.resolve(dir, 'package.json'));

let buildBazelContent;
Expand All @@ -36,23 +40,30 @@ export class BazelPackage {
throw new Error(`unable to read BUILD.bazel file in [${dir}]: ${error.message}`);
}

return new BazelPackage(normalizePath(Path.relative(REPO_ROOT, dir)), pkg, buildBazelContent);
return new BazelPackage(Path.relative(repoRoot, dir), pkg, buildBazelContent);
}

constructor(
/**
* Relative path from the root of the repository to the package directory
* @type {string}
*/
public readonly normalizedRepoRelativeDir: string,
normalizedRepoRelativeDir,
/**
* Parsed package.json file from the package
* @type {import('./types').ParsedPackageJson}
*/
public readonly pkg: ParsedPackageJson,
pkg,
/**
* Content of the BUILD.bazel file
* @type {string | undefined}
*/
private readonly buildBazelContent?: string
) {}
buildBazelContent = undefined
) {
this.normalizedRepoRelativeDir = normalizedRepoRelativeDir;
this.pkg = pkg;
this.buildBazelContent = buildBazelContent;
}

/**
* Returns true if the package includes a `:build` bazel rule
Expand Down Expand Up @@ -83,3 +94,7 @@ export class BazelPackage {
return `BazelPackage<${this.normalizedRepoRelativeDir}>`;
}
}

module.exports = {
BazelPackage,
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
* Side Public License, v 1.
*/

import globby from 'globby';
import Path from 'path';

import { REPO_ROOT } from '@kbn/utils';
const { expandWildcards } = require('./find_files');

/**
* This is a list of repo-relative paths to directories containing packages. Do not
Expand All @@ -19,7 +16,7 @@ import { REPO_ROOT } from '@kbn/utils';
* eg. src/vis_editors => would find a package at src/vis_editors/foo/package.json
* src/vis_editors/* => would find a package at src/vis_editors/foo/bar/package.json
*/
export const BAZEL_PACKAGE_DIRS = [
const BAZEL_PACKAGE_DIRS = [
'packages',
'packages/shared-ux',
'packages/shared-ux/*',
Expand All @@ -34,18 +31,13 @@ export const BAZEL_PACKAGE_DIRS = [

/**
* Resolve all the BAZEL_PACKAGE_DIRS to absolute paths
* @param {string} repoRoot
*/
export function getAllBazelPackageDirs() {
return globby.sync(BAZEL_PACKAGE_DIRS, {
cwd: REPO_ROOT,
onlyDirectories: true,
expandDirectories: false,
});
function getAllBazelPackageDirs(repoRoot) {
return expandWildcards(repoRoot, BAZEL_PACKAGE_DIRS);
}

/**
* Resolve all the BAZEL_PACKAGE_DIRS to repo-relative paths
*/
export function getAllRepoRelativeBazelPackageDirs() {
return getAllBazelPackageDirs().map((path) => Path.relative(REPO_ROOT, path));
}
module.exports = {
BAZEL_PACKAGE_DIRS,
getAllBazelPackageDirs,
};
44 changes: 44 additions & 0 deletions packages/kbn-bazel-packages/src/discover_packages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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 Path = require('path');

const { BazelPackage } = require('./bazel_package');
const { getAllBazelPackageDirs } = require('./bazel_package_dirs');
const { findPackages } = require('./find_files');
const { asyncMapWithLimit } = require('./async');

/**
* @param {string} repoRoot
*/
function discoverBazelPackageLocations(repoRoot) {
const packagesWithBuildBazel = getAllBazelPackageDirs(repoRoot)
.flatMap((packageDir) => findPackages(packageDir, 'BUILD.bazel'))
.map((path) => Path.dirname(path));

// NOTE: only return as discovered packages with a package.json + BUILD.bazel file.
// In the future we should change this to only discover the ones with kibana.jsonc.
return getAllBazelPackageDirs(repoRoot)
.flatMap((packageDir) => findPackages(packageDir, 'package.json'))
.map((path) => Path.dirname(path))
.filter((pkg) => packagesWithBuildBazel.includes(pkg))
.sort((a, b) => a.localeCompare(b));
}

/**
* @param {string} repoRoot
*/
async function discoverBazelPackages(repoRoot) {
return await asyncMapWithLimit(
discoverBazelPackageLocations(repoRoot),
100,
async (dir) => await BazelPackage.fromDir(repoRoot, dir)
);
}

module.exports = { discoverBazelPackageLocations, discoverBazelPackages };
Loading

0 comments on commit 6b0bd5b

Please sign in to comment.