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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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