From e824e8a433080177621544943e2f831a8ab25e02 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 2 Sep 2026 14:25:28 +0200 Subject: [PATCH 1/5] feat(project): Add "server" build flag and cache-only build mode Introduce a "server" flag on the BuildConfiguration. When set, composeTaskList disables tasks whose output the server generates on the fly (currently generateVersionInfo) by default, while still allowing includedTasks to re-enable them. graph.serve() now sets server:true, giving a single declarative source of truth for the server task set instead of callers hand-editing excludedTasks. Also add a cacheOnly option to graph.build(): it runs the build to populate the shared build cache via ProjectBuilder.build() + closeCacheManager() without writing the result to a target directory. ProjectBuilder.build() gains a dependencyIncludes parameter so the cache-only path has the same dependency selection as buildToTarget(). The server flag participates in the build-cache signature, so a server-aligned build (e.g. the upcoming "ui5 build for-server") and a running "ui5 serve" hash to the same cache entries and share results. --- packages/project/lib/build/ProjectBuilder.js | 12 ++++- .../project/lib/build/helpers/BuildContext.js | 2 + .../lib/build/helpers/composeTaskList.js | 11 ++++- packages/project/lib/graph/ProjectGraph.js | 31 ++++++++++++- .../project/test/lib/build/ProjectBuilder.js | 25 +++++++++++ .../test/lib/build/helpers/BuildContext.js | 3 ++ .../test/lib/build/helpers/composeTaskList.js | 45 +++++++++++++++++++ 7 files changed, 125 insertions(+), 4 deletions(-) diff --git a/packages/project/lib/build/ProjectBuilder.js b/packages/project/lib/build/ProjectBuilder.js index 4d28d9ad0e6..cdb780c1028 100644 --- a/packages/project/lib/build/ProjectBuilder.js +++ b/packages/project/lib/build/ProjectBuilder.js @@ -23,6 +23,11 @@ class ProjectBuilder { * @typedef {object} @ui5/project/build/ProjectBuilder~BuildConfiguration * @property {boolean} [selfContained=false] Flag to activate self contained build * @property {boolean} [jsdoc=false] Flag to activate JSDoc build + * @property {boolean} [server=false] + * Flag to activate a server-aligned build. Disables tasks whose output the server generates + * on the fly (currently generateVersionInfo) by default, matching the task set + * used by @ui5/server. The disabled tasks can still be re-enabled via + * includedTasks. * @property {boolean} [createBuildManifest=false] * Whether to create a build manifest file for the root project. * This is currently only supported for projects of type 'library' and 'theme-library' @@ -195,6 +200,10 @@ class ProjectBuilder { * @param {boolean} [parameters.includeRootProject=true] Whether to include the root project * @param {Array.} [parameters.includedDependencies=[]] List of dependencies to include * @param {Array.} [parameters.excludedDependencies=[]] List of dependencies to exclude + * @param {@ui5/project/build/ProjectBuilder~DependencyIncludes} [parameters.dependencyIncludes] + * Alternative to the includedDependencies and excludedDependencies parameters. + * Allows for a more sophisticated configuration for defining which dependencies should be built. + * If this is provided, the other mentioned parameters are ignored. * @param {AbortSignal} [parameters.signal] Signal to abort the build * @param {Function} [projectBuiltCallback] Callback invoked after each project is built * @returns {Promise} Promise resolving with array of processed project names @@ -202,10 +211,11 @@ class ProjectBuilder { async build({ includeRootProject = true, includedDependencies = [], excludedDependencies = [], + dependencyIncludes, signal, }, projectBuiltCallback) { const requestedProjects = this._determineRequestedProjects( - includeRootProject, includedDependencies, excludedDependencies); + includeRootProject, includedDependencies, excludedDependencies, dependencyIncludes); return await this.#build(requestedProjects, projectBuiltCallback, signal); } diff --git a/packages/project/lib/build/helpers/BuildContext.js b/packages/project/lib/build/helpers/BuildContext.js index 4d398e98a1c..3d25f555807 100644 --- a/packages/project/lib/build/helpers/BuildContext.js +++ b/packages/project/lib/build/helpers/BuildContext.js @@ -18,6 +18,7 @@ class BuildContext { constructor(graph, taskRepository, { // buildConfig selfContained = false, jsdoc = false, + server = false, createBuildManifest = false, outputStyle = OutputStyleEnum.Default, includedTasks = [], excludedTasks = [], @@ -69,6 +70,7 @@ class BuildContext { this._buildConfig = { selfContained, jsdoc, + server, createBuildManifest, outputStyle, includedTasks, diff --git a/packages/project/lib/build/helpers/composeTaskList.js b/packages/project/lib/build/helpers/composeTaskList.js index dca89a180e3..34631577908 100644 --- a/packages/project/lib/build/helpers/composeTaskList.js +++ b/packages/project/lib/build/helpers/composeTaskList.js @@ -3,6 +3,8 @@ * * Sets specific tasks to be disabled by default, these tasks need to be included explicitly. * Based on the selected build mode (selfContained|preload), different tasks are enabled. + * When the server flag is set, tasks whose output the server generates on the fly + * (currently generateVersionInfo) are disabled by default. * Tasks can be enabled or disabled. The wildcard * is also supported and affects all tasks. * * @private @@ -11,7 +13,7 @@ * Build configuration * @returns {Array} List of tasks to be executed */ -export default function composeTaskList(allTasks, {selfContained, jsdoc, includedTasks, excludedTasks}) { +export default function composeTaskList(allTasks, {selfContained, jsdoc, server, includedTasks, excludedTasks}) { let selectedTasks = allTasks.reduce((list, key) => { list[key] = true; return list; @@ -59,6 +61,13 @@ export default function composeTaskList(allTasks, {selfContained, jsdoc, include selectedTasks.generateFlexChangesBundle = false; } + if (server) { + // The 'versionInfo' server middleware generates the version info on the fly, so a server + // build does not run generateVersionInfo. Keeping it out of the default set here lets + // 'ui5 serve' and 'ui5 build for-server' share one build-cache entry. + selectedTasks.generateVersionInfo = false; + } + // Exclude tasks for (let i = 0; i < excludedTasks.length; i++) { const taskName = excludedTasks[i]; diff --git a/packages/project/lib/graph/ProjectGraph.js b/packages/project/lib/graph/ProjectGraph.js index 3a6cf7f5327..8ddc8e40e55 100644 --- a/packages/project/lib/graph/ProjectGraph.js +++ b/packages/project/lib/graph/ProjectGraph.js @@ -706,6 +706,14 @@ class ProjectGraph { * part of the build result. If this is provided, the other mentioned parameters will be ignored. * @param {boolean} [parameters.selfContained=false] Flag to activate self contained build * @param {boolean} [parameters.jsdoc=false] Flag to activate JSDoc build + * @param {boolean} [parameters.server=false] + * Flag to activate a server-aligned build. Disables tasks whose output the server generates + * on the fly (currently generateVersionInfo) by default, so the build result + * matches what @ui5/server produces. + * @param {boolean} [parameters.cacheOnly=false] + * Only populate the build cache without writing the build result to destPath. + * Intended to warm the shared build cache that a subsequent ui5 serve reuses. + * When set, destPath and cleanDest are ignored. * @param {boolean} [parameters.createBuildManifest=false] * Whether to create a build manifest file for the root project. * This is currently only supported for projects of type 'library' and 'theme-library' @@ -725,7 +733,8 @@ class ProjectGraph { destPath, cleanDest = false, includedDependencies = [], excludedDependencies = [], dependencyIncludes, - selfContained = false, jsdoc = false, createBuildManifest = false, + selfContained = false, jsdoc = false, server = false, createBuildManifest = false, + cacheOnly = false, includedTasks = [], excludedTasks = [], outputStyle = OutputStyleEnum.Default, cache = Cache.Default, @@ -745,13 +754,28 @@ class ProjectGraph { graph: this, taskRepository: await this._getTaskRepository(), buildConfig: { - selfContained, jsdoc, + selfContained, jsdoc, server, createBuildManifest, includedTasks, excludedTasks, outputStyle, cache }, ui5DataDir, }); + if (cacheOnly) { + // Build to populate the cache only, without writing the result to a target directory. + // buildToTarget closes the CacheManager itself; the reader-based build() does not, so + // close it explicitly once the build has finished. + try { + await builder.build({ + includeRootProject: true, + includedDependencies, excludedDependencies, + dependencyIncludes, + }); + } finally { + builder.closeCacheManager(); + } + return; + } return await builder.buildToTarget({ destPath, cleanDest, includedDependencies, excludedDependencies, @@ -803,6 +827,9 @@ class ProjectGraph { taskRepository: await this._getTaskRepository(), buildConfig: { selfContained, jsdoc, + // A serve is always a server-aligned build: disable tasks the server generates + // on the fly (e.g. generateVersionInfo). See composeTaskList. + server: true, createBuildManifest, includedTasks, excludedTasks, outputStyle: OutputStyleEnum.Default, diff --git a/packages/project/test/lib/build/ProjectBuilder.js b/packages/project/test/lib/build/ProjectBuilder.js index 25d3d4fe614..ee9bd50f668 100644 --- a/packages/project/test/lib/build/ProjectBuilder.js +++ b/packages/project/test/lib/build/ProjectBuilder.js @@ -164,6 +164,30 @@ test("build", async (t) => { t.is(executeCleanupTasksStub.callCount, 1, "_executeCleanupTasksStub got called once"); }); +test("build: forwards dependencyIncludes to _determineRequestedProjects", async (t) => { + const {graph, taskRepository, ProjectBuilder, sinon} = t.context; + + const builder = new ProjectBuilder({graph, taskRepository}); + + const determineRequestedProjectsStub = sinon.stub(builder, "_determineRequestedProjects").returns([]); + // Short-circuit the actual build; we only assert the requested-projects resolution + sinon.stub(builder._buildContext, "getRequiredProjectContexts").resolves(new Map()); + sinon.stub(builder, "_registerCleanupSigHooks").returns("cleanup sig hooks"); + sinon.stub(builder, "_deregisterCleanupSigHooks"); + sinon.stub(builder, "_executeCleanupTasks").resolves(); + + await builder.build({ + includedDependencies: ["dep a"], + excludedDependencies: ["dep b"], + dependencyIncludes: "dependencyIncludes" + }); + + t.is(determineRequestedProjectsStub.callCount, 1, "_determineRequestedProjects got called once"); + t.deepEqual(determineRequestedProjectsStub.getCall(0).args, [ + true, ["dep a"], ["dep b"], "dependencyIncludes" + ], "_determineRequestedProjects got called with dependencyIncludes forwarded"); +}); + test("build: Conflicting dependency parameters", async (t) => { const {graph, taskRepository, ProjectBuilder} = t.context; @@ -645,6 +669,7 @@ test.serial("_writeResults: Create build manifest", async (t) => { excludedTasks: [], includedTasks: [], jsdoc: false, + server: false, selfContained: false, cache: "Default", }, "createBuildManifest got called with correct build configuration"); diff --git a/packages/project/test/lib/build/helpers/BuildContext.js b/packages/project/test/lib/build/helpers/BuildContext.js index 1e67e125c87..a2b8f3d9bd2 100644 --- a/packages/project/test/lib/build/helpers/BuildContext.js +++ b/packages/project/test/lib/build/helpers/BuildContext.js @@ -83,6 +83,7 @@ test("getBuildConfig: Default values", (t) => { selfContained: false, outputStyle: OutputStyleEnum.Default, jsdoc: false, + server: false, createBuildManifest: false, includedTasks: [], excludedTasks: [], @@ -103,6 +104,7 @@ test("getBuildConfig: Custom values", (t) => { selfContained: true, outputStyle: OutputStyleEnum.Namespace, jsdoc: true, + server: true, createBuildManifest: false, includedTasks: ["included tasks"], excludedTasks: ["excluded tasks"], @@ -113,6 +115,7 @@ test("getBuildConfig: Custom values", (t) => { selfContained: true, outputStyle: OutputStyleEnum.Namespace, jsdoc: true, + server: true, createBuildManifest: false, includedTasks: ["included tasks"], excludedTasks: ["excluded tasks"], diff --git a/packages/project/test/lib/build/helpers/composeTaskList.js b/packages/project/test/lib/build/helpers/composeTaskList.js index f1fb5f905c4..f82d71555a4 100644 --- a/packages/project/test/lib/build/helpers/composeTaskList.js +++ b/packages/project/test/lib/build/helpers/composeTaskList.js @@ -123,6 +123,51 @@ const allTasks = [ "generateBundle", ] ], + [ + "composeTaskList: server=true excludes generateVersionInfo by default", { + archive: false, + selfContained: false, + jsdoc: false, + server: true, + includedTasks: [], + excludedTasks: [] + }, [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "minify", + "buildThemes", + "generateLibraryManifest", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateBundle", + "generateLibraryPreload", + ] + ], + [ + "composeTaskList: server=true with includedTasks re-enables generateVersionInfo", { + archive: false, + selfContained: false, + jsdoc: false, + server: true, + includedTasks: ["generateVersionInfo"], + excludedTasks: [] + }, [ + "replaceCopyright", + "replaceVersion", + "replaceBuildtime", + "escapeNonAsciiCharacters", + "minify", + "buildThemes", + "generateLibraryManifest", + "generateVersionInfo", + "generateFlexChangesBundle", + "generateComponentPreload", + "generateBundle", + "generateLibraryPreload", + ] + ], [ "composeTaskList: includedTasks / excludedTasks", { archive: false, From 2ead828abb41120b44603b3c385ecf9483f6de42 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 2 Sep 2026 14:26:31 +0200 Subject: [PATCH 2/5] refactor(server): Delegate generateVersionInfo exclusion to graph.serve() graph.serve() now runs a server-aligned build (server:true) that disables the generateVersionInfo task by default, since the versionInfo middleware generates the version info on the fly. Drop the imperative excludedTasks editing in stack.js and forward the caller's excludedTasks unchanged, keeping the default server task set in one place (@ui5/project composeTaskList). --- packages/server/lib/serve/stack.js | 16 +++------ .../server/test/lib/server/serve/stack.js | 34 ++++++------------- 2 files changed, 15 insertions(+), 35 deletions(-) diff --git a/packages/server/lib/serve/stack.js b/packages/server/lib/serve/stack.js index 40c3ac09bd7..20bd2b1dfb3 100644 --- a/packages/server/lib/serve/stack.js +++ b/packages/server/lib/serve/stack.js @@ -34,9 +34,8 @@ const log = getLogger("server"); export async function buildRouter(graph, config, error, getDegradedError) { const { sendSAPTargetCSP = false, simpleIndex = false, liveReload = false, serveCSPReports = false, - cache, ui5DataDir, includedTasks, webSocketToken = null, + cache, ui5DataDir, includedTasks, excludedTasks, webSocketToken = null, } = config; - let {excludedTasks} = config; const rootProject = graph.getRoot(); const readers = []; @@ -72,15 +71,10 @@ export async function buildRouter(graph, config, error, getDegradedError) { initialBuildIncludedDependencies.push("sap.ui.core"); } - // Explicitly exclude task "generateVersionInfo" for Server builds - // because middleware "versionInfo" will generate the version info anyways. - if (!Array.isArray(excludedTasks)) { - excludedTasks = []; - } - if (!excludedTasks.includes("generateVersionInfo")) { - excludedTasks = [...excludedTasks, "generateVersionInfo"]; - } - + // graph.serve() runs a server-aligned build (server:true), which disables the + // "generateVersionInfo" task by default because the "versionInfo" middleware generates the + // version info on the fly. The default task set is owned by @ui5/project (composeTaskList), + // so no task filtering is applied here. const buildServer = await graph.serve({ initialBuildIncludedDependencies, includedTasks, diff --git a/packages/server/test/lib/server/serve/stack.js b/packages/server/test/lib/server/serve/stack.js index 480b030a064..f6e7925b74c 100644 --- a/packages/server/test/lib/server/serve/stack.js +++ b/packages/server/test/lib/server/serve/stack.js @@ -109,23 +109,10 @@ test("buildRouter() getDegradedError is undefined for the embedding path (no sup "no degraded accessor is threaded when none was supplied"); }); -test("buildRouter() adds generateVersionInfo to excludedTasks when undefined", async (t) => { - // The versionInfo middleware generates the version info, so the build must skip - // the generateVersionInfo task. Verify it lands in the excludedTasks passed to - // graph.serve() even when the caller excludes nothing. - const buildServer = createBuildServer(); - const graph = createGraph(buildServer); - const applyMiddleware = sinon.stub().resolves(); - - const {buildRouter} = await importBuildRouter(applyMiddleware); - await buildRouter(graph, {excludedTasks: undefined}); - - t.true(graph.serve.calledOnce); - const callArgs = graph.serve.firstCall.args[0]; - t.deepEqual(callArgs.excludedTasks, ["generateVersionInfo"]); -}); - -test("buildRouter() appends generateVersionInfo to existing excludedTasks", async (t) => { +test("buildRouter() passes the caller's excludedTasks through unchanged", async (t) => { + // The versionInfo middleware generates the version info, so the build must skip the + // generateVersionInfo task. That default is now owned by graph.serve() (server:true) in + // @ui5/project, so stack.js no longer edits excludedTasks: verify it forwards them as-is. const buildServer = createBuildServer(); const graph = createGraph(buildServer); const applyMiddleware = sinon.stub().resolves(); @@ -136,23 +123,22 @@ test("buildRouter() appends generateVersionInfo to existing excludedTasks", asyn t.true(graph.serve.calledOnce); const callArgs = graph.serve.firstCall.args[0]; - t.deepEqual(callArgs.excludedTasks, ["anotherTask", "anotherTask2", "generateVersionInfo"]); + t.is(callArgs.excludedTasks, originalExcludedTasks, + "the caller's excludedTasks are forwarded unchanged, without appending generateVersionInfo"); t.deepEqual(originalExcludedTasks, ["anotherTask", "anotherTask2"], "the caller's excludedTasks array is not mutated"); }); -test("buildRouter() keeps generateVersionInfo when already excluded", async (t) => { +test("buildRouter() forwards an undefined excludedTasks without adding generateVersionInfo", async (t) => { const buildServer = createBuildServer(); const graph = createGraph(buildServer); const applyMiddleware = sinon.stub().resolves(); const {buildRouter} = await importBuildRouter(applyMiddleware); - const originalExcludedTasks = ["anotherTask", "generateVersionInfo", "anotherTask2"]; - await buildRouter(graph, {excludedTasks: originalExcludedTasks}); + await buildRouter(graph, {excludedTasks: undefined}); t.true(graph.serve.calledOnce); const callArgs = graph.serve.firstCall.args[0]; - t.deepEqual(callArgs.excludedTasks, ["anotherTask", "generateVersionInfo", "anotherTask2"]); - t.deepEqual(originalExcludedTasks, ["anotherTask", "generateVersionInfo", "anotherTask2"], - "the caller's excludedTasks array is not mutated"); + t.is(callArgs.excludedTasks, undefined, + "stack.js does not synthesize a generateVersionInfo exclusion"); }); From 5b3ce9e279620c89687b9edaf9e825b7e7d14a19 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 2 Sep 2026 14:27:38 +0200 Subject: [PATCH 3/5] feat(cli): Add "build for-server" command Add a "ui5 build for-server" subcommand that warms the shared build cache the "ui5 serve" command reuses. It runs a server-aligned build (server:true, so the generateVersionInfo task is skipped) and only populates the cache (cacheOnly), without writing the build result to the destination directory. Because its BuildConfiguration matches what "ui5 serve" produces, the two share the same build-cache entries, so a subsequent serve can reuse the pre-built results. --- packages/cli/lib/cli/commands/build.js | 10 ++++++++++ packages/cli/test/lib/cli/commands/build.js | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/packages/cli/lib/cli/commands/build.js b/packages/cli/lib/cli/commands/build.js index bd9c9d00c80..928325301f4 100644 --- a/packages/cli/lib/cli/commands/build.js +++ b/packages/cli/lib/cli/commands/build.js @@ -32,6 +32,14 @@ build.builder = function(cli) { builder: noop, middlewares: [baseMiddleware] }) + .command("for-server", + "Build the project into the shared build cache for reuse by 'ui5 serve'. " + + "Aligns the task set with the server (e.g. skips 'generateVersionInfo') and does not " + + "write the build result to the destination directory.", { + handler: handleBuild, + builder: noop, + middlewares: [baseMiddleware] + }) .option("include-all-dependencies", { describe: "Include all dependencies in the build result. " + "This is equivalent to '--include-dependency \"*\"'", @@ -215,6 +223,8 @@ async function handleBuild(argv) { }, selfContained: command === "self-contained", jsdoc: command === "jsdoc", + server: command === "for-server", + cacheOnly: command === "for-server", includedTasks: argv["include-task"], excludedTasks: argv["exclude-task"], outputStyle: argv["output-style"], diff --git a/packages/cli/test/lib/cli/commands/build.js b/packages/cli/test/lib/cli/commands/build.js index d439bde5cf7..1dca6308013 100644 --- a/packages/cli/test/lib/cli/commands/build.js +++ b/packages/cli/test/lib/cli/commands/build.js @@ -52,6 +52,8 @@ function getDefaultBuilderArgs() { createBuildManifest: false, selfContained: false, jsdoc: false, + server: false, + cacheOnly: false, includedTasks: undefined, excludedTasks: undefined, outputStyle: "Default" @@ -120,6 +122,19 @@ test.serial("ui5 build jsdoc", async (t) => { t.deepEqual(builder.getCall(0).args[0], expectedBuilderArgs, "JSDoc build called with expected arguments"); }); +test.serial("ui5 build for-server", async (t) => { + const {build, argv, builder, expectedBuilderArgs} = t.context; + + argv._.push("for-server"); + + await build.handler(argv); + + expectedBuilderArgs.server = true; + expectedBuilderArgs.cacheOnly = true; + t.deepEqual(builder.getCall(0).args[0], expectedBuilderArgs, + "for-server build called with expected arguments"); +}); + test.serial("ui5 build --framework-version", async (t) => { const {build, argv, graphFromPackageDependenciesStub} = t.context; From 470bf59264681ea30df53c552b17b09d64188565 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 2 Sep 2026 14:28:40 +0200 Subject: [PATCH 4/5] docs(documentation): Document "ui5 build for-server" Describe the new "ui5 build for-server" command in the Builder page: it warms the shared cache that "ui5 serve" reuses by running the server's task set (which skips generateVersionInfo) without writing a build result. Update the generateVersionInfo footnote to name both server entry points. --- internal/documentation/docs/pages/Builder.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/documentation/docs/pages/Builder.md b/internal/documentation/docs/pages/Builder.md index 5ac7c647c48..1f3a3e63efc 100644 --- a/internal/documentation/docs/pages/Builder.md +++ b/internal/documentation/docs/pages/Builder.md @@ -58,7 +58,7 @@ All available standard tasks are documented under **API -> @ui5/builder -> tasks 3 Enabled in `self-contained` build, which disables `generateComponentPreload` and `generateLibraryPreload` 4 Enabled for projects defining a [bundle configuration](./Configuration.md#custom-bundling) 5 Can be enabled for framework projects via the `includeTask` option. For other projects, this task is skipped -6 Disabled for the server due to a corresponding middleware producing the same output +6 Disabled for the server (`ui5 serve` and `ui5 build for-server`) due to a corresponding middleware producing the same output 7 Enabled for Specification Version 4.0 and lower, and for framework projects. For other projects using Specification Version 5.0 and higher, this task is skipped ### minify @@ -227,6 +227,8 @@ The cache may grow over time. It can be deleted at any time to reclaim disk spac ::: info By default, build caches created by `ui5 build` and `ui5 serve` are **separate and cannot be mixed**. Each command executes a distinct set of tasks, resulting in separate caches tailored to its specific use case. For more details on server caching, see the [UI5 Server documentation](./Server.md). + +To pre-populate the cache that `ui5 serve` reuses, run `ui5 build for-server`. It executes the same task set as the server (for example, it skips `generateVersionInfo`, which the server generates on the fly) and only warms the cache without writing a build result to the destination directory. A subsequent `ui5 serve` then reuses those cached results. ::: From 8c54b04b2271c1470802a9e6ea3062cfe4bc27b8 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 2 Sep 2026 14:42:22 +0200 Subject: [PATCH 5/5] test(project): Update BuildServer tests for server-aligned serve() default graph.serve() now runs a server-aligned build (server:true), which disables generateVersionInfo by default. Because that task collected the version info of all libraries, it previously forced every library to build on the initial serve. Without it, serving an application resource builds only application.a; libraries build lazily when their resources (or a not-found lookup) first reach them. Update the integration expectations accordingly: drop generateVersionInfo from the skipped-task lists, expect only the requested projects to build, and rework the "test exclusion of generateVersionInfo" case to assert the new default (excluded) plus re-enabling it via includedTasks. --- .../test/lib/build/BuildServer.integration.js | 90 ++++++------------- 1 file changed, 26 insertions(+), 64 deletions(-) diff --git a/packages/project/test/lib/build/BuildServer.integration.js b/packages/project/test/lib/build/BuildServer.integration.js index 61832f020e8..318afbe9623 100644 --- a/packages/project/test/lib/build/BuildServer.integration.js +++ b/packages/project/test/lib/build/BuildServer.integration.js @@ -218,10 +218,6 @@ test.serial("Serve application.a, request application resource", async (t) => { resource: "/test.js", assertions: { projects: { - "library.d": {}, - "library.a": {}, - "library.b": {}, - "library.c": {}, "application.a": {} } } @@ -251,8 +247,7 @@ test.serial("Serve application.a, request application resource", async (t) => { // Note: replaceCopyright is skipped because no copyright is configured in the project "replaceCopyright", "enhanceManifest", - "generateFlexChangesBundle", - "generateVersionInfo" + "generateFlexChangesBundle" ] } } @@ -319,10 +314,6 @@ test.serial("Serve application.a, create and delete a source file", async (t) => resource: "/created.js", assertions: { projects: { - "library.d": {}, - "library.a": {}, - "library.b": {}, - "library.c": {}, "application.a": {} } } @@ -354,8 +345,7 @@ test.serial("Serve application.a, create and delete a source file", async (t) => "escapeNonAsciiCharacters", "replaceCopyright", "enhanceManifest", - "generateFlexChangesBundle", - "generateVersionInfo" + "generateFlexChangesBundle" ] } } @@ -377,13 +367,19 @@ test.serial("Serve application.a, create and delete a source file", async (t) => } }); - // #5 the second file is no longer served, thus requesting it shouldn't trigger a rebuild - // (all projects are still cached from the previous builds) + // #5 the second file is no longer served. Resolving the not-found lookup searches the + // dependencies, which builds the libraries for the first time (the server build no longer + // pre-builds them via generateVersionInfo). await fixtureTester.requestResource({ resource: "/another.js", notFound: true, assertions: { - projects: {} + projects: { + "library.d": {}, + "library.a": {}, + "library.b": {}, + "library.c": {} + } } }); @@ -403,8 +399,7 @@ test.serial("Serve application.a, create and delete a source file", async (t) => "escapeNonAsciiCharacters", "replaceCopyright", "enhanceManifest", - "generateFlexChangesBundle", - "generateVersionInfo" + "generateFlexChangesBundle" ] } } @@ -583,10 +578,9 @@ test.serial("Serve application.a, request application resource AND library resou resources: ["/test.js", "/resources/library/a/.library"], assertions: { projects: { - "library.d": {}, + // Only the requested projects build. Libraries are no longer pulled in by + // generateVersionInfo, which the server build disables by default. "library.a": {}, - "library.b": {}, - "library.c": {}, "application.a": {} } } @@ -658,10 +652,6 @@ test.serial("Serve application.a with --cache=Default", async (t) => { resource: "/test.js", assertions: { projects: { - "library.d": {}, - "library.a": {}, - "library.b": {}, - "library.c": {}, "application.a": {} } } @@ -690,8 +680,7 @@ test.serial("Serve application.a with --cache=Default", async (t) => { "escapeNonAsciiCharacters", "replaceCopyright", "enhanceManifest", - "generateFlexChangesBundle", - "generateVersionInfo" + "generateFlexChangesBundle" ] } } @@ -714,10 +703,6 @@ test.serial("Serve application.a with --cache=Off", async (t) => { resource: "/test.js", assertions: { projects: { - "library.d": {}, - "library.a": {}, - "library.b": {}, - "library.c": {}, "application.a": {} } } @@ -741,10 +726,6 @@ test.serial("Serve application.a with --cache=Off", async (t) => { resource: "/test.js", assertions: { projects: { - "library.d": {}, - "library.a": {}, - "library.b": {}, - "library.c": {}, "application.a": {} } } @@ -765,10 +746,6 @@ test.serial("Serve application.a with --cache=Off", async (t) => { resource: "/test.js", assertions: { projects: { - "library.d": {}, - "library.a": {}, - "library.b": {}, - "library.c": {}, "application.a": {} } } @@ -791,10 +768,6 @@ test.serial("Serve application.a with --cache=Off", async (t) => { resource: "/test.js", assertions: { projects: { - "library.d": {}, - "library.a": {}, - "library.b": {}, - "library.c": {}, "application.a": {} } } @@ -810,10 +783,6 @@ test.serial("Serve application.a with --cache=ReadOnly", async (t) => { resource: "/test.js", assertions: { projects: { - "library.d": {}, - "library.a": {}, - "library.b": {}, - "library.c": {}, "application.a": {} } } @@ -846,8 +815,7 @@ test.serial("Serve application.a with --cache=ReadOnly", async (t) => { "escapeNonAsciiCharacters", "replaceCopyright", "enhanceManifest", - "generateFlexChangesBundle", - "generateVersionInfo" + "generateFlexChangesBundle" ] } } @@ -876,8 +844,7 @@ test.serial("Serve application.a with --cache=ReadOnly", async (t) => { "escapeNonAsciiCharacters", "replaceCopyright", "enhanceManifest", - "generateFlexChangesBundle", - "generateVersionInfo" + "generateFlexChangesBundle" ] } } @@ -894,10 +861,6 @@ test.serial("Serve application.a with --cache=Force (1)", async (t) => { resource: "/test.js", assertions: { projects: { - "library.d": {}, - "library.a": {}, - "library.b": {}, - "library.c": {}, "application.a": {} } } @@ -1238,17 +1201,15 @@ test.serial("Source change during second build retries cleanly without no_cache }); test.serial("Serve application.a (test exclusion of generateVersionInfo)", async (t) => { - // This test verifies that the "generateVersionInfo" task - // can be excluded from the server build via the "excludedTasks" config option. + // The server build (graph.serve, server:true) disables the "generateVersionInfo" task by + // default, because the "versionInfo" middleware generates the version info on the fly. This + // test verifies the default exclusion and that "includedTasks" can re-enable the task. const fixtureTester = t.context.fixtureTester = await FixtureTester.create(t, "application.a"); - // #1 Exclude "generateVersionInfo": - await fixtureTester.serveProject({ - config: { - excludedTasks: ["generateVersionInfo"], - } - }); + // #1 Default server build: "generateVersionInfo" is excluded by default. It does not pull in + // the libraries, so only application.a builds to serve the request. + await fixtureTester.serveProject(); // Request a resource to trigger the build: await fixtureTester.requestResource({ @@ -1274,10 +1235,11 @@ test.serial("Serve application.a (test exclusion of generateVersionInfo)", async await fixtureTester.teardown(); - // #2 Don't exclude tasks (includes "generateVersionInfo"): + // #2 Re-enable "generateVersionInfo" via includedTasks. It collects the version info of all + // libraries, so those get built as well. await fixtureTester.serveProject({ config: { - excludedTasks: [], + includedTasks: ["generateVersionInfo"], } });