Skip to content

Build cache: buildThemes cache is invalidated by irrelevant library.js / .library content changes #1523

Description

@matz3

Expected Behavior

The buildThemes task should only be re-executed by the incremental (delta) build system
when an input that actually affects its output changes (e.g. a library.source.less file, or
a .library / library.js file being added or removed, which changes the set of
directories a theme is built for).

A change to the content of a .library or library.js file — while the file continues to
exist in the same directory — must not invalidate the buildThemes cache, because the task
does not read the content of these files.

Current Behavior

The buildThemes task is re-executed on any content change to a .library or library.js
file within the project (or its dependencies), even though the theme output cannot possibly
differ.

Root cause: For non-root library projects, buildThemes is configured with a
librariesPattern glob (packages/project/lib/build/definitions/library.js and
themeLibrary.js):

librariesPattern: !taskUtil.isRootProject() ? "/resources/**/(*.library|library.js)" : undefined,

The task requests these files via combo.byGlob(librariesPattern)
(packages/builder/lib/tasks/buildThemes.js) and uses them only to derive the set of
directories that contain a library
:

(await pAvailableLibraries).forEach((resource) => {
    const library = path.dirname(resource.getPath()); // only the PATH is used
    if (!availableLibraries.includes(library)) {
        availableLibraries.push(library);
    }
});

The resource content is never read. However, the incremental build cache tracks every
resource a task requests by its content hash (integrity), so the buildThemes stage
signature incorporates the content of every matched .library / library.js file. Any content
change to such a file therefore changes the stage signature and forces buildThemes to
re-execute — a needless cache miss that slows down incremental rebuilds in watch/serve mode.

Note: The over-tracking is by content hash, so a change that is later stripped (e.g. a JS
comment removed by minify, whose output buildThemes reads) does not trigger the rebuild.
Only changes that survive into the workspace buildThemes reads (e.g. a changed return value in
library.js, or any content change in the non-minified .library) cause the spurious
re-execution.

Steps to Reproduce the Issue

  1. Serve an application with a non-root library dependency that has a theme
    (fixture application.alibrary.a, which contains
    themes/base/library.source.less).
  2. Add a library.js next to the library's .library file and warm the build cache by
    requesting a resource.
  3. Change only the content of library.js in a way that survives minification (e.g. change
    a returned value), and request a theme resource
    (/resources/library/a/themes/base/library.css).
  4. Observe the emitted ui5.project-build-status events for library.a: buildThemes fires
    task-start / task-end (re-executed) instead of task-skip.

The following integration test (using the BuildServer harness with a mocked @parcel/watcher)
reproduces the issue deterministically, independent of the OS-level file watcher:

test.serial("Serve application.a, library.js content change must not re-run buildThemes", async (t) => {
	const fixtureTester = t.context.fixtureTester = await FixtureTester.create(t, "application.a");

	await fixtureTester.serveProject();

	// Add a library.js to library.a. It is matched by the buildThemes `librariesPattern` glob, but
	// buildThemes uses only its path, not its content.
	const libraryJsPath =
		`${fixtureTester.fixturePath}/node_modules/collection/library.a/src/library/a/library.js`;
	await fs.writeFile(libraryJsPath, `sap.ui.define([], function() {\n\t"use strict";\n\treturn 1;\n});\n`);
	await fixtureTester.fireWatcherEvent("create", libraryJsPath);

	// #1 initial request builds library.a (and its dependencies), warming the cache. Request the
	// theme output itself so it is unambiguous that the later request (#3) is a rebuild of an
	// already-built resource rather than a first build.
	await fixtureTester.requestResource({resource: "/resources/library/a/themes/base/library.css"});

	// #2 request again with a warm cache — nothing rebuilds.
	await fixtureTester.requestResource({
		resource: "/resources/library/a/themes/base/library.css",
		assertions: {projects: {}},
	});

	// Change ONLY the content of library.js — buildThemes never reads this content. Use a change
	// that survives minification (a returned value) so the resource buildThemes tracks really differs.
	await fs.writeFile(libraryJsPath, `sap.ui.define([], function() {\n\t"use strict";\n\treturn 42;\n});\n`);
	await fixtureTester.fireWatcherEvent("update", libraryJsPath);

	// #3 rebuild after the change. buildThemes must be skipped for library.a — the changed file's
	// content is irrelevant to it. This assertion FAILS today: buildThemes is missing from the
	// skipped set because it is spuriously re-executed.
	await fixtureTester.requestResource({
		resource: "/resources/library/a/themes/base/library.css",
		assertions: {
			projects: {
				"library.a": {
					skippedTasks: [
						"buildThemes",
						"enhanceManifest",
						"escapeNonAsciiCharacters",
						"replaceBuildtime",
					]
				}
			}
		}
	});
});

To run it:

npm ci --engine-strict
npx ava test/lib/build/BuildServer.integration.js \
  --match="*library.js content change must not re-run buildThemes*"

(run from packages/project)

Context

  • UI5 Module Version (output of ui5 --version when using the CLI): 5.0.0-alpha.7
  • Node.js Version: 26.5.0
  • npm Version: 11.19.0
  • OS/Platform: macOS 26.6.1 (darwin)
  • Browser (if relevant): unknown
  • Other information regarding your environment (optional): Reproduced via the
    BuildServer integration test harness (mocked @parcel/watcher), so it is independent of
    the OS-level file watcher.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions