diff --git a/lib/cli/commands/build.js b/lib/cli/commands/build.js index 9c5bf6c7..9c63cae1 100644 --- a/lib/cli/commands/build.js +++ b/lib/cli/commands/build.js @@ -1,6 +1,7 @@ // Build const baseMiddleware = require("../middlewares/base.js"); +const buildHelper = require("../../utils/buildHelper"); const build = { command: "build", @@ -37,6 +38,38 @@ build.builder = function(cli) { default: false, type: "boolean" }) + .option("include-dependency", { + describe: "A list of dependencies to be included into the build process. You can use the asterisk '*' as" + + " an alias for including all dependencies into the build process. The listed dependencies cannot be" + + " overruled by dependencies defined in 'exclude-dependency'.", + type: "array" + }) + .option("include-dependency-regexp", { + describe: "A list of regular expressions defining dependencies to be included into the build process." + + " This list is prioritized like 'include-dependency'.", + type: "array" + }) + .option("include-dependency-tree", { + describe: "A list of dependencies to be included into the build process. Transitive dependencies are" + + " implicitly included and do not need to be part of this list. These dependencies overrule" + + " the selection of 'exclude-dependency-tree' but can be overruled by 'exclude-dependency'.", + type: "array" + }) + .option("exclude-dependency", { + describe: "A list of dependencies to be excluded from the build process. The listed dependencies can" + + " be overruled by dependencies defined in 'include-dependency'.", + type: "array" + }) + .option("exclude-dependency-regexp", { + describe: "A list of regular expressions defining dependencies to be excluded from the build process." + + " This list is prioritized like 'exclude-dependency'.", + type: "array" + }) + .option("exclude-dependency-tree", { + describe: "A list of dependencies to be excluded from the build process. Transitive dependencies are" + + " implicitly included and do not need to be part of this list.", + type: "array" + }) .option("dest", { describe: "Path of build destination", default: "./dist", @@ -98,11 +131,29 @@ async function handleBuild(argv) { } const tree = await normalizer.generateProjectTree(normalizerOptions); + const buildSettings = (tree.builder && tree.builder.settings) || {}; + + const {includedDependencies, excludedDependencies} = buildHelper.createDependencyLists({ + tree: tree, + includeDependency: argv["include-dependency"], + includeDependencyRegExp: argv["include-dependency-regexp"], + includeDependencyTree: argv["include-dependency-tree"], + excludeDependency: argv["exclude-dependency"], + excludeDependencyRegExp: argv["exclude-dependency-regexp"], + excludeDependencyTree: argv["exclude-dependency-tree"], + defaultIncludeDependency: buildSettings.includeDependency, + defaultIncludeDependencyRegExp: buildSettings.includeDependencyRegExp, + defaultIncludeDependencyTree: buildSettings.includeDependencyTree + }); + const buildAll = buildHelper.alignWithBuilderApi(argv.all, includedDependencies, excludedDependencies); + await builder.build({ tree: tree, destPath: argv.dest, cleanDest: argv["clean-dest"], - buildDependencies: argv.all, + buildDependencies: buildAll, + includedDependencies: includedDependencies, + excludedDependencies: excludedDependencies, dev: command === "dev", selfContained: command === "self-contained", jsdoc: command === "jsdoc", diff --git a/lib/utils/buildHelper.js b/lib/utils/buildHelper.js new file mode 100644 index 00000000..546e2e01 --- /dev/null +++ b/lib/utils/buildHelper.js @@ -0,0 +1,216 @@ +const log = require("@ui5/logger").getLogger("cli:utils:buildHelper"); + +/** + * Creates an object containing the flattened project dependency tree. Each dependency is defined as an object key while + * its value is an array of all of its transitive dependencies. + * + * @param {object} tree Project tree as generated by the [@ui5/project.normalizer]{@link module:@ui5/project.normalizer} + * @returns {object} An object with dependency names as key and each with an array of its transitive + * dependencies as value + */ +function getFlattenedDependencyTree(tree) { + const dependencyInfo = {}; + + function _getTransitiveDependencies(project, dependencies) { + project.dependencies.forEach((dep) => { + if (!dependencies.includes(dep.metadata.name)) { + dependencies.push(dep.metadata.name); + _getTransitiveDependencies(dep, dependencies); + } + }); + return dependencies; + } + function _processDependencies(project) { + project.dependencies.forEach((dep) => { + if (!dependencyInfo[dep.metadata.name]) { + dependencyInfo[dep.metadata.name] = _getTransitiveDependencies(dep, []); + _processDependencies(dep); + } + }); + } + + _processDependencies(tree); + return dependencyInfo; +} + +/** + * Creates dependency lists for 'includedDependencies' and 'excludedDependencies'. Regular expressions are directly + * applied to a list of all project dependencies so that they don't need to be evaluated in later processing steps. + * Generally, includes are handled with a higher priority than excludes. Additionally, operations for processing + * transitive dependencies are handled with a lower priority than explicitly mentioned dependencies. The default + * dependencies set in the build settings are appended in the end. + * + * The priority of the various dependency lists is applied in the following order, but note that a later list can't + * overrule earlier ones: + *
    + *
  1. includeDependency, includeDependencyRegExp
  2. + *
  3. excludeDependency, excludeDependencyRegExp
  4. + *
  5. includeDependencyTree
  6. + *
  7. excludeDependencyTree
  8. + *
  9. defaultIncludeDependency, defaultIncludeDependencyRegExp, defaultIncludeDependencyTree
  10. + *
+ * + * @param {object} parameters Parameters + * @param {object} parameters.tree Project tree as generated by the + * [@ui5/project.normalizer]{@link module:@ui5/project.normalizer} + * @param {string[]} parameters.includeDependency The dependencies to be considered in 'includedDependencies'; the + * "*" character can be used as wildcard for all dependencies and is an alias for the CLI option "--all" + * @param {string[]} parameters.includeDependencyRegExp Strings which are interpreted as regular expressions + * to describe the selection of dependencies to be considered in 'includedDependencies' + * @param {string[]} parameters.includeDependencyTree The dependencies to be considered in 'includedDependencies'; + * transitive dependencies are also appended + * @param {string[]} parameters.excludeDependency The dependencies to be considered in 'excludedDependencies' + * @param {string[]} parameters.excludeDependencyRegExp Strings which are interpreted as regular expressions + * to describe the selection of dependencies to be considered in 'excludedDependencies' + * @param {string[]} parameters.excludeDependencyTree The dependencies to be considered in 'excludedDependencies'; + * transitive dependencies are also appended + * @param {string[]} parameters.defaultIncludeDependency Same as 'includeDependency' parameter; used for build + * settings + * @param {string[]} parameters.defaultIncludeDependencyRegExp Same as 'includeDependencyRegExp' parameter; used + * for build settings + * @param {string[]} parameters.defaultIncludeDependencyTree Same as 'includeDependencyTree' parameter; used for + * build settings + * @returns {{includedDependencies:string[],excludedDependencies:string[]}} An object containing the + * 'includedDependencies' and 'excludedDependencies' + */ +function createDependencyLists({ + tree, + includeDependency = [], includeDependencyRegExp = [], includeDependencyTree = [], + excludeDependency = [], excludeDependencyRegExp = [], excludeDependencyTree = [], + defaultIncludeDependency = [], defaultIncludeDependencyRegExp = [], defaultIncludeDependencyTree = [] +}) { + if ( + !includeDependency.length && !includeDependencyRegExp.length && !includeDependencyTree.length && + !excludeDependency.length && !excludeDependencyRegExp.length && !excludeDependencyTree.length && + !defaultIncludeDependency.length && !defaultIncludeDependencyRegExp.length && + !defaultIncludeDependencyTree.length + ) { + return {includedDependencies: [], excludedDependencies: []}; + } + + const flattenedDependencyTree = getFlattenedDependencyTree(tree); + + function isExcluded(excludeList, depName) { + return excludeList && excludeList.has(depName); + } + function processDependencies({targetList, dependencies, dependenciesRegExp = [], excludeList, handleSubtree}) { + if (handleSubtree && dependenciesRegExp.length) { + throw new Error("dependenciesRegExp can't be combined with handleSubtree:true option"); + } + dependencies.forEach((depName) => { + if (depName === "*") { + targetList.add(depName); + } else if (flattenedDependencyTree[depName]) { + if (!isExcluded(excludeList, depName)) { + targetList.add(depName); + } + if (handleSubtree) { + flattenedDependencyTree[depName].forEach((dep) => { + if (!isExcluded(excludeList, dep)) { + targetList.add(dep); + } + }); + } + } else { + log.warn( + `Could not find dependency "${depName}" for project ${tree.metadata.name}. Dependency filter is ` + + `ignored`); + } + }); + dependenciesRegExp.map((exp) => new RegExp(exp)).forEach((regExp) => { + for (const depName in flattenedDependencyTree) { + if (regExp.test(depName) && !isExcluded(excludeList, depName)) { + targetList.add(depName); + } + } + }); + } + + const includedDependencies = new Set(); + const excludedDependencies = new Set(); + + // add dependencies defined in includeDependency and includeDependencyRegExp to the list of includedDependencies + processDependencies({ + targetList: includedDependencies, + dependencies: includeDependency, + dependenciesRegExp: includeDependencyRegExp + }); + // add dependencies defined in excludeDependency and excludeDependencyRegExp to the list of excludedDependencies + processDependencies({ + targetList: excludedDependencies, + dependencies: excludeDependency, + dependenciesRegExp: excludeDependencyRegExp + }); + // add dependencies defined in includeDependencyTree with their transitive dependencies to the list of + // includedDependencies; due to prioritization only those dependencies are added which are not excluded + // by excludedDependencies + processDependencies({ + targetList: includedDependencies, + dependencies: includeDependencyTree, + excludeList: excludedDependencies, + handleSubtree: true + }); + // add dependencies defined in excludeDependencyTree with their transitive dependencies to the list of + // excludedDependencies; due to prioritization only those dependencies are added which are not excluded + // by includedDependencies + processDependencies({ + targetList: excludedDependencies, + dependencies: excludeDependencyTree, + excludeList: includedDependencies, + handleSubtree: true + }); + // due to the lowest priority only add the dependencies defined in build settings if they are not excluded + // by any other dependency defined in excludedDependencies + processDependencies({ + targetList: includedDependencies, + dependencies: defaultIncludeDependency, + dependenciesRegExp: defaultIncludeDependencyRegExp, + excludeList: excludedDependencies + }); + processDependencies({ + targetList: includedDependencies, + dependencies: defaultIncludeDependencyTree, + excludeList: excludedDependencies, + handleSubtree: true + }); + + return { + includedDependencies: Array.from(includedDependencies), + excludedDependencies: Array.from(excludedDependencies) + }; +} + +/** + * Returns whether project dependencies have to be built influenced by includedDependencies and + * excludedDependencies. + * If only selected dependencies (via includedDependencies) have to be built, the "*" character + * is added to the excludedDependencies to make sure that all other dependencies are + * excluded. + * In case a "*" character is included in includedDependencies, it is removed and the + * buildAll flag is set to true as it behaves as an alias. + * + * @param {boolean} buildAll The value of the all command line parameter to decide if project + * dependencies have to be built + * @param {string[]} includedDependencies The list of included dependencies + * @param {string[]} excludedDependencies The list of excluded dependencies + * @returns {boolean} Whether it is required to build project dependencies + */ +function alignWithBuilderApi(buildAll, includedDependencies, excludedDependencies) { + if ((!buildAll && !includedDependencies.includes("*")) && includedDependencies.length) { + excludedDependencies.push("*"); + } + if (includedDependencies.includes("*")) { + buildAll = true; + includedDependencies.splice(includedDependencies.indexOf("*"), 1); + } + if (!buildAll && includedDependencies.length) { + buildAll = true; + } + return buildAll; +} + +module.exports = { + getFlattenedDependencyTree, + createDependencyLists, + alignWithBuilderApi +}; diff --git a/test/lib/cli/commands/build.js b/test/lib/cli/commands/build.js index 2b4fffb9..d64cf07a 100644 --- a/test/lib/cli/commands/build.js +++ b/test/lib/cli/commands/build.js @@ -1,6 +1,6 @@ const test = require("ava"); const sinon = require("sinon"); -const build = require("../../../../lib/cli/commands/build"); +const mock = require("mock-require"); const normalizer = require("@ui5/project").normalizer; const builder = require("@ui5/builder").builder; const logger = require("@ui5/logger"); @@ -18,10 +18,13 @@ const defaultBuilderArgs = { tree: { metadata: { name: "Sample" - } + }, + dependencies: [] }, destPath: "./dist", buildDependencies: undefined, + includedDependencies: [], + excludedDependencies: [], cleanDest: undefined, dev: false, selfContained: false, @@ -35,33 +38,40 @@ test.beforeEach((t) => { normalizerStub = sinon.stub(normalizer, "generateProjectTree"); builderStub = sinon.stub(builder, "build").returns(Promise.resolve()); sinon.stub(logger, "setShowProgress"); + t.context.log = { + warn: sinon.stub() + }; + sinon.stub(logger, "getLogger").withArgs("cli:utils:buildHelper").returns(t.context.log); + mock.reRequire("../../../../lib/utils/buildHelper"); // rerequire buildHelper to ensure usage of stubbed logger + t.context.build = mock.reRequire("../../../../lib/cli/commands/build"); }); test.afterEach.always((t) => { sinon.restore(); + mock.stopAll(); }); test.serial("ui5 build (default) ", async (t) => { normalizerStub.resolves({ - metadata: - { + metadata: { name: "Sample" - } + }, + dependencies: [] }); args._ = ["build"]; - await build.handler(args); + await t.context.build.handler(args); t.deepEqual(builderStub.getCall(0).args[0], defaultBuilderArgs, "default build triggered with expected arguments"); }); test.serial("ui5 build dev", async (t) => { normalizerStub.resolves({ - metadata: - { + metadata: { name: "Sample" - } + }, + dependencies: [] }); args._ = ["build", "dev"]; - await build.handler(args); + await t.context.build.handler(args); t.deepEqual( builderStub.getCall(0).args[0], Object.assign({}, defaultBuilderArgs, {dev: true}), @@ -71,13 +81,13 @@ test.serial("ui5 build dev", async (t) => { test.serial("ui5 build self-contained", async (t) => { normalizerStub.resolves({ - metadata: - { + metadata: { name: "Sample" - } + }, + dependencies: [] }); args._ = ["build", "self-contained"]; - await build.handler(args); + await t.context.build.handler(args); t.deepEqual( builderStub.getCall(0).args[0], Object.assign({}, defaultBuilderArgs, {selfContained: true}), @@ -87,13 +97,13 @@ test.serial("ui5 build self-contained", async (t) => { test.serial("ui5 build jsdoc", async (t) => { normalizerStub.resolves({ - metadata: - { + metadata: { name: "Sample" - } + }, + dependencies: [] }); args._ = ["build", "jsdoc"]; - await build.handler(args); + await t.context.build.handler(args); t.deepEqual( builderStub.getCall(0).args[0], Object.assign({}, defaultBuilderArgs, {jsdoc: true}), @@ -103,15 +113,15 @@ test.serial("ui5 build jsdoc", async (t) => { test.serial("ui5 build --framework-version 1.99", async (t) => { normalizerStub.resolves({ - metadata: - { + metadata: { name: "Sample" - } + }, + dependencies: [] }); args._ = ["build"]; args.frameworkVersion = "1.99.0"; - await build.handler(args); + await t.context.build.handler(args); t.deepEqual( normalizerStub.getCall(0).args[0], { @@ -123,3 +133,357 @@ test.serial("ui5 build --framework-version 1.99", async (t) => { }, "generateProjectTree got called with expected arguments" ); }); + +async function assertIncludeDependency(t, { + treeDeps, includeDeps, includeDepsRegExp, includeDepsTree, excludeDeps, excludeDepsRegExp, excludeDepsTree, + expectedBuilderArgs +}) { + const tree = Object.assign({metadata: {name: "Sample"}}, treeDeps); + const _args = Object.assign({}, args); // copy default args to ensure it is not modified + normalizerStub.resolves(tree); + _args._ = ["build"]; + _args["include-dependency"] = includeDeps; + _args["include-dependency-regexp"] = includeDepsRegExp; + _args["include-dependency-tree"] = includeDepsTree; + _args["exclude-dependency"] = excludeDeps; + _args["exclude-dependency-regexp"] = excludeDepsRegExp; + _args["exclude-dependency-tree"] = excludeDepsTree; + await t.context.build.handler(_args); + t.deepEqual(builderStub.getCall(0).args[0], + Object.assign({}, defaultBuilderArgs, {tree: tree}, expectedBuilderArgs), + "default build triggered with expected arguments"); +} + +test.serial("ui5 build --include-dependency", async (t) => { + await assertIncludeDependency(t, { + treeDeps: { + dependencies: [{ + metadata: { + name: "sap.ui.core" + }, + dependencies: [] + }] + }, + includeDeps: ["sap.ui.core"], + expectedBuilderArgs: { + buildDependencies: true, + includedDependencies: ["sap.ui.core"], + excludedDependencies: ["*"] + } + }); +}); + +[{ + title: "no excludes", + excludeDepsRegExp: [], + excludeDepsTree: [], + expectedBuilderArgs: { + buildDependencies: true, + includedDependencies: ["a", "b0", "b1", "c"], + excludedDependencies: ["*"] + } +}, { + title: "overridden by excludes", + excludeDepsRegExp: ["^b0$"], + excludeDepsTree: ["b1"], + expectedBuilderArgs: { + buildDependencies: true, + includedDependencies: ["a"], + excludedDependencies: ["b0", "b1", "c", "*"] + } +}].forEach((fixture) => { + test.serial(`ui5 build (dependency included via ui5.yaml); ${fixture.title}`, async (t) => { + await assertIncludeDependency(t, { + treeDeps: { + dependencies: [{ + metadata: { + name: "a" + }, + dependencies: [{ + metadata: { + name: "b0" + }, + dependencies: [] + }, { + metadata: { + name: "b1" + }, + dependencies: [{ + metadata: { + name: "c" + }, + dependencies: [] + }] + }] + }], + builder: { + settings: { + includeDependency: ["a"], + includeDependencyRegExp: ["^b0$"], + includeDependencyTree: ["b1"], + } + } + }, + excludeDepsRegExp: fixture.excludeDepsRegExp, + excludeDepsTree: fixture.excludeDepsTree, + expectedBuilderArgs: fixture.expectedBuilderArgs + }); + }); +}); + +test.serial("ui5 build --include-dependency-regexp", async (t) => { + await assertIncludeDependency(t, { + treeDeps: { + dependencies: [{ + metadata: { + name: "sap.ui.core" + }, + dependencies: [] + }, { + metadata: { + name: "sap.m" + }, + dependencies: [] + }, { + metadata: { + name: "sap.f" + }, + dependencies: [] + }] + }, + includeDepsRegExp: ["^sap.[mf]$"], + expectedBuilderArgs: { + buildDependencies: true, + includedDependencies: ["sap.m", "sap.f"], + excludedDependencies: ["*"] + } + }); +}); + +test.serial("ui5 build --include-dependency-tree", async (t) => { + await assertIncludeDependency(t, { + treeDeps: { + dependencies: [{ + metadata: { + name: "a" + }, + dependencies: [{ + metadata: { + name: "b0" + }, + dependencies: [] + }, { + metadata: { + name: "b1" + }, + dependencies: [{ + metadata: { + name: "c" + }, + dependencies: [] + }] + }] + }] + }, + includeDepsTree: ["a"], + expectedBuilderArgs: { + buildDependencies: true, + includedDependencies: ["a", "b0", "b1", "c"], + excludedDependencies: ["*"] + } + }); +}); + +test.serial("ui5 build include/exclude tree (two subtrees, sharing a transitive dependency)", async (t) => { + await assertIncludeDependency(t, { + treeDeps: { + dependencies: [{ + metadata: { + name: "a0" + }, + dependencies: [{ + metadata: { + name: "b0" + }, + dependencies: [] + }, { + metadata: { + name: "b1" + }, + dependencies: [{ + metadata: { + name: "c" + }, + dependencies: [] + }] + }] + }, { + metadata: { + name: "a1" + }, + dependencies: [{ + metadata: { + name: "b0" + }, + dependencies: [] + }, { + metadata: { + name: "b1" + }, + dependencies: [{ + metadata: { + name: "c" + }, + dependencies: [] + }] + }] + }] + }, + includeDepsTree: ["a0"], + excludeDepsTree: ["a1"], + expectedBuilderArgs: { + buildDependencies: true, + includedDependencies: ["a0", "b0", "b1", "c"], + excludedDependencies: ["a1", "*"] + } + }); +}); + +test.serial("ui5 build --include-dependency --include-dependency-tree (select a transitive dependency)", async (t) => { + await assertIncludeDependency(t, { + treeDeps: { + dependencies: [{ + metadata: { + name: "a" + }, + dependencies: [{ + metadata: { + name: "b0" + }, + dependencies: [{ + metadata: { + name: "b0.c" + }, + dependencies: [] + }] + }, { + metadata: { + name: "b1" + }, + dependencies: [{ + metadata: { + name: "b1.c" + }, + dependencies: [] + }] + }] + }] + }, + includeDeps: ["b0"], + includeDepsTree: ["b1"], + expectedBuilderArgs: { + buildDependencies: true, + includedDependencies: ["b0", "b1", "b1.c"], + excludedDependencies: ["*"] + } + }); +}); + +test.serial("ui5 build --include-dependency=* --exclude-dependency=sap.ui.core", async (t) => { + await assertIncludeDependency(t, { + treeDeps: { + dependencies: [{ + metadata: { + name: "sap.ui.core" + }, + dependencies: [] + }] + }, + includeDeps: ["*"], + excludeDeps: ["sap.ui.core"], + expectedBuilderArgs: { + buildDependencies: true, + includedDependencies: [], + excludedDependencies: ["sap.ui.core"] + } + }); +}); + +test.serial("ui5 build --include-dependency-tree=a --exclude-dependency=a", async (t) => { + await assertIncludeDependency(t, { + treeDeps: { + dependencies: [{ + metadata: { + name: "a" + }, + dependencies: [{ + metadata: { + name: "b0" + }, + dependencies: [] + }, { + metadata: { + name: "b1" + }, + dependencies: [] + }] + }] + }, + includeDepsTree: ["a"], + excludeDeps: ["a"], + expectedBuilderArgs: { + buildDependencies: true, + includedDependencies: ["b0", "b1"], + excludedDependencies: ["a", "*"] + } + }); +}); + +test.serial("ui5 build --include-dependency-tree include/exclude tree has a lower priority", async (t) => { + await assertIncludeDependency(t, { + treeDeps: { + dependencies: [{ + metadata: { + name: "a" + }, + dependencies: [{ + metadata: { + name: "b0" + }, + dependencies: [] + }, { + metadata: { + name: "b1" + }, + dependencies: [{ + metadata: { + name: "c" + }, + dependencies: [] + }] + }] + }] + }, + includeDepsTree: ["a"], + excludeDepsRegExp: ["^b.$"], + expectedBuilderArgs: { + buildDependencies: true, + includedDependencies: ["a", "c"], + excludedDependencies: ["b0", "b1", "*"] + } + }); +}); + +test.serial("ui5 build --include-dependency (dependency not found)", async (t) => { + const {log} = t.context; + await assertIncludeDependency(t, { + treeDeps: { + dependencies: [] + }, + includeDeps: ["sap.ui.core"] + }); + t.is(log.warn.callCount, 1, "log.warn should be called once"); + t.deepEqual(log.warn.getCall(0).args, + ["Could not find dependency \"sap.ui.core\" for project Sample. Dependency filter is ignored"], + "logger.warn should be called with expected string"); +}); diff --git a/test/lib/utils/buildHelper.js b/test/lib/utils/buildHelper.js new file mode 100644 index 00000000..b7308925 --- /dev/null +++ b/test/lib/utils/buildHelper.js @@ -0,0 +1,263 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); +const logger = require("@ui5/logger"); + +test.beforeEach((t) => { + t.context.log = { + warn: sinon.stub() + }; + sinon.stub(logger, "getLogger").withArgs("cli:utils:buildHelper").returns(t.context.log); + t.context.buildHelper = mock.reRequire("../../../lib/utils/buildHelper"); +}); + +test.afterEach.always((t) => { + sinon.restore(); + mock.stopAll(); +}); + +test.serial("getFlattenedDependencyTree", (t) => { + const {getFlattenedDependencyTree} = t.context.buildHelper; + const tree = { + metadata: { + name: "Sample" + }, + dependencies: [{ + metadata: { + name: "a0" + }, + dependencies: [{ + metadata: { + name: "b" + }, + dependencies: [{ + metadata: { + name: "c" + }, + dependencies: [{ + metadata: { + name: "d0" + }, + dependencies: [] + }, { + metadata: { + name: "d1" + }, + dependencies: [] + }] + }] + }] + }, { + metadata: { + name: "a1" + }, + dependencies: [{ + metadata: { + name: "c" + }, + dependencies: [{ + metadata: { + name: "d0" + }, + dependencies: [] + }, { + metadata: { + name: "d1" + }, + dependencies: [] + }] + }] + }] + }; + + t.deepEqual(getFlattenedDependencyTree(tree), { + a0: ["b", "c", "d0", "d1"], + a1: ["c", "d0", "d1"], + b: ["c", "d0", "d1"], + c: ["d0", "d1"], + d0: [], + d1: [] + }); +}); + +function assertCreateDependencyLists(t, { + includeDependency, includeDependencyRegExp, includeDependencyTree, + excludeDependency, excludeDependencyRegExp, excludeDependencyTree, + defaultIncludeDependency, defaultIncludeDependencyRegExp, defaultIncludeDependencyTree, + expectedIncludedDependencies, expectedExcludedDependencies +}) { + const {createDependencyLists} = t.context.buildHelper; + const tree = { + metadata: { + name: "Sample" + }, + dependencies: [{ + metadata: { + name: "a0" + }, + dependencies: [{ + metadata: { + name: "b0" + }, + dependencies: [] + }, { + metadata: { + name: "b1" + }, + dependencies: [{ + metadata: { + name: "c" + }, + dependencies: [] + }] + }] + }, { + metadata: { + name: "a1" + }, + dependencies: [{ + metadata: { + name: "b0" + }, + dependencies: [] + }, { + metadata: { + name: "b1" + }, + dependencies: [{ + metadata: { + name: "c" + }, + dependencies: [] + }] + }, { + metadata: { + name: "b2" + }, + dependencies: [] + }] + }, { + metadata: { + name: "a2" + }, + dependencies: [{ + metadata: { + name: "b3" + }, + dependencies: [] + }] + }] + }; + + const {includedDependencies, excludedDependencies} = createDependencyLists({ + tree: tree, + includeDependency: includeDependency, + includeDependencyRegExp: includeDependencyRegExp, + includeDependencyTree: includeDependencyTree, + excludeDependency: excludeDependency, + excludeDependencyRegExp: excludeDependencyRegExp, + excludeDependencyTree: excludeDependencyTree, + defaultIncludeDependency: defaultIncludeDependency, + defaultIncludeDependencyRegExp: defaultIncludeDependencyRegExp, + defaultIncludeDependencyTree: defaultIncludeDependencyTree + }); + t.deepEqual(includedDependencies, expectedIncludedDependencies); + t.deepEqual(excludedDependencies, expectedExcludedDependencies); +} + +test.serial("createDependencyLists: only includes", (t) => { + assertCreateDependencyLists(t, { + includeDependency: ["a1", "b2"], + includeDependencyRegExp: ["^b0$"], + includeDependencyTree: ["a2"], + expectedIncludedDependencies: ["a1", "b2", "b0", "a2", "b3"], + expectedExcludedDependencies: [] + }); +}); + +test.serial("createDependencyLists: only excludes", (t) => { + assertCreateDependencyLists(t, { + excludeDependency: ["a1", "b2"], + excludeDependencyRegExp: ["^b0$"], + excludeDependencyTree: ["a2"], + expectedIncludedDependencies: [], + expectedExcludedDependencies: ["a1", "b2", "b0", "a2", "b3"] + }); +}); + +test.serial("createDependencyLists: includeDependencyTree has lower priority than excludes", (t) => { + assertCreateDependencyLists(t, { + includeDependencyTree: ["a1"], + excludeDependency: ["a1"], + excludeDependencyRegExp: ["^b[0-2]$"], + expectedIncludedDependencies: ["c"], + expectedExcludedDependencies: ["a1", "b0", "b1", "b2"] + }); +}); + +test.serial("createDependencyLists: excludeDependencyTree has lower priority than includes", (t) => { + assertCreateDependencyLists(t, { + includeDependency: ["a1"], + includeDependencyRegExp: ["^b[0-2]$"], + excludeDependencyTree: ["a1"], + expectedIncludedDependencies: ["a1", "b0", "b1", "b2"], + expectedExcludedDependencies: ["c"] + }); +}); + +test.serial("createDependencyLists: includeDependencyTree has higher priority than excludeDependencyTree", (t) => { + assertCreateDependencyLists(t, { + includeDependencyTree: ["a1"], + excludeDependencyTree: ["a1"], + expectedIncludedDependencies: ["a1", "b0", "b1", "c", "b2"], + expectedExcludedDependencies: [] + }); +}); + +test.serial("createDependencyLists: defaultIncludeDependency/RegExp has lower priority than excludes", (t) => { + assertCreateDependencyLists(t, { + defaultIncludeDependency: ["a1", "b2", "c"], + defaultIncludeDependencyRegExp: ["^b0$"], + excludeDependency: ["a1"], + excludeDependencyRegExp: ["^b.$"], + expectedIncludedDependencies: ["c"], + expectedExcludedDependencies: ["a1", "b0", "b1", "b2", "b3"] + }); +}); + +test.serial("createDependencyLists: defaultIncludeDependencyTree has lower priority than excludes", (t) => { + assertCreateDependencyLists(t, { + defaultIncludeDependencyTree: ["a1"], + excludeDependencyTree: ["b1"], + expectedIncludedDependencies: ["a1", "b0", "b2"], + expectedExcludedDependencies: ["b1", "c"] + }); +}); + +test.serial("alignWithBuilderApi: * is added to excludedDependencies", (t) => { + const {alignWithBuilderApi} = t.context.buildHelper; + const args = { + includedDependencies: ["a"], + excludedDependencies: [] + }; + t.is(alignWithBuilderApi(false, args.includedDependencies, args.excludedDependencies), true, + "Should return true if includedDependencies are given"); + t.deepEqual(args, { + includedDependencies: ["a"], + excludedDependencies: ["*"] + }); +}); + +test.serial("alignWithBuilderApi: includedDependencies=* is is an alias for buildAll", (t) => { + const {alignWithBuilderApi} = t.context.buildHelper; + const args = { + includedDependencies: ["*"], + excludedDependencies: [] + }; + t.is(alignWithBuilderApi(false, args.includedDependencies, args.excludedDependencies), true, + "Should return true if includedDependencies are given"); + t.deepEqual(args, { + includedDependencies: [], + excludedDependencies: [] + }); +});